summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Bangert <ben@groovie.org>2010-11-18 14:13:02 -0800
committerBen Bangert <ben@groovie.org>2010-11-18 14:13:02 -0800
commite84116c068f45c68752f89062fa545dd40acd63f (patch)
treeddbe69019eb89fa788eb51b4878605082b0980eb
parent647815ef5fb2ca795b2d4ceb8f6740acefb7c695 (diff)
downloadpyramid-e84116c068f45c68752f89062fa545dd40acd63f.tar.gz
pyramid-e84116c068f45c68752f89062fa545dd40acd63f.tar.bz2
pyramid-e84116c068f45c68752f89062fa545dd40acd63f.zip
- 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 ``/``.
-rw-r--r--CHANGES.txt5
-rw-r--r--docs/narr/urldispatch.rst39
-rw-r--r--pyramid/configuration.py12
-rw-r--r--pyramid/tests/test_urldispatch.py13
-rw-r--r--pyramid/urldispatch.py20
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: