summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt17
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--TODO.txt5
-rw-r--r--docs/narr/hooks.rst13
-rw-r--r--docs/whatsnew-1.3.rst12
-rw-r--r--pyramid/config/__init__.py11
-rw-r--r--pyramid/config/assets.py2
-rw-r--r--pyramid/config/factories.py8
-rw-r--r--pyramid/config/i18n.py3
-rw-r--r--pyramid/config/predicates.py9
-rw-r--r--pyramid/config/rendering.py2
-rw-r--r--pyramid/config/security.py2
-rw-r--r--pyramid/config/testing.py2
-rw-r--r--pyramid/config/util.py61
-rw-r--r--pyramid/config/views.py70
-rw-r--r--pyramid/scaffolds/copydir.py2
-rw-r--r--pyramid/tests/test_config/test_init.py2
-rw-r--r--pyramid/tests/test_config/test_predicates.py12
-rw-r--r--pyramid/tests/test_config/test_util.py30
-rw-r--r--pyramid/tests/test_config/test_views.py22
-rw-r--r--pyramid/tests/test_util.py31
-rw-r--r--pyramid/tests/test_view.py44
-rw-r--r--pyramid/util.py67
-rw-r--r--pyramid/view.py20
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
diff --git a/TODO.txt b/TODO.txt
index af164adc9..46edd8c6b 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -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