changeset 792:58ebf50235a5

routing: Simplify how routes are defined. * No more declaring the type of route parameters -- the sources and generators already know what type each parameter is supposed to be. * Same for variadic parameters -- we know already. * Update cache version to force a clear reload of the config. * Update tests. TODO: simplify code in the `Route` class to use source or generator transparently.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 07 Sep 2016 08:58:41 -0700
parents 504d6817352d
children 57a5cb380eab
files piecrust/__init__.py piecrust/appconfig.py piecrust/commands/builtin/info.py piecrust/generation/base.py piecrust/generation/blogarchives.py piecrust/generation/taxonomy.py piecrust/page.py piecrust/routing.py piecrust/sources/autoconfig.py piecrust/sources/base.py piecrust/sources/default.py piecrust/sources/posts.py tests/test_routing.py
diffstat 13 files changed, 229 insertions(+), 159 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/__init__.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/__init__.py	Wed Sep 07 08:58:41 2016 -0700
@@ -18,7 +18,7 @@
 
 PIECRUST_URL = 'https://bolt80.com/piecrust/'
 
-CACHE_VERSION = 28
+CACHE_VERSION = 29
 
 try:
     from piecrust.__version__ import APP_VERSION
--- a/piecrust/appconfig.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/appconfig.py	Wed Sep 07 08:58:41 2016 -0700
@@ -285,7 +285,7 @@
                 }),
             'routes': [
                 {
-                    'url': '/%path:slug%',
+                    'url': '/%slug%',
                     'source': 'theme_pages',
                     'func': 'pcurl'
                     }
@@ -337,9 +337,9 @@
             'posts_fs': DEFAULT_POSTS_FS,
             'default_page_layout': 'default',
             'default_post_layout': 'post',
-            'post_url': '/%int4:year%/%int2:month%/%int2:day%/%slug%',
-            'year_url': '/archives/%int4:year%',
-            'tag_url': '/tag/%+tag%',
+            'post_url': '/%year%/%month%/%day%/%slug%',
+            'year_url': '/archives/%year%',
+            'tag_url': '/tag/%tag%',
             'category_url': '/%category%',
             'posts_per_page': 5
             })
@@ -363,7 +363,7 @@
                     }),
                 'routes': [
                     {
-                        'url': '/%path:slug%',
+                        'url': '/%slug%',
                         'source': 'pages',
                         'func': 'pcurl'
                         }
--- a/piecrust/commands/builtin/info.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/commands/builtin/info.py	Wed Sep 07 08:58:41 2016 -0700
@@ -85,7 +85,7 @@
             logger.info("    regex: %s" % route.uri_re.pattern)
             logger.info("    function: %s(%s)" % (
                 route.func_name,
-                ', '.join(route.func_parameters)))
+                ', '.join(route.uri_params)))
 
 
 class ShowPathsCommand(ChefCommand):
--- a/piecrust/generation/base.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/generation/base.py	Wed Sep 07 08:58:41 2016 -0700
@@ -133,6 +133,9 @@
         raise Exception("Can't find source '%s' for generator '%s'." % (
             self.source_name, self.name))
 
+    def getSupportedRouteParameters(self):
+        raise NotImplementedError()
+
     def getPageFactory(self, route_metadata):
         # This will raise `PageNotFoundError` naturally if not found.
         return self.page_ref.getFactory()
--- a/piecrust/generation/blogarchives.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/generation/blogarchives.py	Wed Sep 07 08:58:41 2016 -0700
@@ -4,6 +4,7 @@
 from piecrust.data.filters import PaginationFilter, IFilterClause
 from piecrust.data.iterators import PageIterator
 from piecrust.generation.base import PageGenerator, InvalidRecordExtraKey
+from piecrust.routing import RouteParameter
 
 
 logger = logging.getLogger(__name__)
@@ -15,6 +16,9 @@
     def __init__(self, app, name, config):
         super(BlogArchivesPageGenerator, self).__init__(app, name, config)
 
+    def getSupportedRouteParameters(self):
+        return [RouteParameter('year', RouteParameter.TYPE_INT4)]
+
     def onRouteFunctionUsed(self, route, route_metadata):
         pass
 
--- a/piecrust/generation/taxonomy.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/generation/taxonomy.py	Wed Sep 07 08:58:41 2016 -0700
@@ -8,6 +8,7 @@
         PaginationFilter, SettingFilterClause,
         page_value_accessor)
 from piecrust.generation.base import PageGenerator, InvalidRecordExtraKey
+from piecrust.routing import RouteParameter
 
 
 logger = logging.getLogger(__name__)
@@ -68,6 +69,13 @@
         self.slugify_mode = _parse_slugify_mode(sm)
         self.slugifier = _Slugifier(self.taxonomy, self.slugify_mode)
 
+    def getSupportedRouteParameters(self):
+        name = self.taxonomy.term_name
+        param_type = (RouteParameter.TYPE_PATH if self.taxonomy.is_multiple
+                      else RouteParameter.TYPE_STRING)
+        return [RouteParameter(name, param_type,
+                               variadic=self.taxonomy.is_multiple)]
+
     def slugify(self, term):
         return self.slugifier.slugify(term)
 
--- a/piecrust/page.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/page.py	Wed Sep 07 08:58:41 2016 -0700
@@ -11,7 +11,6 @@
 from piecrust.configuration import (
         Configuration, ConfigurationError,
         parse_config_header)
-from piecrust.routing import IRouteMetadataProvider
 
 
 logger = logging.getLogger(__name__)
@@ -37,7 +36,7 @@
 FLAG_RAW_CACHE_VALID = 2**0
 
 
-class Page(IRouteMetadataProvider):
+class Page(object):
     def __init__(self, source, source_metadata, rel_path):
         self.source = source
         self.source_metadata = source_metadata
@@ -140,13 +139,6 @@
         if was_cache_valid:
             self._flags |= FLAG_RAW_CACHE_VALID
 
-    def getRouteMetadata(self):
-        page_dt = self.datetime
-        return {
-            'year': page_dt.year,
-            'month': page_dt.month,
-            'day': page_dt.day}
-
 
 def _parse_config_date(page_date):
     if page_date is None:
--- a/piecrust/routing.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/routing.py	Wed Sep 07 08:58:41 2016 -0700
@@ -24,19 +24,25 @@
 
 def create_route_metadata(page):
     route_metadata = copy.deepcopy(page.source_metadata)
-    route_metadata.update(page.getRouteMetadata())
     return route_metadata
 
 
-class IRouteMetadataProvider(object):
-    def getRouteMetadata(self):
-        raise NotImplementedError()
-
-
 ROUTE_TYPE_SOURCE = 0
 ROUTE_TYPE_GENERATOR = 1
 
 
+class RouteParameter(object):
+    TYPE_STRING = 0
+    TYPE_PATH = 1
+    TYPE_INT2 = 2
+    TYPE_INT4 = 3
+
+    def __init__(self, param_name, param_type=TYPE_STRING, *, variadic=False):
+        self.param_name = param_name
+        self.param_type = param_type
+        self.variadic = variadic
+
+
 class Route(object):
     """ Information about a route for a PieCrust application.
         Each route defines the "shape" of an URL and how it maps to
@@ -51,6 +57,13 @@
             raise InvalidRouteError(
                     "Both `source` and `generator` are specified.")
 
+        self.uri_pattern = cfg['url'].lstrip('/')
+
+        if self.is_source_route:
+            self.supported_params = self.source.getSupportedRouteParameters()
+        else:
+            self.supported_params = self.generator.getSupportedRouteParameters()
+
         self.pretty_urls = app.config.get('site/pretty_urls')
         self.trailing_slash = app.config.get('site/trailing_slash')
         self.show_debug_info = app.config.get('site/show_debug_info')
@@ -58,8 +71,7 @@
                 '__cache/pagination_suffix_format')
         self.uri_root = app.config.get('site/root')
 
-        uri = cfg['url']
-        self.uri_pattern = uri.lstrip('/')
+        self.uri_params = []
         self.uri_format = route_re.sub(self._uriFormatRepl, self.uri_pattern)
 
         # Get the straight-forward regex for matching this URI pattern.
@@ -85,31 +97,17 @@
         else:
             self.uri_re_no_path = None
 
-        # Determine the parameters for the route function.
         self.func_name = self._validateFuncName(cfg.get('func'))
-        self.func_parameters = []
         self.func_has_variadic_parameter = False
-        self.param_types = {}
-        variadic_param_idx = -1
-        for m in route_re.finditer(self.uri_pattern):
-            name = m.group('name')
-            self.func_parameters.append(name)
-
-            qual = m.group('qual')
-            if not qual:
-                qual = self._getBackwardCompatibleParamType(name)
-            if qual:
-                self.param_types[name] = qual
-
-            if m.group('var'):
-                self.func_has_variadic_parameter = True
-                variadic_param_idx = len(self.func_parameters) - 1
-
-        if (variadic_param_idx >= 0 and
-                variadic_param_idx != len(self.func_parameters) - 1):
-            raise Exception(
-                "Only the last route URL parameter can be variadic. "
-                "Got: %s" % self.uri_pattern)
+        for p in self.uri_params[:-1]:
+            param = self.getParameter(p)
+            if param.variadic:
+                raise Exception(
+                    "Only the last route URL parameter can be variadic. "
+                    "Got: %s" % self.uri_pattern)
+        if len(self.uri_params) > 0:
+            last_param = self.getParameter(self.uri_params[-1])
+            self.func_has_variadic_parameter = last_param.variadic
 
     @property
     def route_type(self):
@@ -136,7 +134,7 @@
             if src.name == self.source_name:
                 return src
         raise Exception("Can't find source '%s' for route '%s'." % (
-                self.source_name, self.uri))
+                self.source_name, self.uri_pattern))
 
     @cached_property
     def generator(self):
@@ -146,10 +144,23 @@
             if gen.name == self.generator_name:
                 return gen
         raise Exception("Can't find generator '%s' for route '%s'." % (
-                self.generator_name, self.uri))
+                self.generator_name, self.uri_pattern))
+
+    def hasParameter(self, name):
+        return any(lambda p: p.param_name == name, self.supported_params)
+
+    def getParameter(self, name):
+        for p in self.supported_params:
+            if p.param_name == name:
+                return p
+        raise Exception("No such supported route parameter '%s' in: %s" %
+                        (name, self.uri_pattern))
+
+    def getParameterType(self, name):
+        return self.getParameter(name).param_type
 
     def matchesMetadata(self, route_metadata):
-        return set(self.func_parameters).issubset(route_metadata.keys())
+        return set(self.uri_params).issubset(route_metadata.keys())
 
     def matchUri(self, uri, strict=False):
         if not uri.startswith(self.uri_root):
@@ -178,8 +189,10 @@
             # say, a route's pattern is `/foo/%slug%`, and we're matching an
             # URL like `/foo`.
             matched_keys = set(route_metadata.keys())
-            missing_keys = set(self.func_parameters) - matched_keys
+            missing_keys = set(self.uri_params) - matched_keys
             for k in missing_keys:
+                if self.getParameterType(k) != RouteParameter.TYPE_PATH:
+                    return None
                 route_metadata[k] = ''
 
         for k in route_metadata:
@@ -239,7 +252,7 @@
         return uri
 
     def execTemplateFunc(self, *args):
-        fixed_param_count = len(self.func_parameters)
+        fixed_param_count = len(self.uri_params)
         if self.func_has_variadic_parameter:
             fixed_param_count -= 1
 
@@ -258,7 +271,7 @@
             coerced_args = args
 
         metadata = {}
-        for arg_name, arg_val in zip(self.func_parameters, coerced_args):
+        for arg_name, arg_val in zip(self.uri_params, coerced_args):
             metadata[arg_name] = self._coerceRouteParameter(
                     arg_name, arg_val)
 
@@ -268,87 +281,62 @@
         return self.getUri(metadata)
 
     def _uriFormatRepl(self, m):
-        qual = m.group('qual')
-        name = m.group('name')
+        if m.group('qual') or m.group('var'):
+            # Print a warning only if we're not in a worker process.
+            print_warning = not self.app.config.has('baker/worker_id')
+            if print_warning:
+                logger.warning("Route '%s' specified parameter types -- "
+                               "they're not needed anymore." %
+                               self.uri_pattern)
 
-        # Backwards compatibility... this will print a warning later.
-        if qual is None:
-            if name == 'year':
-                qual = 'int4'
-            elif name in ['month', 'day']:
-                qual = 'int2'
-
-        if qual == 'int4':
-            return '%%(%s)04d' % name
-        elif qual == 'int2':
-            return '%%(%s)02d' % name
-        elif qual and qual != 'path':
-            raise Exception("Unknown route parameter type: %s" % qual)
-        return '%%(%s)s' % name
+        name = m.group('name')
+        self.uri_params.append(name)
+        try:
+            param_type = self.getParameterType(name)
+            if param_type == RouteParameter.TYPE_INT4:
+                return '%%(%s)04d' % name
+            elif param_type == RouteParameter.TYPE_INT2:
+                return '%%(%s)02d' % name
+            return '%%(%s)s' % name
+        except:
+            known = [p.name for p in self.supported_params]
+            raise Exception("Unknown route parameter '%s' for route '%s'. "
+                            "Must be one of: %s'" %
+                            (name, self.uri_pattern, known))
 
     def _uriPatternRepl(self, m):
         name = m.group('name')
-        qual = m.group('qual')
-
-        # Backwards compatibility... this will print a warning later.
-        if qual is None:
-            if name == 'year':
-                qual = 'int4'
-            elif name in ['month', 'day']:
-                qual = 'int2'
-
-        if qual == 'path' or m.group('var'):
+        param_type = self.getParameterType(name)
+        if param_type == RouteParameter.TYPE_PATH:
             return r'(?P<%s>[^\?]*)' % name
-        elif qual == 'int4':
+        elif param_type == RouteParameter.TYPE_INT4:
             return r'(?P<%s>\d{4})' % name
-        elif qual == 'int2':
+        elif param_type == RouteParameter.TYPE_INT2:
             return r'(?P<%s>\d{2})' % name
-        elif qual and qual != 'path':
-            raise Exception("Unknown route parameter type: %s" % qual)
         return r'(?P<%s>[^/\?]+)' % name
 
     def _uriNoPathRepl(self, m):
         name = m.group('name')
-        qualifier = m.group('qual')
-        if qualifier == 'path':
+        param_type = self.getParameterType(name)
+        if param_type == RouteParameter.TYPE_PATH:
             return ''
         return r'(?P<%s>[^/\?]+)' % name
 
     def _coerceRouteParameter(self, name, val):
-        param_type = self.param_types.get(name)
-        if param_type is None:
+        try:
+            param_type = self.getParameterType(name)
+        except:
+            # Unknown parameter... just leave it.
             return val
-        if param_type in ['int', 'int2', 'int4']:
+
+        if param_type in [RouteParameter.TYPE_INT2, RouteParameter.TYPE_INT4]:
             try:
                 return int(val)
             except ValueError:
                 raise Exception(
-                    "Expected route parameter '%s' to be of type "
-                    "'%s', but was: %s" %
-                    (name, param_type, val))
-        if param_type == 'path':
-            return val
-        raise Exception("Unknown route parameter type: %s" % param_type)
-
-    def _getBackwardCompatibleParamType(self, name):
-        # Print a warning only if we're not in a worker process.
-        print_warning = not self.app.config.has('baker/worker_id')
-
-        if name in ['year']:
-            if print_warning:
-                logger.warning(
-                    "Route parameter '%%%s%%' has no type qualifier. "
-                    "You probably meant '%%int4:%s%%' so we'll use that." %
-                    (name, name))
-            return 'int4'
-        if name in ['month', 'day']:
-            if print_warning:
-                logger.warning(
-                    "Route parameter '%%%s%%' has no type qualifier. "
-                    "You probably meant '%%int2:%s%%' so we'll use that." %
-                    (name, name))
-            return 'int2'
-        return None
+                    "Expected route parameter '%s' to be an integer, "
+                    "but was: %s" % (name, param_type, val))
+        return val
 
     def _validateFuncName(self, name):
         if not name:
@@ -369,12 +357,12 @@
 
     def addFunc(self, route):
         if self._arg_names is None:
-            self._arg_names = list(route.func_parameters)
+            self._arg_names = list(route.uri_params)
 
-        if route.func_parameters != self._arg_names:
+        if route.uri_params != self._arg_names:
             raise Exception("Cannot merge route function with arguments '%s' "
                             "with route function with arguments '%s'." %
-                            (route.func_parameters, self._arg_names))
+                            (route.uri_params, self._arg_names))
         self._routes.append(route)
 
     def __call__(self, *args, **kwargs):
--- a/piecrust/sources/autoconfig.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/sources/autoconfig.py	Wed Sep 07 08:58:41 2016 -0700
@@ -3,6 +3,7 @@
 import os.path
 import logging
 from piecrust.configuration import ConfigurationError
+from piecrust.routing import RouteParameter
 from piecrust.sources.base import (
         PageSource, PageFactory, InvalidFileSystemEndpointError)
 from piecrust.sources.default import (
@@ -33,6 +34,10 @@
                                      "one of: path, dirname, filename" %
                                      name)
 
+    def getSupportedRouteParameters(self):
+        return [
+            RouteParameter('slug', RouteParameter.TYPE_PATH)]
+
     def buildPageFactories(self):
         logger.debug("Scanning for pages in: %s" % self.fs_endpoint_path)
         if not os.path.isdir(self.fs_endpoint_path):
--- a/piecrust/sources/base.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/sources/base.py	Wed Sep 07 08:58:41 2016 -0700
@@ -106,6 +106,9 @@
             self._factories = list(self.buildPageFactories())
         return self._factories
 
+    def getSupportedRouteParameters(self):
+        raise NotImplementedError()
+
     def buildPageFactories(self):
         raise NotImplementedError()
 
--- a/piecrust/sources/default.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/sources/default.py	Wed Sep 07 08:58:41 2016 -0700
@@ -1,6 +1,7 @@
 import os.path
 import logging
 from piecrust import osutil
+from piecrust.routing import RouteParameter
 from piecrust.sources.base import (
         PageFactory, PageSource, InvalidFileSystemEndpointError,
         MODE_CREATING)
@@ -36,6 +37,10 @@
                 app.config.get('site/auto_formats').keys())
         self.default_auto_format = app.config.get('site/default_auto_format')
 
+    def getSupportedRouteParameters(self):
+        return [
+            RouteParameter('slug', RouteParameter.TYPE_PATH)]
+
     def buildPageFactories(self):
         logger.debug("Scanning for pages in: %s" % self.fs_endpoint_path)
         if not os.path.isdir(self.fs_endpoint_path):
--- a/piecrust/sources/posts.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/piecrust/sources/posts.py	Wed Sep 07 08:58:41 2016 -0700
@@ -4,6 +4,7 @@
 import logging
 import datetime
 from piecrust import osutil
+from piecrust.routing import RouteParameter
 from piecrust.sources.base import (
         PageSource, InvalidFileSystemEndpointError, PageFactory,
         MODE_CREATING, MODE_PARSING)
@@ -36,6 +37,13 @@
         metadata = self._parseMetadataFromPath(ref_path)
         return path, metadata
 
+    def getSupportedRouteParameters(self):
+        return [
+            RouteParameter('slug', RouteParameter.TYPE_STRING),
+            RouteParameter('day', RouteParameter.TYPE_INT2),
+            RouteParameter('month', RouteParameter.TYPE_INT2),
+            RouteParameter('year', RouteParameter.TYPE_INT4)]
+
     def buildPageFactory(self, path):
         if not path.startswith(self.fs_endpoint_path):
             raise Exception("Page path '%s' isn't inside '%s'." % (
--- a/tests/test_routing.py	Mon Sep 05 22:30:05 2016 -0700
+++ b/tests/test_routing.py	Wed Sep 07 08:58:41 2016 -0700
@@ -1,24 +1,48 @@
 import urllib.parse
+import mock
 import pytest
-from piecrust.routing import Route
+from piecrust.routing import Route, RouteParameter
+from piecrust.sources.base import PageSource
 from .mockutil import get_mock_app
 
 
+def _getMockSource(name, params):
+    route_params = []
+    for p in params:
+        if isinstance(p, tuple):
+            if p[1] == 'path':
+                t = RouteParameter.TYPE_PATH
+            elif p[1] == 'int2':
+                t = RouteParameter.TYPE_INT2
+            elif p[2] == 'int4':
+                t = RouteParameter.TYPE_INT4
+            route_params.append(RouteParameter(p[0], t))
+        else:
+            route_params.append(RouteParameter(p, RouteParameter.TYPE_STRING))
+
+    src = mock.MagicMock(spec=PageSource)
+    src.name = name
+    src.getSupportedRouteParameters = lambda: route_params
+    return src
+
+
 @pytest.mark.parametrize(
-        'config, metadata, expected',
+        'config, metadata, params, expected',
         [
             ({'url': '/%foo%'},
-                {'foo': 'bar'}, True),
+                {'foo': 'bar'}, ['foo'], True),
             ({'url': '/%foo%'},
-                {'zoo': 'zar', 'foo': 'bar'}, True),
+                {'zoo': 'zar', 'foo': 'bar'}, ['foo'], True),
             ({'url': '/%foo%'},
-                {'zoo': 'zar'}, False),
+                {'zoo': 'zar'}, ['foo'], False),
             ({'url': '/%foo%/%zoo%'},
-                {'zoo': 'zar'}, False)
+                {'zoo': 'zar'}, ['foo', 'zoo'], False)
             ])
-def test_matches_metadata(config, metadata, expected):
+def test_matches_metadata(config, metadata, params, expected):
     app = get_mock_app()
     app.config.set('site/root', '/')
+    app.sources = [_getMockSource('blah', params)]
+
     config.setdefault('source', 'blah')
     route = Route(app, config)
     m = route.matchesMetadata(metadata)
@@ -26,103 +50,128 @@
 
 
 @pytest.mark.parametrize(
-        'site_root, route_pattern, expected_func_parameters',
+        'site_root, route_pattern, params, expected_func_parameters',
         [
-            ('/', '/%foo%', ['foo']),
-            ('/', '/%path:foo%', ['foo']),
-            ('/', '/%foo%/%bar%', ['foo', 'bar']),
-            ('/', '/%foo%/%path:bar%', ['foo', 'bar']),
-            ('/something', '/%foo%', ['foo']),
-            ('/something', '/%path:foo%', ['foo']),
-            ('/something', '/%foo%/%bar%', ['foo', 'bar']),
-            ('/something', '/%foo%/%path:bar%', ['foo', 'bar']),
-            ('/~johndoe', '/%foo%', ['foo']),
-            ('/~johndoe', '/%path:foo%', ['foo']),
-            ('/~johndoe', '/%foo%/%bar%', ['foo', 'bar']),
-            ('/~johndoe', '/%foo%/%path:bar%', ['foo', 'bar'])
+            ('/', '/%foo%', ['foo'], ['foo']),
+            ('/', '/%foo%', [('foo', 'path')], ['foo']),
+            ('/', '/%foo%/%bar%', ['foo', 'bar'], ['foo', 'bar']),
+            ('/', '/%foo%/%bar%', ['foo', ('bar', 'path')], ['foo', 'bar']),
+            ('/something', '/%foo%', ['foo'], ['foo']),
+            ('/something', '/%foo%', [('foo', 'path')], ['foo']),
+            ('/something', '/%foo%/%bar%', ['foo', 'bar'], ['foo', 'bar']),
+            ('/something', '/%foo%/%bar%', ['foo', ('bar', 'path')], ['foo', 'bar']),
+            ('/~johndoe', '/%foo%', ['foo'], ['foo']),
+            ('/~johndoe', '/%foo%', [('foo', 'path')], ['foo']),
+            ('/~johndoe', '/%foo%/%bar%', ['foo', 'bar'], ['foo', 'bar']),
+            ('/~johndoe', '/%foo%/%bar%', ['foo', ('bar', 'path')], ['foo', 'bar'])
             ])
-def test_required_metadata(site_root, route_pattern,
+def test_required_metadata(site_root, route_pattern, params,
                            expected_func_parameters):
     app = get_mock_app()
     app.config.set('site/root', site_root.rstrip('/') + '/')
+    app.sources = [_getMockSource('blah', params)]
+
     config = {'url': route_pattern, 'source': 'blah'}
     route = Route(app, config)
-    assert route.func_parameters == expected_func_parameters
+    assert route.uri_params == expected_func_parameters
 
 
 @pytest.mark.parametrize(
-        'site_root, config, uri, expected_match',
+        'site_root, config, params, uri, expected_match',
         [
             ('/', {'url': '/%foo%'},
+                ['foo'],
                 'something',
                 {'foo': 'something'}),
             ('/', {'url': '/%foo%'},
+                ['foo'],
                 'something/other',
                 None),
-            ('/', {'url': '/%path:foo%'},
+            ('/', {'url': '/%foo%'},
+                [('foo', 'path')],
                 'something/other',
                 {'foo': 'something/other'}),
-            ('/', {'url': '/%path:foo%'},
+            ('/', {'url': '/%foo%'},
+                [('foo', 'path')],
                 '',
                 {'foo': ''}),
-            ('/', {'url': '/prefix/%path:foo%'},
+            ('/', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix/something/other',
                 {'foo': 'something/other'}),
-            ('/', {'url': '/prefix/%path:foo%'},
+            ('/', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix/',
                 {'foo': ''}),
-            ('/', {'url': '/prefix/%path:foo%'},
+            ('/', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix',
                 {'foo': ''}),
 
             ('/blah', {'url': '/%foo%'},
+                ['foo'],
                 'something',
                 {'foo': 'something'}),
             ('/blah', {'url': '/%foo%'},
+                ['foo'],
                 'something/other',
                 None),
-            ('/blah', {'url': '/%path:foo%'},
+            ('/blah', {'url': '/%foo%'},
+                [('foo', 'path')],
                 'something/other',
                 {'foo': 'something/other'}),
-            ('/blah', {'url': '/%path:foo%'},
+            ('/blah', {'url': '/%foo%'},
+                [('foo', 'path')],
                 '',
                 {'foo': ''}),
-            ('/blah', {'url': '/prefix/%path:foo%'},
+            ('/blah', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix/something/other',
                 {'foo': 'something/other'}),
-            ('/blah', {'url': '/prefix/%path:foo%'},
+            ('/blah', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix/',
                 {'foo': ''}),
-            ('/blah', {'url': '/prefix/%path:foo%'},
+            ('/blah', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix',
                 {'foo': ''}),
 
             ('/~johndoe', {'url': '/%foo%'},
+                ['foo'],
                 'something',
                 {'foo': 'something'}),
             ('/~johndoe', {'url': '/%foo%'},
+                ['foo'],
                 'something/other',
                 None),
-            ('/~johndoe', {'url': '/%path:foo%'},
+            ('/~johndoe', {'url': '/%foo%'},
+                [('foo', 'path')],
                 'something/other',
                 {'foo': 'something/other'}),
-            ('/~johndoe', {'url': '/%path:foo%'},
+            ('/~johndoe', {'url': '/%foo%'},
+                [('foo', 'path')],
                 '',
                 {'foo': ''}),
-            ('/~johndoe', {'url': '/prefix/%path:foo%'},
+            ('/~johndoe', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix/something/other',
                 {'foo': 'something/other'}),
-            ('/~johndoe', {'url': '/prefix/%path:foo%'},
+            ('/~johndoe', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix/',
                 {'foo': ''}),
-            ('/~johndoe', {'url': '/prefix/%path:foo%'},
+            ('/~johndoe', {'url': '/prefix/%foo%'},
+                [('foo', 'path')],
                 'prefix',
                 {'foo': ''}),
             ])
-def test_match_uri(site_root, config, uri, expected_match):
+def test_match_uri(site_root, config, params, uri, expected_match):
     site_root = site_root.rstrip('/') + '/'
     app = get_mock_app()
     app.config.set('site/root', urllib.parse.quote(site_root))
+    app.sources = [_getMockSource('blah', params)]
+
     config.setdefault('source', 'blah')
     route = Route(app, config)
     assert route.uri_pattern == config['url'].lstrip('/')
@@ -133,13 +182,17 @@
 @pytest.mark.parametrize(
         'site_root',
         [
-            ('/'), ('/whatever'), ('/~johndoe')
+            ('/'),
+            ('/whatever'),
+            ('/~johndoe')
             ])
 def test_match_uri_requires_absolute_uri(site_root):
     with pytest.raises(Exception):
         app = get_mock_app()
         app.config.set('site/root', site_root.rstrip('/') + '/')
-        config = {'url': '/%path:slug%', 'source': 'blah'}
+        app.sources = [_getMockSource('blah', [('slug', 'path')])]
+
+        config = {'url': '/%slug%', 'source': 'blah'}
         route = Route(app, config)
         route.matchUri('notabsuri')
 
@@ -181,8 +234,9 @@
         app.config.set('site/pretty_urls', pretty)
         app.config.set('site/trailing_slash', False)
         app.config.set('__cache/pagination_suffix_format', '/%(num)d')
+        app.sources = [_getMockSource('blah', [('slug', 'path')])]
 
-        config = {'url': '/%path:slug%', 'source': 'blah'}
+        config = {'url': '/%slug%', 'source': 'blah'}
         route = Route(app, config)
         uri = route.getUri({'slug': slug}, sub_num=page_num)
         assert uri == (urllib.parse.quote(root) + expected)