From 208ee5a8d6409bcdce361009dee6a2e335de1679 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 15 Jul 2010 17:41:05 +0000 Subject: Features -------- - New view predicate: match_val. The ``match_val`` value represents the presence of a value in the structure added to the request named ``matchdict`` during URL dispatch representing the match values from the route pattern (e.g. if the route pattern has ``:foo`` in it, and the route matches, a key will exist in the matchdict named ``foo``). Like all other view predicates, this feature is exposed via the ``bfg_view`` API, the Configurator ``add_view`` API, and the ZCML ``view`` directive. Documentation ------------- - API documentation for the ``add_view`` method of the configurator changed to include ``match_val``. - ZCML documentation for ``view`` ZCML directive changed to include ``match_val``. - The ``Views`` narrative chapter now contains a description of the ``match_val`` predicate. Bug Fixes --------- - The ``header`` predicate (when used as either a view predicate or a route predicate) had a problem when specified with a name/regex pair. When the header did not exist in the headers dictionary, the regex match could be fed ``None``, causing it to throw a ``TypeError: expected string or buffer`` exception. Now, the predicate returns False as intended. Internal -------- - Remove ``repoze.bfg.configuration.isclass`` function in favor of using ``inspect.isclass``. --- CHANGES.txt | 37 ++++++++++++++ docs/narr/views.rst | 27 ++++++++++ docs/zcml/view.rst | 24 +++++++++ repoze/bfg/configuration.py | 81 ++++++++++++++++++++++++------ repoze/bfg/tests/test_configuration.py | 90 ++++++++++++++++++++++++++++++++++ repoze/bfg/zcml.py | 9 +++- 6 files changed, 252 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 86ae92d8b..6acb3cde6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,40 @@ +Next release +============ + +Features +-------- + +- New view predicate: match_val. The ``match_val`` value represents + the presence of a value in the structure added to the request named + ``matchdict`` during URL dispatch representing the match values from + the route pattern (e.g. if the route pattern has ``:foo`` in it, and + the route matches, a key will exist in the matchdict named ``foo``). + Like all other view predicates, this feature is exposed via the + ``bfg_view`` API, the Configurator ``add_view`` API, and the ZCML + ``view`` directive. + +Documentation +------------- + +- API documentation for the ``add_view`` method of the configurator + changed to include ``match_val``. + +- ZCML documentation for ``view`` ZCML directive changed to include + ``match_val``. + +- The ``Views`` narrative chapter now contains a description of the + ``match_val`` predicate. + +Bug Fixes +--------- + +- The ``header`` predicate (when used as either a view predicate or a + route predicate) had a problem when specified with a name/regex + pair. When the header did not exist in the headers dictionary, the + regex match could be fed ``None``, causing it to throw a + ``TypeError: expected string or buffer`` exception. Now, the + predicate returns False as intended. + 1.3a5 (2010-07-14) ================== diff --git a/docs/narr/views.rst b/docs/narr/views.rst index eebaa63de..9b7d6b2dd 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -1358,6 +1358,33 @@ Predicate Arguments taken into consideration when deciding whether or not to invoke the associated view callable. +``match_val`` + + This value represents :term:`URL dispatch` ``request.matchdict`` + name or a matchdict name/value pair. ``request.matchdict`` is a + dictionary representing the match values from the route pattern + (e.g. if the route pattern has ``:foo`` in it, and the route + matches, a key will exist in the matchdict named ``foo``). + + If ``match_val`` is specified, it must be the name of a key presumed + to be present in the ``matchdict`` or a ``key:regex`` pair. + + If ``match_val`` is specified without a colon in it + (e.g. ``action``), the predicate will return true if the + ``matchdict`` has a key which exists with any value. + + When the ``match_val`` contains a ``:`` (colon), it will be split at + the first colon; the left hand of that split will considered a key + and the right hand a regular expression. For example, in + ``action:\w+\.html``, the key part is ``action``, the regex part is + ``\w+\.html``. The resulting predicate will only be true if the + matchdict contains a key that matches the key part *and* the + matchdict value is matched by the regex part. + + If ``match_val`` is not specified, the composition, presence or + absence of values in the matchdict is not taken into consideration + when deciding whether or not to invoke the associated view callable. + ``custom_predicates`` If ``custom_predicates`` is specified, it must be a sequence of references to custom predicate callables. Use custom predicates diff --git a/docs/zcml/view.rst b/docs/zcml/view.rst index d33a9a9a5..9fe63738b 100644 --- a/docs/zcml/view.rst +++ b/docs/zcml/view.rst @@ -224,6 +224,30 @@ Predicate Attributes .. note:: This feature is new as of :mod:`repoze.bfg` 1.1. +``match_val`` + + The ``match_val`` value represents the presence of a value in the + :term:`URL dispatch` structure added to the request named + ``matchdict``. ``matchdict`` represents the match values from the + route pattern (e.g. if the route pattern has ``:foo`` in it, and the + route matches, a key will exist in the matchdict named ``foo``). If + the value does not contain a colon, the entire value will be + considered to be the name of a matchdict key (e.g. ``action``). If + the value does contain a ``:`` (colon), it will be considered a + name/value pair (e.g. ``action:generate.html`` or + ``action:\w+.html``). The right hand side following the colon + should be a regular expression. + + If the value does not contain a colon, the key specified by the name + must be present in the URL dispatch matchdict for this predicate to + be true; the value of the key is ignored. If the value does contain + a colon, the name implied by the right hand must be present in the + matchdict *and* the regular expression specified on the right hand + side of the colon must match the value for the name in the matchdict + for this predicate to be true. + + .. note:: This feature is new as of :mod:`repoze.bfg` 1.3. + ``custom_predicates`` This value should be a sequence of references to custom predicate callables (e.g. ``dotted.name.one dotted.name.two``, if used in diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py index 1e760a064..4073719b4 100644 --- a/repoze/bfg/configuration.py +++ b/repoze/bfg/configuration.py @@ -2,7 +2,6 @@ import os import re import sys import threading -import types import inspect from webob import Response @@ -526,8 +525,8 @@ class Configurator(object): request_type=None, route_name=None, request_method=None, request_param=None, containment=None, attr=None, renderer=None, wrapper=None, xhr=False, accept=None, - header=None, path_info=None, custom_predicates=(), - context=None, _info=u''): + header=None, path_info=None, match_val=None, + custom_predicates=(), context=None, _info=u''): """ Add a :term:`view configuration` to the current configuration state. Arguments to ``add_view`` are broken down below into *predicate* arguments and *non-predicate* @@ -746,6 +745,33 @@ class Configurator(object): variable. If the regex matches, this predicate will be ``True``. + + match_val + + The ``match_val`` value represents the presence of a value + in the :term:`URL dispatch` structure added to the request + named ``matchdict``. ``matchdict`` represents the match + values from the route pattern (e.g. if the route pattern has + ``:foo`` in it, and the route matches, a key will exist in + the matchdict named ``foo``). If the value does not contain + a colon, the entire value will be considered to be the name + of a matchdict key (e.g. ``action``). If the value does + contain a ``:`` (colon), it will be considered a name/value + pair (e.g. ``action:generate.html`` or ``action:\w+.html``). + The right hand side following the colon should be a regular + expression. + + If the value does not contain a colon, the key specified by + the name must be present in the URL dispatch matchdict for + this predicate to be true; the value of the key is ignored. + If the value does contain a colon, the name implied by the + right hand must be present in the matchdict *and* the + regular expression specified on the right hand side of the + colon must match the value for the name in the matchdict for + this predicate to be true. + + .. note:: This feature is new as of :mod:`repoze.bfg` 1.3. + custom_predicates This value should be a sequence of references to custom @@ -796,18 +822,18 @@ class Configurator(object): request_method=request_method, request_param=request_param, containment=containment, attr=attr, renderer=renderer, wrapper=wrapper, xhr=xhr, accept=accept, - header=header, path_info=path_info, custom_predicates=(), - context=context, _info=u'' + header=header, path_info=path_info, match_val=match_val, + custom_predicates=(), context=context, _info=u'' ) view_info = deferred_views.setdefault(route_name, []) view_info.append(info) return - order, predicates, phash = _make_predicates( - xhr=xhr, request_method=request_method, path_info=path_info, + order, predicates, phash = _make_predicates(xhr=xhr, + request_method=request_method, path_info=path_info, request_param=request_param, header=header, accept=accept, containment=containment, request_type=request_type, - custom=custom_predicates) + view_match_val=match_val, custom=custom_predicates) derived_view = self._derive_view(view, permission, predicates, attr, renderer, wrapper, name, accept, order, @@ -1655,7 +1681,8 @@ class Configurator(object): def _make_predicates(xhr=None, request_method=None, path_info=None, request_param=None, header=None, accept=None, - containment=None, request_type=None, custom=()): + containment=None, request_type=None, + view_match_val=None, custom=()): # PREDICATES # ---------- @@ -1708,7 +1735,7 @@ def _make_predicates(xhr=None, request_method=None, path_info=None, if xhr: def xhr_predicate(context, request): return request.is_xhr - weights.append(1 << 0) + weights.append(1 << 1) predicates.append(xhr_predicate) h.update('xhr:%r' % bool(xhr)) @@ -1755,6 +1782,8 @@ def _make_predicates(xhr=None, request_method=None, path_info=None, if header_val is None: return header_name in request.headers val = request.headers.get(header_name) + if val is None: + return False return header_val.match(val) is not None weights.append(1 << 5) predicates.append(header_predicate) @@ -1781,11 +1810,34 @@ def _make_predicates(xhr=None, request_method=None, path_info=None, predicates.append(request_type_predicate) h.update('request_type:%r' % id(request_type)) + if view_match_val is not None: + match_name = view_match_val + match_val = None + if ':' in match_name: + match_name, match_val = match_name.split(':', 1) + try: + match_val = re.compile(match_val) + except re.error, why: + raise ConfigurationError(why[0]) + def view_match_val_predicate(context, request): + matchdict = getattr(request, 'matchdict', None) + if matchdict is None: + return False + if match_val is None: + return match_name in matchdict + val = matchdict.get(match_name) + if val is None: + return False + return match_val.match(val) is not None + weights.append(1 << 9) + predicates.append(view_match_val_predicate) + h.update('view_match_val:%r=%r' % (match_name, match_val)) + if custom: for num, predicate in enumerate(custom): predicates.append(predicate) h.update('custom%s:%r' % (num, id(predicate))) - weights.append(1 << 9) + weights.append(1 << 10) score = 0 for bit in weights: @@ -2151,11 +2203,10 @@ def _attr_wrap(view, accept, order, phash): decorate_view(attr_view, view) return attr_view -def isclass(o): - return isinstance(o, (type, types.ClassType)) - def isexception(o): - return isinstance(o, Exception) or isclass(o) and issubclass(o, Exception) + return isinstance(o, Exception) or ( + inspect.isclass(o) and issubclass(o, Exception) + ) # note that ``options`` is a b/w compat alias for ``settings`` and # ``Configurator`` is a testing dep inj diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py index 4762ed416..4df7af1c2 100644 --- a/repoze/bfg/tests/test_configuration.py +++ b/repoze/bfg/tests/test_configuration.py @@ -1334,6 +1334,16 @@ class ConfiguratorTests(unittest.TestCase): request.headers = {'Host':'abc'} self._assertNotFound(wrapper, None, request) + def test_add_view_with_header_val_missing(self): + from repoze.bfg.exceptions import NotFound + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, header=r'Host:\d') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.headers = {'NoHost':'1'} + self.assertRaises(NotFound, wrapper, None, request) + def test_add_view_with_accept_match(self): view = lambda *arg: 'OK' config = self._makeOne() @@ -1395,6 +1405,68 @@ class ConfiguratorTests(unittest.TestCase): request.path_info = '/' self._assertNotFound(wrapper, None, request) + def test_add_view_with_match_val_badregex(self): + from repoze.bfg.exceptions import ConfigurationError + view = lambda *arg: 'OK' + config = self._makeOne() + self.assertRaises(ConfigurationError, + config.add_view, view=view, match_val='action:a\\') + + def test_add_view_with_match_val_no_matchdict(self): + from repoze.bfg.exceptions import NotFound + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, match_val='action') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + self.assertRaises(NotFound, wrapper, None, request) + + def test_add_view_with_match_val_noval_match(self): + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, match_val='action') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.matchdict = {'action':'whatever'} + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_match_val_noval_nomatch(self): + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, match_val='action') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.matchdict = {'notaction':'whatever'} + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_match_val_val_match(self): + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, match_val='action:\d') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.matchdict = {'action':'1'} + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_match_val_val_nomatch(self): + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, match_val=r'action:\d') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.matchdict = {'action':'abc'} + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_match_val_val_missing(self): + from repoze.bfg.exceptions import NotFound + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, match_val=r'action:\d') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.matchdict = {'notaction':'1'} + self.assertRaises(NotFound, wrapper, None, request) + def test_add_view_with_custom_predicates_match(self): view = lambda *arg: 'OK' config = self._makeOne() @@ -2914,6 +2986,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', + view_match_val='view_match_val', custom=('a',) ) order2, _, _ = self._callFUT( @@ -2925,6 +2998,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', + view_match_val='view_match_val', custom=('a',) ) order3, _, _ = self._callFUT( @@ -2936,6 +3010,7 @@ class Test__make_predicates(unittest.TestCase): accept='accept', containment='containment', request_type='request_type', + view_match_val='view_match_val', ) order4, _, _ = self._callFUT( xhr='xhr', @@ -2945,6 +3020,7 @@ class Test__make_predicates(unittest.TestCase): header='header', accept='accept', containment='containment', + request_type='request_type', ) order5, _, _ = self._callFUT( xhr='xhr', @@ -2953,6 +3029,7 @@ class Test__make_predicates(unittest.TestCase): request_param='param', header='header', accept='accept', + containment='containment', ) order6, _, _ = self._callFUT( xhr='xhr', @@ -2960,26 +3037,34 @@ class Test__make_predicates(unittest.TestCase): path_info='path_info', request_param='param', header='header', + accept='accept', ) order7, _, _ = self._callFUT( xhr='xhr', request_method='request_method', path_info='path_info', request_param='param', + header='header', ) order8, _, _ = self._callFUT( xhr='xhr', request_method='request_method', path_info='path_info', + request_param='param', ) order9, _, _ = self._callFUT( xhr='xhr', request_method='request_method', + path_info='path_info', ) order10, _, _ = self._callFUT( xhr='xhr', + request_method='request_method', ) order11, _, _ = self._callFUT( + xhr='xhr', + ) + order12, _, _ = self._callFUT( ) self.assertEqual(order1, order2) self.failUnless(order3 > order2) @@ -2991,6 +3076,7 @@ class Test__make_predicates(unittest.TestCase): self.failUnless(order9 > order8) self.failUnless(order10 > order9) self.failUnless(order11 > order10) + self.failUnless(order12 > order11) def test_ordering_importance_of_predicates(self): order1, _, _ = self._callFUT( @@ -3018,6 +3104,9 @@ class Test__make_predicates(unittest.TestCase): request_type='request_type', ) order9, _, _ = self._callFUT( + view_match_val='view_match_val', + ) + order10, _, _ = self._callFUT( custom=('a',), ) self.failUnless(order1 > order2) @@ -3028,6 +3117,7 @@ class Test__make_predicates(unittest.TestCase): self.failUnless(order6 > order7) self.failUnless(order7 > order8) self.failUnless(order8 > order9) + self.failUnless(order9 > order10) def test_ordering_importance_and_number(self): order1, _, _ = self._callFUT( diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py index d55dd1d1d..b8ca9eb21 100644 --- a/repoze/bfg/zcml.py +++ b/repoze/bfg/zcml.py @@ -141,6 +141,11 @@ class IViewDirective(Interface): description=(u'Accepts a regular expression.'), required = False) + match_val = TextLine( + title=u'Matchdict name/value pair in the form "name="', + description=u'Regular expression matching for matchdict values', + required = False) + custom_predicates = Tokens( title=u"One or more custom dotted names to custom predicate callables", description=(u"A list of dotted name references to callables that " @@ -168,6 +173,7 @@ def view( accept=None, header=None, path_info=None, + match_val=None, custom_predicates=(), context=None, cacheable=True, # not used, here for b/w compat < 0.8 @@ -199,7 +205,8 @@ def view( request_method=request_method, request_param=request_param, containment=containment, attr=attr, renderer=renderer, wrapper=wrapper, xhr=xhr, accept=accept, header=header, - path_info=path_info, custom_predicates=custom_predicates, + path_info=path_info, match_val=match_val, + custom_predicates=custom_predicates, _info=_context.info) discriminator = ['view', context, name, request_type, IView, containment, -- cgit v1.2.3