diff options
31 files changed, 398 insertions, 172 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 43a910f96..f5c5c9449 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,12 +1,23 @@ Next release ============ +Bug Fixes +--------- + +- A failure when trying to locate the attribute ``__text__`` on route and view + predicates existed when the ``debug_routematch`` setting was true or when the + ``pviews`` command was used. See https://github.com/Pylons/pyramid/pull/727 + +1.4a4 (2012-11-14) +================== + Features -------- - ``pyramid.authentication.AuthTktAuthenticationPolicy`` has been updated to support newer hashing algorithms such as ``sha512``. Existing applications - should consider updating if possible. + should consider updating if possible for improved security over the default + md5 hashing. - Added an ``effective_principals`` route and view predicate. @@ -21,17 +32,19 @@ Features - Slightly better debug logging from ``pyramid.authentication.RepozeWho1AuthenticationPolicy``. -- ``pyramid.security.view_execution_permitted`` used to return `True` if no +- ``pyramid.security.view_execution_permitted`` used to return ``True`` if no view could be found. It now raises a ``TypeError`` exception in that case, as it doesn't make sense to assert that a nonexistent view is execution-permitted. See https://github.com/Pylons/pyramid/issues/299. -- Get rid of shady monkeypatching of ``pyramid.request.Request`` and - ``pyramid.response.Response`` done within the ``__init__.py`` of Pyramid. - Webob no longer relies on this being done. Instead, the ResponseClass - attribute of the Pyramid Request class is assigned to the Pyramid response - class; that's enough to satisfy WebOb and behave as it did before with the - monkeypatching. +- Allow a ``_depth`` argument to ``pyramid.view.view_config``, which will + permit limited composition reuse of the decorator by other software that + wants to provide custom decorators that are much like view_config. + +- Allow an iterable of decorators to be passed to + ``pyramid.config.Configurator.add_view``. This allows views to be wrapped + by more than one decorator without requiring combining the decorators + yourself. Bug Fixes --------- @@ -48,14 +61,30 @@ Bug Fixes attribute of the request. It no longer fails in this case. See https://github.com/Pylons/pyramid/issues/700 +- Be more tolerant of potential error conditions in ``match_param`` and + ``physical_path`` predicate implementations; instead of raising an exception, + return False. + +- ``pyramid.view.render_view`` was not functioning properly under Python 3.x + due to a byte/unicode discrepancy. See + http://github.com/Pylons/pyramid/issues/721 + Deprecations ------------ -- ``pyramid.authentication.AuthTktAuthenticationPolicy`` will emit a warning - if an application is using the policy without explicitly setting the - ``hashalg``. This is because the default is "md5" which is considered - insecure. If you really want "md5" then you must specify it explicitly to - get rid of the warning. +- ``pyramid.authentication.AuthTktAuthenticationPolicy`` will emit a warning if + an application is using the policy without explicitly passing a ``hashalg`` + argument. This is because the default is "md5" which is considered + theoretically subject to collision attacks. If you really want "md5" then you + must specify it explicitly to get rid of the warning. + +Documentation +------------- + +- All of the tutorials that use + ``pyramid.authentication.AuthTktAuthenticationPolicy`` now explicitly pass + ``sha512`` as a ``hashalg`` argument. + Internals --------- @@ -68,6 +97,13 @@ Internals because that package should never be imported from non-Pyramid code. TopologicalSorter is still not an API, but may become one. +- Get rid of shady monkeypatching of ``pyramid.request.Request`` and + ``pyramid.response.Response`` done within the ``__init__.py`` of Pyramid. + Webob no longer relies on this being done. Instead, the ResponseClass + attribute of the Pyramid Request class is assigned to the Pyramid response + class; that's enough to satisfy WebOb and behave as it did before with the + monkeypatching. + 1.4a3 (2012-10-26) ================== diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 85b520975..971c172f8 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -189,4 +189,6 @@ Contributors - David Gay, 2012/09/16 +- Robert Jackiewicz, 2012/11/12 + - John Anderson, 2012/11/14 @@ -4,8 +4,6 @@ Pyramid TODOs Nice-to-Have ------------ -- config.set_registry_attr (with conflict detection). - - Provide the presumed renderer name to the called view as an attribute of the request. @@ -177,3 +175,6 @@ Probably Bad Ideas - _fix_registry should dictify the registry being fixed. +- config.set_registry_attr (with conflict detection)... bad idea because it + won't take effect until after a commit and folks will be confused by that. + diff --git a/docs/conf.py b/docs/conf.py index 9bda4c798..5e17de18a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -81,7 +81,7 @@ copyright = '%s, Agendaless Consulting' % datetime.datetime.now().year # other places throughout the built documents. # # The short X.Y version. -version = '1.4a3' +version = '1.4a4' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 96fa77a07..ea75e5fe4 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -180,11 +180,14 @@ as a forbidden view: config.scan() Like any other view, the forbidden view must accept at least a ``request`` -parameter, or both ``context`` and ``request``. The ``context`` (available -as ``request.context`` if you're using the request-only view argument -pattern) is the context found by the router when the view invocation was -denied. The ``request`` is the current :term:`request` representing the -denied action. +parameter, or both ``context`` and ``request``. If a forbidden view +callable accepts both ``context`` and ``request``, the HTTP Exception is passed +as context. The ``context`` as found by the router when view was +denied (that you normally would expect) is available as +``request.context``. The ``request`` is the current :term:`request` +representing the denied action. + + Here's some sample code that implements a minimal forbidden view: diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index d90897d16..f32053202 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -289,13 +289,6 @@ Minor Feature Additions not a new feature, it just provides an API for adding a resource url adapter without needing to use the ZCA API. -- The :meth:`pyramid.config.Configurator.scan` method can now be passed an - ``ignore`` argument, which can be a string, a callable, or a list - consisting of strings and/or callables. This feature allows submodules, - subpackages, and global objects from being scanned. See - http://readthedocs.org/docs/venusian/en/latest/#ignore-scan-argument for - more information about how to use the ``ignore`` argument to ``scan``. - - Better error messages when a view callable returns a value that cannot be converted to a response (for example, when a view callable returns a dictionary without a renderer defined, or doesn't return any value at all). @@ -486,6 +479,11 @@ Deprecations was designed to offer in Pylons. It will continue to exist "forever" but it will not be recommended or mentioned in the docs. +- Remove references to do-nothing ``pyramid.debug_templates`` setting in all + Pyramid-provided .ini files. This setting previously told Chameleon to render + better exceptions; now Chameleon always renders nice exceptions regardless of + the value of this setting. + Known Issues ------------ diff --git a/docs/whatsnew-1.4.rst b/docs/whatsnew-1.4.rst index 59e1f7a96..5da28bb03 100644 --- a/docs/whatsnew-1.4.rst +++ b/docs/whatsnew-1.4.rst @@ -77,6 +77,11 @@ Subrequest Support Minor Feature Additions ----------------------- +- :class:`pyramid.authentication.AuthTktAuthenticationPolicy` has been updated + to support newer hashing algorithms such as ``sha512``. Existing applications + should consider updating if possible for improved security over the default + md5 hashing. + - :meth:`pyramid.config.Configurator.add_directive` now accepts arbitrary callables like partials or objects implementing ``__call__`` which don't have ``__name__`` and ``__doc__`` attributes. See @@ -182,7 +187,6 @@ Minor Feature Additions :meth:`pyramid.config.testing_securitypolicy` now sets a ``forgotten`` value on the policy (the value ``True``) when its ``forget`` method is called. - - The DummySecurityPolicy created by :meth:`pyramid.config.testing_securitypolicy` now sets a ``remembered`` value on the policy, which is the value of the ``principal`` @@ -196,6 +200,31 @@ Minor Feature Additions view when some object is traversed to, but you can't be sure about what kind of object it will be, so you can't use the ``context`` predicate. +- Added an ``effective_principals`` route and view predicate. + +- Do not allow the userid returned from the + :func:`pyramid.security.authenticated_userid` or the userid that is one of the + list of principals returned by :func:`pyramid.security.effective_principals` + to be either of the strings ``system.Everyone`` or ``system.Authenticated`` + when any of the built-in authorization policies that live in + :mod:`pyramid.authentication` are in use. These two strings are reserved for + internal usage by Pyramid and they will no longer be accepted as valid + userids. + +- Allow a ``_depth`` argument to :class:`pyramid.view.view_config`, which will + permit limited composition reuse of the decorator by other software that + wants to provide custom decorators that are much like view_config. + +- Allow an iterable of decorators to be passed to + :meth:`pyramid.config.Configurator.add_view`. This allows views to be wrapped + by more than one decorator without requiring combining the decorators + yourself. + +- :func:`pyramid.security.view_execution_permitted` used to return `True` if no + view could be found. It now raises a :exc:`TypeError` exception in that case, + as it doesn't make sense to assert that a nonexistent view is + execution-permitted. See https://github.com/Pylons/pyramid/issues/299. + Backwards Incompatibilities --------------------------- @@ -289,6 +318,12 @@ Deprecations used in its place (it has all of the same capabilities but can also extend the request object with methods). +- :class:`pyramid.authentication.AuthTktAuthenticationPolicy` will emit a + deprecation warning if an application is using the policy without explicitly + passing a ``hashalg`` argument. This is because the default is "md5" which is + considered theoretically subject to collision attacks. If you really want + "md5" then you must specify it explicitly to get rid of the warning. + Documentation Enhancements -------------------------- @@ -299,6 +334,10 @@ Documentation Enhancements - Added a :ref:`subrequest_chapter` chapter to the narrative documentation. +- All of the tutorials that use + :class:`pyramid.authentication.AuthTktAuthenticationPolicy` now explicitly + pass ``sha512`` as a ``hashalg`` argument. + - Many cleanups and improvements to narrative and API docs. Dependency Changes diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 1dc438597..40edaa324 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -70,16 +70,17 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import ( - action_method, - ActionInfo, - PredicateList, - ) +from pyramid.config.util import PredicateList from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin from pyramid.path import DottedNameResolver +from pyramid.util import ( + action_method, + ActionInfo, + ) + empty = text_('') _marker = object() diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index c93431987..5d4682349 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -8,7 +8,7 @@ from pyramid.interfaces import IPackageOverrides from pyramid.exceptions import ConfigurationError from pyramid.threadlocal import get_current_registry -from pyramid.config.util import action_method +from pyramid.util import action_method class OverrideProvider(pkg_resources.DefaultProvider): def __init__(self, module): diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 01b1fb22e..ef7975d92 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -1,7 +1,5 @@ from zope.interface import implementer -from pyramid.config.util import action_method - from pyramid.interfaces import ( IDefaultRootFactory, IRequestFactory, @@ -11,7 +9,11 @@ from pyramid.interfaces import ( ) from pyramid.traversal import DefaultRootFactory -from pyramid.util import InstancePropertyMixin + +from pyramid.util import ( + action_method, + InstancePropertyMixin, + ) class FactoriesConfiguratorMixin(object): @action_method diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index 67a7e2018..9eb59e1c7 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -13,8 +13,7 @@ from pyramid.exceptions import ConfigurationError from pyramid.i18n import get_localizer from pyramid.path import package_path from pyramid.threadlocal import get_current_request - -from pyramid.config.util import action_method +from pyramid.util import action_method class I18NConfiguratorMixin(object): @action_method diff --git a/pyramid/config/predicates.py b/pyramid/config/predicates.py index e31425899..ded8fbfbf 100644 --- a/pyramid/config/predicates.py +++ b/pyramid/config/predicates.py @@ -17,6 +17,8 @@ from pyramid.security import effective_principals from .util import as_sorted_tuple +_marker = object() + class XHRPredicate(object): def __init__(self, val, config): self.val = bool(val) @@ -174,6 +176,9 @@ class MatchParamPredicate(object): phash = text def __call__(self, context, request): + if not request.matchdict: + # might be None + return False for k, v in self.reqs: if request.matchdict.get(k) != v: return False @@ -266,7 +271,9 @@ class PhysicalPathPredicate(object): phash = text def __call__(self, context, request): - return resource_path_tuple(context) == self.val + if getattr(context, '__name__', _marker) is not _marker: + return resource_path_tuple(context) == self.val + return False class EffectivePrincipalsPredicate(object): def __init__(self, val, config): diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py index 926511b7b..4f33b23d9 100644 --- a/pyramid/config/rendering.py +++ b/pyramid/config/rendering.py @@ -6,7 +6,7 @@ from pyramid.interfaces import ( PHASE1_CONFIG, ) -from pyramid.config.util import action_method +from pyramid.util import action_method from pyramid import ( renderers, diff --git a/pyramid/config/security.py b/pyramid/config/security.py index 567999cc4..6a1257b6a 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -7,7 +7,7 @@ from pyramid.interfaces import ( ) from pyramid.exceptions import ConfigurationError -from pyramid.config.util import action_method +from pyramid.util import action_method class SecurityConfiguratorMixin(object): @action_method diff --git a/pyramid/config/testing.py b/pyramid/config/testing.py index abbbffc10..7141a5049 100644 --- a/pyramid/config/testing.py +++ b/pyramid/config/testing.py @@ -14,7 +14,7 @@ from pyramid.traversal import ( split_path_info, ) -from pyramid.config.util import action_method +from pyramid.util import action_method class TestingConfiguratorMixin(object): # testing API diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 1c6e1ca15..a83e23798 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,10 +1,4 @@ -import traceback - -from functools import update_wrapper - -from zope.interface import implementer - -from pyramid.interfaces import IActionInfo +from hashlib import md5 from pyramid.compat import ( bytes_, @@ -13,56 +7,19 @@ from pyramid.compat import ( from pyramid.exceptions import ConfigurationError from pyramid.registry import predvalseq -from pyramid.util import TopologicalSorter -from hashlib import md5 +from pyramid.util import ( + TopologicalSorter, + action_method, + ActionInfo, + ) + +action_method = action_method # support bw compat imports +ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() -@implementer(IActionInfo) -class ActionInfo(object): - def __init__(self, file, line, function, src): - self.file = file - self.line = line - self.function = function - self.src = src - - def __str__(self): - srclines = self.src.split('\n') - src = '\n'.join(' %s' % x for x in srclines) - return 'Line %s of file %s:\n%s' % (self.line, self.file, src) - -def action_method(wrapped): - """ Wrapper to provide the right conflict info report data when a method - that calls Configurator.action calls another that does the same""" - def wrapper(self, *arg, **kw): - if self._ainfo is None: - self._ainfo = [] - info = kw.pop('_info', None) - # backframes for outer decorators to actionmethods - backframes = kw.pop('_backframes', 2) - if is_nonstr_iter(info) and len(info) == 4: - # _info permitted as extract_stack tuple - info = ActionInfo(*info) - if info is None: - try: - f = traceback.extract_stack(limit=3) - info = ActionInfo(*f[-backframes]) - except: # pragma: no cover - info = ActionInfo(None, 0, '', '') - self._ainfo.append(info) - try: - result = wrapped(self, *arg, **kw) - finally: - self._ainfo.pop() - return result - - if hasattr(wrapped, '__name__'): - update_wrapper(wrapper, wrapped) - wrapper.__docobj__ = wrapped - return wrapper - def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) @@ -85,8 +42,12 @@ class PredicateList(object): ## weighs_more_than = self.last_added or FIRST ## weighs_less_than = LAST self.last_added = name - self.sorter.add(name, factory, after=weighs_more_than, - before=weighs_less_than) + self.sorter.add( + name, + factory, + after=weighs_more_than, + before=weighs_less_than, + ) def make(self, config, **kw): # Given a configurator and a list of keywords, a predicate list is diff --git a/pyramid/config/views.py b/pyramid/config/views.py index b01d17efd..745b6f810 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,7 +1,6 @@ import inspect import operator import os -from functools import wraps from zope.interface import ( Interface, @@ -42,6 +41,7 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, + is_nonstr_iter ) from pyramid.exceptions import ( @@ -70,6 +70,8 @@ from pyramid.view import ( from pyramid.util import ( object_description, + viewdefaults, + action_method, ) import pyramid.config.predicates @@ -77,7 +79,6 @@ import pyramid.config.predicates from pyramid.config.util import ( DEFAULT_PHASH, MAX_ORDER, - action_method, ) urljoin = urlparse.urljoin @@ -620,21 +621,6 @@ class MultiView(object): continue raise PredicateMismatch(self.name) -def viewdefaults(wrapped): - def wrapper(self, *arg, **kw): - defaults = {} - if arg: - view = arg[0] - else: - view = kw.get('view') - view = self.maybe_dotted(view) - if inspect.isclass(view): - defaults = getattr(view, '__view_defaults__', {}).copy() - defaults.update(kw) - defaults['_backframes'] = 3 # for action_method - return wrapped(self, *arg, **defaults) - return wraps(wrapped)(wrapper) - class ViewsConfiguratorMixin(object): @viewdefaults @action_method @@ -837,14 +823,40 @@ class ViewsConfiguratorMixin(object): decorator - A :term:`dotted Python name` to function (or the function itself) - which will be used to decorate the registered :term:`view - callable`. The decorator function will be called with the view - callable as a single argument. The view callable it is passed will - accept ``(context, request)``. The decorator must return a + A :term:`dotted Python name` to function (or the function itself, + or an iterable of the aforementioned) which will be used to + decorate the registered :term:`view callable`. The decorator + function(s) will be called with the view callable as a single + argument. The view callable it is passed will accept + ``(context, request)``. The decorator(s) must return a replacement view callable which also accepts ``(context, request)``. + If decorator is an iterable, the callables will be combined and + used in the order provided as a decorator. + For example:: + + @view_config(..., + decorator=(decorator2, + decorator1)) + def myview(request): + .... + + Is similar to doing:: + + @view_config(...) + @decorator2 + @decorator1 + def myview(request): + ... + + Except with the existing benefits of ``decorator=`` (having a common + decorator syntax for all view calling conventions and not having to + think about preserving function attributes such as ``__name__`` and + ``__module__`` within decorator logic). + + Passing an iterable is only supported as of :app:`Pyramid` 1.4a4. + mapper A Python object or :term:`dotted Python name` which refers to a @@ -1071,7 +1083,19 @@ class ViewsConfiguratorMixin(object): for_ = self.maybe_dotted(for_) containment = self.maybe_dotted(containment) mapper = self.maybe_dotted(mapper) - decorator = self.maybe_dotted(decorator) + + def combine(*decorators): + def decorated(view_callable): + # reversed() is allows a more natural ordering in the api + for decorator in reversed(decorators): + view_callable = decorator(view_callable) + return view_callable + return decorated + + if is_nonstr_iter(decorator): + decorator = combine(*map(self.maybe_dotted, decorator)) + else: + decorator = self.maybe_dotted(decorator) if not view: if renderer: diff --git a/pyramid/router.py b/pyramid/router.py index 0c7f61071..9b6138ea9 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -103,7 +103,7 @@ class Router(object): request.path_info, route.pattern, match, - ', '.join([p.__text__ for p in route.predicates])) + ', '.join([p.text() for p in route.predicates])) ) logger and logger.debug(msg) diff --git a/pyramid/scaffolds/copydir.py b/pyramid/scaffolds/copydir.py index d55ea165a..ba0988523 100644 --- a/pyramid/scaffolds/copydir.py +++ b/pyramid/scaffolds/copydir.py @@ -245,7 +245,7 @@ Responses: def makedirs(dir, verbosity, pad): parent = os.path.dirname(os.path.abspath(dir)) if not os.path.exists(parent): - makedirs(parent, verbosity, pad) + makedirs(parent, verbosity, pad) # pragma: no cover os.mkdir(dir) def substitute_filename(fn, vars): diff --git a/pyramid/scripts/pviews.py b/pyramid/scripts/pviews.py index a9db59dc1..60aecb9bb 100644 --- a/pyramid/scripts/pviews.py +++ b/pyramid/scripts/pviews.py @@ -187,7 +187,7 @@ class PViewsCommand(object): self.out("%sroute pattern: %s" % (indent, route.pattern)) self.out("%sroute path: %s" % (indent, route.path)) self.out("%ssubpath: %s" % (indent, '/'.join(attrs['subpath']))) - predicates = ', '.join([p.__text__ for p in route.predicates]) + predicates = ', '.join([p.text() for p in route.predicates]) if predicates != '': self.out("%sroute predicates (%s)" % (indent, predicates)) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 2cf9a269a..7c2880a18 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -772,7 +772,7 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual(config.action('discrim', kw={'a':1}), None) def test_action_autocommit_with_introspectables(self): - from pyramid.config.util import ActionInfo + from pyramid.util import ActionInfo config = self._makeOne(autocommit=True) intr = DummyIntrospectable() config.action('discrim', introspectables=(intr,)) diff --git a/pyramid/tests/test_config/test_predicates.py b/pyramid/tests/test_config/test_predicates.py index 91dfb0fb6..1cd6050bf 100644 --- a/pyramid/tests/test_config/test_predicates.py +++ b/pyramid/tests/test_config/test_predicates.py @@ -187,6 +187,13 @@ class TestMatchParamPredicate(unittest.TestCase): result = inst(None, request) self.assertFalse(result) + def test___call___matchdict_is_None(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = None + result = inst(None, request) + self.assertFalse(result) + def test_text(self): inst = self._makeOne(('def= 1', 'abc =2')) self.assertEqual(inst.text(), 'match_param abc=2,def=1') @@ -436,6 +443,11 @@ class Test_PhysicalPathPredicate(unittest.TestCase): context.__parent__ = root self.assertFalse(inst(context, None)) + def test_it_call_context_has_no_name(self): + inst = self._makeOne('/', None) + context = Dummy() + self.assertFalse(inst(context, None)) + class Test_EffectivePrincipalsPredicate(unittest.TestCase): def setUp(self): self.config = testing.setUp() diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 8c3cd7455..b32f9c6ef 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -366,36 +366,6 @@ class TestPredicateList(unittest.TestCase): self.assertRaises(ConfigurationError, self._callFUT, unknown=1) -class TestActionInfo(unittest.TestCase): - def _getTargetClass(self): - from pyramid.config.util import ActionInfo - return ActionInfo - - def _makeOne(self, filename, lineno, function, linerepr): - return self._getTargetClass()(filename, lineno, function, linerepr) - - def test_class_conforms(self): - from zope.interface.verify import verifyClass - from pyramid.interfaces import IActionInfo - verifyClass(IActionInfo, self._getTargetClass()) - - def test_instance_conforms(self): - from zope.interface.verify import verifyObject - from pyramid.interfaces import IActionInfo - verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) - - def test_ctor(self): - inst = self._makeOne('filename', 10, 'function', 'src') - self.assertEqual(inst.file, 'filename') - self.assertEqual(inst.line, 10) - self.assertEqual(inst.function, 'function') - self.assertEqual(inst.src, 'src') - - def test___str__(self): - inst = self._makeOne('filename', 0, 'function', ' linerepr ') - self.assertEqual(str(inst), - "Line 0 of file filename:\n linerepr ") - class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 575d8c738..8324eb2b9 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -185,6 +185,28 @@ class TestViewsConfigurationMixin(unittest.TestCase): result = wrapper(None, None) self.assertEqual(result, 'OK') + def test_add_view_with_decorator_tuple(self): + from pyramid.renderers import null_renderer + def view(request): + """ ABC """ + return 'OK' + def view_wrapper1(fn): + def inner(context, request): + return 'wrapped1' + fn(context, request) + return inner + def view_wrapper2(fn): + def inner(context, request): + return 'wrapped2' + fn(context, request) + return inner + config = self._makeOne(autocommit=True) + config.add_view(view=view, decorator=(view_wrapper2, view_wrapper1), + renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertFalse(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) + result = wrapper(None, None) + self.assertEqual(result, 'wrapped2wrapped1OK') + def test_add_view_with_http_cache(self): import datetime from pyramid.response import Response diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 778b27473..65152ca05 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -24,7 +24,7 @@ class TestRouter(unittest.TestCase): if mapper is None: mapper = RoutesMapper() self.registry.registerUtility(mapper, IRoutesMapper) - mapper.connect(name, path, factory) + return mapper.connect(name, path, factory) def _registerLogger(self): from pyramid.interfaces import IDebugLogger @@ -657,7 +657,8 @@ class TestRouter(unittest.TestCase): root = object() def factory(request): return root - self._connectRoute('foo', 'archives/:action/:article', factory) + route = self._connectRoute('foo', 'archives/:action/:article', factory) + route.predicates = [DummyPredicate()] context = DummyContext() self._registerTraverserFactory(context) response = DummyResponse() @@ -686,7 +687,11 @@ class TestRouter(unittest.TestCase): "route matched for url http://localhost:8080" "/archives/action1/article1; " "route_name: 'foo', " - "path_info: ")) + "path_info: ") + ) + self.assertTrue( + "predicates: 'predicate'" in logger.messages[0] + ) def test_call_route_match_miss_debug_routematch(self): from pyramid.httpexceptions import HTTPNotFound @@ -1159,6 +1164,12 @@ class TestRouter(unittest.TestCase): start_response = DummyStartResponse() self.assertRaises(RuntimeError, router, environ, start_response) +class DummyPredicate(object): + def __call__(self, info, request): + return True + def text(self): + return 'predicate' + class DummyContext: pass diff --git a/pyramid/tests/test_scripts/test_pviews.py b/pyramid/tests/test_scripts/test_pviews.py index 6a919c31b..266d1ec90 100644 --- a/pyramid/tests/test_scripts/test_pviews.py +++ b/pyramid/tests/test_scripts/test_pviews.py @@ -379,7 +379,7 @@ class TestPViewsCommand(unittest.TestCase): L = [] command.out = L.append def predicate(): pass - predicate.__text__ = "predicate = x" + predicate.text = lambda *arg: "predicate = x" route = dummy.DummyRoute('a', '/a', matchdict={}, predicate=predicate) view = dummy.DummyView(context='context', view_name='a', matched_route=route, subpath='') diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 785950230..2ca4c4a66 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -545,6 +545,37 @@ class TestSentinel(unittest.TestCase): r = repr(Sentinel('ABC')) self.assertEqual(r, 'ABC') +class TestActionInfo(unittest.TestCase): + def _getTargetClass(self): + from pyramid.util import ActionInfo + return ActionInfo + + def _makeOne(self, filename, lineno, function, linerepr): + return self._getTargetClass()(filename, lineno, function, linerepr) + + def test_class_conforms(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IActionInfo + verifyClass(IActionInfo, self._getTargetClass()) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IActionInfo + verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) + + def test_ctor(self): + inst = self._makeOne('filename', 10, 'function', 'src') + self.assertEqual(inst.file, 'filename') + self.assertEqual(inst.line, 10) + self.assertEqual(inst.function, 'function') + self.assertEqual(inst.src, 'src') + + def test___str__(self): + inst = self._makeOne('filename', 0, 'function', ' linerepr ') + self.assertEqual(str(inst), + "Line 0 of file filename:\n linerepr ") + + def dummyfunc(): pass class Dummy(object): diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index f63e17bd8..a78b0cbab 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -224,12 +224,29 @@ class RenderViewToIterableTests(BaseTest, unittest.TestCase): response = DummyResponse() view = make_view(response) def anotherview(context, request): - return DummyResponse('anotherview') + return DummyResponse(b'anotherview') view.__call_permissive__ = anotherview self._registerView(request.registry, view, 'registered') iterable = self._callFUT(context, request, name='registered', secure=False) - self.assertEqual(iterable, ['anotherview']) + self.assertEqual(iterable, [b'anotherview']) + + def test_verify_output_bytestring(self): + from pyramid.request import Request + from pyramid.config import Configurator + from pyramid.view import render_view + from webob.compat import text_type + config = Configurator(settings={}) + def view(request): + request.response.text = text_type('<body></body>') + return request.response + + config.add_view(name='test', view=view) + config.commit() + + r = Request({}) + r.registry = config.registry + self.assertEqual(render_view(object(), r, 'test'), b'<body></body>') def test_call_request_has_no_registry(self): request = self._makeRequest() @@ -261,7 +278,7 @@ class RenderViewTests(BaseTest, unittest.TestCase): view = make_view(response) self._registerView(request.registry, view, 'registered') s = self._callFUT(context, request, name='registered', secure=True) - self.assertEqual(s, '') + self.assertEqual(s, b'') def test_call_view_registered_insecure_no_call_permissive(self): context = self._makeContext() @@ -270,7 +287,7 @@ class RenderViewTests(BaseTest, unittest.TestCase): view = make_view(response) self._registerView(request.registry, view, 'registered') s = self._callFUT(context, request, name='registered', secure=False) - self.assertEqual(s, '') + self.assertEqual(s, b'') def test_call_view_registered_insecure_with_call_permissive(self): context = self._makeContext() @@ -278,11 +295,11 @@ class RenderViewTests(BaseTest, unittest.TestCase): response = DummyResponse() view = make_view(response) def anotherview(context, request): - return DummyResponse('anotherview') + return DummyResponse(b'anotherview') view.__call_permissive__ = anotherview self._registerView(request.registry, view, 'registered') s = self._callFUT(context, request, name='registered', secure=False) - self.assertEqual(s, 'anotherview') + self.assertEqual(s, b'anotherview') class TestIsResponse(unittest.TestCase): def setUp(self): @@ -372,6 +389,10 @@ class TestViewConfigDecorator(unittest.TestCase): def test_create_with_other_predicates(self): decorator = self._makeOne(foo=1) self.assertEqual(decorator.foo, 1) + + def test_create_decorator_tuple(self): + decorator = self._makeOne(decorator=('decorator1', 'decorator2')) + self.assertEqual(decorator.decorator, ('decorator1', 'decorator2')) def test_call_function(self): decorator = self._makeOne() @@ -519,6 +540,14 @@ class TestViewConfigDecorator(unittest.TestCase): self.assertTrue(renderer is renderer_helper) self.assertEqual(config.pkg, pyramid.tests) + def test_call_withdepth(self): + decorator = self._makeOne(_depth=2) + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + decorator(foo) + self.assertEqual(venusian.depth, 2) + class Test_append_slash_notfound_view(BaseTest, unittest.TestCase): def _callFUT(self, context, request): from pyramid.view import append_slash_notfound_view @@ -746,8 +775,9 @@ class DummyVenusian(object): self.info = info self.attachments = [] - def attach(self, wrapped, callback, category=None): + def attach(self, wrapped, callback, category=None, depth=1): self.attachments.append((wrapped, callback, category)) + self.depth = depth return self.info class DummyRegistry(object): diff --git a/pyramid/util.py b/pyramid/util.py index d83837322..ca7f5951c 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,6 +1,10 @@ +import functools import inspect +import traceback import weakref +from zope.interface import implementer + from pyramid.exceptions import ( ConfigurationError, CyclicDependencyError, @@ -15,6 +19,7 @@ from pyramid.compat import ( PY3, ) +from pyramid.interfaces import IActionInfo from pyramid.path import DottedNameResolver as _DottedNameResolver class DottedNameResolver(_DottedNameResolver): @@ -453,3 +458,65 @@ class TopologicalSorter(object): return result +def viewdefaults(wrapped): + """ Decorator for add_view-like methods which takes into account + __view_defaults__ attached to view it is passed. Not a documented API but + used by some external systems.""" + def wrapper(self, *arg, **kw): + defaults = {} + if arg: + view = arg[0] + else: + view = kw.get('view') + view = self.maybe_dotted(view) + if inspect.isclass(view): + defaults = getattr(view, '__view_defaults__', {}).copy() + defaults.update(kw) + defaults['_backframes'] = 3 # for action_method + return wrapped(self, *arg, **defaults) + return functools.wraps(wrapped)(wrapper) + +@implementer(IActionInfo) +class ActionInfo(object): + def __init__(self, file, line, function, src): + self.file = file + self.line = line + self.function = function + self.src = src + + def __str__(self): + srclines = self.src.split('\n') + src = '\n'.join(' %s' % x for x in srclines) + return 'Line %s of file %s:\n%s' % (self.line, self.file, src) + +def action_method(wrapped): + """ Wrapper to provide the right conflict info report data when a method + that calls Configurator.action calls another that does the same. Not a + documented API but used by some external systems.""" + def wrapper(self, *arg, **kw): + if self._ainfo is None: + self._ainfo = [] + info = kw.pop('_info', None) + # backframes for outer decorators to actionmethods + backframes = kw.pop('_backframes', 2) + if is_nonstr_iter(info) and len(info) == 4: + # _info permitted as extract_stack tuple + info = ActionInfo(*info) + if info is None: + try: + f = traceback.extract_stack(limit=3) + info = ActionInfo(*f[-backframes]) + except: # pragma: no cover + info = ActionInfo(None, 0, '', '') + self._ainfo.append(info) + try: + result = wrapped(self, *arg, **kw) + finally: + self._ainfo.pop() + return result + + if hasattr(wrapped, '__name__'): + functools.update_wrapper(wrapper, wrapped) + wrapper.__docobj__ = wrapped + return wrapper + diff --git a/pyramid/view.py b/pyramid/view.py index 51ded423c..1a66c9e9c 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -3,6 +3,7 @@ import venusian from zope.interface import providedBy from zope.deprecation import deprecated + from pyramid.interfaces import ( IRoutesMapper, IView, @@ -93,8 +94,8 @@ def render_view_to_iterable(context, request, name='', secure=True): :exc:`ValueError` if a view function is found and called but the view function's result does not have an ``app_iter`` attribute. - You can usually get the string representation of the return value - of this function by calling ``''.join(iterable)``, or just use + You can usually get the bytestring representation of the return value of + this function by calling ``b''.join(iterable)``, or just use :func:`pyramid.view.render_view` instead. If ``secure`` is ``True``, and the view is protected by a permission, the @@ -116,7 +117,7 @@ def render_view(context, request, name='', secure=True): configuration` that matches the :term:`view name` ``name`` registered against the specified ``context`` and ``request`` and unwind the view response's ``app_iter`` (see - :ref:`the_response`) into a single string. This function will + :ref:`the_response`) into a single bytestring. This function will return ``None`` if a corresponding :term:`view callable` cannot be found (when no :term:`view configuration` matches the combination of ``name`` / ``context`` / and ``request``). Additionally, this @@ -136,7 +137,7 @@ def render_view(context, request, name='', secure=True): iterable = render_view_to_iterable(context, request, name, secure) if iterable is None: return None - return ''.join(iterable) + return b''.join(iterable) class view_config(object): """ A function, class or method :term:`decorator` which allows a @@ -176,6 +177,13 @@ class view_config(object): :meth:`pyramid.config.Configurator.add_view`. If any argument is left out, its default will be the equivalent ``add_view`` default. + An additional keyword argument named ``_depth`` is provided for people who + wish to reuse this class from another decorator. It will be passed in to + the :term:`venusian` ``attach`` function as the depth of the callstack when + Venusian checks if the decorator is being used in a class or module + context. It's not often used, but it can be useful in this circumstance. + See the ``attach`` function in Venusian for more information. + See :ref:`mapping_views_using_a_decorator_section` for details about using :class:`view_config`. @@ -189,12 +197,14 @@ class view_config(object): def __call__(self, wrapped): settings = self.__dict__.copy() + depth = settings.pop('_depth', 1) def callback(context, name, ob): config = context.config.with_package(info.module) config.add_view(view=ob, **settings) - info = self.venusian.attach(wrapped, callback, category='pyramid') + info = self.venusian.attach(wrapped, callback, category='pyramid', + depth=depth) if info.scope == 'class': # if the decorator was attached to a method in a class, or @@ -68,7 +68,7 @@ testing_extras = tests_require + [ ] setup(name='pyramid', - version='1.4a3', + version='1.4a4', description=('The Pyramid web application development framework, a ' 'Pylons project'), long_description=README + '\n\n' + CHANGES, |
