diff options
| -rw-r--r-- | CHANGES.txt | 17 | ||||
| -rw-r--r-- | CONTRIBUTORS.txt | 2 | ||||
| -rw-r--r-- | TODO.txt | 5 | ||||
| -rw-r--r-- | docs/narr/hooks.rst | 13 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 12 | ||||
| -rw-r--r-- | pyramid/config/__init__.py | 11 | ||||
| -rw-r--r-- | pyramid/config/assets.py | 2 | ||||
| -rw-r--r-- | pyramid/config/factories.py | 8 | ||||
| -rw-r--r-- | pyramid/config/i18n.py | 3 | ||||
| -rw-r--r-- | pyramid/config/predicates.py | 9 | ||||
| -rw-r--r-- | pyramid/config/rendering.py | 2 | ||||
| -rw-r--r-- | pyramid/config/security.py | 2 | ||||
| -rw-r--r-- | pyramid/config/testing.py | 2 | ||||
| -rw-r--r-- | pyramid/config/util.py | 61 | ||||
| -rw-r--r-- | pyramid/config/views.py | 70 | ||||
| -rw-r--r-- | pyramid/scaffolds/copydir.py | 2 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_init.py | 2 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_predicates.py | 12 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_util.py | 30 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 22 | ||||
| -rw-r--r-- | pyramid/tests/test_util.py | 31 | ||||
| -rw-r--r-- | pyramid/tests/test_view.py | 44 | ||||
| -rw-r--r-- | pyramid/util.py | 67 | ||||
| -rw-r--r-- | pyramid/view.py | 20 |
24 files changed, 301 insertions, 148 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 43a910f96..16e3d8586 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -33,6 +33,15 @@ Features 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,6 +57,14 @@ 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. + +- :func:`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 ------------ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index d03da3e62..34d904d0f 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -188,3 +188,5 @@ Contributors - Domen Kozar, 2012/09/11 - David Gay, 2012/09/16 + +- Robert Jackiewicz, 2012/11/12 @@ -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/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/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..c16755a75 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,) 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/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/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_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 |
