summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2010-07-15 17:41:05 +0000
committerChris McDonough <chrism@agendaless.com>2010-07-15 17:41:05 +0000
commit208ee5a8d6409bcdce361009dee6a2e335de1679 (patch)
tree3e47bda8becf94ae96d4b705c410fd8a2a925ffb
parent6ef5b21bffe62c3ad6d276b36ba4229f681128ba (diff)
downloadpyramid-208ee5a8d6409bcdce361009dee6a2e335de1679.tar.gz
pyramid-208ee5a8d6409bcdce361009dee6a2e335de1679.tar.bz2
pyramid-208ee5a8d6409bcdce361009dee6a2e335de1679.zip
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``.
-rw-r--r--CHANGES.txt37
-rw-r--r--docs/narr/views.rst27
-rw-r--r--docs/zcml/view.rst24
-rw-r--r--repoze/bfg/configuration.py81
-rw-r--r--repoze/bfg/tests/test_configuration.py90
-rw-r--r--repoze/bfg/zcml.py9
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=<regex>"',
+ 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,