Ben Bangert
2010-11-18 e84116c068f45c68752f89062fa545dd40acd63f
- 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 ``/``.
5 files modified
89 ■■■■ changed files
CHANGES.txt 5 ●●●●● patch | view | raw | blame | history
docs/narr/urldispatch.rst 39 ●●●● patch | view | raw | blame | history
pyramid/configuration.py 12 ●●●● patch | view | raw | blame | history
pyramid/tests/test_urldispatch.py 13 ●●●● patch | view | raw | blame | history
pyramid/urldispatch.py 20 ●●●●● patch | view | raw | blame | history
CHANGES.txt
@@ -4,6 +4,11 @@
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
docs/narr/urldispatch.rst
@@ -208,16 +208,16 @@
   /: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 @@
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 @@
     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
pyramid/configuration.py
@@ -1188,6 +1188,7 @@
    def add_route(self,
                  name,
                  pattern=None,
                  marker_pattern=None,
                  view=None,
                  view_for=None,
                  permission=None,
@@ -1306,7 +1307,13 @@
             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 @@
            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
pyramid/tests/test_urldispatch.py
@@ -218,9 +218,9 @@
        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 @@
        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 @@
        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'})
pyramid/urldispatch.py
@@ -17,10 +17,10 @@
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 @@
        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 @@
        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 @@
        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: