From e84116c068f45c68752f89062fa545dd40acd63f Mon Sep 17 00:00:00 2001 From: Ben Bangert Date: Thu, 18 Nov 2010 14:13:02 -0800 Subject: - URL Dispatch now allows for replacement markers to be located anywhere in the pattern, instead of immediately following a ``/``. - Added ``marker_pattern`` option to ``add_route`` to supply a dict of regular expressions to be used for markers in the pattern instead of the default regular expression that matched everything except a ``/``. --- CHANGES.txt | 5 +++++ docs/narr/urldispatch.rst | 39 +++++++++++++++++++++++++++++---------- pyramid/configuration.py | 12 ++++++++++-- pyramid/tests/test_urldispatch.py | 13 ++++++++++--- pyramid/urldispatch.py | 20 +++++++++++--------- 5 files changed, 65 insertions(+), 24 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 604f28cf4..b418565fa 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,11 @@ Next release Features -------- +- URL Dispatch now allows for replacement markers to be located anywhere + in the pattern, instead of immediately following a ``/``. +- Added ``marker_pattern`` option to ``add_route`` to supply a dict of + regular expressions to be used for markers in the pattern instead of the + default regular expression that matched everything except a ``/``. - Add a ``pyramid.url.route_path`` API, allowing folks to generate relative URLs. Calling ``route_path`` is the same as calling ``pyramid.url.route_url`` with the argument ``_app_url`` equal to the empty diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 4442be355..0170b36e2 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -208,16 +208,16 @@ and: /:foo/bar/baz -A patttern segment (an individual item between ``/`` characters in the -pattern) may either be a literal string (e.g. ``foo``) *or* it may be -a segment replacement marker (e.g. ``:foo``) or a certain combination -of both. +A pattern segment (an individual item between ``/`` characters in the pattern) +may either be a literal string (e.g. ``foo``) *or* it may be a replacement +marker (e.g. ``:foo``) or a certain combination of both. A replacement marker +does not need to be preceded by a ``/`` character. -A segment replacement marker is in the format ``:name``, where this -means "accept any characters up to the next nonalphaunumeric character +A replacement marker is in the format ``:name``, where this +means "accept any characters up to the next non-alphanumeric character and use this as the ``name`` matchdict value." For example, the following pattern defines one literal segment ("foo") and two dynamic -segments ("baz", and "bar"): +replacement markers ("baz", and "bar"): .. code-block:: text @@ -252,9 +252,21 @@ literal path ``/foo/biz`` will not match, because it does not contain a literal ``.html`` at the end of the segment represented by ``:name.html`` (it only contains ``biz``, not ``biz.html``). -This does not mean, however, that you can use two segment replacement -markers in the same segment. For instance, ``/:foo:bar`` is a -nonsensical route pattern. It will never match anything. +To capture both segments, two replacement markers can be used: + +.. code-block:: text + + foo/:name.:ext + +The literal path ``/foo/biz.html`` will match the above route pattern, and the +match result will be ``{'name': 'biz', 'ext': 'html'}``. This occurs because +the replacement marker ``:name`` has a literal part of ``.`` between the other +replacement marker ``:ext``. + +It is possible to use two replacement markers without any literal characters +between them, for instance ``/:foo:bar``. This would be a nonsensical pattern +without specifying any ``pattern_regexes`` to restrict valid values of each +replacement marker. Segments must contain at least one character in order to match a segment replacement marker. For example, for the URL ``/abc/``: @@ -471,6 +483,13 @@ represent neither predicates nor view configuration information. as ``path``. ``path`` continues to work as an alias for ``pattern``. +``marker_pattern`` + A dict of regular expression replacements for replacement markers in the + pattern to use when generating the complete regular expression used to + match the route. By default, every replacement marker in the pattern is + replaced with the regular expression ``[^/]+``. Values in this dict will + be used instead if present. + ``xhr`` This value should be either ``True`` or ``False``. If this value is specified and is ``True``, the :term:`request` must possess an diff --git a/pyramid/configuration.py b/pyramid/configuration.py index 3f959aabf..41b774e65 100644 --- a/pyramid/configuration.py +++ b/pyramid/configuration.py @@ -1188,6 +1188,7 @@ class Configurator(object): def add_route(self, name, pattern=None, + marker_pattern=None, view=None, view_for=None, permission=None, @@ -1306,7 +1307,13 @@ class Configurator(object): to this function will be used to represent the pattern value if the ``pattern`` argument is ``None``. If both ``path`` and ``pattern`` are passed, ``pattern`` wins. - + + marker_pattern + + A dict of regular expression's that will be used in the place + of the default ``[^/]+`` regular expression for all replacement + markers in the route pattern. + xhr This value should be either ``True`` or ``False``. If this @@ -1529,7 +1536,8 @@ class Configurator(object): raise ConfigurationError('"pattern" argument may not be None') return mapper.connect(name, pattern, factory, predicates=predicates, - pregenerator=pregenerator) + pregenerator=pregenerator, + marker_pattern=marker_pattern) def get_routes_mapper(self): """ Return the :term:`routes mapper` object associated with diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py index 799f4986f..f77ebbbb4 100644 --- a/pyramid/tests/test_urldispatch.py +++ b/pyramid/tests/test_urldispatch.py @@ -218,9 +218,9 @@ class RoutesMapperTests(unittest.TestCase): self.assertEqual(mapper.generate('abc', {}), 123) class TestCompileRoute(unittest.TestCase): - def _callFUT(self, pattern): + def _callFUT(self, pattern, marker_pattern=None): from pyramid.urldispatch import _compile_route - return _compile_route(pattern) + return _compile_route(pattern, marker_pattern) def test_no_star(self): matcher, generator = self._callFUT('/foo/:baz/biz/:buz/bar') @@ -251,6 +251,14 @@ class TestCompileRoute(unittest.TestCase): from pyramid.exceptions import URLDecodeError matcher, generator = self._callFUT('/:foo') self.assertRaises(URLDecodeError, matcher, '/%FF%FE%8B%00') + + def test_custom_regex(self): + matcher, generator = self._callFUT('foo/:baz/biz/:buz.:bar', + {'buz': '[^/\.]+'}) + self.assertEqual(matcher('/foo/baz/biz/buz.bar'), + {'baz':'baz', 'buz':'buz', 'bar':'bar'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2, 'bar': 'html'}), '/foo/1/biz/2.html') class TestCompileRouteMatchFunctional(unittest.TestCase): def matches(self, pattern, path, expected): @@ -271,7 +279,6 @@ class TestCompileRouteMatchFunctional(unittest.TestCase): self.matches('/:x', '', None) self.matches('/:x', '/', None) self.matches('/abc/:def', '/abc/', None) - self.matches('/abc/:def:baz', '/abc/bleep', None) # bad pattern self.matches('', '/', {}) self.matches('/', '/', {}) self.matches('/:x', '/a', {'x':'a'}) diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py index aa0bddfe9..06ec647e5 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -17,10 +17,10 @@ _marker = object() class Route(object): implements(IRoute) def __init__(self, name, pattern, factory=None, predicates=(), - pregenerator=None): + pregenerator=None, marker_pattern=None): self.pattern = pattern self.path = pattern # indefinite b/w compat, not in interface - self.match, self.generate = _compile_route(pattern) + self.match, self.generate = _compile_route(pattern, marker_pattern) self.name = name self.factory = factory self.predicates = predicates @@ -42,11 +42,12 @@ class RoutesMapper(object): return self.routes.get(name) def connect(self, name, pattern, factory=None, predicates=(), - pregenerator=None): + pregenerator=None, marker_pattern=None): if name in self.routes: oldroute = self.routes[name] self.routelist.remove(oldroute) - route = Route(name, pattern, factory, predicates, pregenerator) + route = Route(name, pattern, factory, predicates, pregenerator, + marker_pattern) self.routelist.append(route) self.routes[name] = route return route @@ -74,8 +75,9 @@ class RoutesMapper(object): return {'route':None, 'match':None} # stolen from bobo and modified -route_re = re.compile(r'(/:[a-zA-Z]\w*)') -def _compile_route(route): +route_re = re.compile(r'(:[a-zA-Z]\w*)') +def _compile_route(route, marker_pattern=None): + marker_pattern = marker_pattern or {} if not route.startswith('/'): route = '/' + route star = None @@ -91,9 +93,9 @@ def _compile_route(route): gen.append(prefix) while pat: name = pat.pop() - name = name[2:] - gen.append('/%%(%s)s' % name) - name = '/(?P<%s>[^/]+)' % name + name = name[1:] + gen.append('%%(%s)s' % name) + name = '(?P<%s>%s)' % (name, marker_pattern.get(name, '[^/]+')) rpat.append(name) s = pat.pop() if s: -- cgit v1.2.3