summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt62
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--TODO.txt5
-rw-r--r--docs/conf.py2
-rw-r--r--docs/narr/hooks.rst13
-rw-r--r--docs/whatsnew-1.3.rst12
-rw-r--r--docs/whatsnew-1.4.rst41
-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.py69
-rw-r--r--pyramid/config/views.py70
-rw-r--r--pyramid/router.py2
-rw-r--r--pyramid/scaffolds/copydir.py2
-rw-r--r--pyramid/scripts/pviews.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_router.py17
-rw-r--r--pyramid/tests/test_scripts/test_pviews.py2
-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
-rw-r--r--setup.py2
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
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/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
diff --git a/setup.py b/setup.py
index 4ea63a3ee..2356e76ac 100644
--- a/setup.py
+++ b/setup.py
@@ -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,