diff options
| -rw-r--r-- | CHANGES.txt | 36 | ||||
| -rw-r--r-- | docs/narr/viewconfig.rst | 11 | ||||
| -rw-r--r-- | docs/whatsnew-1.4.rst | 10 | ||||
| -rw-r--r-- | pyramid/chameleon_zpt.py | 10 | ||||
| -rw-r--r-- | pyramid/config/predicates.py | 49 | ||||
| -rw-r--r-- | pyramid/config/testing.py | 26 | ||||
| -rw-r--r-- | pyramid/config/views.py | 9 | ||||
| -rw-r--r-- | pyramid/renderers.py | 31 | ||||
| -rw-r--r-- | pyramid/session.py | 5 | ||||
| -rw-r--r-- | pyramid/testing.py | 15 | ||||
| -rw-r--r-- | pyramid/tests/fixtures/withmacro.pt | 1 | ||||
| -rw-r--r-- | pyramid/tests/test_chameleon_zpt.py | 9 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_predicates.py | 86 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_testing.py | 24 | ||||
| -rw-r--r-- | pyramid/tests/test_renderers.py | 7 |
15 files changed, 263 insertions, 66 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index e40401528..95375e5ba 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,9 +1,19 @@ Next release ============ +Features +-------- + +- Allow multiple values to be specified to the ``request_param`` view/route + predicate as a sequence. Previously only a single string value was allowed. + See https://github.com/Pylons/pyramid/pull/705 + Bug Fixes --------- +- The match_param predicate's text method was fixed to sort its values. + Part of https://github.com/Pylons/pyramid/pull/705 + - 1.4a ``pyramid.scripting.prepare`` behaved differently than 1.3 series function of same name. In particular, if passed a request, it would not set the ``registry`` attribute of the request like 1.3 did. A symptom @@ -14,6 +24,32 @@ Bug Fixes 1.4 too (a registry is attached to a request passed to bootstrap or prepare). +- When registering a view configuration that named a Chameleon ZPT renderer + with a macro name in it (e.g. ``renderer='some/template#somemacro.pt``) as + well as a view configuration without a macro name it it that pointed to the + same template (e.g. ``renderer='some/template.pt'), internal caching could + confuse the two, and your code might have rendered one instead of the + other. + +Features +-------- + +- The Configurator ``testing_securitypolicy`` method now returns the policy + object it creates. + +- The Configurator ``testing_securitypolicy`` method accepts two new + arguments: ``remember_result`` and ``forget_result``. If supplied, these + values influence the result of the policy's ``remember`` and ``forget`` + methods, respectively. + +- The DummySecurityPolicy created by ``testing_securitypolicy`` now sets a + ``forgotten`` value on the policy (the value ``True``) when its ``forget`` + method is called. + +- The DummySecurityPolicy created by ``testing_securitypolicy`` now sets a + ``remembered`` value on the policy, which is the value of the ``principal`` + argument it's called with when its ``remember`` method is called. + 1.4a2 (2012-09-27) ================== diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index f65435cc6..3c7897969 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -290,12 +290,13 @@ configured view. of the ``REQUEST_METHOD`` of the :term:`WSGI` environment. ``request_param`` - This value can be any string. A view declaration with this argument - ensures that the view will only be called when the :term:`request` has a - key in the ``request.params`` dictionary (an HTTP ``GET`` or ``POST`` - variable) that has a name which matches the supplied value. + This value can be any string or a sequence of strings. A view declaration + with this argument ensures that the view will only be called when the + :term:`request` has a key in the ``request.params`` dictionary (an HTTP + ``GET`` or ``POST`` variable) that has a name which matches the a + supplied value. - If the value supplied has a ``=`` sign in it, + If any value supplied has a ``=`` sign in it, e.g. ``request_param="foo=123"``, then the key (``foo``) must both exist in the ``request.params`` dictionary, *and* the value must match the right hand side of the expression (``123``) for the view to "match" the current diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst index 4e64d8162..6aa390e64 100644 --- a/docs/whatsnew-1.4.rst +++ b/docs/whatsnew-1.4.rst @@ -63,7 +63,7 @@ Partial Mako and Chameleon Template Renderings of rendering the entire template. An example asset spec: ``package:path/to/template#macroname.pt``. This will render the macro defined as ``macroname`` within the ``template.pt`` template instead of the - entire templae. + entire template. Subrequest Support ~~~~~~~~~~~~~~~~~~ @@ -78,7 +78,7 @@ Minor Feature Additions ----------------------- - :meth:`pyramid.config.Configurator.add_directive` now accepts arbitrary - callables like partials or objects implementing ``__call__`` which dont + callables like partials or objects implementing ``__call__`` which don't have ``__name__`` and ``__doc__`` attributes. See https://github.com/Pylons/pyramid/issues/621 and https://github.com/Pylons/pyramid/pull/647. @@ -112,7 +112,7 @@ Minor Feature Additions - An :meth:`pyramid.config.Configurator.add_permission` directive method was added to the Configurator. This directive registers a free-standing permission introspectable into the Pyramid introspection system. - Frameworks built atop Pyramid can thus use the the ``permissions`` + Frameworks built atop Pyramid can thus use the ``permissions`` introspectable category data to build a comprehensive list of permissions supported by a running system. Before this method was added, permissions were already registered in this introspectable category as a side effect of @@ -172,7 +172,7 @@ Backwards Incompatibilities ``bfg.routes.matchdict`` to the request's WSGI environment dictionary. These values were docs-deprecated in ``repoze.bfg`` 1.0 (effectively seven minor releases ago). If your code depended on these values, use - request.matched_route and request.matchdict instead. + ``request.matched_route`` and ``request.matchdict`` instead. - It is no longer possible to pass an environ dictionary directly to ``pyramid.traversal.ResourceTreeTraverser.__call__`` (aka @@ -223,7 +223,7 @@ Backwards Incompatibilities * ``registerEventListener``, use :meth:`pyramid.config.Configurator.testing_add_subscriber` instead. - * ``registerTemplateRenderer`` (aka `registerDummyRenderer``), use + * ``registerTemplateRenderer`` (aka ``registerDummyRenderer``), use :meth:`pyramid.config.Configurator.testing_add_template` instead. * ``registerView``, use :meth:`pyramid.config.Configurator.add_view` instead. diff --git a/pyramid/chameleon_zpt.py b/pyramid/chameleon_zpt.py index 73203a7cb..d8a8ee1be 100644 --- a/pyramid/chameleon_zpt.py +++ b/pyramid/chameleon_zpt.py @@ -18,10 +18,12 @@ class ZPTTemplateRenderer(object): @reify # avoid looking up reload_templates before manager pushed def template(self): - tf = PageTemplateFile(self.path, - auto_reload=self.lookup.auto_reload, - debug=self.lookup.debug, - translate=self.lookup.translate) + tf = PageTemplateFile( + self.path, + auto_reload=self.lookup.auto_reload, + debug=self.lookup.debug, + translate=self.lookup.translate + ) if self.macro: # render only the portion of the template included in a # define-macro named the value of self.macro diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index 77b55d9b3..100c9454e 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -1,7 +1,5 @@ import re -from pyramid.compat import is_nonstr_iter - from pyramid.exceptions import ConfigurationError from pyramid.traversal import ( @@ -64,43 +62,48 @@ class PathInfoPredicate(object): class RequestParamPredicate(object): def __init__(self, val, config): - name = val - v = None - if '=' in name: - name, v = name.split('=', 1) - name, v = name.strip(), v.strip() - if v is None: - self._text = 'request_param %s' % (name,) - else: - self._text = 'request_param %s = %s' % (name, v) - self.name = name - self.val = v + val = as_sorted_tuple(val) + reqs = [] + for p in val: + k = p + v = None + if '=' in p: + k, v = p.split('=', 1) + k, v = k.strip(), v.strip() + reqs.append((k, v)) + self.val = val + self.reqs = reqs def text(self): - return self._text + return 'request_param %s' % ','.join( + ['%s=%s' % (x,y) if y else x for x, y in self.reqs] + ) phash = text def __call__(self, context, request): - if self.val is None: - return self.name in request.params - return request.params.get(self.name) == self.val - + for k, v in self.reqs: + actual = request.params.get(k) + if actual is None: + return False + if v is not None and actual != v: + return False + return True class HeaderPredicate(object): def __init__(self, val, config): name = val v = None if ':' in name: - name, v = name.split(':', 1) + name, val_str = name.split(':', 1) try: - v = re.compile(v) + v = re.compile(val_str) except re.error as why: raise ConfigurationError(why.args[0]) if v is None: self._text = 'header %s' % (name,) else: - self._text = 'header %s = %s' % (name, v) + self._text = 'header %s=%s' % (name, val_str) self.name = name self.val = v @@ -156,9 +159,7 @@ class RequestTypePredicate(object): class MatchParamPredicate(object): def __init__(self, val, config): - if not is_nonstr_iter(val): - val = (val,) - val = sorted(val) + val = as_sorted_tuple(val) self.val = val reqs = [ p.split('=', 1) for p in val ] self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py index f40cf25a7..abbbffc10 100644 --- a/pyramid/config/testing.py +++ b/pyramid/config/testing.py @@ -19,7 +19,8 @@ from pyramid.config.util import action_method class TestingConfiguratorMixin(object): # testing API def testing_securitypolicy(self, userid=None, groupids=(), - permissive=True): + permissive=True, remember_result=None, + forget_result=None): """Unit/integration testing helper: Registers a pair of faux :app:`Pyramid` security policies: a :term:`authentication policy` and a :term:`authorization policy`. @@ -31,6 +32,24 @@ class TestingConfiguratorMixin(object): nonpermissive :term:`authorization policy` is registered; this policy denies all access. + ``remember_result``, if provided, should be the result returned by + the ``remember`` method of the faux authentication policy. If it is + not provided (or it is provided, and is ``None``), the default value + ``[]`` (the empty list) will be returned by ``remember``. + + .. note:: + + ``remember_result`` is new as of Pyramid 1.4. + + ``forget_result``, if provided, should be the result returned by + the ``forget`` method of the faux authentication policy. If it is + not provided (or it is provided, and is ``None``), the default value + ``[]`` (the empty list) will be returned by ``forget``. + + .. note:: + + ``forget_result`` is new as of Pyramid 1.4. + The behavior of the registered :term:`authentication policy` depends on the values provided for the ``userid`` and ``groupids`` argument. The authentication policy will return @@ -47,9 +66,12 @@ class TestingConfiguratorMixin(object): :func:`pyramid.security.principals_allowed_by_permission`. """ from pyramid.testing import DummySecurityPolicy - policy = DummySecurityPolicy(userid, groupids, permissive) + policy = DummySecurityPolicy( + userid, groupids, permissive, remember_result, forget_result + ) self.registry.registerUtility(policy, IAuthorizationPolicy) self.registry.registerUtility(policy, IAuthenticationPolicy) + return policy def testing_resources(self, resources): """Unit/integration testing helper: registers a dictionary of diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 9ace96c1d..15263ad04 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -904,11 +904,12 @@ class ViewsConfiguratorMixin(object): request_param - This value can be any string. A view declaration with this - argument ensures that the view will only be called when the - :term:`request` has a key in the ``request.params`` + This value can be any string or any sequence of strings. A view + declaration with this argument ensures that the view will only be + called when the :term:`request` has a key in the ``request.params`` dictionary (an HTTP ``GET`` or ``POST`` variable) that has a - name which matches the supplied value. If the value + name which matches the supplied value (if the value is a string) + or values (if the value is a tuple). If any value supplied has a ``=`` sign in it, e.g. ``request_param="foo=123"``, then the key (``foo``) must both exist in the ``request.params`` dictionary, *and* diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 1368e190e..57a61ebba 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -367,6 +367,12 @@ class JSONP(JSON): @implementer(IChameleonLookup) class ChameleonRendererLookup(object): + spec_re = re.compile( + r'(?P<asset>[\w_.:/-]+)' + r'(?:\#(?P<defname>[\w_]+))?' + r'(\.(?P<ext>.*))' + ) + def __init__(self, impl, registry): self.impl = impl self.registry = registry @@ -417,6 +423,12 @@ class ChameleonRendererLookup(object): return False return settings.get('reload_templates', False) + def _crack_spec(self, spec): + asset, macro, ext = self.spec_re.match(spec).group( + 'asset', 'defname', 'ext' + ) + return asset, macro, ext + def __call__(self, info): spec = self.get_spec(info.name, info.package) registry = info.registry @@ -436,27 +448,22 @@ class ChameleonRendererLookup(object): # spec is a package:relpath asset spec renderer = registry.queryUtility(ITemplateRenderer, name=spec) if renderer is None: - p = re.compile( - r'(?P<asset>[\w_.:/-]+)' - r'(?:\#(?P<defname>[\w_]+))?' - r'(\.(?P<ext>.*))' - ) - asset, macro, ext = p.match(spec).group( - 'asset', 'defname', 'ext' - ) - spec = '%s.%s' % (asset, ext) + asset, macro, ext = self._crack_spec(spec) + spec_without_macro = '%s.%s' % (asset, ext) try: - package_name, filename = spec.split(':', 1) + package_name, filename = spec_without_macro.split(':', 1) except ValueError: # pragma: no cover # somehow we were passed a relative pathname; this # should die package_name = caller_package(4).__name__ - filename = spec + filename = spec_without_macro abspath = pkg_resources.resource_filename(package_name, filename) if not pkg_resources.resource_exists(package_name, filename): raise ValueError( - 'Missing template asset: %s (%s)' % (spec, abspath)) + 'Missing template asset: %s (%s)' % ( + spec_without_macro, abspath) + ) renderer = self.impl(abspath, self, macro=macro) settings = info.settings if not settings.get('reload_assets'): diff --git a/pyramid/session.py b/pyramid/session.py index 3b2834693..a5e6a8d3a 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -83,8 +83,9 @@ def signed_deserialize(serialized, secret, hmac=hmac): def check_csrf_token(request, token='csrf_token', raises=True): """ Check the CSRF token in the request's session against the value in - ``request.params.get(token)``. If ``token`` is not supplied, the string - value ``csrf_token`` will be used as the token value. If the value in + ``request.params.get(token)``. If a ``token`` keyword is not supplied + to this function, the string ``csrf_token`` will be used to look up + the token within ``request.params``. If the value in ``request.params.get(token)`` doesn't match the value supplied by ``request.session.get_csrf_token()``, and ``raises`` is ``True``, this function will raise an :exc:`pyramid.httpexceptions.HTTPBadRequest` diff --git a/pyramid/testing.py b/pyramid/testing.py index 9e8f2bff3..cecf13469 100644 --- a/pyramid/testing.py +++ b/pyramid/testing.py @@ -53,10 +53,17 @@ class DummyRootFactory(object): class DummySecurityPolicy(object): """ A standin for both an IAuthentication and IAuthorization policy """ - def __init__(self, userid=None, groupids=(), permissive=True): + def __init__(self, userid=None, groupids=(), permissive=True, + remember_result=None, forget_result=None): self.userid = userid self.groupids = groupids self.permissive = permissive + if remember_result is None: + remember_result = [] + if forget_result is None: + forget_result = [] + self.remember_result = remember_result + self.forget_result = forget_result def authenticated_userid(self, request): return self.userid @@ -73,10 +80,12 @@ class DummySecurityPolicy(object): return effective_principals def remember(self, request, principal, **kw): - return [] + self.remembered = principal + return self.remember_result def forget(self, request): - return [] + self.forgotten = True + return self.forget_result def permits(self, context, principals, permission): return self.permissive diff --git a/pyramid/tests/fixtures/withmacro.pt b/pyramid/tests/fixtures/withmacro.pt index 8bca01e4d..6fa654645 100644 --- a/pyramid/tests/fixtures/withmacro.pt +++ b/pyramid/tests/fixtures/withmacro.pt @@ -1,4 +1,5 @@ <html> +Outside macro <metal:m define-macro="foo"> Hello! </metal:m> diff --git a/pyramid/tests/test_chameleon_zpt.py b/pyramid/tests/test_chameleon_zpt.py index 37538e83e..5ac57f869 100644 --- a/pyramid/tests/test_chameleon_zpt.py +++ b/pyramid/tests/test_chameleon_zpt.py @@ -132,8 +132,13 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase): result = instance.implementation()() self.assertEqual(result, '\n Hello!\n') - - + def test_macro_notsupplied(self): + minimal = self._getTemplatePath('withmacro.pt') + lookup = DummyLookup() + instance = self._makeOne(minimal, lookup) + result = instance.implementation()() + self.assertEqual(result, + '<html>\nOutside macro\n\n Hello!\n\n</html>\n\n') class DummyLookup(object): auto_reload=True diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 005b1b27a..2f0ef4132 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -117,6 +117,20 @@ class TestRequestParamPredicate(unittest.TestCase): result = inst(None, request) self.assertTrue(result) + def test___call___true_multi(self): + inst = self._makeOne(('abc', 'def =2 ')) + request = Dummy() + request.params = {'abc':'1', 'def': '2'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false_multi(self): + inst = self._makeOne(('abc=3', 'def =2 ')) + request = Dummy() + request.params = {'abc':'3', 'def': '1'} + result = inst(None, request) + self.assertFalse(result) + def test___call___false(self): inst = self._makeOne('abc') request = Dummy() @@ -130,7 +144,11 @@ class TestRequestParamPredicate(unittest.TestCase): def test_text_withval(self): inst = self._makeOne('abc= 1') - self.assertEqual(inst.text(), 'request_param abc = 1') + self.assertEqual(inst.text(), 'request_param abc=1') + + def test_text_multi(self): + inst = self._makeOne(('abc= 1', 'def')) + self.assertEqual(inst.text(), 'request_param abc=1,def') def test_phash_exists(self): inst = self._makeOne('abc') @@ -138,7 +156,7 @@ class TestRequestParamPredicate(unittest.TestCase): def test_phash_withval(self): inst = self._makeOne('abc= 1') - self.assertEqual(inst.phash(), "request_param abc = 1") + self.assertEqual(inst.phash(), "request_param abc=1") class TestMatchParamPredicate(unittest.TestCase): def _makeOne(self, val): @@ -299,6 +317,70 @@ class Test_CheckCSRFTokenPredicate(unittest.TestCase): result = inst(None, request) self.assertEqual(result, True) +class TestHeaderPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.config.predicates import HeaderPredicate + return HeaderPredicate(val, None) + + def test___call___true_exists(self): + inst = self._makeOne('abc') + request = Dummy() + request.headers = {'abc':1} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withval(self): + inst = self._makeOne('abc:1') + request = Dummy() + request.headers = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withregex(self): + inst = self._makeOne(r'abc:\d+') + request = Dummy() + request.headers = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false_withregex(self): + inst = self._makeOne(r'abc:\d+') + request = Dummy() + request.headers = {'abc':'a'} + result = inst(None, request) + self.assertFalse(result) + + def test___call___false(self): + inst = self._makeOne('abc') + request = Dummy() + request.headers = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.text(), 'header abc') + + def test_text_withval(self): + inst = self._makeOne('abc:1') + self.assertEqual(inst.text(), 'header abc=1') + + def test_text_withregex(self): + inst = self._makeOne(r'abc:\d+') + self.assertEqual(inst.text(), r'header abc=\d+') + + def test_phash_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.phash(), 'header abc') + + def test_phash_withval(self): + inst = self._makeOne('abc:1') + self.assertEqual(inst.phash(), "header abc=1") + + def test_phash_withregex(self): + inst = self._makeOne(r'abc:\d+') + self.assertEqual(inst.phash(), r'header abc=\d+') + class predicate(object): def __repr__(self): return 'predicate' diff --git a/pyramid/tests/test_config/test_testing.py b/pyramid/tests/test_config/test_testing.py index 6c048b46d..1089f09fc 100644 --- a/pyramid/tests/test_config/test_testing.py +++ b/pyramid/tests/test_config/test_testing.py @@ -23,6 +23,30 @@ class TestingConfiguratorMixinTests(unittest.TestCase): self.assertEqual(ut.groupids, ('group1', 'group2')) self.assertEqual(ut.permissive, False) + def test_testing_securitypolicy_remember_result(self): + from pyramid.security import remember + config = self._makeOne(autocommit=True) + pol = config.testing_securitypolicy( + 'user', ('group1', 'group2'), + permissive=False, remember_result=True) + request = DummyRequest() + request.registry = config.registry + val = remember(request, 'fred') + self.assertEqual(pol.remembered, 'fred') + self.assertEqual(val, True) + + def test_testing_securitypolicy_forget_result(self): + from pyramid.security import forget + config = self._makeOne(autocommit=True) + pol = config.testing_securitypolicy( + 'user', ('group1', 'group2'), + permissive=False, forget_result=True) + request = DummyRequest() + request.registry = config.registry + val = forget(request) + self.assertEqual(pol.forgotten, True) + self.assertEqual(val, True) + def test_testing_resources(self): from pyramid.traversal import find_resource from pyramid.interfaces import ITraverser diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index af9188abc..cb6c364a7 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -295,6 +295,7 @@ class TestChameleonRendererLookup(unittest.TestCase): self.assertEqual(factory.kw, {'macro':None}) def test___call__spec_withmacro(self): + from pyramid.interfaces import ITemplateRenderer import os from pyramid import tests module_name = tests.__name__ @@ -302,10 +303,11 @@ class TestChameleonRendererLookup(unittest.TestCase): renderer = {} factory = DummyFactory(renderer) spec = '%s:%s' % (module_name, relpath) + reg = self.config.registry info = DummyRendererInfo({ 'name':spec, 'package':None, - 'registry':self.config.registry, + 'registry':reg, 'settings':{}, 'type':'type', }) @@ -318,6 +320,9 @@ class TestChameleonRendererLookup(unittest.TestCase): 'withmacro.pt') self.assertTrue(factory.path.startswith(path)) self.assertEqual(factory.kw, {'macro':'foo'}) + self.assertTrue( + reg.getUtility(ITemplateRenderer, name=spec) is renderer + ) def test___call__reload_assets_true(self): import pyramid.tests |
