summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMichael Merickel <github@m.merickel.org>2018-10-15 09:03:53 -0500
committerGitHub <noreply@github.com>2018-10-15 09:03:53 -0500
commit81576ee51564c49d5ff3c1c07f214f22a8438231 (patch)
tree5b3fe0b39a0fc33d545733d821738845909f638c /src
parent433efe06191a7007ca8c5bf8fafee5c7c1439ebb (diff)
parent17e3abf320f6d9cd90f7e5a0352280c2fef584af (diff)
downloadpyramid-81576ee51564c49d5ff3c1c07f214f22a8438231.tar.gz
pyramid-81576ee51564c49d5ff3c1c07f214f22a8438231.tar.bz2
pyramid-81576ee51564c49d5ff3c1c07f214f22a8438231.zip
Merge pull request #3387 from mmerickel/src-folder-refactor
refactor pyramid tests into a tests folder and package into a src folder
Diffstat (limited to 'src')
-rw-r--r--src/pyramid/__init__.py1
-rw-r--r--src/pyramid/asset.py44
-rw-r--r--src/pyramid/authentication.py1201
-rw-r--r--src/pyramid/authorization.py146
-rw-r--r--src/pyramid/compat.py281
-rw-r--r--src/pyramid/config/__init__.py1409
-rw-r--r--src/pyramid/config/adapters.py326
-rw-r--r--src/pyramid/config/assets.py396
-rw-r--r--src/pyramid/config/factories.py245
-rw-r--r--src/pyramid/config/i18n.py120
-rw-r--r--src/pyramid/config/predicates.py2
-rw-r--r--src/pyramid/config/rendering.py51
-rw-r--r--src/pyramid/config/routes.py560
-rw-r--r--src/pyramid/config/security.py265
-rw-r--r--src/pyramid/config/settings.py107
-rw-r--r--src/pyramid/config/testing.py167
-rw-r--r--src/pyramid/config/tweens.py196
-rw-r--r--src/pyramid/config/util.py281
-rw-r--r--src/pyramid/config/views.py2327
-rw-r--r--src/pyramid/config/zca.py20
-rw-r--r--src/pyramid/csrf.py336
-rw-r--r--src/pyramid/decorator.py45
-rw-r--r--src/pyramid/encode.py84
-rw-r--r--src/pyramid/events.py289
-rw-r--r--src/pyramid/exceptions.py127
-rw-r--r--src/pyramid/httpexceptions.py1182
-rw-r--r--src/pyramid/i18n.py397
-rw-r--r--src/pyramid/interfaces.py1354
-rw-r--r--src/pyramid/location.py66
-rw-r--r--src/pyramid/paster.py111
-rw-r--r--src/pyramid/path.py436
-rw-r--r--src/pyramid/predicates.py336
-rw-r--r--src/pyramid/registry.py297
-rw-r--r--src/pyramid/renderers.py529
-rw-r--r--src/pyramid/request.py334
-rw-r--r--src/pyramid/resource.py5
-rw-r--r--src/pyramid/response.py211
-rw-r--r--src/pyramid/router.py278
-rw-r--r--src/pyramid/scaffolds/__init__.py65
-rw-r--r--src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl3
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/__init__.py12
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl74
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/models/meta.py16
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/models/mymodel.py18
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/routes.py3
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py1
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py45
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/static/pyramid.pngbin0 -> 12901 bytes
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/static/theme.css154
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl8
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl66
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl8
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl65
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/views/__init__.py0
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl33
-rw-r--r--src/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl7
-rw-r--r--src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl4
-rw-r--r--src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl2
-rw-r--r--src/pyramid/scaffolds/alchemy/README.txt_tmpl14
-rw-r--r--src/pyramid/scaffolds/alchemy/development.ini_tmpl69
-rw-r--r--src/pyramid/scaffolds/alchemy/production.ini_tmpl59
-rw-r--r--src/pyramid/scaffolds/alchemy/pytest.ini_tmpl3
-rw-r--r--src/pyramid/scaffolds/alchemy/setup.py_tmpl55
-rw-r--r--src/pyramid/scaffolds/copydir.py301
-rw-r--r--src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl3
-rw-r--r--src/pyramid/scaffolds/starter/+package+/__init__.py12
-rw-r--r--src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--src/pyramid/scaffolds/starter/+package+/static/pyramid.pngbin0 -> 12901 bytes
-rw-r--r--src/pyramid/scaffolds/starter/+package+/static/theme.css152
-rw-r--r--src/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl66
-rw-r--r--src/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl8
-rw-r--r--src/pyramid/scaffolds/starter/+package+/tests.py_tmpl29
-rw-r--r--src/pyramid/scaffolds/starter/+package+/views.py_tmpl6
-rw-r--r--src/pyramid/scaffolds/starter/CHANGES.txt_tmpl4
-rw-r--r--src/pyramid/scaffolds/starter/MANIFEST.in_tmpl2
-rw-r--r--src/pyramid/scaffolds/starter/README.txt_tmpl12
-rw-r--r--src/pyramid/scaffolds/starter/development.ini_tmpl59
-rw-r--r--src/pyramid/scaffolds/starter/production.ini_tmpl53
-rw-r--r--src/pyramid/scaffolds/starter/pytest.ini_tmpl3
-rw-r--r--src/pyramid/scaffolds/starter/setup.py_tmpl49
-rw-r--r--src/pyramid/scaffolds/template.py172
-rw-r--r--src/pyramid/scaffolds/tests.py75
-rw-r--r--src/pyramid/scaffolds/zodb/+dot+coveragerc_tmpl3
-rw-r--r--src/pyramid/scaffolds/zodb/+package+/__init__.py20
-rw-r--r--src/pyramid/scaffolds/zodb/+package+/models.py14
-rw-r--r--src/pyramid/scaffolds/zodb/+package+/static/pyramid-16x16.pngbin0 -> 1319 bytes
-rw-r--r--src/pyramid/scaffolds/zodb/+package+/static/pyramid.pngbin0 -> 12901 bytes
-rw-r--r--src/pyramid/scaffolds/zodb/+package+/static/theme.css154
-rw-r--r--src/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt_tmpl67
-rw-r--r--src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl17
-rw-r--r--src/pyramid/scaffolds/zodb/+package+/views.py_tmpl7
-rw-r--r--src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl4
-rw-r--r--src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl2
-rw-r--r--src/pyramid/scaffolds/zodb/README.txt_tmpl12
-rw-r--r--src/pyramid/scaffolds/zodb/development.ini_tmpl64
-rw-r--r--src/pyramid/scaffolds/zodb/production.ini_tmpl59
-rw-r--r--src/pyramid/scaffolds/zodb/pytest.ini_tmpl3
-rw-r--r--src/pyramid/scaffolds/zodb/setup.py_tmpl53
-rw-r--r--src/pyramid/scripting.py141
-rw-r--r--src/pyramid/scripts/__init__.py1
-rw-r--r--src/pyramid/scripts/common.py23
-rw-r--r--src/pyramid/scripts/pcreate.py251
-rw-r--r--src/pyramid/scripts/pdistreport.py43
-rw-r--r--src/pyramid/scripts/prequest.py207
-rw-r--r--src/pyramid/scripts/proutes.py416
-rw-r--r--src/pyramid/scripts/pserve.py383
-rw-r--r--src/pyramid/scripts/pshell.py270
-rw-r--r--src/pyramid/scripts/ptweens.py109
-rw-r--r--src/pyramid/scripts/pviews.py289
-rw-r--r--src/pyramid/security.py427
-rw-r--r--src/pyramid/session.py712
-rw-r--r--src/pyramid/settings.py33
-rw-r--r--src/pyramid/static.py301
-rw-r--r--src/pyramid/testing.py641
-rw-r--r--src/pyramid/threadlocal.py83
-rw-r--r--src/pyramid/traversal.py760
-rw-r--r--src/pyramid/tweens.py48
-rw-r--r--src/pyramid/url.py894
-rw-r--r--src/pyramid/urldispatch.py249
-rw-r--r--src/pyramid/util.py651
-rw-r--r--src/pyramid/view.py761
-rw-r--r--src/pyramid/viewderivers.py472
-rw-r--r--src/pyramid/wsgi.py85
124 files changed, 25081 insertions, 0 deletions
diff --git a/src/pyramid/__init__.py b/src/pyramid/__init__.py
new file mode 100644
index 000000000..5bb534f79
--- /dev/null
+++ b/src/pyramid/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/src/pyramid/asset.py b/src/pyramid/asset.py
new file mode 100644
index 000000000..9d7a3ee63
--- /dev/null
+++ b/src/pyramid/asset.py
@@ -0,0 +1,44 @@
+import os
+import pkg_resources
+
+from pyramid.compat import string_types
+
+from pyramid.path import (
+ package_path,
+ package_name,
+ )
+
+def resolve_asset_spec(spec, pname='__main__'):
+ if pname and not isinstance(pname, string_types):
+ pname = pname.__name__ # as package
+ if os.path.isabs(spec):
+ return None, spec
+ filename = spec
+ if ':' in spec:
+ pname, filename = spec.split(':', 1)
+ elif pname is None:
+ pname, filename = None, spec
+ return pname, filename
+
+def asset_spec_from_abspath(abspath, package):
+ """ Try to convert an absolute path to a resource in a package to
+ a resource specification if possible; otherwise return the
+ absolute path. """
+ if getattr(package, '__name__', None) == '__main__':
+ return abspath
+ pp = package_path(package) + os.path.sep
+ if abspath.startswith(pp):
+ relpath = abspath[len(pp):]
+ return '%s:%s' % (package_name(package),
+ relpath.replace(os.path.sep, '/'))
+ return abspath
+
+# bw compat only; use pyramid.path.AssetResolver().resolve(spec).abspath()
+def abspath_from_asset_spec(spec, pname='__main__'):
+ if pname is None:
+ return spec
+ pname, filename = resolve_asset_spec(spec, pname)
+ if pname is None:
+ return filename
+ return pkg_resources.resource_filename(pname, filename)
+
diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py
new file mode 100644
index 000000000..a9604e336
--- /dev/null
+++ b/src/pyramid/authentication.py
@@ -0,0 +1,1201 @@
+import binascii
+from codecs import utf_8_decode
+from codecs import utf_8_encode
+from collections import namedtuple
+import hashlib
+import base64
+import re
+import time as time_mod
+import warnings
+
+from zope.interface import implementer
+
+from webob.cookies import CookieProfile
+
+from pyramid.compat import (
+ long,
+ text_type,
+ binary_type,
+ url_unquote,
+ url_quote,
+ bytes_,
+ ascii_native_,
+ native_,
+ )
+
+from pyramid.interfaces import (
+ IAuthenticationPolicy,
+ IDebugLogger,
+ )
+
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+ )
+
+from pyramid.util import strings_differ
+from pyramid.util import SimpleSerializer
+
+VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$")
+
+
+class CallbackAuthenticationPolicy(object):
+ """ Abstract class """
+
+ debug = False
+ callback = None
+
+ def _log(self, msg, methodname, request):
+ logger = request.registry.queryUtility(IDebugLogger)
+ if logger:
+ cls = self.__class__
+ classname = cls.__module__ + '.' + cls.__name__
+ methodname = classname + '.' + methodname
+ logger.debug(methodname + ': ' + msg)
+
+ def _clean_principal(self, princid):
+ if princid in (Authenticated, Everyone):
+ princid = None
+ return princid
+
+ def authenticated_userid(self, request):
+ """ Return the authenticated userid or ``None``.
+
+ If no callback is registered, this will be the same as
+ ``unauthenticated_userid``.
+
+ If a ``callback`` is registered, this will return the userid if
+ and only if the callback returns a value that is not ``None``.
+
+ """
+ debug = self.debug
+ userid = self.unauthenticated_userid(request)
+ if userid is None:
+ debug and self._log(
+ 'call to unauthenticated_userid returned None; returning None',
+ 'authenticated_userid',
+ request)
+ return None
+ if self._clean_principal(userid) is None:
+ debug and self._log(
+ ('use of userid %r is disallowed by any built-in Pyramid '
+ 'security policy, returning None' % userid),
+ 'authenticated_userid',
+ request)
+ return None
+
+ if self.callback is None:
+ debug and self._log(
+ 'there was no groupfinder callback; returning %r' % (userid,),
+ 'authenticated_userid',
+ request)
+ return userid
+ callback_ok = self.callback(userid, request)
+ if callback_ok is not None: # is not None!
+ debug and self._log(
+ 'groupfinder callback returned %r; returning %r' % (
+ callback_ok, userid),
+ 'authenticated_userid',
+ request
+ )
+ return userid
+ debug and self._log(
+ 'groupfinder callback returned None; returning None',
+ 'authenticated_userid',
+ request
+ )
+
+ def effective_principals(self, request):
+ """ A list of effective principals derived from request.
+
+ This will return a list of principals including, at least,
+ :data:`pyramid.security.Everyone`. If there is no authenticated
+ userid, or the ``callback`` returns ``None``, this will be the
+ only principal:
+
+ .. code-block:: python
+
+ return [Everyone]
+
+ If the ``callback`` does not return ``None`` and an authenticated
+ userid is found, then the principals will include
+ :data:`pyramid.security.Authenticated`, the ``authenticated_userid``
+ and the list of principals returned by the ``callback``:
+
+ .. code-block:: python
+
+ extra_principals = callback(userid, request)
+ return [Everyone, Authenticated, userid] + extra_principals
+
+ """
+ debug = self.debug
+ effective_principals = [Everyone]
+ userid = self.unauthenticated_userid(request)
+
+ if userid is None:
+ debug and self._log(
+ 'unauthenticated_userid returned %r; returning %r' % (
+ userid, effective_principals),
+ 'effective_principals',
+ request
+ )
+ return effective_principals
+
+ if self._clean_principal(userid) is None:
+ debug and self._log(
+ ('unauthenticated_userid returned disallowed %r; returning %r '
+ 'as if it was None' % (userid, effective_principals)),
+ 'effective_principals',
+ request
+ )
+ return effective_principals
+
+ if self.callback is None:
+ debug and self._log(
+ 'groupfinder callback is None, so groups is []',
+ 'effective_principals',
+ request)
+ groups = []
+ else:
+ groups = self.callback(userid, request)
+ debug and self._log(
+ 'groupfinder callback returned %r as groups' % (groups,),
+ 'effective_principals',
+ request)
+
+ if groups is None: # is None!
+ debug and self._log(
+ 'returning effective principals: %r' % (
+ effective_principals,),
+ 'effective_principals',
+ request
+ )
+ return effective_principals
+
+ effective_principals.append(Authenticated)
+ effective_principals.append(userid)
+ effective_principals.extend(groups)
+
+ debug and self._log(
+ 'returning effective principals: %r' % (
+ effective_principals,),
+ 'effective_principals',
+ request
+ )
+ return effective_principals
+
+
+@implementer(IAuthenticationPolicy)
+class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy):
+ """ A :app:`Pyramid` :term:`authentication policy` which
+ obtains data from the :mod:`repoze.who` 1.X WSGI 'API' (the
+ ``repoze.who.identity`` key in the WSGI environment).
+
+ Constructor Arguments
+
+ ``identifier_name``
+
+ Default: ``auth_tkt``. The :mod:`repoze.who` plugin name that
+ performs remember/forget. Optional.
+
+ ``callback``
+
+ Default: ``None``. A callback passed the :mod:`repoze.who` identity
+ and the :term:`request`, expected to return ``None`` if the user
+ represented by the identity doesn't exist or a sequence of principal
+ identifiers (possibly empty) representing groups if the user does
+ exist. If ``callback`` is None, the userid will be assumed to exist
+ with no group principals.
+
+ Objects of this class implement the interface described by
+ :class:`pyramid.interfaces.IAuthenticationPolicy`.
+ """
+
+ def __init__(self, identifier_name='auth_tkt', callback=None):
+ self.identifier_name = identifier_name
+ self.callback = callback
+
+ def _get_identity(self, request):
+ return request.environ.get('repoze.who.identity')
+
+ def _get_identifier(self, request):
+ plugins = request.environ.get('repoze.who.plugins')
+ if plugins is None:
+ return None
+ identifier = plugins[self.identifier_name]
+ return identifier
+
+ def authenticated_userid(self, request):
+ """ Return the authenticated userid or ``None``.
+
+ If no callback is registered, this will be the same as
+ ``unauthenticated_userid``.
+
+ If a ``callback`` is registered, this will return the userid if
+ and only if the callback returns a value that is not ``None``.
+
+ """
+ identity = self._get_identity(request)
+
+ if identity is None:
+ self.debug and self._log(
+ 'repoze.who identity is None, returning None',
+ 'authenticated_userid',
+ request)
+ return None
+
+ userid = identity['repoze.who.userid']
+
+ if userid is None:
+ self.debug and self._log(
+ 'repoze.who.userid is None, returning None' % userid,
+ 'authenticated_userid',
+ request)
+ return None
+
+ if self._clean_principal(userid) is None:
+ self.debug and self._log(
+ ('use of userid %r is disallowed by any built-in Pyramid '
+ 'security policy, returning None' % userid),
+ 'authenticated_userid',
+ request)
+ return None
+
+ if self.callback is None:
+ return userid
+
+ if self.callback(identity, request) is not None: # is not None!
+ return userid
+
+ def unauthenticated_userid(self, request):
+ """ Return the ``repoze.who.userid`` key from the detected identity."""
+ identity = self._get_identity(request)
+ if identity is None:
+ return None
+ return identity['repoze.who.userid']
+
+ def effective_principals(self, request):
+ """ A list of effective principals derived from the identity.
+
+ This will return a list of principals including, at least,
+ :data:`pyramid.security.Everyone`. If there is no identity, or
+ the ``callback`` returns ``None``, this will be the only principal.
+
+ If the ``callback`` does not return ``None`` and an identity is
+ found, then the principals will include
+ :data:`pyramid.security.Authenticated`, the ``authenticated_userid``
+ and the list of principals returned by the ``callback``.
+
+ """
+ effective_principals = [Everyone]
+ identity = self._get_identity(request)
+
+ if identity is None:
+ self.debug and self._log(
+ ('repoze.who identity was None; returning %r' %
+ effective_principals),
+ 'effective_principals',
+ request
+ )
+ return effective_principals
+
+ if self.callback is None:
+ groups = []
+ else:
+ groups = self.callback(identity, request)
+
+ if groups is None: # is None!
+ self.debug and self._log(
+ ('security policy groups callback returned None; returning %r' %
+ effective_principals),
+ 'effective_principals',
+ request
+ )
+ return effective_principals
+
+ userid = identity['repoze.who.userid']
+
+ if userid is None:
+ self.debug and self._log(
+ ('repoze.who.userid was None; returning %r' %
+ effective_principals),
+ 'effective_principals',
+ request
+ )
+ return effective_principals
+
+ if self._clean_principal(userid) is None:
+ self.debug and self._log(
+ ('unauthenticated_userid returned disallowed %r; returning %r '
+ 'as if it was None' % (userid, effective_principals)),
+ 'effective_principals',
+ request
+ )
+ return effective_principals
+
+ effective_principals.append(Authenticated)
+ effective_principals.append(userid)
+ effective_principals.extend(groups)
+ return effective_principals
+
+ def remember(self, request, userid, **kw):
+ """ Store the ``userid`` as ``repoze.who.userid``.
+
+ The identity to authenticated to :mod:`repoze.who`
+ will contain the given userid as ``userid``, and
+ provide all keyword arguments as additional identity
+ keys. Useful keys could be ``max_age`` or ``userdata``.
+ """
+ identifier = self._get_identifier(request)
+ if identifier is None:
+ return []
+ environ = request.environ
+ identity = kw
+ identity['repoze.who.userid'] = userid
+ return identifier.remember(environ, identity)
+
+ def forget(self, request):
+ """ Forget the current authenticated user.
+
+ Return headers that, if included in a response, will delete the
+ cookie responsible for tracking the current user.
+
+ """
+ identifier = self._get_identifier(request)
+ if identifier is None:
+ return []
+ identity = self._get_identity(request)
+ return identifier.forget(request.environ, identity)
+
+@implementer(IAuthenticationPolicy)
+class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy):
+ """ A :app:`Pyramid` :term:`authentication policy` which
+ obtains data from the ``REMOTE_USER`` WSGI environment variable.
+
+ Constructor Arguments
+
+ ``environ_key``
+
+ Default: ``REMOTE_USER``. The key in the WSGI environ which
+ provides the userid.
+
+ ``callback``
+
+ Default: ``None``. A callback passed the userid and the request,
+ expected to return None if the userid doesn't exist or a sequence of
+ principal identifiers (possibly empty) representing groups if the
+ user does exist. If ``callback`` is None, the userid will be assumed
+ to exist with no group principals.
+
+ ``debug``
+
+ Default: ``False``. If ``debug`` is ``True``, log messages to the
+ Pyramid debug logger about the results of various authentication
+ steps. The output from debugging is useful for reporting to maillist
+ or IRC channels when asking for support.
+
+ Objects of this class implement the interface described by
+ :class:`pyramid.interfaces.IAuthenticationPolicy`.
+ """
+
+ def __init__(self, environ_key='REMOTE_USER', callback=None, debug=False):
+ self.environ_key = environ_key
+ self.callback = callback
+ self.debug = debug
+
+ def unauthenticated_userid(self, request):
+ """ The ``REMOTE_USER`` value found within the ``environ``."""
+ return request.environ.get(self.environ_key)
+
+ def remember(self, request, userid, **kw):
+ """ A no-op. The ``REMOTE_USER`` does not provide a protocol for
+ remembering the user. This will be application-specific and can
+ be done somewhere else or in a subclass."""
+ return []
+
+ def forget(self, request):
+ """ A no-op. The ``REMOTE_USER`` does not provide a protocol for
+ forgetting the user. This will be application-specific and can
+ be done somewhere else or in a subclass."""
+ return []
+
+@implementer(IAuthenticationPolicy)
+class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy):
+ """A :app:`Pyramid` :term:`authentication policy` which
+ obtains data from a Pyramid "auth ticket" cookie.
+
+ Constructor Arguments
+
+ ``secret``
+
+ The secret (a string) used for auth_tkt cookie signing. This value
+ should be unique across all values provided to Pyramid for various
+ subsystem secrets (see :ref:`admonishment_against_secret_sharing`).
+ Required.
+
+ ``callback``
+
+ Default: ``None``. A callback passed the userid and the
+ request, expected to return ``None`` if the userid doesn't
+ exist or a sequence of principal identifiers (possibly empty) if
+ the user does exist. If ``callback`` is ``None``, the userid
+ will be assumed to exist with no principals. Optional.
+
+ ``cookie_name``
+
+ Default: ``auth_tkt``. The cookie name used
+ (string). Optional.
+
+ ``secure``
+
+ Default: ``False``. Only send the cookie back over a secure
+ conn. Optional.
+
+ ``include_ip``
+
+ Default: ``False``. Make the requesting IP address part of
+ the authentication data in the cookie. Optional.
+
+ For IPv6 this option is not recommended. The ``mod_auth_tkt``
+ specification does not specify how to handle IPv6 addresses, so using
+ this option in combination with IPv6 addresses may cause an
+ incompatible cookie. It ties the authentication ticket to that
+ individual's IPv6 address.
+
+ ``timeout``
+
+ Default: ``None``. Maximum number of seconds which a newly
+ issued ticket will be considered valid. After this amount of
+ time, the ticket will expire (effectively logging the user
+ out). If this value is ``None``, the ticket never expires.
+ Optional.
+
+ ``reissue_time``
+
+ Default: ``None``. If this parameter is set, it represents the number
+ of seconds that must pass before an authentication token cookie is
+ automatically reissued as the result of a request which requires
+ authentication. The duration is measured as the number of seconds
+ since the last auth_tkt cookie was issued and 'now'. If this value is
+ ``0``, a new ticket cookie will be reissued on every request which
+ requires authentication.
+
+ A good rule of thumb: if you want auto-expired cookies based on
+ inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
+ ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
+ (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
+ than the ``reissue_time`` value, as the ticket will never be reissued
+ if so. However, such a configuration is not explicitly prevented.
+
+ Optional.
+
+ ``max_age``
+
+ Default: ``None``. The max age of the auth_tkt cookie, in
+ seconds. This differs from ``timeout`` inasmuch as ``timeout``
+ represents the lifetime of the ticket contained in the cookie,
+ while this value represents the lifetime of the cookie itself.
+ When this value is set, the cookie's ``Max-Age`` and
+ ``Expires`` settings will be set, allowing the auth_tkt cookie
+ to last between browser sessions. It is typically nonsensical
+ to set this to a value that is lower than ``timeout`` or
+ ``reissue_time``, although it is not explicitly prevented.
+ Optional.
+
+ ``path``
+
+ Default: ``/``. The path for which the auth_tkt cookie is valid.
+ May be desirable if the application only serves part of a domain.
+ Optional.
+
+ ``http_only``
+
+ Default: ``False``. Hide cookie from JavaScript by setting the
+ HttpOnly flag. Not honored by all browsers.
+ Optional.
+
+ ``wild_domain``
+
+ Default: ``True``. An auth_tkt cookie will be generated for the
+ wildcard domain. If your site is hosted as ``example.com`` this
+ will make the cookie available for sites underneath ``example.com``
+ such as ``www.example.com``.
+ Optional.
+
+ ``parent_domain``
+
+ Default: ``False``. An auth_tkt cookie will be generated for the
+ parent domain of the current site. For example if your site is
+ hosted under ``www.example.com`` a cookie will be generated for
+ ``.example.com``. This can be useful if you have multiple sites
+ sharing the same domain. This option supercedes the ``wild_domain``
+ option.
+ Optional.
+
+ ``domain``
+
+ Default: ``None``. If provided the auth_tkt cookie will only be
+ set for this domain. This option is not compatible with ``wild_domain``
+ and ``parent_domain``.
+ Optional.
+
+ ``hashalg``
+
+ Default: ``sha512`` (the literal string).
+
+ Any hash algorithm supported by Python's ``hashlib.new()`` function
+ can be used as the ``hashalg``.
+
+ Cookies generated by different instances of AuthTktAuthenticationPolicy
+ using different ``hashalg`` options are not compatible. Switching the
+ ``hashalg`` will imply that all existing users with a valid cookie will
+ be required to re-login.
+
+ Optional.
+
+ ``debug``
+
+ Default: ``False``. If ``debug`` is ``True``, log messages to the
+ Pyramid debug logger about the results of various authentication
+ steps. The output from debugging is useful for reporting to maillist
+ or IRC channels when asking for support.
+
+ ``samesite``
+
+ Default: ``'Lax'``. The 'samesite' option of the session cookie. Set
+ the value to ``None`` to turn off the samesite option.
+
+ This option is available as of :app:`Pyramid` 1.10.
+
+ .. versionchanged:: 1.4
+
+ Added the ``hashalg`` option, defaulting to ``sha512``.
+
+ .. versionchanged:: 1.5
+
+ Added the ``domain`` option.
+
+ Added the ``parent_domain`` option.
+
+ .. versionchanged:: 1.10
+
+ Added the ``samesite`` option and made the default ``'Lax'``.
+
+ Objects of this class implement the interface described by
+ :class:`pyramid.interfaces.IAuthenticationPolicy`.
+
+ """
+
+ def __init__(self,
+ secret,
+ callback=None,
+ cookie_name='auth_tkt',
+ secure=False,
+ include_ip=False,
+ timeout=None,
+ reissue_time=None,
+ max_age=None,
+ path="/",
+ http_only=False,
+ wild_domain=True,
+ debug=False,
+ hashalg='sha512',
+ parent_domain=False,
+ domain=None,
+ samesite='Lax',
+ ):
+ self.cookie = AuthTktCookieHelper(
+ secret,
+ cookie_name=cookie_name,
+ secure=secure,
+ include_ip=include_ip,
+ timeout=timeout,
+ reissue_time=reissue_time,
+ max_age=max_age,
+ http_only=http_only,
+ path=path,
+ wild_domain=wild_domain,
+ hashalg=hashalg,
+ parent_domain=parent_domain,
+ domain=domain,
+ samesite=samesite,
+ )
+ self.callback = callback
+ self.debug = debug
+
+ def unauthenticated_userid(self, request):
+ """ The userid key within the auth_tkt cookie."""
+ result = self.cookie.identify(request)
+ if result:
+ return result['userid']
+
+ def remember(self, request, userid, **kw):
+ """ Accepts the following kw args: ``max_age=<int-seconds>,
+ ``tokens=<sequence-of-ascii-strings>``.
+
+ Return a list of headers which will set appropriate cookies on
+ the response.
+
+ """
+ return self.cookie.remember(request, userid, **kw)
+
+ def forget(self, request):
+ """ A list of headers which will delete appropriate cookies."""
+ return self.cookie.forget(request)
+
+def b64encode(v):
+ return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'')
+
+def b64decode(v):
+ return base64.b64decode(bytes_(v))
+
+# this class licensed under the MIT license (stolen from Paste)
+class AuthTicket(object):
+ """
+ This class represents an authentication token. You must pass in
+ the shared secret, the userid, and the IP address. Optionally you
+ can include tokens (a list of strings, representing role names),
+ 'user_data', which is arbitrary data available for your own use in
+ later scripts. Lastly, you can override the cookie name and
+ timestamp.
+
+ Once you provide all the arguments, use .cookie_value() to
+ generate the appropriate authentication ticket.
+
+ Usage::
+
+ token = AuthTicket('sharedsecret', 'username',
+ os.environ['REMOTE_ADDR'], tokens=['admin'])
+ val = token.cookie_value()
+
+ """
+
+ def __init__(self, secret, userid, ip, tokens=(), user_data='',
+ time=None, cookie_name='auth_tkt', secure=False,
+ hashalg='md5'):
+ self.secret = secret
+ self.userid = userid
+ self.ip = ip
+ self.tokens = ','.join(tokens)
+ self.user_data = user_data
+ if time is None:
+ self.time = time_mod.time()
+ else:
+ self.time = time
+ self.cookie_name = cookie_name
+ self.secure = secure
+ self.hashalg = hashalg
+
+ def digest(self):
+ return calculate_digest(
+ self.ip, self.time, self.secret, self.userid, self.tokens,
+ self.user_data, self.hashalg)
+
+ def cookie_value(self):
+ v = '%s%08x%s!' % (self.digest(), int(self.time),
+ url_quote(self.userid))
+ if self.tokens:
+ v += self.tokens + '!'
+ v += self.user_data
+ return v
+
+# this class licensed under the MIT license (stolen from Paste)
+class BadTicket(Exception):
+ """
+ Exception raised when a ticket can't be parsed. If we get far enough to
+ determine what the expected digest should have been, expected is set.
+ This should not be shown by default, but can be useful for debugging.
+ """
+ def __init__(self, msg, expected=None):
+ self.expected = expected
+ Exception.__init__(self, msg)
+
+# this function licensed under the MIT license (stolen from Paste)
+def parse_ticket(secret, ticket, ip, hashalg='md5'):
+ """
+ Parse the ticket, returning (timestamp, userid, tokens, user_data).
+
+ If the ticket cannot be parsed, a ``BadTicket`` exception will be raised
+ with an explanation.
+ """
+ ticket = native_(ticket).strip('"')
+ digest_size = hashlib.new(hashalg).digest_size * 2
+ digest = ticket[:digest_size]
+ try:
+ timestamp = int(ticket[digest_size:digest_size + 8], 16)
+ except ValueError as e:
+ raise BadTicket('Timestamp is not a hex integer: %s' % e)
+ try:
+ userid, data = ticket[digest_size + 8:].split('!', 1)
+ except ValueError:
+ raise BadTicket('userid is not followed by !')
+ userid = url_unquote(userid)
+ if '!' in data:
+ tokens, user_data = data.split('!', 1)
+ else: # pragma: no cover (never generated)
+ # @@: Is this the right order?
+ tokens = ''
+ user_data = data
+
+ expected = calculate_digest(ip, timestamp, secret,
+ userid, tokens, user_data, hashalg)
+
+ # Avoid timing attacks (see
+ # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf)
+ if strings_differ(expected, digest):
+ raise BadTicket('Digest signature is not correct',
+ expected=(expected, digest))
+
+ tokens = tokens.split(',')
+
+ return (timestamp, userid, tokens, user_data)
+
+# this function licensed under the MIT license (stolen from Paste)
+def calculate_digest(ip, timestamp, secret, userid, tokens, user_data,
+ hashalg='md5'):
+ secret = bytes_(secret, 'utf-8')
+ userid = bytes_(userid, 'utf-8')
+ tokens = bytes_(tokens, 'utf-8')
+ user_data = bytes_(user_data, 'utf-8')
+ hash_obj = hashlib.new(hashalg)
+
+ # Check to see if this is an IPv6 address
+ if ':' in ip:
+ ip_timestamp = ip + str(int(timestamp))
+ ip_timestamp = bytes_(ip_timestamp)
+ else:
+ # encode_ip_timestamp not required, left in for backwards compatibility
+ ip_timestamp = encode_ip_timestamp(ip, timestamp)
+
+ hash_obj.update(ip_timestamp + secret + userid + b'\0' +
+ tokens + b'\0' + user_data)
+ digest = hash_obj.hexdigest()
+ hash_obj2 = hashlib.new(hashalg)
+ hash_obj2.update(bytes_(digest) + secret)
+ return hash_obj2.hexdigest()
+
+# this function licensed under the MIT license (stolen from Paste)
+def encode_ip_timestamp(ip, timestamp):
+ ip_chars = ''.join(map(chr, map(int, ip.split('.'))))
+ t = int(timestamp)
+ ts = ((t & 0xff000000) >> 24,
+ (t & 0xff0000) >> 16,
+ (t & 0xff00) >> 8,
+ t & 0xff)
+ ts_chars = ''.join(map(chr, ts))
+ return bytes_(ip_chars + ts_chars)
+
+class AuthTktCookieHelper(object):
+ """
+ A helper class for use in third-party authentication policy
+ implementations. See
+ :class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the
+ meanings of the constructor arguments.
+ """
+ parse_ticket = staticmethod(parse_ticket) # for tests
+ AuthTicket = AuthTicket # for tests
+ BadTicket = BadTicket # for tests
+ now = None # for tests
+
+ userid_type_decoders = {
+ 'int':int,
+ 'unicode':lambda x: utf_8_decode(x)[0], # bw compat for old cookies
+ 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0],
+ 'b64str': lambda x: b64decode(x),
+ }
+
+ userid_type_encoders = {
+ int: ('int', str),
+ long: ('int', str),
+ text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])),
+ binary_type: ('b64str', lambda x: b64encode(x)),
+ }
+
+ def __init__(self,
+ secret,
+ cookie_name='auth_tkt',
+ secure=False,
+ include_ip=False,
+ timeout=None,
+ reissue_time=None,
+ max_age=None,
+ http_only=False,
+ path="/",
+ wild_domain=True,
+ hashalg='md5',
+ parent_domain=False,
+ domain=None,
+ samesite='Lax',
+ ):
+
+ serializer = SimpleSerializer()
+
+ self.cookie_profile = CookieProfile(
+ cookie_name=cookie_name,
+ secure=secure,
+ max_age=max_age,
+ httponly=http_only,
+ path=path,
+ serializer=serializer,
+ samesite=samesite,
+ )
+
+ self.secret = secret
+ self.cookie_name = cookie_name
+ self.secure = secure
+ self.include_ip = include_ip
+ self.timeout = timeout if timeout is None else int(timeout)
+ self.reissue_time = reissue_time if reissue_time is None else int(reissue_time)
+ self.max_age = max_age if max_age is None else int(max_age)
+ self.wild_domain = wild_domain
+ self.parent_domain = parent_domain
+ self.domain = domain
+ self.hashalg = hashalg
+
+ def _get_cookies(self, request, value, max_age=None):
+ cur_domain = request.domain
+
+ domains = []
+ if self.domain:
+ domains.append(self.domain)
+ else:
+ if self.parent_domain and cur_domain.count('.') > 1:
+ domains.append('.' + cur_domain.split('.', 1)[1])
+ else:
+ domains.append(None)
+ domains.append(cur_domain)
+ if self.wild_domain:
+ domains.append('.' + cur_domain)
+
+ profile = self.cookie_profile(request)
+
+ kw = {}
+ kw['domains'] = domains
+ if max_age is not None:
+ kw['max_age'] = max_age
+
+ headers = profile.get_headers(value, **kw)
+ return headers
+
+ def identify(self, request):
+ """ Return a dictionary with authentication information, or ``None``
+ if no valid auth_tkt is attached to ``request``"""
+ environ = request.environ
+ cookie = request.cookies.get(self.cookie_name)
+
+ if cookie is None:
+ return None
+
+ if self.include_ip:
+ remote_addr = environ['REMOTE_ADDR']
+ else:
+ remote_addr = '0.0.0.0'
+
+ try:
+ timestamp, userid, tokens, user_data = self.parse_ticket(
+ self.secret, cookie, remote_addr, self.hashalg)
+ except self.BadTicket:
+ return None
+
+ now = self.now # service tests
+
+ if now is None:
+ now = time_mod.time()
+
+ if self.timeout and ( (timestamp + self.timeout) < now ):
+ # the auth_tkt data has expired
+ return None
+
+ userid_typename = 'userid_type:'
+ user_data_info = user_data.split('|')
+ for datum in filter(None, user_data_info):
+ if datum.startswith(userid_typename):
+ userid_type = datum[len(userid_typename):]
+ decoder = self.userid_type_decoders.get(userid_type)
+ if decoder:
+ userid = decoder(userid)
+
+ reissue = self.reissue_time is not None
+
+ if reissue and not hasattr(request, '_authtkt_reissued'):
+ if ( (now - timestamp) > self.reissue_time ):
+ # See https://github.com/Pylons/pyramid/issues#issue/108
+ tokens = list(filter(None, tokens))
+ headers = self.remember(request, userid, max_age=self.max_age,
+ tokens=tokens)
+ def reissue_authtkt(request, response):
+ if not hasattr(request, '_authtkt_reissue_revoked'):
+ for k, v in headers:
+ response.headerlist.append((k, v))
+ request.add_response_callback(reissue_authtkt)
+ request._authtkt_reissued = True
+
+ environ['REMOTE_USER_TOKENS'] = tokens
+ environ['REMOTE_USER_DATA'] = user_data
+ environ['AUTH_TYPE'] = 'cookie'
+
+ identity = {}
+ identity['timestamp'] = timestamp
+ identity['userid'] = userid
+ identity['tokens'] = tokens
+ identity['userdata'] = user_data
+ return identity
+
+ def forget(self, request):
+ """ Return a set of expires Set-Cookie headers, which will destroy
+ any existing auth_tkt cookie when attached to a response"""
+ request._authtkt_reissue_revoked = True
+ return self._get_cookies(request, None)
+
+ def remember(self, request, userid, max_age=None, tokens=()):
+ """ Return a set of Set-Cookie headers; when set into a response,
+ these headers will represent a valid authentication ticket.
+
+ ``max_age``
+ The max age of the auth_tkt cookie, in seconds. When this value is
+ set, the cookie's ``Max-Age`` and ``Expires`` settings will be set,
+ allowing the auth_tkt cookie to last between browser sessions. If
+ this value is ``None``, the ``max_age`` value provided to the
+ helper itself will be used as the ``max_age`` value. Default:
+ ``None``.
+
+ ``tokens``
+ A sequence of strings that will be placed into the auth_tkt tokens
+ field. Each string in the sequence must be of the Python ``str``
+ type and must match the regex ``^[A-Za-z][A-Za-z0-9+_-]*$``.
+ Tokens are available in the returned identity when an auth_tkt is
+ found in the request and unpacked. Default: ``()``.
+ """
+ max_age = self.max_age if max_age is None else int(max_age)
+
+ environ = request.environ
+
+ if self.include_ip:
+ remote_addr = environ['REMOTE_ADDR']
+ else:
+ remote_addr = '0.0.0.0'
+
+ user_data = ''
+
+ encoding_data = self.userid_type_encoders.get(type(userid))
+
+ if encoding_data:
+ encoding, encoder = encoding_data
+ else:
+ warnings.warn(
+ "userid is of type {}, and is not supported by the "
+ "AuthTktAuthenticationPolicy. Explicitly converting to string "
+ "and storing as base64. Subsequent requests will receive a "
+ "string as the userid, it will not be decoded back to the type "
+ "provided.".format(type(userid)), RuntimeWarning
+ )
+ encoding, encoder = self.userid_type_encoders.get(text_type)
+ userid = str(userid)
+
+ userid = encoder(userid)
+ user_data = 'userid_type:%s' % encoding
+
+ new_tokens = []
+ for token in tokens:
+ if isinstance(token, text_type):
+ try:
+ token = ascii_native_(token)
+ except UnicodeEncodeError:
+ raise ValueError("Invalid token %r" % (token,))
+ if not (isinstance(token, str) and VALID_TOKEN.match(token)):
+ raise ValueError("Invalid token %r" % (token,))
+ new_tokens.append(token)
+ tokens = tuple(new_tokens)
+
+ if hasattr(request, '_authtkt_reissued'):
+ request._authtkt_reissue_revoked = True
+
+ ticket = self.AuthTicket(
+ self.secret,
+ userid,
+ remote_addr,
+ tokens=tokens,
+ user_data=user_data,
+ cookie_name=self.cookie_name,
+ secure=self.secure,
+ hashalg=self.hashalg
+ )
+
+ cookie_value = ticket.cookie_value()
+ return self._get_cookies(request, cookie_value, max_age)
+
+@implementer(IAuthenticationPolicy)
+class SessionAuthenticationPolicy(CallbackAuthenticationPolicy):
+ """ A :app:`Pyramid` authentication policy which gets its data from the
+ configured :term:`session`. For this authentication policy to work, you
+ will have to follow the instructions in the :ref:`sessions_chapter` to
+ configure a :term:`session factory`.
+
+ Constructor Arguments
+
+ ``prefix``
+
+ A prefix used when storing the authentication parameters in the
+ session. Defaults to 'auth.'. Optional.
+
+ ``callback``
+
+ Default: ``None``. A callback passed the userid and the
+ request, expected to return ``None`` if the userid doesn't
+ exist or a sequence of principal identifiers (possibly empty) if
+ the user does exist. If ``callback`` is ``None``, the userid
+ will be assumed to exist with no principals. Optional.
+
+ ``debug``
+
+ Default: ``False``. If ``debug`` is ``True``, log messages to the
+ Pyramid debug logger about the results of various authentication
+ steps. The output from debugging is useful for reporting to maillist
+ or IRC channels when asking for support.
+
+ """
+
+ def __init__(self, prefix='auth.', callback=None, debug=False):
+ self.callback = callback
+ self.prefix = prefix or ''
+ self.userid_key = prefix + 'userid'
+ self.debug = debug
+
+ def remember(self, request, userid, **kw):
+ """ Store a userid in the session."""
+ request.session[self.userid_key] = userid
+ return []
+
+ def forget(self, request):
+ """ Remove the stored userid from the session."""
+ if self.userid_key in request.session:
+ del request.session[self.userid_key]
+ return []
+
+ def unauthenticated_userid(self, request):
+ return request.session.get(self.userid_key)
+
+
+@implementer(IAuthenticationPolicy)
+class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy):
+ """ A :app:`Pyramid` authentication policy which uses HTTP standard basic
+ authentication protocol to authenticate users. To use this policy you will
+ need to provide a callback which checks the supplied user credentials
+ against your source of login data.
+
+ Constructor Arguments
+
+ ``check``
+
+ A callback function passed a username, password and request, in that
+ order as positional arguments. Expected to return ``None`` if the
+ userid doesn't exist or a sequence of principal identifiers (possibly
+ empty) if the user does exist.
+
+ ``realm``
+
+ Default: ``"Realm"``. The Basic Auth Realm string. Usually displayed to
+ the user by the browser in the login dialog.
+
+ ``debug``
+
+ Default: ``False``. If ``debug`` is ``True``, log messages to the
+ Pyramid debug logger about the results of various authentication
+ steps. The output from debugging is useful for reporting to maillist
+ or IRC channels when asking for support.
+
+ **Issuing a challenge**
+
+ Regular browsers will not send username/password credentials unless they
+ first receive a challenge from the server. The following recipe will
+ register a view that will send a Basic Auth challenge to the user whenever
+ there is an attempt to call a view which results in a Forbidden response::
+
+ from pyramid.httpexceptions import HTTPUnauthorized
+ from pyramid.security import forget
+ from pyramid.view import forbidden_view_config
+
+ @forbidden_view_config()
+ def forbidden_view(request):
+ if request.authenticated_userid is None:
+ response = HTTPUnauthorized()
+ response.headers.update(forget(request))
+ return response
+ return HTTPForbidden()
+ """
+ def __init__(self, check, realm='Realm', debug=False):
+ self.check = check
+ self.realm = realm
+ self.debug = debug
+
+ def unauthenticated_userid(self, request):
+ """ The userid parsed from the ``Authorization`` request header."""
+ credentials = extract_http_basic_credentials(request)
+ if credentials:
+ return credentials.username
+
+ def remember(self, request, userid, **kw):
+ """ A no-op. Basic authentication does not provide a protocol for
+ remembering the user. Credentials are sent on every request.
+
+ """
+ return []
+
+ def forget(self, request):
+ """ Returns challenge headers. This should be attached to a response
+ to indicate that credentials are required."""
+ return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)]
+
+ def callback(self, username, request):
+ # Username arg is ignored. Unfortunately
+ # extract_http_basic_credentials winds up getting called twice when
+ # authenticated_userid is called. Avoiding that, however,
+ # winds up duplicating logic from the superclass.
+ credentials = extract_http_basic_credentials(request)
+ if credentials:
+ username, password = credentials
+ return self.check(username, password, request)
+
+
+HTTPBasicCredentials = namedtuple(
+ 'HTTPBasicCredentials', ['username', 'password'])
+
+
+def extract_http_basic_credentials(request):
+ """ A helper function for extraction of HTTP Basic credentials
+ from a given :term:`request`.
+
+ Returns a :class:`.HTTPBasicCredentials` 2-tuple with ``username`` and
+ ``password`` attributes or ``None`` if no credentials could be found.
+
+ """
+ authorization = request.headers.get('Authorization')
+ if not authorization:
+ return None
+
+ try:
+ authmeth, auth = authorization.split(' ', 1)
+ except ValueError: # not enough values to unpack
+ return None
+
+ if authmeth.lower() != 'basic':
+ return None
+
+ try:
+ authbytes = b64decode(auth.strip())
+ except (TypeError, binascii.Error): # can't decode
+ return None
+
+ # try utf-8 first, then latin-1; see discussion in
+ # https://github.com/Pylons/pyramid/issues/898
+ try:
+ auth = authbytes.decode('utf-8')
+ except UnicodeDecodeError:
+ auth = authbytes.decode('latin-1')
+
+ try:
+ username, password = auth.split(':', 1)
+ except ValueError: # not enough values to unpack
+ return None
+
+ return HTTPBasicCredentials(username, password)
diff --git a/src/pyramid/authorization.py b/src/pyramid/authorization.py
new file mode 100644
index 000000000..4845762ef
--- /dev/null
+++ b/src/pyramid/authorization.py
@@ -0,0 +1,146 @@
+from zope.interface import implementer
+
+from pyramid.interfaces import IAuthorizationPolicy
+
+from pyramid.location import lineage
+
+from pyramid.compat import is_nonstr_iter
+
+from pyramid.security import (
+ ACLAllowed,
+ ACLDenied,
+ Allow,
+ Deny,
+ Everyone,
+ )
+
+@implementer(IAuthorizationPolicy)
+class ACLAuthorizationPolicy(object):
+ """ An :term:`authorization policy` which consults an :term:`ACL`
+ object attached to a :term:`context` to determine authorization
+ information about a :term:`principal` or multiple principals.
+ If the context is part of a :term:`lineage`, the context's parents
+ are consulted for ACL information too. The following is true
+ about this security policy.
+
+ - When checking whether the 'current' user is permitted (via the
+ ``permits`` method), the security policy consults the
+ ``context`` for an ACL first. If no ACL exists on the context,
+ or one does exist but the ACL does not explicitly allow or deny
+ access for any of the effective principals, consult the
+ context's parent ACL, and so on, until the lineage is exhausted
+ or we determine that the policy permits or denies.
+
+ During this processing, if any :data:`pyramid.security.Deny`
+ ACE is found matching any principal in ``principals``, stop
+ processing by returning an
+ :class:`pyramid.security.ACLDenied` instance (equals
+ ``False``) immediately. If any
+ :data:`pyramid.security.Allow` ACE is found matching any
+ principal, stop processing by returning an
+ :class:`pyramid.security.ACLAllowed` instance (equals
+ ``True``) immediately. If we exhaust the context's
+ :term:`lineage`, and no ACE has explicitly permitted or denied
+ access, return an instance of
+ :class:`pyramid.security.ACLDenied` (equals ``False``).
+
+ - When computing principals allowed by a permission via the
+ :func:`pyramid.security.principals_allowed_by_permission`
+ method, we compute the set of principals that are explicitly
+ granted the ``permission`` in the provided ``context``. We do
+ this by walking 'up' the object graph *from the root* to the
+ context. During this walking process, if we find an explicit
+ :data:`pyramid.security.Allow` ACE for a principal that
+ matches the ``permission``, the principal is included in the
+ allow list. However, if later in the walking process that
+ principal is mentioned in any :data:`pyramid.security.Deny`
+ ACE for the permission, the principal is removed from the allow
+ list. If a :data:`pyramid.security.Deny` to the principal
+ :data:`pyramid.security.Everyone` is encountered during the
+ walking process that matches the ``permission``, the allow list
+ is cleared for all principals encountered in previous ACLs. The
+ walking process ends after we've processed the any ACL directly
+ attached to ``context``; a set of principals is returned.
+
+ Objects of this class implement the
+ :class:`pyramid.interfaces.IAuthorizationPolicy` interface.
+ """
+
+ def permits(self, context, principals, permission):
+ """ Return an instance of
+ :class:`pyramid.security.ACLAllowed` instance if the policy
+ permits access, return an instance of
+ :class:`pyramid.security.ACLDenied` if not."""
+
+ acl = '<No ACL found on any object in resource lineage>'
+
+ for location in lineage(context):
+ try:
+ acl = location.__acl__
+ except AttributeError:
+ continue
+
+ if acl and callable(acl):
+ acl = acl()
+
+ for ace in acl:
+ ace_action, ace_principal, ace_permissions = ace
+ if ace_principal in principals:
+ if not is_nonstr_iter(ace_permissions):
+ ace_permissions = [ace_permissions]
+ if permission in ace_permissions:
+ if ace_action == Allow:
+ return ACLAllowed(ace, acl, permission,
+ principals, location)
+ else:
+ return ACLDenied(ace, acl, permission,
+ principals, location)
+
+ # default deny (if no ACL in lineage at all, or if none of the
+ # principals were mentioned in any ACE we found)
+ return ACLDenied(
+ '<default deny>',
+ acl,
+ permission,
+ principals,
+ context)
+
+ def principals_allowed_by_permission(self, context, permission):
+ """ Return the set of principals explicitly granted the
+ permission named ``permission`` according to the ACL directly
+ attached to the ``context`` as well as inherited ACLs based on
+ the :term:`lineage`."""
+ allowed = set()
+
+ for location in reversed(list(lineage(context))):
+ # NB: we're walking *up* the object graph from the root
+ try:
+ acl = location.__acl__
+ except AttributeError:
+ continue
+
+ allowed_here = set()
+ denied_here = set()
+
+ if acl and callable(acl):
+ acl = acl()
+
+ for ace_action, ace_principal, ace_permissions in acl:
+ if not is_nonstr_iter(ace_permissions):
+ ace_permissions = [ace_permissions]
+ if (ace_action == Allow) and (permission in ace_permissions):
+ if ace_principal not in denied_here:
+ allowed_here.add(ace_principal)
+ if (ace_action == Deny) and (permission in ace_permissions):
+ denied_here.add(ace_principal)
+ if ace_principal == Everyone:
+ # clear the entire allowed set, as we've hit a
+ # deny of Everyone ala (Deny, Everyone, ALL)
+ allowed = set()
+ break
+ elif ace_principal in allowed:
+ allowed.remove(ace_principal)
+
+ allowed.update(allowed_here)
+
+ return allowed
diff --git a/src/pyramid/compat.py b/src/pyramid/compat.py
new file mode 100644
index 000000000..a7f9c1287
--- /dev/null
+++ b/src/pyramid/compat.py
@@ -0,0 +1,281 @@
+import inspect
+import platform
+import sys
+import types
+
+WIN = platform.system() == 'Windows'
+
+try: # pragma: no cover
+ import __pypy__
+ PYPY = True
+except: # pragma: no cover
+ __pypy__ = None
+ PYPY = False
+
+try:
+ import cPickle as pickle
+except ImportError: # pragma: no cover
+ import pickle
+
+try:
+ from functools import lru_cache
+except ImportError:
+ from repoze.lru import lru_cache
+
+# PY3 is left as bw-compat but PY2 should be used for most checks.
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+if PY2:
+ string_types = basestring,
+ integer_types = (int, long)
+ class_types = (type, types.ClassType)
+ text_type = unicode
+ binary_type = str
+ long = long
+else:
+ string_types = str,
+ integer_types = int,
+ class_types = type,
+ text_type = str
+ binary_type = bytes
+ long = int
+
+def text_(s, encoding='latin-1', errors='strict'):
+ """ If ``s`` is an instance of ``binary_type``, return
+ ``s.decode(encoding, errors)``, otherwise return ``s``"""
+ if isinstance(s, binary_type):
+ return s.decode(encoding, errors)
+ return s
+
+def bytes_(s, encoding='latin-1', errors='strict'):
+ """ If ``s`` is an instance of ``text_type``, return
+ ``s.encode(encoding, errors)``, otherwise return ``s``"""
+ if isinstance(s, text_type):
+ return s.encode(encoding, errors)
+ return s
+
+if PY2:
+ def ascii_native_(s):
+ if isinstance(s, text_type):
+ s = s.encode('ascii')
+ return str(s)
+else:
+ def ascii_native_(s):
+ if isinstance(s, text_type):
+ s = s.encode('ascii')
+ return str(s, 'ascii', 'strict')
+
+ascii_native_.__doc__ = """
+Python 3: If ``s`` is an instance of ``text_type``, return
+``s.encode('ascii')``, otherwise return ``str(s, 'ascii', 'strict')``
+
+Python 2: If ``s`` is an instance of ``text_type``, return
+``s.encode('ascii')``, otherwise return ``str(s)``
+"""
+
+
+if PY2:
+ def native_(s, encoding='latin-1', errors='strict'):
+ """ If ``s`` is an instance of ``text_type``, return
+ ``s.encode(encoding, errors)``, otherwise return ``str(s)``"""
+ if isinstance(s, text_type):
+ return s.encode(encoding, errors)
+ return str(s)
+else:
+ def native_(s, encoding='latin-1', errors='strict'):
+ """ If ``s`` is an instance of ``text_type``, return
+ ``s``, otherwise return ``str(s, encoding, errors)``"""
+ if isinstance(s, text_type):
+ return s
+ return str(s, encoding, errors)
+
+native_.__doc__ = """
+Python 3: If ``s`` is an instance of ``text_type``, return ``s``, otherwise
+return ``str(s, encoding, errors)``
+
+Python 2: If ``s`` is an instance of ``text_type``, return
+``s.encode(encoding, errors)``, otherwise return ``str(s)``
+"""
+
+if PY2:
+ import urlparse
+ from urllib import quote as url_quote
+ from urllib import quote_plus as url_quote_plus
+ from urllib import unquote as url_unquote
+ from urllib import urlencode as url_encode
+ from urllib2 import urlopen as url_open
+
+ def url_unquote_text(v, encoding='utf-8', errors='replace'): # pragma: no cover
+ v = url_unquote(v)
+ return v.decode(encoding, errors)
+
+ def url_unquote_native(v, encoding='utf-8', errors='replace'): # pragma: no cover
+ return native_(url_unquote_text(v, encoding, errors))
+else:
+ from urllib import parse
+ urlparse = parse
+ from urllib.parse import quote as url_quote
+ from urllib.parse import quote_plus as url_quote_plus
+ from urllib.parse import unquote as url_unquote
+ from urllib.parse import urlencode as url_encode
+ from urllib.request import urlopen as url_open
+ url_unquote_text = url_unquote
+ url_unquote_native = url_unquote
+
+
+if PY2: # pragma: no cover
+ def exec_(code, globs=None, locs=None):
+ """Execute code in a namespace."""
+ if globs is None:
+ frame = sys._getframe(1)
+ globs = frame.f_globals
+ if locs is None:
+ locs = frame.f_locals
+ del frame
+ elif locs is None:
+ locs = globs
+ exec("""exec code in globs, locs""")
+
+ exec_("""def reraise(tp, value, tb=None):
+ raise tp, value, tb
+""")
+
+else: # pragma: no cover
+ import builtins
+ exec_ = getattr(builtins, "exec")
+
+ def reraise(tp, value, tb=None):
+ if value is None:
+ value = tp
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+
+ del builtins
+
+
+if PY2: # pragma: no cover
+ def iteritems_(d):
+ return d.iteritems()
+
+ def itervalues_(d):
+ return d.itervalues()
+
+ def iterkeys_(d):
+ return d.iterkeys()
+else: # pragma: no cover
+ def iteritems_(d):
+ return d.items()
+
+ def itervalues_(d):
+ return d.values()
+
+ def iterkeys_(d):
+ return d.keys()
+
+
+if PY2:
+ map_ = map
+else:
+ def map_(*arg):
+ return list(map(*arg))
+
+if PY2:
+ def is_nonstr_iter(v):
+ return hasattr(v, '__iter__')
+else:
+ def is_nonstr_iter(v):
+ if isinstance(v, str):
+ return False
+ return hasattr(v, '__iter__')
+
+if PY2:
+ im_func = 'im_func'
+ im_self = 'im_self'
+else:
+ im_func = '__func__'
+ im_self = '__self__'
+
+try:
+ import configparser
+except ImportError:
+ import ConfigParser as configparser
+
+try:
+ from http.cookies import SimpleCookie
+except ImportError:
+ from Cookie import SimpleCookie
+
+if PY2:
+ from cgi import escape
+else:
+ from html import escape
+
+if PY2:
+ input_ = raw_input
+else:
+ input_ = input
+
+if PY2:
+ from io import BytesIO as NativeIO
+else:
+ from io import StringIO as NativeIO
+
+# "json" is not an API; it's here to support older pyramid_debugtoolbar
+# versions which attempt to import it
+import json
+
+if PY2:
+ def decode_path_info(path):
+ return path.decode('utf-8')
+else:
+ # see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before
+ # decoding it to utf-8
+ def decode_path_info(path):
+ return path.encode('latin-1').decode('utf-8')
+
+if PY2:
+ from urlparse import unquote as unquote_to_bytes
+
+ def unquote_bytes_to_wsgi(bytestring):
+ return unquote_to_bytes(bytestring)
+else:
+ # see PEP 3333 for why we decode the path to latin-1
+ from urllib.parse import unquote_to_bytes
+
+ def unquote_bytes_to_wsgi(bytestring):
+ return unquote_to_bytes(bytestring).decode('latin-1')
+
+
+def is_bound_method(ob):
+ return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None
+
+# support annotations and keyword-only arguments in PY3
+if PY2:
+ from inspect import getargspec
+else:
+ from inspect import getfullargspec as getargspec
+
+if PY2:
+ from itertools import izip_longest as zip_longest
+else:
+ from itertools import zip_longest
+
+def is_unbound_method(fn):
+ """
+ This consistently verifies that the callable is bound to a
+ class.
+ """
+ is_bound = is_bound_method(fn)
+
+ if not is_bound and inspect.isroutine(fn):
+ spec = getargspec(fn)
+ has_self = len(spec.args) > 0 and spec.args[0] == 'self'
+
+ if PY2 and inspect.ismethod(fn):
+ return True
+ elif inspect.isfunction(fn) and has_self:
+ return True
+
+ return False
diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py
new file mode 100644
index 000000000..2f4e133f0
--- /dev/null
+++ b/src/pyramid/config/__init__.py
@@ -0,0 +1,1409 @@
+import inspect
+import itertools
+import logging
+import operator
+import os
+import sys
+import threading
+import venusian
+
+from webob.exc import WSGIHTTPException as WebobWSGIHTTPException
+
+from pyramid.interfaces import (
+ IDebugLogger,
+ IExceptionResponse,
+ IPredicateList,
+ PHASE0_CONFIG,
+ PHASE1_CONFIG,
+ PHASE2_CONFIG,
+ PHASE3_CONFIG,
+ )
+
+from pyramid.asset import resolve_asset_spec
+
+from pyramid.authorization import ACLAuthorizationPolicy
+
+from pyramid.compat import (
+ text_,
+ reraise,
+ string_types,
+ )
+
+from pyramid.events import ApplicationCreated
+
+from pyramid.exceptions import (
+ ConfigurationConflictError,
+ ConfigurationError,
+ ConfigurationExecutionError,
+ )
+
+from pyramid.httpexceptions import default_exceptionresponse_view
+
+from pyramid.path import (
+ caller_package,
+ package_of,
+ )
+
+from pyramid.registry import (
+ Introspectable,
+ Introspector,
+ Registry,
+ undefer,
+ )
+
+from pyramid.router import Router
+
+from pyramid.settings import aslist
+
+from pyramid.threadlocal import manager
+
+from pyramid.util import (
+ WeakOrderedSet,
+ object_description,
+ )
+
+from pyramid.config.util import (
+ ActionInfo,
+ PredicateList,
+ action_method,
+ not_,
+)
+
+from pyramid.config.adapters import AdaptersConfiguratorMixin
+from pyramid.config.assets import AssetsConfiguratorMixin
+from pyramid.config.factories import FactoriesConfiguratorMixin
+from pyramid.config.i18n import I18NConfiguratorMixin
+from pyramid.config.rendering import RenderingConfiguratorMixin
+from pyramid.config.routes import RoutesConfiguratorMixin
+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.views import ViewsConfiguratorMixin
+from pyramid.config.zca import ZCAConfiguratorMixin
+
+from pyramid.path import DottedNameResolver
+
+empty = text_('')
+_marker = object()
+
+not_ = not_ # api
+
+PHASE0_CONFIG = PHASE0_CONFIG # api
+PHASE1_CONFIG = PHASE1_CONFIG # api
+PHASE2_CONFIG = PHASE2_CONFIG # api
+PHASE3_CONFIG = PHASE3_CONFIG # api
+
+class Configurator(
+ TestingConfiguratorMixin,
+ TweensConfiguratorMixin,
+ SecurityConfiguratorMixin,
+ ViewsConfiguratorMixin,
+ RoutesConfiguratorMixin,
+ ZCAConfiguratorMixin,
+ I18NConfiguratorMixin,
+ RenderingConfiguratorMixin,
+ AssetsConfiguratorMixin,
+ SettingsConfiguratorMixin,
+ FactoriesConfiguratorMixin,
+ AdaptersConfiguratorMixin,
+ ):
+ """
+ A Configurator is used to configure a :app:`Pyramid`
+ :term:`application registry`.
+
+ The Configurator lifecycle can be managed by using a context manager to
+ automatically handle calling :meth:`pyramid.config.Configurator.begin` and
+ :meth:`pyramid.config.Configurator.end` as well as
+ :meth:`pyramid.config.Configurator.commit`.
+
+ .. code-block:: python
+
+ with Configurator(settings=settings) as config:
+ config.add_route('home', '/')
+ app = config.make_wsgi_app()
+
+ If the ``registry`` argument is not ``None``, it must
+ be an instance of the :class:`pyramid.registry.Registry` class
+ representing the registry to configure. If ``registry`` is ``None``, the
+ configurator will create a :class:`pyramid.registry.Registry` instance
+ itself; it will also perform some default configuration that would not
+ otherwise be done. After its construction, the configurator may be used
+ to add further configuration to the registry.
+
+ .. warning:: If ``registry`` is assigned the above-mentioned class
+ instance, all other constructor arguments are ignored,
+ with the exception of ``package``.
+
+ If the ``package`` argument is passed, it must be a reference to a Python
+ :term:`package` (e.g. ``sys.modules['thepackage']``) or a :term:`dotted
+ Python name` to the same. This value is used as a basis to convert
+ relative paths passed to various configuration methods, such as methods
+ which accept a ``renderer`` argument, into absolute paths. If ``None``
+ is passed (the default), the package is assumed to be the Python package
+ in which the *caller* of the ``Configurator`` constructor lives.
+
+ If the ``root_package`` is passed, it will propagate through the
+ configuration hierarchy as a way for included packages to locate
+ resources relative to the package in which the main ``Configurator`` was
+ created. If ``None`` is passed (the default), the ``root_package`` will
+ be derived from the ``package`` argument. The ``package`` attribute is
+ always pointing at the package being included when using :meth:`.include`,
+ whereas the ``root_package`` does not change.
+
+ If the ``settings`` argument is passed, it should be a Python dictionary
+ representing the :term:`deployment settings` for this application. These
+ are later retrievable using the
+ :attr:`pyramid.registry.Registry.settings` attribute (aka
+ ``request.registry.settings``).
+
+ If the ``root_factory`` argument is passed, it should be an object
+ representing the default :term:`root factory` for your application or a
+ :term:`dotted Python name` to the same. If it is ``None``, a default
+ root factory will be used.
+
+ If ``authentication_policy`` is passed, it should be an instance
+ of an :term:`authentication policy` or a :term:`dotted Python
+ name` to the same.
+
+ If ``authorization_policy`` is passed, it should be an instance of
+ an :term:`authorization policy` or a :term:`dotted Python name` to
+ the same.
+
+ .. note:: A ``ConfigurationError`` will be raised when an
+ authorization policy is supplied without also supplying an
+ authentication policy (authorization requires authentication).
+
+ If ``renderers`` is ``None`` (the default), a default set of
+ :term:`renderer` factories is used. Else, it should be a list of
+ tuples representing a set of renderer factories which should be
+ configured into this application, and each tuple representing a set of
+ positional values that should be passed to
+ :meth:`pyramid.config.Configurator.add_renderer`.
+
+ If ``debug_logger`` is not passed, a default debug logger that logs to a
+ logger will be used (the logger name will be the package name of the
+ *caller* of this configurator). If it is passed, it should be an
+ instance of the :class:`logging.Logger` (PEP 282) standard library class
+ or a Python logger name. The debug logger is used by :app:`Pyramid`
+ itself to log warnings and authorization debugging information.
+
+ If ``locale_negotiator`` is passed, it should be a :term:`locale
+ negotiator` implementation or a :term:`dotted Python name` to
+ same. See :ref:`custom_locale_negotiator`.
+
+ If ``request_factory`` is passed, it should be a :term:`request
+ factory` implementation or a :term:`dotted Python name` to the same.
+ See :ref:`changing_the_request_factory`. By default it is ``None``,
+ which means use the default request factory.
+
+ If ``response_factory`` is passed, it should be a :term:`response
+ factory` implementation or a :term:`dotted Python name` to the same.
+ See :ref:`changing_the_response_factory`. By default it is ``None``,
+ which means use the default response factory.
+
+ If ``default_permission`` is passed, it should be a
+ :term:`permission` string to be used as the default permission for
+ all view configuration registrations performed against this
+ Configurator. An example of a permission string:``'view'``.
+ Adding a default permission makes it unnecessary to protect each
+ view configuration with an explicit permission, unless your
+ application policy requires some exception for a particular view.
+ By default, ``default_permission`` is ``None``, meaning that view
+ configurations which do not explicitly declare a permission will
+ always be executable by entirely anonymous users (any
+ authorization policy in effect is ignored).
+
+ .. seealso::
+
+ See also :ref:`setting_a_default_permission`.
+
+ If ``session_factory`` is passed, it should be an object which
+ implements the :term:`session factory` interface. If a nondefault
+ value is passed, the ``session_factory`` will be used to create a
+ session object when ``request.session`` is accessed. Note that
+ the same outcome can be achieved by calling
+ :meth:`pyramid.config.Configurator.set_session_factory`. By
+ default, this argument is ``None``, indicating that no session
+ factory will be configured (and thus accessing ``request.session``
+ will throw an error) unless ``set_session_factory`` is called later
+ during configuration.
+
+ If ``autocommit`` is ``True``, every method called on the configurator
+ will cause an immediate action, and no configuration conflict detection
+ will be used. If ``autocommit`` is ``False``, most methods of the
+ configurator will defer their action until
+ :meth:`pyramid.config.Configurator.commit` is called. When
+ :meth:`pyramid.config.Configurator.commit` is called, the actions implied
+ by the called methods will be checked for configuration conflicts unless
+ ``autocommit`` is ``True``. If a conflict is detected, a
+ ``ConfigurationConflictError`` will be raised. Calling
+ :meth:`pyramid.config.Configurator.make_wsgi_app` always implies a final
+ commit.
+
+ If ``default_view_mapper`` is passed, it will be used as the default
+ :term:`view mapper` factory for view configurations that don't otherwise
+ specify one (see :class:`pyramid.interfaces.IViewMapperFactory`). If
+ ``default_view_mapper`` is not passed, a superdefault view mapper will be
+ used.
+
+ If ``exceptionresponse_view`` is passed, it must be a :term:`view
+ callable` or ``None``. If it is a view callable, it will be used as an
+ exception view callable when an :term:`exception response` is raised. If
+ ``exceptionresponse_view`` is ``None``, no exception response view will
+ be registered, and all raised exception responses will be bubbled up to
+ Pyramid's caller. By
+ default, the ``pyramid.httpexceptions.default_exceptionresponse_view``
+ function is used as the ``exceptionresponse_view``.
+
+ If ``route_prefix`` is passed, all routes added with
+ :meth:`pyramid.config.Configurator.add_route` will have the specified path
+ prepended to their pattern.
+
+ If ``introspection`` is passed, it must be a boolean value. If it's
+ ``True``, introspection values during actions will be kept for use
+ for tools like the debug toolbar. If it's ``False``, introspection
+ values provided by registrations will be ignored. By default, it is
+ ``True``.
+
+ .. versionadded:: 1.1
+ The ``exceptionresponse_view`` argument.
+
+ .. versionadded:: 1.2
+ The ``route_prefix`` argument.
+
+ .. versionadded:: 1.3
+ The ``introspection`` argument.
+
+ .. versionadded:: 1.6
+ The ``root_package`` argument.
+ The ``response_factory`` argument.
+
+ .. versionadded:: 1.9
+ The ability to use the configurator as a context manager with the
+ ``with``-statement to make threadlocal configuration available for
+ further configuration with an implicit commit.
+ """
+ manager = manager # for testing injection
+ venusian = venusian # for testing injection
+ _ainfo = None
+ basepath = None
+ includepath = ()
+ info = ''
+ object_description = staticmethod(object_description)
+ introspectable = Introspectable
+ inspect = inspect
+
+ def __init__(self,
+ registry=None,
+ package=None,
+ settings=None,
+ root_factory=None,
+ authentication_policy=None,
+ authorization_policy=None,
+ renderers=None,
+ debug_logger=None,
+ locale_negotiator=None,
+ request_factory=None,
+ response_factory=None,
+ default_permission=None,
+ session_factory=None,
+ default_view_mapper=None,
+ autocommit=False,
+ exceptionresponse_view=default_exceptionresponse_view,
+ route_prefix=None,
+ introspection=True,
+ root_package=None,
+ ):
+ if package is None:
+ package = caller_package()
+ if root_package is None:
+ root_package = package
+ name_resolver = DottedNameResolver(package)
+ self.name_resolver = name_resolver
+ self.package_name = name_resolver.get_package_name()
+ self.package = name_resolver.get_package()
+ self.root_package = root_package
+ self.registry = registry
+ self.autocommit = autocommit
+ self.route_prefix = route_prefix
+ self.introspection = introspection
+ if registry is None:
+ registry = Registry(self.package_name)
+ self.registry = registry
+ self.setup_registry(
+ settings=settings,
+ root_factory=root_factory,
+ authentication_policy=authentication_policy,
+ authorization_policy=authorization_policy,
+ renderers=renderers,
+ debug_logger=debug_logger,
+ locale_negotiator=locale_negotiator,
+ request_factory=request_factory,
+ response_factory=response_factory,
+ default_permission=default_permission,
+ session_factory=session_factory,
+ default_view_mapper=default_view_mapper,
+ exceptionresponse_view=exceptionresponse_view,
+ )
+
+ def setup_registry(self,
+ settings=None,
+ root_factory=None,
+ authentication_policy=None,
+ authorization_policy=None,
+ renderers=None,
+ debug_logger=None,
+ locale_negotiator=None,
+ request_factory=None,
+ response_factory=None,
+ default_permission=None,
+ session_factory=None,
+ default_view_mapper=None,
+ exceptionresponse_view=default_exceptionresponse_view,
+ ):
+ """ When you pass a non-``None`` ``registry`` argument to the
+ :term:`Configurator` constructor, no initial setup is performed
+ against the registry. This is because the registry you pass in may
+ have already been initialized for use under :app:`Pyramid` via a
+ different configurator. However, in some circumstances (such as when
+ you want to use a global registry instead of a registry created as a
+ result of the Configurator constructor), or when you want to reset
+ the initial setup of a registry, you *do* want to explicitly
+ initialize the registry associated with a Configurator for use under
+ :app:`Pyramid`. Use ``setup_registry`` to do this initialization.
+
+ ``setup_registry`` configures settings, a root factory, security
+ policies, renderers, a debug logger, a locale negotiator, and various
+ other settings using the configurator's current registry, as per the
+ descriptions in the Configurator constructor."""
+
+ registry = self.registry
+
+ self._fix_registry()
+
+ self._set_settings(settings)
+
+ if isinstance(debug_logger, string_types):
+ debug_logger = logging.getLogger(debug_logger)
+
+ if debug_logger is None:
+ debug_logger = logging.getLogger(self.package_name)
+
+ registry.registerUtility(debug_logger, IDebugLogger)
+
+ self.add_default_response_adapters()
+ self.add_default_renderers()
+ self.add_default_accept_view_order()
+ self.add_default_view_predicates()
+ self.add_default_view_derivers()
+ self.add_default_route_predicates()
+ self.add_default_tweens()
+ self.add_default_security()
+
+ if exceptionresponse_view is not None:
+ exceptionresponse_view = self.maybe_dotted(exceptionresponse_view)
+ self.add_view(exceptionresponse_view, context=IExceptionResponse)
+ self.add_view(exceptionresponse_view,context=WebobWSGIHTTPException)
+
+ # commit below because:
+ #
+ # - the default exceptionresponse_view requires the superdefault view
+ # mapper, so we need to configure it before adding default_view_mapper
+ #
+ # - superdefault renderers should be overrideable without requiring
+ # the user to commit before calling config.add_renderer
+
+ self.commit()
+
+ # self.commit() should not be called within this method after this
+ # point because the following registrations should be treated as
+ # analogues of methods called by the user after configurator
+ # construction. Rationale: user-supplied implementations should be
+ # preferred rather than add-on author implementations with the help of
+ # automatic conflict resolution.
+
+ if authentication_policy and not authorization_policy:
+ authorization_policy = ACLAuthorizationPolicy() # default
+
+ if authorization_policy:
+ self.set_authorization_policy(authorization_policy)
+
+ if authentication_policy:
+ self.set_authentication_policy(authentication_policy)
+
+ if default_view_mapper is not None:
+ self.set_view_mapper(default_view_mapper)
+
+ if renderers:
+ for name, renderer in renderers:
+ self.add_renderer(name, renderer)
+
+ if root_factory is not None:
+ self.set_root_factory(root_factory)
+
+ if locale_negotiator:
+ self.set_locale_negotiator(locale_negotiator)
+
+ if request_factory:
+ self.set_request_factory(request_factory)
+
+ if response_factory:
+ self.set_response_factory(response_factory)
+
+ if default_permission:
+ self.set_default_permission(default_permission)
+
+ if session_factory is not None:
+ self.set_session_factory(session_factory)
+
+ tweens = aslist(registry.settings.get('pyramid.tweens', []))
+ for factory in tweens:
+ self._add_tween(factory, explicit=True)
+
+ includes = aslist(registry.settings.get('pyramid.includes', []))
+ for inc in includes:
+ self.include(inc)
+
+ def _make_spec(self, path_or_spec):
+ package, filename = resolve_asset_spec(path_or_spec, self.package_name)
+ if package is None:
+ return filename # absolute filename
+ return '%s:%s' % (package, filename)
+
+ def _fix_registry(self):
+ """ Fix up a ZCA component registry that is not a
+ pyramid.registry.Registry by adding analogues of ``has_listeners``,
+ ``notify``, ``queryAdapterOrSelf``, and ``registerSelfAdapter``
+ through monkey-patching."""
+
+ _registry = self.registry
+
+ if not hasattr(_registry, 'notify'):
+ def notify(*events):
+ [ _ for _ in _registry.subscribers(events, None) ]
+ _registry.notify = notify
+
+ if not hasattr(_registry, 'has_listeners'):
+ _registry.has_listeners = True
+
+ if not hasattr(_registry, 'queryAdapterOrSelf'):
+ def queryAdapterOrSelf(object, interface, default=None):
+ if not interface.providedBy(object):
+ return _registry.queryAdapter(object, interface,
+ default=default)
+ return object
+ _registry.queryAdapterOrSelf = queryAdapterOrSelf
+
+ if not hasattr(_registry, 'registerSelfAdapter'):
+ def registerSelfAdapter(required=None, provided=None,
+ name=empty, info=empty, event=True):
+ return _registry.registerAdapter(lambda x: x,
+ required=required,
+ provided=provided, name=name,
+ info=info, event=event)
+ _registry.registerSelfAdapter = registerSelfAdapter
+
+ if not hasattr(_registry, '_lock'):
+ _registry._lock = threading.Lock()
+
+ if not hasattr(_registry, '_clear_view_lookup_cache'):
+ def _clear_view_lookup_cache():
+ _registry._view_lookup_cache = {}
+ _registry._clear_view_lookup_cache = _clear_view_lookup_cache
+
+
+ # API
+
+ def _get_introspector(self):
+ introspector = getattr(self.registry, 'introspector', _marker)
+ if introspector is _marker:
+ introspector = Introspector()
+ self._set_introspector(introspector)
+ return introspector
+
+ def _set_introspector(self, introspector):
+ self.registry.introspector = introspector
+
+ def _del_introspector(self):
+ del self.registry.introspector
+
+ introspector = property(
+ _get_introspector, _set_introspector, _del_introspector
+ )
+
+ def get_predlist(self, name):
+ predlist = self.registry.queryUtility(IPredicateList, name=name)
+ if predlist is None:
+ predlist = PredicateList()
+ self.registry.registerUtility(predlist, IPredicateList, name=name)
+ return predlist
+
+
+ def _add_predicate(self, type, name, factory, weighs_more_than=None,
+ weighs_less_than=None):
+ factory = self.maybe_dotted(factory)
+ discriminator = ('%s option' % type, name)
+ intr = self.introspectable(
+ '%s predicates' % type,
+ discriminator,
+ '%s predicate named %s' % (type, name),
+ '%s predicate' % type)
+ intr['name'] = name
+ intr['factory'] = factory
+ intr['weighs_more_than'] = weighs_more_than
+ intr['weighs_less_than'] = weighs_less_than
+ def register():
+ predlist = self.get_predlist(type)
+ predlist.add(name, factory, weighs_more_than=weighs_more_than,
+ weighs_less_than=weighs_less_than)
+ self.action(discriminator, register, introspectables=(intr,),
+ order=PHASE1_CONFIG) # must be registered early
+
+ @property
+ def action_info(self):
+ info = self.info # usually a ZCML action (ParserInfo) if self.info
+ if not info:
+ # Try to provide more accurate info for conflict reports
+ if self._ainfo:
+ info = self._ainfo[0]
+ else:
+ info = ActionInfo(None, 0, '', '')
+ return info
+
+ def action(self, discriminator, callable=None, args=(), kw=None, order=0,
+ introspectables=(), **extra):
+ """ Register an action which will be executed when
+ :meth:`pyramid.config.Configurator.commit` is called (or executed
+ immediately if ``autocommit`` is ``True``).
+
+ .. warning:: This method is typically only used by :app:`Pyramid`
+ framework extension authors, not by :app:`Pyramid` application
+ developers.
+
+ The ``discriminator`` uniquely identifies the action. It must be
+ given, but it can be ``None``, to indicate that the action never
+ conflicts. It must be a hashable value.
+
+ The ``callable`` is a callable object which performs the task
+ associated with the action when the action is executed. It is
+ optional.
+
+ ``args`` and ``kw`` are tuple and dict objects respectively, which
+ are passed to ``callable`` when this action is executed. Both are
+ optional.
+
+ ``order`` is a grouping mechanism; an action with a lower order will
+ be executed before an action with a higher order (has no effect when
+ autocommit is ``True``).
+
+ ``introspectables`` is a sequence of :term:`introspectable` objects
+ (or the empty sequence if no introspectable objects are associated
+ with this action). If this configurator's ``introspection``
+ attribute is ``False``, these introspectables will be ignored.
+
+ ``extra`` provides a facility for inserting extra keys and values
+ into an action dictionary.
+ """
+ # catch nonhashable discriminators here; most unit tests use
+ # autocommit=False, which won't catch unhashable discriminators
+ assert hash(discriminator)
+
+ if kw is None:
+ kw = {}
+
+ autocommit = self.autocommit
+ action_info = self.action_info
+
+ if not self.introspection:
+ # if we're not introspecting, ignore any introspectables passed
+ # to us
+ introspectables = ()
+
+ if autocommit:
+ # callables can depend on the side effects of resolving a
+ # deferred discriminator
+ self.begin()
+ try:
+ undefer(discriminator)
+ if callable is not None:
+ callable(*args, **kw)
+ for introspectable in introspectables:
+ introspectable.register(self.introspector, action_info)
+ finally:
+ self.end()
+
+ else:
+ action = extra
+ action.update(
+ dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ order=order,
+ info=action_info,
+ includepath=self.includepath,
+ introspectables=introspectables,
+ )
+ )
+ self.action_state.action(**action)
+
+ def _get_action_state(self):
+ registry = self.registry
+ try:
+ state = registry.action_state
+ except AttributeError:
+ state = ActionState()
+ registry.action_state = state
+ return state
+
+ def _set_action_state(self, state):
+ self.registry.action_state = state
+
+ action_state = property(_get_action_state, _set_action_state)
+
+ _ctx = action_state # bw compat
+
+ def commit(self):
+ """
+ Commit any pending configuration actions. If a configuration
+ conflict is detected in the pending configuration actions, this method
+ will raise a :exc:`ConfigurationConflictError`; within the traceback
+ of this error will be information about the source of the conflict,
+ usually including file names and line numbers of the cause of the
+ configuration conflicts.
+
+ .. warning::
+ You should think very carefully before manually invoking
+ ``commit()``. Especially not as part of any reusable configuration
+ methods. Normally it should only be done by an application author at
+ the end of configuration in order to override certain aspects of an
+ addon.
+
+ """
+ self.begin()
+ try:
+ self.action_state.execute_actions(introspector=self.introspector)
+ finally:
+ self.end()
+ self.action_state = ActionState() # old actions have been processed
+
+ def include(self, callable, route_prefix=None):
+ """Include a configuration callable, to support imperative
+ application extensibility.
+
+ .. warning:: In versions of :app:`Pyramid` prior to 1.2, this
+ function accepted ``*callables``, but this has been changed
+ to support only a single callable.
+
+ A configuration callable should be a callable that accepts a single
+ argument named ``config``, which will be an instance of a
+ :term:`Configurator`. However, be warned that it will not be the same
+ configurator instance on which you call this method. The
+ code which runs as a result of calling the callable should invoke
+ methods on the configurator passed to it which add configuration
+ state. The return value of a callable will be ignored.
+
+ Values allowed to be presented via the ``callable`` argument to
+ this method: any callable Python object or any :term:`dotted Python
+ name` which resolves to a callable Python object. It may also be a
+ Python :term:`module`, in which case, the module will be searched for
+ a callable named ``includeme``, which will be treated as the
+ configuration callable.
+
+ For example, if the ``includeme`` function below lives in a module
+ named ``myapp.myconfig``:
+
+ .. code-block:: python
+ :linenos:
+
+ # myapp.myconfig module
+
+ def my_view(request):
+ from pyramid.response import Response
+ return Response('OK')
+
+ def includeme(config):
+ config.add_view(my_view)
+
+ You might cause it to be included within your Pyramid application like
+ so:
+
+ .. code-block:: python
+ :linenos:
+
+ from pyramid.config import Configurator
+
+ def main(global_config, **settings):
+ config = Configurator()
+ config.include('myapp.myconfig.includeme')
+
+ Because the function is named ``includeme``, the function name can
+ also be omitted from the dotted name reference:
+
+ .. code-block:: python
+ :linenos:
+
+ from pyramid.config import Configurator
+
+ def main(global_config, **settings):
+ config = Configurator()
+ config.include('myapp.myconfig')
+
+ Included configuration statements will be overridden by local
+ configuration statements if an included callable causes a
+ configuration conflict by registering something with the same
+ configuration parameters.
+
+ If the ``route_prefix`` is supplied, it must be a string. Any calls
+ to :meth:`pyramid.config.Configurator.add_route` within the included
+ callable will have their pattern prefixed with the value of
+ ``route_prefix``. This can be used to help mount a set of routes at a
+ different location than the included callable's author intended, while
+ still maintaining the same route names. For example:
+
+ .. code-block:: python
+ :linenos:
+
+ from pyramid.config import Configurator
+
+ def included(config):
+ config.add_route('show_users', '/show')
+
+ def main(global_config, **settings):
+ config = Configurator()
+ config.include(included, route_prefix='/users')
+
+ In the above configuration, the ``show_users`` route will have an
+ effective route pattern of ``/users/show``, instead of ``/show``
+ because the ``route_prefix`` argument will be prepended to the
+ pattern.
+
+ .. versionadded:: 1.2
+ The ``route_prefix`` parameter.
+
+ .. versionchanged:: 1.9
+ The included function is wrapped with a call to
+ :meth:`pyramid.config.Configurator.begin` and
+ :meth:`pyramid.config.Configurator.end` while it is executed.
+
+ """
+ # """ <-- emacs
+
+ action_state = self.action_state
+
+ c = self.maybe_dotted(callable)
+ module = self.inspect.getmodule(c)
+ if module is c:
+ try:
+ c = getattr(module, 'includeme')
+ except AttributeError:
+ raise ConfigurationError(
+ "module %r has no attribute 'includeme'" % (module.__name__)
+ )
+
+ spec = module.__name__ + ':' + c.__name__
+ sourcefile = self.inspect.getsourcefile(c)
+
+ if sourcefile is None:
+ raise ConfigurationError(
+ 'No source file for module %r (.py file must exist, '
+ 'refusing to use orphan .pyc or .pyo file).' % module.__name__)
+
+
+ if action_state.processSpec(spec):
+ with self.route_prefix_context(route_prefix):
+ configurator = self.__class__(
+ registry=self.registry,
+ package=package_of(module),
+ root_package=self.root_package,
+ autocommit=self.autocommit,
+ route_prefix=self.route_prefix,
+ )
+ configurator.basepath = os.path.dirname(sourcefile)
+ configurator.includepath = self.includepath + (spec,)
+
+ self.begin()
+ try:
+ c(configurator)
+ finally:
+ self.end()
+
+ def add_directive(self, name, directive, action_wrap=True):
+ """
+ Add a directive method to the configurator.
+
+ .. warning:: This method is typically only used by :app:`Pyramid`
+ framework extension authors, not by :app:`Pyramid` application
+ developers.
+
+ Framework extenders can add directive methods to a configurator by
+ instructing their users to call ``config.add_directive('somename',
+ 'some.callable')``. This will make ``some.callable`` accessible as
+ ``config.somename``. ``some.callable`` should be a function which
+ accepts ``config`` as a first argument, and arbitrary positional and
+ keyword arguments following. It should use config.action as
+ necessary to perform actions. Directive methods can then be invoked
+ like 'built-in' directives such as ``add_view``, ``add_route``, etc.
+
+ The ``action_wrap`` argument should be ``True`` for directives which
+ perform ``config.action`` with potentially conflicting
+ discriminators. ``action_wrap`` will cause the directive to be
+ wrapped in a decorator which provides more accurate conflict
+ cause information.
+
+ ``add_directive`` does not participate in conflict detection, and
+ later calls to ``add_directive`` will override earlier calls.
+ """
+ c = self.maybe_dotted(directive)
+ if not hasattr(self.registry, '_directives'):
+ self.registry._directives = {}
+ self.registry._directives[name] = (c, action_wrap)
+
+ def __getattr__(self, name):
+ # allow directive extension names to work
+ directives = getattr(self.registry, '_directives', {})
+ c = directives.get(name)
+ if c is None:
+ raise AttributeError(name)
+ c, action_wrap = c
+ if action_wrap:
+ c = action_method(c)
+ # Create a bound method (works on both Py2 and Py3)
+ # http://stackoverflow.com/a/1015405/209039
+ m = c.__get__(self, self.__class__)
+ return m
+
+ def with_package(self, package):
+ """ Return a new Configurator instance with the same registry
+ as this configurator. ``package`` may be an actual Python package
+ object or a :term:`dotted Python name` representing a package."""
+ configurator = self.__class__(
+ registry=self.registry,
+ package=package,
+ root_package=self.root_package,
+ autocommit=self.autocommit,
+ route_prefix=self.route_prefix,
+ introspection=self.introspection,
+ )
+ configurator.basepath = self.basepath
+ configurator.includepath = self.includepath
+ configurator.info = self.info
+ return configurator
+
+ def maybe_dotted(self, dotted):
+ """ Resolve the :term:`dotted Python name` ``dotted`` to a
+ global Python object. If ``dotted`` is not a string, return
+ it without attempting to do any name resolution. If
+ ``dotted`` is a relative dotted name (e.g. ``.foo.bar``,
+ consider it relative to the ``package`` argument supplied to
+ this Configurator's constructor."""
+ return self.name_resolver.maybe_resolve(dotted)
+
+ def absolute_asset_spec(self, relative_spec):
+ """ Resolve the potentially relative :term:`asset
+ specification` string passed as ``relative_spec`` into an
+ absolute asset specification string and return the string.
+ Use the ``package`` of this configurator as the package to
+ which the asset specification will be considered relative
+ when generating an absolute asset specification. If the
+ provided ``relative_spec`` argument is already absolute, or if
+ the ``relative_spec`` is not a string, it is simply returned."""
+ if not isinstance(relative_spec, string_types):
+ return relative_spec
+ return self._make_spec(relative_spec)
+
+ absolute_resource_spec = absolute_asset_spec # b/w compat forever
+
+ def begin(self, request=_marker):
+ """ Indicate that application or test configuration has begun.
+ This pushes a dictionary containing the :term:`application
+ registry` implied by ``registry`` attribute of this
+ configurator and the :term:`request` implied by the
+ ``request`` argument onto the :term:`thread local` stack
+ consulted by various :mod:`pyramid.threadlocal` API
+ functions.
+
+ If ``request`` is not specified and the registry owned by the
+ configurator is already pushed as the current threadlocal registry
+ then this method will keep the current threadlocal request unchanged.
+
+ .. versionchanged:: 1.8
+ The current threadlocal request is propagated if the current
+ threadlocal registry remains unchanged.
+
+ """
+ if request is _marker:
+ current = self.manager.get()
+ if current['registry'] == self.registry:
+ request = current['request']
+ else:
+ request = None
+ self.manager.push({'registry':self.registry, 'request':request})
+
+ def end(self):
+ """ Indicate that application or test configuration has ended.
+ This pops the last value pushed onto the :term:`thread local`
+ stack (usually by the ``begin`` method) and returns that
+ value.
+ """
+ return self.manager.pop()
+
+ def __enter__(self):
+ self.begin()
+ return self
+
+ def __exit__(self, exc_type, exc_value, exc_traceback):
+ self.end()
+
+ if exc_value is None:
+ self.commit()
+
+ # this is *not* an action method (uses caller_package)
+ def scan(self, package=None, categories=None, onerror=None, ignore=None,
+ **kw):
+ """Scan a Python package and any of its subpackages for objects
+ marked with :term:`configuration decoration` such as
+ :class:`pyramid.view.view_config`. Any decorated object found will
+ influence the current configuration state.
+
+ The ``package`` argument should be a Python :term:`package` or module
+ object (or a :term:`dotted Python name` which refers to such a
+ package or module). If ``package`` is ``None``, the package of the
+ *caller* is used.
+
+ The ``categories`` argument, if provided, should be the
+ :term:`Venusian` 'scan categories' to use during scanning. Providing
+ this argument is not often necessary; specifying scan categories is
+ an extremely advanced usage. By default, ``categories`` is ``None``
+ which will execute *all* Venusian decorator callbacks including
+ :app:`Pyramid`-related decorators such as
+ :class:`pyramid.view.view_config`. See the :term:`Venusian`
+ documentation for more information about limiting a scan by using an
+ explicit set of categories.
+
+ The ``onerror`` argument, if provided, should be a Venusian
+ ``onerror`` callback function. The onerror function is passed to
+ :meth:`venusian.Scanner.scan` to influence error behavior when an
+ exception is raised during the scanning process. See the
+ :term:`Venusian` documentation for more information about ``onerror``
+ callbacks.
+
+ The ``ignore`` argument, if provided, should be a Venusian ``ignore``
+ value. Providing an ``ignore`` argument allows the scan to ignore
+ particular modules, packages, or global objects during a scan.
+ ``ignore`` can be a string or a callable, or a list containing
+ strings or callables. The simplest usage of ``ignore`` is to provide
+ a module or package by providing a full path to its dotted name. For
+ example: ``config.scan(ignore='my.module.subpackage')`` would ignore
+ the ``my.module.subpackage`` package during a scan, which would
+ prevent the subpackage and any of its submodules from being imported
+ and scanned. See the :term:`Venusian` documentation for more
+ information about the ``ignore`` argument.
+
+ To perform a ``scan``, Pyramid creates a Venusian ``Scanner`` object.
+ The ``kw`` argument represents a set of keyword arguments to pass to
+ the Venusian ``Scanner`` object's constructor. See the
+ :term:`venusian` documentation (its ``Scanner`` class) for more
+ information about the constructor. By default, the only keyword
+ arguments passed to the Scanner constructor are ``{'config':self}``
+ where ``self`` is this configurator object. This services the
+ requirement of all built-in Pyramid decorators, but extension systems
+ may require additional arguments. Providing this argument is not
+ often necessary; it's an advanced usage.
+
+ .. versionadded:: 1.1
+ The ``**kw`` argument.
+
+ .. versionadded:: 1.3
+ The ``ignore`` argument.
+
+ """
+ package = self.maybe_dotted(package)
+ if package is None: # pragma: no cover
+ package = caller_package()
+
+ ctorkw = {'config': self}
+ ctorkw.update(kw)
+
+ scanner = self.venusian.Scanner(**ctorkw)
+
+ scanner.scan(package, categories=categories, onerror=onerror,
+ ignore=ignore)
+
+ def make_wsgi_app(self):
+ """ Commits any pending configuration statements, sends a
+ :class:`pyramid.events.ApplicationCreated` event to all listeners,
+ adds this configuration's registry to
+ :attr:`pyramid.config.global_registries`, and returns a
+ :app:`Pyramid` WSGI application representing the committed
+ configuration state."""
+ self.commit()
+ app = Router(self.registry)
+
+ # Allow tools like "pshell development.ini" to find the 'last'
+ # registry configured.
+ global_registries.add(self.registry)
+
+ # Push the registry onto the stack in case any code that depends on
+ # the registry threadlocal APIs used in listeners subscribed to the
+ # IApplicationCreated event.
+ self.begin()
+ try:
+ self.registry.notify(ApplicationCreated(app))
+ finally:
+ self.end()
+
+ return app
+
+
+# this class is licensed under the ZPL (stolen from Zope)
+class ActionState(object):
+ def __init__(self):
+ # NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func
+ self.actions = []
+ self._seen_files = set()
+
+ def processSpec(self, spec):
+ """Check whether a callable needs to be processed. The ``spec``
+ refers to a unique identifier for the callable.
+
+ Return True if processing is needed and False otherwise. If
+ the callable needs to be processed, it will be marked as
+ processed, assuming that the caller will procces the callable if
+ it needs to be processed.
+ """
+ if spec in self._seen_files:
+ return False
+ self._seen_files.add(spec)
+ return True
+
+ def action(self, discriminator, callable=None, args=(), kw=None, order=0,
+ includepath=(), info=None, introspectables=(), **extra):
+ """Add an action with the given discriminator, callable and arguments
+ """
+ if kw is None:
+ kw = {}
+ action = extra
+ action.update(
+ dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ includepath=includepath,
+ info=info,
+ order=order,
+ introspectables=introspectables,
+ )
+ )
+ self.actions.append(action)
+
+ def execute_actions(self, clear=True, introspector=None):
+ """Execute the configuration actions
+
+ This calls the action callables after resolving conflicts
+
+ For example:
+
+ >>> output = []
+ >>> def f(*a, **k):
+ ... output.append(('f', a, k))
+ >>> context = ActionState()
+ >>> context.actions = [
+ ... (1, f, (1,)),
+ ... (1, f, (11,), {}, ('x', )),
+ ... (2, f, (2,)),
+ ... ]
+ >>> context.execute_actions()
+ >>> output
+ [('f', (1,), {}), ('f', (2,), {})]
+
+ If the action raises an error, we convert it to a
+ ConfigurationExecutionError.
+
+ >>> output = []
+ >>> def bad():
+ ... bad.xxx
+ >>> context.actions = [
+ ... (1, f, (1,)),
+ ... (1, f, (11,), {}, ('x', )),
+ ... (2, f, (2,)),
+ ... (3, bad, (), {}, (), 'oops')
+ ... ]
+ >>> try:
+ ... v = context.execute_actions()
+ ... except ConfigurationExecutionError, v:
+ ... pass
+ >>> print(v)
+ exceptions.AttributeError: 'function' object has no attribute 'xxx'
+ in:
+ oops
+
+ Note that actions executed before the error still have an effect:
+
+ >>> output
+ [('f', (1,), {}), ('f', (2,), {})]
+
+ The execution is re-entrant such that actions may be added by other
+ actions with the one caveat that the order of any added actions must
+ be equal to or larger than the current action.
+
+ >>> output = []
+ >>> def f(*a, **k):
+ ... output.append(('f', a, k))
+ ... context.actions.append((3, g, (8,), {}))
+ >>> def g(*a, **k):
+ ... output.append(('g', a, k))
+ >>> context.actions = [
+ ... (1, f, (1,)),
+ ... ]
+ >>> context.execute_actions()
+ >>> output
+ [('f', (1,), {}), ('g', (8,), {})]
+
+ """
+ try:
+ all_actions = []
+ executed_actions = []
+ action_iter = iter([])
+ conflict_state = ConflictResolverState()
+
+ while True:
+ # We clear the actions list prior to execution so if there
+ # are some new actions then we add them to the mix and resolve
+ # conflicts again. This orders the new actions as well as
+ # ensures that the previously executed actions have no new
+ # conflicts.
+ if self.actions:
+ all_actions.extend(self.actions)
+ action_iter = resolveConflicts(
+ self.actions,
+ state=conflict_state,
+ )
+ self.actions = []
+
+ action = next(action_iter, None)
+ if action is None:
+ # we are done!
+ break
+
+ callable = action['callable']
+ args = action['args']
+ kw = action['kw']
+ info = action['info']
+ # we use "get" below in case an action was added via a ZCML
+ # directive that did not know about introspectables
+ introspectables = action.get('introspectables', ())
+
+ try:
+ if callable is not None:
+ callable(*args, **kw)
+ except Exception:
+ t, v, tb = sys.exc_info()
+ try:
+ reraise(ConfigurationExecutionError,
+ ConfigurationExecutionError(t, v, info),
+ tb)
+ finally:
+ del t, v, tb
+
+ if introspector is not None:
+ for introspectable in introspectables:
+ introspectable.register(introspector, info)
+
+ executed_actions.append(action)
+
+ self.actions = all_actions
+ return executed_actions
+
+ finally:
+ if clear:
+ self.actions = []
+
+
+class ConflictResolverState(object):
+ def __init__(self):
+ # keep a set of resolved discriminators to test against to ensure
+ # that a new action does not conflict with something already executed
+ self.resolved_ainfos = {}
+
+ # actions left over from a previous iteration
+ self.remaining_actions = []
+
+ # after executing an action we memoize its order to avoid any new
+ # actions sending us backward
+ self.min_order = None
+
+ # unique tracks the index of the action so we need it to increase
+ # monotonically across invocations to resolveConflicts
+ self.start = 0
+
+
+# this function is licensed under the ZPL (stolen from Zope)
+def resolveConflicts(actions, state=None):
+ """Resolve conflicting actions
+
+ Given an actions list, identify and try to resolve conflicting actions.
+ Actions conflict if they have the same non-None discriminator.
+
+ Conflicting actions can be resolved if the include path of one of
+ the actions is a prefix of the includepaths of the other
+ conflicting actions and is unequal to the include paths in the
+ other conflicting actions.
+
+ Actions are resolved on a per-order basis because some discriminators
+ cannot be computed until earlier actions have executed. An action in an
+ earlier order may execute successfully only to find out later that it was
+ overridden by another action with a smaller include path. This will result
+ in a conflict as there is no way to revert the original action.
+
+ ``state`` may be an instance of ``ConflictResolverState`` that
+ can be used to resume execution and resolve the new actions against the
+ list of executed actions from a previous call.
+
+ """
+ if state is None:
+ state = ConflictResolverState()
+
+ # pick up where we left off last time, but track the new actions as well
+ state.remaining_actions.extend(normalize_actions(actions))
+ actions = state.remaining_actions
+
+ def orderandpos(v):
+ n, v = v
+ return (v['order'] or 0, n)
+
+ def orderonly(v):
+ n, v = v
+ return v['order'] or 0
+
+ sactions = sorted(enumerate(actions, start=state.start), key=orderandpos)
+ for order, actiongroup in itertools.groupby(sactions, orderonly):
+ # "order" is an integer grouping. Actions in a lower order will be
+ # executed before actions in a higher order. All of the actions in
+ # one grouping will be executed (its callable, if any will be called)
+ # before any of the actions in the next.
+ output = []
+ unique = {}
+
+ # error out if we went backward in order
+ if state.min_order is not None and order < state.min_order:
+ r = ['Actions were added to order={0} after execution had moved '
+ 'on to order={1}. Conflicting actions: '
+ .format(order, state.min_order)]
+ for i, action in actiongroup:
+ for line in str(action['info']).rstrip().split('\n'):
+ r.append(" " + line)
+ raise ConfigurationError('\n'.join(r))
+
+ for i, action in actiongroup:
+ # Within an order, actions are executed sequentially based on
+ # original action ordering ("i").
+
+ # "ainfo" is a tuple of (i, action) where "i" is an integer
+ # expressing the relative position of this action in the action
+ # list being resolved, and "action" is an action dictionary. The
+ # purpose of an ainfo is to associate an "i" with a particular
+ # action; "i" exists for sorting after conflict resolution.
+ ainfo = (i, action)
+
+ # wait to defer discriminators until we are on their order because
+ # the discriminator may depend on state from a previous order
+ discriminator = undefer(action['discriminator'])
+ action['discriminator'] = discriminator
+
+ if discriminator is None:
+ # The discriminator is None, so this action can never conflict.
+ # We can add it directly to the result.
+ output.append(ainfo)
+ continue
+
+ L = unique.setdefault(discriminator, [])
+ L.append(ainfo)
+
+ # Check for conflicts
+ conflicts = {}
+ for discriminator, ainfos in unique.items():
+ # We use (includepath, i) as a sort key because we need to
+ # sort the actions by the paths so that the shortest path with a
+ # given prefix comes first. The "first" action is the one with the
+ # shortest include path. We break sorting ties using "i".
+ def bypath(ainfo):
+ path, i = ainfo[1]['includepath'], ainfo[0]
+ return path, order, i
+
+ ainfos.sort(key=bypath)
+ ainfo, rest = ainfos[0], ainfos[1:]
+ _, action = ainfo
+
+ # ensure this new action does not conflict with a previously
+ # resolved action from an earlier order / invocation
+ prev_ainfo = state.resolved_ainfos.get(discriminator)
+ if prev_ainfo is not None:
+ _, paction = prev_ainfo
+ basepath, baseinfo = paction['includepath'], paction['info']
+ includepath = action['includepath']
+ # if the new action conflicts with the resolved action then
+ # note the conflict, otherwise drop the action as it's
+ # effectively overriden by the previous action
+ if (includepath[:len(basepath)] != basepath or
+ includepath == basepath):
+ L = conflicts.setdefault(discriminator, [baseinfo])
+ L.append(action['info'])
+
+ else:
+ output.append(ainfo)
+
+ basepath, baseinfo = action['includepath'], action['info']
+ for _, action in rest:
+ includepath = action['includepath']
+ # Test whether path is a prefix of opath
+ if (includepath[:len(basepath)] != basepath or # not a prefix
+ includepath == basepath):
+ L = conflicts.setdefault(discriminator, [baseinfo])
+ L.append(action['info'])
+
+ if conflicts:
+ raise ConfigurationConflictError(conflicts)
+
+ # sort resolved actions by "i" and yield them one by one
+ for i, action in sorted(output, key=operator.itemgetter(0)):
+ # do not memoize the order until we resolve an action inside it
+ state.min_order = action['order']
+ state.start = i + 1
+ state.remaining_actions.remove(action)
+ state.resolved_ainfos[action['discriminator']] = (i, action)
+ yield action
+
+
+def normalize_actions(actions):
+ """Convert old-style tuple actions to new-style dicts."""
+ result = []
+ for v in actions:
+ if not isinstance(v, dict):
+ v = expand_action_tuple(*v)
+ result.append(v)
+ return result
+
+
+def expand_action_tuple(
+ discriminator, callable=None, args=(), kw=None, includepath=(),
+ info=None, order=0, introspectables=(),
+):
+ if kw is None:
+ kw = {}
+ return dict(
+ discriminator=discriminator,
+ callable=callable,
+ args=args,
+ kw=kw,
+ includepath=includepath,
+ info=info,
+ order=order,
+ introspectables=introspectables,
+ )
+
+
+global_registries = WeakOrderedSet()
diff --git a/src/pyramid/config/adapters.py b/src/pyramid/config/adapters.py
new file mode 100644
index 000000000..945faa3c6
--- /dev/null
+++ b/src/pyramid/config/adapters.py
@@ -0,0 +1,326 @@
+from webob import Response as WebobResponse
+
+from functools import update_wrapper
+
+from zope.interface import Interface
+
+from pyramid.interfaces import (
+ IResponse,
+ ITraverser,
+ IResourceURL,
+ )
+
+from pyramid.util import takes_one_arg
+
+from pyramid.config.util import action_method
+
+
+class AdaptersConfiguratorMixin(object):
+ @action_method
+ def add_subscriber(self, subscriber, iface=None, **predicates):
+ """Add an event :term:`subscriber` for the event stream
+ implied by the supplied ``iface`` interface.
+
+ The ``subscriber`` argument represents a callable object (or a
+ :term:`dotted Python name` which identifies a callable); it will be
+ called with a single object ``event`` whenever :app:`Pyramid` emits
+ an :term:`event` associated with the ``iface``, which may be an
+ :term:`interface` or a class or a :term:`dotted Python name` to a
+ global object representing an interface or a class.
+
+ Using the default ``iface`` value, ``None`` will cause the subscriber
+ to be registered for all event types. See :ref:`events_chapter` for
+ more information about events and subscribers.
+
+ Any number of predicate keyword arguments may be passed in
+ ``**predicates``. Each predicate named will narrow the set of
+ circumstances in which the subscriber will be invoked. Each named
+ predicate must have been registered via
+ :meth:`pyramid.config.Configurator.add_subscriber_predicate` before it
+ can be used. See :ref:`subscriber_predicates` for more information.
+
+ .. versionadded:: 1.4
+ The ``**predicates`` argument.
+ """
+ dotted = self.maybe_dotted
+ subscriber, iface = dotted(subscriber), dotted(iface)
+ if iface is None:
+ iface = (Interface,)
+ if not isinstance(iface, (tuple, list)):
+ iface = (iface,)
+
+ def register():
+ predlist = self.get_predlist('subscriber')
+ order, preds, phash = predlist.make(self, **predicates)
+
+ derived_predicates = [ self._derive_predicate(p) for p in preds ]
+ derived_subscriber = self._derive_subscriber(
+ subscriber,
+ derived_predicates,
+ )
+
+ intr.update(
+ {'phash':phash,
+ 'order':order,
+ 'predicates':preds,
+ 'derived_predicates':derived_predicates,
+ 'derived_subscriber':derived_subscriber,
+ }
+ )
+
+ self.registry.registerHandler(derived_subscriber, iface)
+
+ intr = self.introspectable(
+ 'subscribers',
+ id(subscriber),
+ self.object_description(subscriber),
+ 'subscriber'
+ )
+
+ intr['subscriber'] = subscriber
+ intr['interfaces'] = iface
+
+ self.action(None, register, introspectables=(intr,))
+ return subscriber
+
+ def _derive_predicate(self, predicate):
+ derived_predicate = predicate
+
+ if eventonly(predicate):
+ def derived_predicate(*arg):
+ return predicate(arg[0])
+ # seems pointless to try to fix __doc__, __module__, etc as
+ # predicate will invariably be an instance
+
+ return derived_predicate
+
+ def _derive_subscriber(self, subscriber, predicates):
+ derived_subscriber = subscriber
+
+ if eventonly(subscriber):
+ def derived_subscriber(*arg):
+ return subscriber(arg[0])
+ if hasattr(subscriber, '__name__'):
+ update_wrapper(derived_subscriber, subscriber)
+
+ if not predicates:
+ return derived_subscriber
+
+ def subscriber_wrapper(*arg):
+ # We need to accept *arg and pass it along because zope subscribers
+ # are designed awkwardly. Notification via
+ # registry.adapter.subscribers will always call an associated
+ # subscriber with all of the objects involved in the subscription
+ # lookup, despite the fact that the event sender always has the
+ # option to attach those objects to the event object itself, and
+ # almost always does.
+ #
+ # The "eventonly" jazz sprinkled in this function and related
+ # functions allows users to define subscribers and predicates which
+ # accept only an event argument without needing to accept the rest
+ # of the adaptation arguments. Had I been smart enough early on to
+ # use .subscriptions to find the subscriber functions in order to
+ # call them manually with a single "event" argument instead of
+ # relying on .subscribers to both find and call them implicitly
+ # with all args, the eventonly hack would not have been required.
+ # At this point, though, using .subscriptions and manual execution
+ # is not possible without badly breaking backwards compatibility.
+ if all((predicate(*arg) for predicate in predicates)):
+ return derived_subscriber(*arg)
+
+ if hasattr(subscriber, '__name__'):
+ update_wrapper(subscriber_wrapper, subscriber)
+
+ return subscriber_wrapper
+
+ @action_method
+ def add_subscriber_predicate(self, name, factory, weighs_more_than=None,
+ weighs_less_than=None):
+ """
+ .. versionadded:: 1.4
+
+ Adds a subscriber predicate factory. The associated subscriber
+ predicate can later be named as a keyword argument to
+ :meth:`pyramid.config.Configurator.add_subscriber` in the
+ ``**predicates`` anonymous keyword argument dictionary.
+
+ ``name`` should be the name of the predicate. It must be a valid
+ Python identifier (it will be used as a ``**predicates`` keyword
+ argument to :meth:`~pyramid.config.Configurator.add_subscriber`).
+
+ ``factory`` should be a :term:`predicate factory` or :term:`dotted
+ Python name` which refers to a predicate factory.
+
+ See :ref:`subscriber_predicates` for more information.
+
+ """
+ self._add_predicate(
+ 'subscriber',
+ name,
+ factory,
+ weighs_more_than=weighs_more_than,
+ weighs_less_than=weighs_less_than
+ )
+
+ @action_method
+ def add_response_adapter(self, adapter, type_or_iface):
+ """ When an object of type (or interface) ``type_or_iface`` is
+ returned from a view callable, Pyramid will use the adapter
+ ``adapter`` to convert it into an object which implements the
+ :class:`pyramid.interfaces.IResponse` interface. If ``adapter`` is
+ None, an object returned of type (or interface) ``type_or_iface``
+ will itself be used as a response object.
+
+ ``adapter`` and ``type_or_interface`` may be Python objects or
+ strings representing dotted names to importable Python global
+ objects.
+
+ See :ref:`using_iresponse` for more information."""
+ adapter = self.maybe_dotted(adapter)
+ type_or_iface = self.maybe_dotted(type_or_iface)
+ def register():
+ reg = self.registry
+ if adapter is None:
+ reg.registerSelfAdapter((type_or_iface,), IResponse)
+ else:
+ reg.registerAdapter(adapter, (type_or_iface,), IResponse)
+ discriminator = (IResponse, type_or_iface)
+ intr = self.introspectable(
+ 'response adapters',
+ discriminator,
+ self.object_description(adapter),
+ 'response adapter')
+ intr['adapter'] = adapter
+ intr['type'] = type_or_iface
+ self.action(discriminator, register, introspectables=(intr,))
+
+ def add_default_response_adapters(self):
+ # cope with WebOb response objects that aren't decorated with IResponse
+ self.add_response_adapter(None, WebobResponse)
+
+ @action_method
+ def add_traverser(self, adapter, iface=None):
+ """
+ The superdefault :term:`traversal` algorithm that :app:`Pyramid` uses
+ is explained in :ref:`traversal_algorithm`. Though it is rarely
+ necessary, this default algorithm can be swapped out selectively for
+ a different traversal pattern via configuration. The section
+ entitled :ref:`changing_the_traverser` details how to create a
+ traverser class.
+
+ For example, to override the superdefault traverser used by Pyramid,
+ you might do something like this:
+
+ .. code-block:: python
+
+ from myapp.traversal import MyCustomTraverser
+ config.add_traverser(MyCustomTraverser)
+
+ This would cause the Pyramid superdefault traverser to never be used;
+ instead all traversal would be done using your ``MyCustomTraverser``
+ class, no matter which object was returned by the :term:`root
+ factory` of this application. Note that we passed no arguments to
+ the ``iface`` keyword parameter. The default value of ``iface``,
+ ``None`` represents that the registered traverser should be used when
+ no other more specific traverser is available for the object returned
+ by the root factory.
+
+ However, more than one traversal algorithm can be active at the same
+ time. The traverser used can depend on the result of the :term:`root
+ factory`. For instance, if your root factory returns more than one
+ type of object conditionally, you could claim that an alternate
+ traverser adapter should be used against one particular class or
+ interface returned by that root factory. When the root factory
+ returned an object that implemented that class or interface, a custom
+ traverser would be used. Otherwise, the default traverser would be
+ used. The ``iface`` argument represents the class of the object that
+ the root factory might return or an :term:`interface` that the object
+ might implement.
+
+ To use a particular traverser only when the root factory returns a
+ particular class:
+
+ .. code-block:: python
+
+ config.add_traverser(MyCustomTraverser, MyRootClass)
+
+ When more than one traverser is active, the "most specific" traverser
+ will be used (the one that matches the class or interface of the
+ value returned by the root factory most closely).
+
+ Note that either ``adapter`` or ``iface`` can be a :term:`dotted
+ Python name` or a Python object.
+
+ See :ref:`changing_the_traverser` for more information.
+ """
+ iface = self.maybe_dotted(iface)
+ adapter = self.maybe_dotted(adapter)
+ def register(iface=iface):
+ if iface is None:
+ iface = Interface
+ self.registry.registerAdapter(adapter, (iface,), ITraverser)
+ discriminator = ('traverser', iface)
+ intr = self.introspectable(
+ 'traversers',
+ discriminator,
+ 'traverser for %r' % iface,
+ 'traverser',
+ )
+ intr['adapter'] = adapter
+ intr['iface'] = iface
+ self.action(discriminator, register, introspectables=(intr,))
+
+ @action_method
+ def add_resource_url_adapter(self, adapter, resource_iface=None):
+ """
+ .. versionadded:: 1.3
+
+ When you add a traverser as described in
+ :ref:`changing_the_traverser`, it's convenient to continue to use the
+ :meth:`pyramid.request.Request.resource_url` API. However, since the
+ way traversal is done may have been modified, the URLs that
+ ``resource_url`` generates by default may be incorrect when resources
+ are returned by a custom traverser.
+
+ If you've added a traverser, you can change how
+ :meth:`~pyramid.request.Request.resource_url` generates a URL for a
+ specific type of resource by calling this method.
+
+ The ``adapter`` argument represents a class that implements the
+ :class:`~pyramid.interfaces.IResourceURL` interface. The class
+ constructor should accept two arguments in its constructor (the
+ resource and the request) and the resulting instance should provide
+ the attributes detailed in that interface (``virtual_path`` and
+ ``physical_path``, in particular).
+
+ The ``resource_iface`` argument represents a class or interface that
+ the resource should possess for this url adapter to be used when
+ :meth:`pyramid.request.Request.resource_url` looks up a resource url
+ adapter. If ``resource_iface`` is not passed, or it is passed as
+ ``None``, the url adapter will be used for every type of resource.
+
+ See :ref:`changing_resource_url` for more information.
+ """
+ adapter = self.maybe_dotted(adapter)
+ resource_iface = self.maybe_dotted(resource_iface)
+ def register(resource_iface=resource_iface):
+ if resource_iface is None:
+ resource_iface = Interface
+ self.registry.registerAdapter(
+ adapter,
+ (resource_iface, Interface),
+ IResourceURL,
+ )
+ discriminator = ('resource url adapter', resource_iface)
+ intr = self.introspectable(
+ 'resource url adapters',
+ discriminator,
+ 'resource url adapter for resource iface %r' % resource_iface,
+ 'resource url adapter',
+ )
+ intr['adapter'] = adapter
+ intr['resource_iface'] = resource_iface
+ self.action(discriminator, register, introspectables=(intr,))
+
+def eventonly(callee):
+ return takes_one_arg(callee, argname='event')
diff --git a/src/pyramid/config/assets.py b/src/pyramid/config/assets.py
new file mode 100644
index 000000000..b9536df42
--- /dev/null
+++ b/src/pyramid/config/assets.py
@@ -0,0 +1,396 @@
+import os
+import pkg_resources
+import sys
+
+from zope.interface import implementer
+
+from pyramid.interfaces import (
+ IPackageOverrides,
+ PHASE1_CONFIG,
+)
+
+from pyramid.exceptions import ConfigurationError
+from pyramid.threadlocal import get_current_registry
+
+from pyramid.config.util import action_method
+
+class OverrideProvider(pkg_resources.DefaultProvider):
+ def __init__(self, module):
+ pkg_resources.DefaultProvider.__init__(self, module)
+ self.module_name = module.__name__
+
+ def _get_overrides(self):
+ reg = get_current_registry()
+ overrides = reg.queryUtility(IPackageOverrides, self.module_name)
+ return overrides
+
+ def get_resource_filename(self, manager, resource_name):
+ """ Return a true filesystem path for resource_name,
+ co-ordinating the extraction with manager, if the resource
+ must be unpacked to the filesystem.
+ """
+ overrides = self._get_overrides()
+ if overrides is not None:
+ filename = overrides.get_filename(resource_name)
+ if filename is not None:
+ return filename
+ return pkg_resources.DefaultProvider.get_resource_filename(
+ self, manager, resource_name)
+
+ def get_resource_stream(self, manager, resource_name):
+ """ Return a readable file-like object for resource_name."""
+ overrides = self._get_overrides()
+ if overrides is not None:
+ stream = overrides.get_stream(resource_name)
+ if stream is not None:
+ return stream
+ return pkg_resources.DefaultProvider.get_resource_stream(
+ self, manager, resource_name)
+
+ def get_resource_string(self, manager, resource_name):
+ """ Return a string containing the contents of resource_name."""
+ overrides = self._get_overrides()
+ if overrides is not None:
+ string = overrides.get_string(resource_name)
+ if string is not None:
+ return string
+ return pkg_resources.DefaultProvider.get_resource_string(
+ self, manager, resource_name)
+
+ def has_resource(self, resource_name):
+ overrides = self._get_overrides()
+ if overrides is not None:
+ result = overrides.has_resource(resource_name)
+ if result is not None:
+ return result
+ return pkg_resources.DefaultProvider.has_resource(
+ self, resource_name)
+
+ def resource_isdir(self, resource_name):
+ overrides = self._get_overrides()
+ if overrides is not None:
+ result = overrides.isdir(resource_name)
+ if result is not None:
+ return result
+ return pkg_resources.DefaultProvider.resource_isdir(
+ self, resource_name)
+
+ def resource_listdir(self, resource_name):
+ overrides = self._get_overrides()
+ if overrides is not None:
+ result = overrides.listdir(resource_name)
+ if result is not None:
+ return result
+ return pkg_resources.DefaultProvider.resource_listdir(
+ self, resource_name)
+
+
+@implementer(IPackageOverrides)
+class PackageOverrides(object):
+ # pkg_resources arg in kw args below for testing
+ def __init__(self, package, pkg_resources=pkg_resources):
+ loader = self._real_loader = getattr(package, '__loader__', None)
+ if isinstance(loader, self.__class__):
+ self._real_loader = None
+ # We register ourselves as a __loader__ *only* to support the
+ # setuptools _find_adapter adapter lookup; this class doesn't
+ # actually support the PEP 302 loader "API". This is
+ # excusable due to the following statement in the spec:
+ # ... Loader objects are not
+ # required to offer any useful functionality (any such functionality,
+ # such as the zipimport get_data() method mentioned above, is
+ # optional)...
+ # A __loader__ attribute is basically metadata, and setuptools
+ # uses it as such.
+ package.__loader__ = self
+ # we call register_loader_type for every instantiation of this
+ # class; that's OK, it's idempotent to do it more than once.
+ pkg_resources.register_loader_type(self.__class__, OverrideProvider)
+ self.overrides = []
+ self.overridden_package_name = package.__name__
+
+ def insert(self, path, source):
+ if not path or path.endswith('/'):
+ override = DirectoryOverride(path, source)
+ else:
+ override = FileOverride(path, source)
+ self.overrides.insert(0, override)
+ return override
+
+ def filtered_sources(self, resource_name):
+ for override in self.overrides:
+ o = override(resource_name)
+ if o is not None:
+ yield o
+
+ def get_filename(self, resource_name):
+ for source, path in self.filtered_sources(resource_name):
+ result = source.get_filename(path)
+ if result is not None:
+ return result
+
+ def get_stream(self, resource_name):
+ for source, path in self.filtered_sources(resource_name):
+ result = source.get_stream(path)
+ if result is not None:
+ return result
+
+ def get_string(self, resource_name):
+ for source, path in self.filtered_sources(resource_name):
+ result = source.get_string(path)
+ if result is not None:
+ return result
+
+ def has_resource(self, resource_name):
+ for source, path in self.filtered_sources(resource_name):
+ if source.exists(path):
+ return True
+
+ def isdir(self, resource_name):
+ for source, path in self.filtered_sources(resource_name):
+ result = source.isdir(path)
+ if result is not None:
+ return result
+
+ def listdir(self, resource_name):
+ for source, path in self.filtered_sources(resource_name):
+ result = source.listdir(path)
+ if result is not None:
+ return result
+
+ @property
+ def real_loader(self):
+ if self._real_loader is None:
+ raise NotImplementedError()
+ return self._real_loader
+
+ def get_data(self, path):
+ """ See IPEP302Loader.
+ """
+ return self.real_loader.get_data(path)
+
+ def is_package(self, fullname):
+ """ See IPEP302Loader.
+ """
+ return self.real_loader.is_package(fullname)
+
+ def get_code(self, fullname):
+ """ See IPEP302Loader.
+ """
+ return self.real_loader.get_code(fullname)
+
+ def get_source(self, fullname):
+ """ See IPEP302Loader.
+ """
+ return self.real_loader.get_source(fullname)
+
+
+class DirectoryOverride:
+ def __init__(self, path, source):
+ self.path = path
+ self.pathlen = len(self.path)
+ self.source = source
+
+ def __call__(self, resource_name):
+ if resource_name.startswith(self.path):
+ new_path = resource_name[self.pathlen:]
+ return self.source, new_path
+
+class FileOverride:
+ def __init__(self, path, source):
+ self.path = path
+ self.source = source
+
+ def __call__(self, resource_name):
+ if resource_name == self.path:
+ return self.source, ''
+
+
+class PackageAssetSource(object):
+ """
+ An asset source relative to a package.
+
+ If this asset source is a file, then we expect the ``prefix`` to point
+ to the new name of the file, and the incoming ``resource_name`` will be
+ the empty string, as returned by the ``FileOverride``.
+
+ """
+ def __init__(self, package, prefix):
+ self.package = package
+ if hasattr(package, '__name__'):
+ self.pkg_name = package.__name__
+ else:
+ self.pkg_name = package
+ self.prefix = prefix
+
+ def get_path(self, resource_name):
+ return '%s%s' % (self.prefix, resource_name)
+
+ def get_filename(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_filename(self.pkg_name, path)
+
+ def get_stream(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_stream(self.pkg_name, path)
+
+ def get_string(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_string(self.pkg_name, path)
+
+ def exists(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return True
+
+ def isdir(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_isdir(self.pkg_name, path)
+
+ def listdir(self, resource_name):
+ path = self.get_path(resource_name)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_listdir(self.pkg_name, path)
+
+
+class FSAssetSource(object):
+ """
+ An asset source relative to a path in the filesystem.
+
+ """
+ def __init__(self, prefix):
+ self.prefix = prefix
+
+ def get_path(self, resource_name):
+ if resource_name:
+ path = os.path.join(self.prefix, resource_name)
+ else:
+ path = self.prefix
+ return path
+
+ def get_filename(self, resource_name):
+ path = self.get_path(resource_name)
+ if os.path.exists(path):
+ return path
+
+ def get_stream(self, resource_name):
+ path = self.get_filename(resource_name)
+ if path is not None:
+ return open(path, 'rb')
+
+ def get_string(self, resource_name):
+ stream = self.get_stream(resource_name)
+ if stream is not None:
+ with stream:
+ return stream.read()
+
+ def exists(self, resource_name):
+ path = self.get_filename(resource_name)
+ if path is not None:
+ return True
+
+ def isdir(self, resource_name):
+ path = self.get_filename(resource_name)
+ if path is not None:
+ return os.path.isdir(path)
+
+ def listdir(self, resource_name):
+ path = self.get_filename(resource_name)
+ if path is not None:
+ return os.listdir(path)
+
+
+class AssetsConfiguratorMixin(object):
+ def _override(self, package, path, override_source,
+ PackageOverrides=PackageOverrides):
+ pkg_name = package.__name__
+ override = self.registry.queryUtility(IPackageOverrides, name=pkg_name)
+ if override is None:
+ override = PackageOverrides(package)
+ self.registry.registerUtility(override, IPackageOverrides,
+ name=pkg_name)
+ override.insert(path, override_source)
+
+ @action_method
+ def override_asset(self, to_override, override_with, _override=None):
+ """ Add a :app:`Pyramid` asset override to the current
+ configuration state.
+
+ ``to_override`` is an :term:`asset specification` to the
+ asset being overridden.
+
+ ``override_with`` is an :term:`asset specification` to the
+ asset that is performing the override. This may also be an absolute
+ path.
+
+ See :ref:`assets_chapter` for more
+ information about asset overrides."""
+ if to_override == override_with:
+ raise ConfigurationError(
+ 'You cannot override an asset with itself')
+
+ package = to_override
+ path = ''
+ if ':' in to_override:
+ package, path = to_override.split(':', 1)
+
+ # *_isdir = override is package or directory
+ overridden_isdir = path == '' or path.endswith('/')
+
+ if os.path.isabs(override_with):
+ override_source = FSAssetSource(override_with)
+ if not os.path.exists(override_with):
+ raise ConfigurationError(
+ 'Cannot override asset with an absolute path that does '
+ 'not exist')
+ override_isdir = os.path.isdir(override_with)
+ override_package = None
+ override_prefix = override_with
+ else:
+ override_package = override_with
+ override_prefix = ''
+ if ':' in override_with:
+ override_package, override_prefix = override_with.split(':', 1)
+
+ __import__(override_package)
+ to_package = sys.modules[override_package]
+ override_source = PackageAssetSource(to_package, override_prefix)
+
+ override_isdir = (
+ override_prefix == '' or
+ override_with.endswith('/')
+ )
+
+ if overridden_isdir and (not override_isdir):
+ raise ConfigurationError(
+ 'A directory cannot be overridden with a file (put a '
+ 'slash at the end of override_with if necessary)')
+
+ if (not overridden_isdir) and override_isdir:
+ raise ConfigurationError(
+ 'A file cannot be overridden with a directory (put a '
+ 'slash at the end of to_override if necessary)')
+
+ override = _override or self._override # test jig
+
+ def register():
+ __import__(package)
+ from_package = sys.modules[package]
+ override(from_package, path, override_source)
+
+ intr = self.introspectable(
+ 'asset overrides',
+ (package, override_package, path, override_prefix),
+ '%s -> %s' % (to_override, override_with),
+ 'asset override',
+ )
+ intr['to_override'] = to_override
+ intr['override_with'] = override_with
+ self.action(None, register, introspectables=(intr,),
+ order=PHASE1_CONFIG)
+
+ override_resource = override_asset # bw compat
diff --git a/src/pyramid/config/factories.py b/src/pyramid/config/factories.py
new file mode 100644
index 000000000..52248269d
--- /dev/null
+++ b/src/pyramid/config/factories.py
@@ -0,0 +1,245 @@
+from zope.interface import implementer
+
+from pyramid.interfaces import (
+ IDefaultRootFactory,
+ IExecutionPolicy,
+ IRequestFactory,
+ IResponseFactory,
+ IRequestExtensions,
+ IRootFactory,
+ ISessionFactory,
+ )
+
+from pyramid.router import default_execution_policy
+from pyramid.traversal import DefaultRootFactory
+
+from pyramid.util import (
+ get_callable_name,
+ InstancePropertyHelper,
+ )
+
+from pyramid.config.util import action_method
+
+class FactoriesConfiguratorMixin(object):
+ @action_method
+ def set_root_factory(self, factory):
+ """ Add a :term:`root factory` to the current configuration
+ state. If the ``factory`` argument is ``None`` a default root
+ factory will be registered.
+
+ .. note::
+
+ Using the ``root_factory`` argument to the
+ :class:`pyramid.config.Configurator` constructor can be used to
+ achieve the same purpose.
+ """
+ factory = self.maybe_dotted(factory)
+ if factory is None:
+ factory = DefaultRootFactory
+
+ def register():
+ self.registry.registerUtility(factory, IRootFactory)
+ self.registry.registerUtility(factory, IDefaultRootFactory) # b/c
+
+ intr = self.introspectable('root factories',
+ None,
+ self.object_description(factory),
+ 'root factory')
+ intr['factory'] = factory
+ self.action(IRootFactory, register, introspectables=(intr,))
+
+ _set_root_factory = set_root_factory # bw compat
+
+ @action_method
+ def set_session_factory(self, factory):
+ """
+ Configure the application with a :term:`session factory`. If this
+ method is called, the ``factory`` argument must be a session
+ factory callable or a :term:`dotted Python name` to that factory.
+
+ .. note::
+
+ Using the ``session_factory`` argument to the
+ :class:`pyramid.config.Configurator` constructor can be used to
+ achieve the same purpose.
+ """
+ factory = self.maybe_dotted(factory)
+
+ def register():
+ self.registry.registerUtility(factory, ISessionFactory)
+ intr = self.introspectable('session factory', None,
+ self.object_description(factory),
+ 'session factory')
+ intr['factory'] = factory
+ self.action(ISessionFactory, register, introspectables=(intr,))
+
+ @action_method
+ def set_request_factory(self, factory):
+ """ The object passed as ``factory`` should be an object (or a
+ :term:`dotted Python name` which refers to an object) which
+ will be used by the :app:`Pyramid` router to create all
+ request objects. This factory object must have the same
+ methods and attributes as the
+ :class:`pyramid.request.Request` class (particularly
+ ``__call__``, and ``blank``).
+
+ See :meth:`pyramid.config.Configurator.add_request_method`
+ for a less intrusive way to extend the request objects with
+ custom methods and properties.
+
+ .. note::
+
+ Using the ``request_factory`` argument to the
+ :class:`pyramid.config.Configurator` constructor
+ can be used to achieve the same purpose.
+ """
+ factory = self.maybe_dotted(factory)
+
+ def register():
+ self.registry.registerUtility(factory, IRequestFactory)
+ intr = self.introspectable('request factory', None,
+ self.object_description(factory),
+ 'request factory')
+ intr['factory'] = factory
+ self.action(IRequestFactory, register, introspectables=(intr,))
+
+ @action_method
+ def set_response_factory(self, factory):
+ """ The object passed as ``factory`` should be an object (or a
+ :term:`dotted Python name` which refers to an object) which
+ will be used by the :app:`Pyramid` as the default response
+ objects. The factory should conform to the
+ :class:`pyramid.interfaces.IResponseFactory` interface.
+
+ .. note::
+
+ Using the ``response_factory`` argument to the
+ :class:`pyramid.config.Configurator` constructor
+ can be used to achieve the same purpose.
+ """
+ factory = self.maybe_dotted(factory)
+
+ def register():
+ self.registry.registerUtility(factory, IResponseFactory)
+
+ intr = self.introspectable('response factory', None,
+ self.object_description(factory),
+ 'response factory')
+ intr['factory'] = factory
+ self.action(IResponseFactory, register, introspectables=(intr,))
+
+ @action_method
+ def add_request_method(self,
+ callable=None,
+ name=None,
+ property=False,
+ reify=False):
+ """ Add a property or method to the request object.
+
+ When adding a method to the request, ``callable`` may be any
+ function that receives the request object as the first
+ parameter. If ``name`` is ``None`` then it will be computed
+ from the name of the ``callable``.
+
+ When adding a property to the request, ``callable`` can either
+ be a callable that accepts the request as its single positional
+ parameter, or it can be a property descriptor. If ``callable`` is
+ a property descriptor, it has to be an instance of a class which is
+ a subclass of ``property``. If ``name`` is ``None``, the name of
+ the property will be computed from the name of the ``callable``.
+
+ If the ``callable`` is a property descriptor a ``ValueError``
+ will be raised if ``name`` is ``None`` or ``reify`` is ``True``.
+
+ See :meth:`pyramid.request.Request.set_property` for more
+ details on ``property`` vs ``reify``. When ``reify`` is
+ ``True``, the value of ``property`` is assumed to also be
+ ``True``.
+
+ In all cases, ``callable`` may also be a
+ :term:`dotted Python name` which refers to either a callable or
+ a property descriptor.
+
+ If ``callable`` is ``None`` then the method is only used to
+ assist in conflict detection between different addons requesting
+ the same attribute on the request object.
+
+ This is the recommended method for extending the request object
+ and should be used in favor of providing a custom request
+ factory via
+ :meth:`pyramid.config.Configurator.set_request_factory`.
+
+ .. versionadded:: 1.4
+ """
+ if callable is not None:
+ callable = self.maybe_dotted(callable)
+
+ property = property or reify
+ if property:
+ name, callable = InstancePropertyHelper.make_property(
+ callable, name=name, reify=reify)
+ elif name is None:
+ name = callable.__name__
+ else:
+ name = get_callable_name(name)
+
+ def register():
+ exts = self.registry.queryUtility(IRequestExtensions)
+
+ if exts is None:
+ exts = _RequestExtensions()
+ self.registry.registerUtility(exts, IRequestExtensions)
+
+ plist = exts.descriptors if property else exts.methods
+ plist[name] = callable
+
+ if callable is None:
+ self.action(('request extensions', name), None)
+ elif property:
+ intr = self.introspectable('request extensions', name,
+ self.object_description(callable),
+ 'request property')
+ intr['callable'] = callable
+ intr['property'] = True
+ intr['reify'] = reify
+ self.action(('request extensions', name), register,
+ introspectables=(intr,))
+ else:
+ intr = self.introspectable('request extensions', name,
+ self.object_description(callable),
+ 'request method')
+ intr['callable'] = callable
+ intr['property'] = False
+ intr['reify'] = False
+ self.action(('request extensions', name), register,
+ introspectables=(intr,))
+
+ @action_method
+ def set_execution_policy(self, policy):
+ """
+ Override the :app:`Pyramid` :term:`execution policy` in the
+ current configuration. The ``policy`` argument must be an instance
+ of an :class:`pyramid.interfaces.IExecutionPolicy` or a
+ :term:`dotted Python name` that points at an instance of an
+ execution policy.
+
+ """
+ policy = self.maybe_dotted(policy)
+ if policy is None:
+ policy = default_execution_policy
+
+ def register():
+ self.registry.registerUtility(policy, IExecutionPolicy)
+
+ intr = self.introspectable('execution policy', None,
+ self.object_description(policy),
+ 'execution policy')
+ intr['policy'] = policy
+ self.action(IExecutionPolicy, register, introspectables=(intr,))
+
+
+@implementer(IRequestExtensions)
+class _RequestExtensions(object):
+ def __init__(self):
+ self.descriptors = {}
+ self.methods = {}
diff --git a/src/pyramid/config/i18n.py b/src/pyramid/config/i18n.py
new file mode 100644
index 000000000..5dabe2845
--- /dev/null
+++ b/src/pyramid/config/i18n.py
@@ -0,0 +1,120 @@
+from pyramid.interfaces import (
+ ILocaleNegotiator,
+ ITranslationDirectories,
+ )
+
+from pyramid.exceptions import ConfigurationError
+from pyramid.path import AssetResolver
+
+from pyramid.config.util import action_method
+
+class I18NConfiguratorMixin(object):
+ @action_method
+ def set_locale_negotiator(self, negotiator):
+ """
+ Set the :term:`locale negotiator` for this application. The
+ :term:`locale negotiator` is a callable which accepts a
+ :term:`request` object and which returns a :term:`locale
+ name`. The ``negotiator`` argument should be the locale
+ negotiator implementation or a :term:`dotted Python name`
+ which refers to such an implementation.
+
+ Later calls to this method override earlier calls; there can
+ be only one locale negotiator active at a time within an
+ application. See :ref:`activating_translation` for more
+ information.
+
+ .. note::
+
+ Using the ``locale_negotiator`` argument to the
+ :class:`pyramid.config.Configurator` constructor can be used to
+ achieve the same purpose.
+ """
+ def register():
+ self._set_locale_negotiator(negotiator)
+ intr = self.introspectable('locale negotiator', None,
+ self.object_description(negotiator),
+ 'locale negotiator')
+ intr['negotiator'] = negotiator
+ self.action(ILocaleNegotiator, register, introspectables=(intr,))
+
+ def _set_locale_negotiator(self, negotiator):
+ locale_negotiator = self.maybe_dotted(negotiator)
+ self.registry.registerUtility(locale_negotiator, ILocaleNegotiator)
+
+ @action_method
+ def add_translation_dirs(self, *specs, **kw):
+ """ Add one or more :term:`translation directory` paths to the
+ current configuration state. The ``specs`` argument is a
+ sequence that may contain absolute directory paths
+ (e.g. ``/usr/share/locale``) or :term:`asset specification`
+ names naming a directory path (e.g. ``some.package:locale``)
+ or a combination of the two.
+
+ Example:
+
+ .. code-block:: python
+
+ config.add_translation_dirs('/usr/share/locale',
+ 'some.package:locale')
+
+ The translation directories are defined as a list in which
+ translations defined later have precedence over translations defined
+ earlier.
+
+ By default, consecutive calls to ``add_translation_dirs`` will add
+ directories to the start of the list. This means later calls to
+ ``add_translation_dirs`` will have their translations trumped by
+ earlier calls. If you explicitly need this call to trump an earlier
+ call then you may set ``override`` to ``True``.
+
+ If multiple specs are provided in a single call to
+ ``add_translation_dirs``, the directories will be inserted in the
+ order they're provided (earlier items are trumped by later items).
+
+ .. versionchanged:: 1.8
+
+ The ``override`` parameter was added to allow a later call
+ to ``add_translation_dirs`` to override an earlier call, inserting
+ folders at the beginning of the translation directory list.
+
+ """
+ introspectables = []
+ override = kw.pop('override', False)
+ if kw:
+ raise TypeError('invalid keyword arguments: %s', sorted(kw.keys()))
+
+ def register():
+ directories = []
+ resolver = AssetResolver(self.package_name)
+
+ # defer spec resolution until register to allow for asset
+ # overrides to take place in an earlier config phase
+ for spec in specs:
+ # the trailing slash helps match asset overrides for folders
+ if not spec.endswith('/'):
+ spec += '/'
+ asset = resolver.resolve(spec)
+ directory = asset.abspath()
+ if not asset.isdir():
+ raise ConfigurationError('"%s" is not a directory' %
+ directory)
+ intr = self.introspectable('translation directories', directory,
+ spec, 'translation directory')
+ intr['directory'] = directory
+ intr['spec'] = spec
+ introspectables.append(intr)
+ directories.append(directory)
+
+ tdirs = self.registry.queryUtility(ITranslationDirectories)
+ if tdirs is None:
+ tdirs = []
+ self.registry.registerUtility(tdirs, ITranslationDirectories)
+ if override:
+ tdirs.extend(directories)
+ else:
+ for directory in reversed(directories):
+ tdirs.insert(0, directory)
+
+ self.action(None, register, introspectables=introspectables)
+
diff --git a/src/pyramid/config/predicates.py b/src/pyramid/config/predicates.py
new file mode 100644
index 000000000..bda763161
--- /dev/null
+++ b/src/pyramid/config/predicates.py
@@ -0,0 +1,2 @@
+import zope.deprecation
+zope.deprecation.moved('pyramid.predicates', 'Pyramid 1.10')
diff --git a/src/pyramid/config/rendering.py b/src/pyramid/config/rendering.py
new file mode 100644
index 000000000..0d55c41e8
--- /dev/null
+++ b/src/pyramid/config/rendering.py
@@ -0,0 +1,51 @@
+from pyramid.interfaces import (
+ IRendererFactory,
+ PHASE1_CONFIG,
+ )
+
+from pyramid import renderers
+from pyramid.config.util import action_method
+
+DEFAULT_RENDERERS = (
+ ('json', renderers.json_renderer_factory),
+ ('string', renderers.string_renderer_factory),
+ )
+
+class RenderingConfiguratorMixin(object):
+ def add_default_renderers(self):
+ for name, renderer in DEFAULT_RENDERERS:
+ self.add_renderer(name, renderer)
+
+ @action_method
+ def add_renderer(self, name, factory):
+ """
+ Add a :app:`Pyramid` :term:`renderer` factory to the
+ current configuration state.
+
+ The ``name`` argument is the renderer name. Use ``None`` to
+ represent the default renderer (a renderer which will be used for all
+ views unless they name another renderer specifically).
+
+ The ``factory`` argument is Python reference to an
+ implementation of a :term:`renderer` factory or a
+ :term:`dotted Python name` to same.
+ """
+ factory = self.maybe_dotted(factory)
+ # if name is None or the empty string, we're trying to register
+ # a default renderer, but registerUtility is too dumb to accept None
+ # as a name
+ if not name:
+ name = ''
+ def register():
+ self.registry.registerUtility(factory, IRendererFactory, name=name)
+ intr = self.introspectable('renderer factories',
+ name,
+ self.object_description(factory),
+ 'renderer factory')
+ intr['factory'] = factory
+ intr['name'] = name
+ # we need to register renderers early (in phase 1) because they are
+ # used during view configuration (which happens in phase 3)
+ self.action((IRendererFactory, name), register, order=PHASE1_CONFIG,
+ introspectables=(intr,))
+
diff --git a/src/pyramid/config/routes.py b/src/pyramid/config/routes.py
new file mode 100644
index 000000000..5d05429a7
--- /dev/null
+++ b/src/pyramid/config/routes.py
@@ -0,0 +1,560 @@
+import contextlib
+import warnings
+
+from pyramid.compat import urlparse
+from pyramid.interfaces import (
+ IRequest,
+ IRouteRequest,
+ IRoutesMapper,
+ PHASE2_CONFIG,
+ )
+
+from pyramid.exceptions import ConfigurationError
+from pyramid.request import route_request_iface
+from pyramid.urldispatch import RoutesMapper
+
+from pyramid.util import (
+ as_sorted_tuple,
+ is_nonstr_iter,
+)
+
+import pyramid.predicates
+
+from pyramid.config.util import (
+ action_method,
+ normalize_accept_offer,
+ predvalseq,
+)
+
+class RoutesConfiguratorMixin(object):
+ @action_method
+ def add_route(self,
+ name,
+ pattern=None,
+ factory=None,
+ for_=None,
+ header=None,
+ xhr=None,
+ accept=None,
+ path_info=None,
+ request_method=None,
+ request_param=None,
+ traverse=None,
+ custom_predicates=(),
+ use_global_views=False,
+ path=None,
+ pregenerator=None,
+ static=False,
+ **predicates):
+ """ Add a :term:`route configuration` to the current
+ configuration state, as well as possibly a :term:`view
+ configuration` to be used to specify a :term:`view callable`
+ that will be invoked when this route matches. The arguments
+ to this method are divided into *predicate*, *non-predicate*,
+ and *view-related* types. :term:`Route predicate` arguments
+ narrow the circumstances in which a route will be match a
+ request; non-predicate arguments are informational.
+
+ Non-Predicate Arguments
+
+ name
+
+ The name of the route, e.g. ``myroute``. This attribute is
+ required. It must be unique among all defined routes in a given
+ application.
+
+ factory
+
+ A Python object (often a function or a class) or a :term:`dotted
+ Python name` which refers to the same object that will generate a
+ :app:`Pyramid` root resource object when this route matches. For
+ example, ``mypackage.resources.MyFactory``. If this argument is
+ not specified, a default root factory will be used. See
+ :ref:`the_resource_tree` for more information about root factories.
+
+ traverse
+
+ If you would like to cause the :term:`context` to be
+ something other than the :term:`root` object when this route
+ matches, you can spell a traversal pattern as the
+ ``traverse`` argument. This traversal pattern will be used
+ as the traversal path: traversal will begin at the root
+ object implied by this route (either the global root, or the
+ object returned by the ``factory`` associated with this
+ route).
+
+ The syntax of the ``traverse`` argument is the same as it is
+ for ``pattern``. For example, if the ``pattern`` provided to
+ ``add_route`` is ``articles/{article}/edit``, and the
+ ``traverse`` argument provided to ``add_route`` is
+ ``/{article}``, when a request comes in that causes the route
+ to match in such a way that the ``article`` match value is
+ ``'1'`` (when the request URI is ``/articles/1/edit``), the
+ traversal path will be generated as ``/1``. This means that
+ the root object's ``__getitem__`` will be called with the
+ name ``'1'`` during the traversal phase. If the ``'1'`` object
+ exists, it will become the :term:`context` of the request.
+ :ref:`traversal_chapter` has more information about
+ traversal.
+
+ If the traversal path contains segment marker names which
+ are not present in the ``pattern`` argument, a runtime error
+ will occur. The ``traverse`` pattern should not contain
+ segment markers that do not exist in the ``pattern``
+ argument.
+
+ A similar combining of routing and traversal is available
+ when a route is matched which contains a ``*traverse``
+ remainder marker in its pattern (see
+ :ref:`using_traverse_in_a_route_pattern`). The ``traverse``
+ argument to add_route allows you to associate route patterns
+ with an arbitrary traversal path without using a
+ ``*traverse`` remainder marker; instead you can use other
+ match information.
+
+ Note that the ``traverse`` argument to ``add_route`` is
+ ignored when attached to a route that has a ``*traverse``
+ remainder marker in its pattern.
+
+ pregenerator
+
+ This option should be a callable object that implements the
+ :class:`pyramid.interfaces.IRoutePregenerator` interface. A
+ :term:`pregenerator` is a callable called by the
+ :meth:`pyramid.request.Request.route_url` function to augment or
+ replace the arguments it is passed when generating a URL for the
+ route. This is a feature not often used directly by applications,
+ it is meant to be hooked by frameworks that use :app:`Pyramid` as
+ a base.
+
+ use_global_views
+
+ When a request matches this route, and view lookup cannot
+ find a view which has a ``route_name`` predicate argument
+ that matches the route, try to fall back to using a view
+ that otherwise matches the context, request, and view name
+ (but which does not match the route_name predicate).
+
+ static
+
+ If ``static`` is ``True``, this route will never match an incoming
+ request; it will only be useful for URL generation. By default,
+ ``static`` is ``False``. See :ref:`static_route_narr`.
+
+ .. versionadded:: 1.1
+
+ Predicate Arguments
+
+ pattern
+
+ The pattern of the route e.g. ``ideas/{idea}``. This
+ argument is required. See :ref:`route_pattern_syntax`
+ for information about the syntax of route patterns. If the
+ pattern doesn't match the current URL, route matching
+ continues.
+
+ .. note::
+
+ For backwards compatibility purposes (as of :app:`Pyramid` 1.0), a
+ ``path`` keyword argument passed to this function will be used to
+ represent the pattern value if the ``pattern`` argument is
+ ``None``. If both ``path`` and ``pattern`` are passed, ``pattern``
+ wins.
+
+ xhr
+
+ This value should be either ``True`` or ``False``. If this
+ value is specified and is ``True``, the :term:`request` must
+ possess an ``HTTP_X_REQUESTED_WITH`` (aka
+ ``X-Requested-With``) header for this route to match. This
+ is useful for detecting AJAX requests issued from jQuery,
+ Prototype and other Javascript libraries. If this predicate
+ returns ``False``, route matching continues.
+
+ request_method
+
+ A string representing an HTTP method name, e.g. ``GET``, ``POST``,
+ ``HEAD``, ``DELETE``, ``PUT`` or a tuple of elements containing
+ HTTP method names. If this argument is not specified, this route
+ will match if the request has *any* request method. If this
+ predicate returns ``False``, route matching continues.
+
+ .. versionchanged:: 1.2
+ The ability to pass a tuple of items as ``request_method``.
+ Previous versions allowed only a string.
+
+ path_info
+
+ This value represents a regular expression pattern that will
+ be tested against the ``PATH_INFO`` WSGI environment
+ variable. If the regex matches, this predicate will return
+ ``True``. If this predicate returns ``False``, route
+ matching continues.
+
+ request_param
+
+ This value can be any string or an iterable of strings. A view
+ declaration with this argument ensures that the associated route will
+ only match when the request has a key in the ``request.params``
+ dictionary (an HTTP ``GET`` or ``POST`` variable) that has a
+ name which matches the supplied value. If the value
+ supplied as the argument has a ``=`` sign in it,
+ e.g. ``request_param="foo=123"``, then the key
+ (``foo``) must both exist in the ``request.params`` dictionary, and
+ the value must match the right hand side of the expression (``123``)
+ for the route to "match" the current request. If this predicate
+ returns ``False``, route matching continues.
+
+ header
+
+ This argument represents an HTTP header name or a header
+ name/value pair. If the argument contains a ``:`` (colon),
+ it will be considered a name/value pair
+ (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). If
+ the value contains a colon, the value portion should be a
+ regular expression. If the value does not contain a colon,
+ the entire value will be considered to be the header name
+ (e.g. ``If-Modified-Since``). If the value evaluates to a
+ header name only without a value, the header specified by
+ the name must be present in the request for this predicate
+ to be true. If the value evaluates to a header name/value
+ pair, the header specified by the name must be present in
+ the request *and* the regular expression specified as the
+ value must match the header value. Whether or not the value
+ represents a header name or a header name/value pair, the
+ case of the header name is not significant. If this
+ predicate returns ``False``, route matching continues.
+
+ accept
+
+ A :term:`media type` that will be matched against the ``Accept``
+ HTTP request header. If this value is specified, it may be a
+ specific media type such as ``text/html``, or a list of the same.
+ If the media type is acceptable by the ``Accept`` header of the
+ request, or if the ``Accept`` header isn't set at all in the request,
+ this predicate will match. If this does not match the ``Accept``
+ header of the request, route matching continues.
+
+ If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is
+ not taken into consideration when deciding whether or not to select
+ the route.
+
+ Unlike the ``accept`` argument to
+ :meth:`pyramid.config.Configurator.add_view`, this value is
+ strictly a predicate and supports :func:`pyramid.config.not_`.
+
+ .. versionchanged:: 1.10
+
+ Specifying a media range is deprecated due to changes in WebOb
+ and ambiguities that occur when trying to match ranges against
+ ranges in the ``Accept`` header. Support will be removed in
+ :app:`Pyramid` 2.0. Use a list of specific media types to match
+ more than one type.
+
+ effective_principals
+
+ If specified, this value should be a :term:`principal` identifier or
+ a sequence of principal identifiers. If the
+ :attr:`pyramid.request.Request.effective_principals` property
+ indicates that every principal named in the argument list is present
+ in the current request, this predicate will return True; otherwise it
+ will return False. For example:
+ ``effective_principals=pyramid.security.Authenticated`` or
+ ``effective_principals=('fred', 'group:admins')``.
+
+ .. versionadded:: 1.4a4
+
+ custom_predicates
+
+ .. deprecated:: 1.5
+ This value should be a sequence of references to custom
+ predicate callables. Use custom predicates when no set of
+ predefined predicates does what you need. Custom predicates
+ can be combined with predefined predicates as necessary.
+ Each custom predicate callable should accept two arguments:
+ ``info`` and ``request`` and should return either ``True``
+ or ``False`` after doing arbitrary evaluation of the info
+ and/or the request. If all custom and non-custom predicate
+ callables return ``True`` the associated route will be
+ considered viable for a given request. If any predicate
+ callable returns ``False``, route matching continues. Note
+ that the value ``info`` passed to a custom route predicate
+ is a dictionary containing matching information; see
+ :ref:`custom_route_predicates` for more information about
+ ``info``.
+
+ predicates
+
+ Pass a key/value pair here to use a third-party predicate
+ registered via
+ :meth:`pyramid.config.Configurator.add_route_predicate`. More than
+ one key/value pair can be used at the same time. See
+ :ref:`view_and_route_predicates` for more information about
+ third-party predicates.
+
+ .. versionadded:: 1.4
+
+ """
+ if custom_predicates:
+ warnings.warn(
+ ('The "custom_predicates" argument to Configurator.add_route '
+ 'is deprecated as of Pyramid 1.5. Use '
+ '"config.add_route_predicate" and use the registered '
+ 'route predicate as a predicate argument to add_route '
+ 'instead. See "Adding A Third Party View, Route, or '
+ 'Subscriber Predicate" in the "Hooks" chapter of the '
+ 'documentation for more information.'),
+ DeprecationWarning,
+ stacklevel=3
+ )
+
+ if accept is not None:
+ if not is_nonstr_iter(accept):
+ if '*' in accept:
+ warnings.warn(
+ ('Passing a media range to the "accept" argument of '
+ 'Configurator.add_route is deprecated as of Pyramid '
+ '1.10. Use a list of explicit media types.'),
+ DeprecationWarning,
+ stacklevel=3,
+ )
+ # XXX switch this to False when range support is dropped
+ accept = [normalize_accept_offer(accept, allow_range=True)]
+
+ else:
+ accept = [
+ normalize_accept_offer(accept_option)
+ for accept_option in accept
+ ]
+
+ # these are route predicates; if they do not match, the next route
+ # in the routelist will be tried
+ if request_method is not None:
+ request_method = as_sorted_tuple(request_method)
+
+ factory = self.maybe_dotted(factory)
+ if pattern is None:
+ pattern = path
+ if pattern is None:
+ raise ConfigurationError('"pattern" argument may not be None')
+
+ # check for an external route; an external route is one which is
+ # is a full url (e.g. 'http://example.com/{id}')
+ parsed = urlparse.urlparse(pattern)
+ external_url = pattern
+
+ if parsed.hostname:
+ pattern = parsed.path
+
+ original_pregenerator = pregenerator
+ def external_url_pregenerator(request, elements, kw):
+ if '_app_url' in kw:
+ raise ValueError(
+ 'You cannot generate a path to an external route '
+ 'pattern via request.route_path nor pass an _app_url '
+ 'to request.route_url when generating a URL for an '
+ 'external route pattern (pattern was "%s") ' %
+ (pattern,)
+ )
+ if '_scheme' in kw:
+ scheme = kw['_scheme']
+ elif parsed.scheme:
+ scheme = parsed.scheme
+ else:
+ scheme = request.scheme
+ kw['_app_url'] = '{0}://{1}'.format(scheme, parsed.netloc)
+
+ if original_pregenerator:
+ elements, kw = original_pregenerator(
+ request, elements, kw)
+ return elements, kw
+
+ pregenerator = external_url_pregenerator
+ static = True
+
+ elif self.route_prefix:
+ pattern = self.route_prefix.rstrip('/') + '/' + pattern.lstrip('/')
+
+ mapper = self.get_routes_mapper()
+
+ introspectables = []
+
+ intr = self.introspectable('routes',
+ name,
+ '%s (pattern: %r)' % (name, pattern),
+ 'route')
+ intr['name'] = name
+ intr['pattern'] = pattern
+ intr['factory'] = factory
+ intr['xhr'] = xhr
+ intr['request_methods'] = request_method
+ intr['path_info'] = path_info
+ intr['request_param'] = request_param
+ intr['header'] = header
+ intr['accept'] = accept
+ intr['traverse'] = traverse
+ intr['custom_predicates'] = custom_predicates
+ intr['pregenerator'] = pregenerator
+ intr['static'] = static
+ intr['use_global_views'] = use_global_views
+
+ if static is True:
+ intr['external_url'] = external_url
+
+ introspectables.append(intr)
+
+ if factory:
+ factory_intr = self.introspectable('root factories',
+ name,
+ self.object_description(factory),
+ 'root factory')
+ factory_intr['factory'] = factory
+ factory_intr['route_name'] = name
+ factory_intr.relate('routes', name)
+ introspectables.append(factory_intr)
+
+ def register_route_request_iface():
+ request_iface = self.registry.queryUtility(IRouteRequest, name=name)
+ if request_iface is None:
+ if use_global_views:
+ bases = (IRequest,)
+ else:
+ bases = ()
+ request_iface = route_request_iface(name, bases)
+ self.registry.registerUtility(
+ request_iface, IRouteRequest, name=name)
+
+ def register_connect():
+ pvals = predicates.copy()
+ pvals.update(
+ dict(
+ xhr=xhr,
+ request_method=request_method,
+ path_info=path_info,
+ request_param=request_param,
+ header=header,
+ accept=accept,
+ traverse=traverse,
+ custom=predvalseq(custom_predicates),
+ )
+ )
+
+ predlist = self.get_predlist('route')
+ _, preds, _ = predlist.make(self, **pvals)
+ route = mapper.connect(
+ name, pattern, factory, predicates=preds,
+ pregenerator=pregenerator, static=static
+ )
+ intr['object'] = route
+ return route
+
+ # We have to connect routes in the order they were provided;
+ # we can't use a phase to do that, because when the actions are
+ # sorted, actions in the same phase lose relative ordering
+ self.action(('route-connect', name), register_connect)
+
+ # But IRouteRequest interfaces must be registered before we begin to
+ # process view registrations (in phase 3)
+ self.action(('route', name), register_route_request_iface,
+ order=PHASE2_CONFIG, introspectables=introspectables)
+
+ @action_method
+ def add_route_predicate(self, name, factory, weighs_more_than=None,
+ weighs_less_than=None):
+ """ Adds a route predicate factory. The view predicate can later be
+ named as a keyword argument to
+ :meth:`pyramid.config.Configurator.add_route`.
+
+ ``name`` should be the name of the predicate. It must be a valid
+ Python identifier (it will be used as a keyword argument to
+ ``add_route``).
+
+ ``factory`` should be a :term:`predicate factory` or :term:`dotted
+ Python name` which refers to a predicate factory.
+
+ See :ref:`view_and_route_predicates` for more information.
+
+ .. versionadded:: 1.4
+ """
+ self._add_predicate(
+ 'route',
+ name,
+ factory,
+ weighs_more_than=weighs_more_than,
+ weighs_less_than=weighs_less_than
+ )
+
+ def add_default_route_predicates(self):
+ p = pyramid.predicates
+ for (name, factory) in (
+ ('xhr', p.XHRPredicate),
+ ('request_method', p.RequestMethodPredicate),
+ ('path_info', p.PathInfoPredicate),
+ ('request_param', p.RequestParamPredicate),
+ ('header', p.HeaderPredicate),
+ ('accept', p.AcceptPredicate),
+ ('effective_principals', p.EffectivePrincipalsPredicate),
+ ('custom', p.CustomPredicate),
+ ('traverse', p.TraversePredicate),
+ ):
+ self.add_route_predicate(name, factory)
+
+ def get_routes_mapper(self):
+ """ Return the :term:`routes mapper` object associated with
+ this configurator's :term:`registry`."""
+ mapper = self.registry.queryUtility(IRoutesMapper)
+ if mapper is None:
+ mapper = RoutesMapper()
+ self.registry.registerUtility(mapper, IRoutesMapper)
+ return mapper
+
+ @contextlib.contextmanager
+ def route_prefix_context(self, route_prefix):
+ """ Return this configurator with the
+ :attr:`pyramid.config.Configurator.route_prefix` attribute mutated to
+ include the new ``route_prefix``.
+
+ When the context exits, the ``route_prefix`` is reset to the original.
+
+ Example Usage:
+
+ >>> config = Configurator()
+ >>> with config.route_prefix_context('foo'):
+ ... config.add_route('bar', '/bar')
+
+ Arguments
+
+ route_prefix
+
+ A string suitable to be used as a route prefix, or ``None``.
+
+ .. versionadded:: 1.10
+ """
+
+ original_route_prefix = self.route_prefix
+
+ if route_prefix is None:
+ route_prefix = ''
+
+ old_route_prefix = self.route_prefix
+ if old_route_prefix is None:
+ old_route_prefix = ''
+
+ route_prefix = '{}/{}'.format(
+ old_route_prefix.rstrip('/'),
+ route_prefix.lstrip('/'),
+ )
+
+ route_prefix = route_prefix.strip('/')
+
+ if not route_prefix:
+ route_prefix = None
+
+ self.begin()
+ try:
+ self.route_prefix = route_prefix
+ yield
+
+ finally:
+ self.route_prefix = original_route_prefix
+ self.end()
diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py
new file mode 100644
index 000000000..c7afbcf4e
--- /dev/null
+++ b/src/pyramid/config/security.py
@@ -0,0 +1,265 @@
+from zope.interface import implementer
+
+from pyramid.interfaces import (
+ IAuthorizationPolicy,
+ IAuthenticationPolicy,
+ ICSRFStoragePolicy,
+ IDefaultCSRFOptions,
+ IDefaultPermission,
+ PHASE1_CONFIG,
+ PHASE2_CONFIG,
+ )
+
+from pyramid.csrf import LegacySessionCSRFStoragePolicy
+from pyramid.exceptions import ConfigurationError
+from pyramid.util import as_sorted_tuple
+
+from pyramid.config.util import action_method
+
+class SecurityConfiguratorMixin(object):
+
+ def add_default_security(self):
+ self.set_csrf_storage_policy(LegacySessionCSRFStoragePolicy())
+
+ @action_method
+ def set_authentication_policy(self, policy):
+ """ Override the :app:`Pyramid` :term:`authentication policy` in the
+ current configuration. The ``policy`` argument must be an instance
+ of an authentication policy or a :term:`dotted Python name`
+ that points at an instance of an authentication policy.
+
+ .. note::
+
+ Using the ``authentication_policy`` argument to the
+ :class:`pyramid.config.Configurator` constructor can be used to
+ achieve the same purpose.
+
+ """
+ def register():
+ self._set_authentication_policy(policy)
+ if self.registry.queryUtility(IAuthorizationPolicy) is None:
+ raise ConfigurationError(
+ 'Cannot configure an authentication policy without '
+ 'also configuring an authorization policy '
+ '(use the set_authorization_policy method)')
+ intr = self.introspectable('authentication policy', None,
+ self.object_description(policy),
+ 'authentication policy')
+ intr['policy'] = policy
+ # authentication policy used by view config (phase 3)
+ self.action(IAuthenticationPolicy, register, order=PHASE2_CONFIG,
+ introspectables=(intr,))
+
+ def _set_authentication_policy(self, policy):
+ policy = self.maybe_dotted(policy)
+ self.registry.registerUtility(policy, IAuthenticationPolicy)
+
+ @action_method
+ def set_authorization_policy(self, policy):
+ """ Override the :app:`Pyramid` :term:`authorization policy` in the
+ current configuration. The ``policy`` argument must be an instance
+ of an authorization policy or a :term:`dotted Python name` that points
+ at an instance of an authorization policy.
+
+ .. note::
+
+ Using the ``authorization_policy`` argument to the
+ :class:`pyramid.config.Configurator` constructor can be used to
+ achieve the same purpose.
+ """
+ def register():
+ self._set_authorization_policy(policy)
+ def ensure():
+ if self.autocommit:
+ return
+ if self.registry.queryUtility(IAuthenticationPolicy) is None:
+ raise ConfigurationError(
+ 'Cannot configure an authorization policy without '
+ 'also configuring an authentication policy '
+ '(use the set_authorization_policy method)')
+
+ intr = self.introspectable('authorization policy', None,
+ self.object_description(policy),
+ 'authorization policy')
+ intr['policy'] = policy
+ # authorization policy used by view config (phase 3) and
+ # authentication policy (phase 2)
+ self.action(IAuthorizationPolicy, register, order=PHASE1_CONFIG,
+ introspectables=(intr,))
+ self.action(None, ensure)
+
+ def _set_authorization_policy(self, policy):
+ policy = self.maybe_dotted(policy)
+ self.registry.registerUtility(policy, IAuthorizationPolicy)
+
+ @action_method
+ def set_default_permission(self, permission):
+ """
+ Set the default permission to be used by all subsequent
+ :term:`view configuration` registrations. ``permission``
+ should be a :term:`permission` string to be used as the
+ default permission. An example of a permission
+ string:``'view'``. Adding a default permission makes it
+ unnecessary to protect each view configuration with an
+ explicit permission, unless your application policy requires
+ some exception for a particular view.
+
+ If a default permission is *not* set, views represented by
+ view configuration registrations which do not explicitly
+ declare a permission will be executable by entirely anonymous
+ users (any authorization policy is ignored).
+
+ Later calls to this method override will conflict with earlier calls;
+ there can be only one default permission active at a time within an
+ application.
+
+ .. warning::
+
+ If a default permission is in effect, view configurations meant to
+ create a truly anonymously accessible view (even :term:`exception
+ view` views) *must* use the value of the permission importable as
+ :data:`pyramid.security.NO_PERMISSION_REQUIRED`. When this string
+ is used as the ``permission`` for a view configuration, the default
+ permission is ignored, and the view is registered, making it
+ available to all callers regardless of their credentials.
+
+ .. seealso::
+
+ See also :ref:`setting_a_default_permission`.
+
+ .. note::
+
+ Using the ``default_permission`` argument to the
+ :class:`pyramid.config.Configurator` constructor can be used to
+ achieve the same purpose.
+ """
+ def register():
+ self.registry.registerUtility(permission, IDefaultPermission)
+ intr = self.introspectable('default permission',
+ None,
+ permission,
+ 'default permission')
+ intr['value'] = permission
+ perm_intr = self.introspectable('permissions',
+ permission,
+ permission,
+ 'permission')
+ perm_intr['value'] = permission
+ # default permission used during view registration (phase 3)
+ self.action(IDefaultPermission, register, order=PHASE1_CONFIG,
+ introspectables=(intr, perm_intr,))
+
+ def add_permission(self, permission_name):
+ """
+ A configurator directive which registers a free-standing
+ permission without associating it with a view callable. This can be
+ used so that the permission shows up in the introspectable data under
+ the ``permissions`` category (permissions mentioned via ``add_view``
+ already end up in there). For example::
+
+ config = Configurator()
+ config.add_permission('view')
+ """
+ intr = self.introspectable(
+ 'permissions',
+ permission_name,
+ permission_name,
+ 'permission'
+ )
+ intr['value'] = permission_name
+ self.action(None, introspectables=(intr,))
+
+ @action_method
+ def set_default_csrf_options(
+ self,
+ require_csrf=True,
+ token='csrf_token',
+ header='X-CSRF-Token',
+ safe_methods=('GET', 'HEAD', 'OPTIONS', 'TRACE'),
+ callback=None,
+ ):
+ """
+ Set the default CSRF options used by subsequent view registrations.
+
+ ``require_csrf`` controls whether CSRF checks will be automatically
+ enabled on each view in the application. This value is used as the
+ fallback when ``require_csrf`` is left at the default of ``None`` on
+ :meth:`pyramid.config.Configurator.add_view`.
+
+ ``token`` is the name of the CSRF token used in the body of the
+ request, accessed via ``request.POST[token]``. Default: ``csrf_token``.
+
+ ``header`` is the name of the header containing the CSRF token,
+ accessed via ``request.headers[header]``. Default: ``X-CSRF-Token``.
+
+ If ``token`` or ``header`` are set to ``None`` they will not be used
+ for checking CSRF tokens.
+
+ ``safe_methods`` is an iterable of HTTP methods which are expected to
+ not contain side-effects as defined by RFC2616. Safe methods will
+ never be automatically checked for CSRF tokens.
+ Default: ``('GET', 'HEAD', 'OPTIONS', TRACE')``.
+
+ If ``callback`` is set, it must be a callable accepting ``(request)``
+ and returning ``True`` if the request should be checked for a valid
+ CSRF token. This callback allows an application to support
+ alternate authentication methods that do not rely on cookies which
+ are not subject to CSRF attacks. For example, if a request is
+ authenticated using the ``Authorization`` header instead of a cookie,
+ this may return ``False`` for that request so that clients do not
+ need to send the ``X-CSRF-Token`` header. The callback is only tested
+ for non-safe methods as defined by ``safe_methods``.
+
+ .. versionadded:: 1.7
+
+ .. versionchanged:: 1.8
+ Added the ``callback`` option.
+
+ """
+ options = DefaultCSRFOptions(
+ require_csrf, token, header, safe_methods, callback,
+ )
+ def register():
+ self.registry.registerUtility(options, IDefaultCSRFOptions)
+ intr = self.introspectable('default csrf view options',
+ None,
+ options,
+ 'default csrf view options')
+ intr['require_csrf'] = require_csrf
+ intr['token'] = token
+ intr['header'] = header
+ intr['safe_methods'] = as_sorted_tuple(safe_methods)
+ intr['callback'] = callback
+
+ self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG,
+ introspectables=(intr,))
+
+ @action_method
+ def set_csrf_storage_policy(self, policy):
+ """
+ Set the :term:`CSRF storage policy` used by subsequent view
+ registrations.
+
+ ``policy`` is a class that implements the
+ :meth:`pyramid.interfaces.ICSRFStoragePolicy` interface and defines
+ how to generate and persist CSRF tokens.
+
+ """
+ def register():
+ self.registry.registerUtility(policy, ICSRFStoragePolicy)
+ intr = self.introspectable('csrf storage policy',
+ None,
+ policy,
+ 'csrf storage policy')
+ intr['policy'] = policy
+ self.action(ICSRFStoragePolicy, register, introspectables=(intr,))
+
+
+@implementer(IDefaultCSRFOptions)
+class DefaultCSRFOptions(object):
+ def __init__(self, require_csrf, token, header, safe_methods, callback):
+ self.require_csrf = require_csrf
+ self.token = token
+ self.header = header
+ self.safe_methods = frozenset(safe_methods)
+ self.callback = callback
diff --git a/src/pyramid/config/settings.py b/src/pyramid/config/settings.py
new file mode 100644
index 000000000..11a1f7d8c
--- /dev/null
+++ b/src/pyramid/config/settings.py
@@ -0,0 +1,107 @@
+import os
+
+from pyramid.settings import asbool, aslist
+
+class SettingsConfiguratorMixin(object):
+ def _set_settings(self, mapping):
+ if mapping is None:
+ mapping = {}
+ settings = Settings(mapping)
+ self.registry.settings = settings
+ return settings
+
+ def add_settings(self, settings=None, **kw):
+ """Augment the :term:`deployment settings` with one or more
+ key/value pairs.
+
+ You may pass a dictionary::
+
+ config.add_settings({'external_uri':'http://example.com'})
+
+ Or a set of key/value pairs::
+
+ config.add_settings(external_uri='http://example.com')
+
+ This function is useful when you need to test code that accesses the
+ :attr:`pyramid.registry.Registry.settings` API (or the
+ :meth:`pyramid.config.Configurator.get_settings` API) and
+ which uses values from that API.
+ """
+ if settings is None:
+ settings = {}
+ utility = self.registry.settings
+ if utility is None:
+ utility = self._set_settings(settings)
+ utility.update(settings)
+ utility.update(kw)
+
+ def get_settings(self):
+ """
+ Return a :term:`deployment settings` object for the current
+ application. A deployment settings object is a dictionary-like
+ object that contains key/value pairs based on the dictionary passed
+ as the ``settings`` argument to the
+ :class:`pyramid.config.Configurator` constructor.
+
+ .. note:: the :attr:`pyramid.registry.Registry.settings` API
+ performs the same duty.
+ """
+ return self.registry.settings
+
+
+def Settings(d=None, _environ_=os.environ, **kw):
+ """ Deployment settings. Update application settings (usually
+ from PasteDeploy keywords) with framework-specific key/value pairs
+ (e.g. find ``PYRAMID_DEBUG_AUTHORIZATION`` in os.environ and jam into
+ keyword args)."""
+ if d is None:
+ d = {}
+ d = dict(d)
+ d.update(**kw)
+
+ eget = _environ_.get
+ def expand_key(key):
+ keys = [key]
+ if not key.startswith('pyramid.'):
+ keys.append('pyramid.' + key)
+ return keys
+ def S(settings_key, env_key=None, type_=str, default=False):
+ value = default
+ keys = expand_key(settings_key)
+ for key in keys:
+ value = d.get(key, value)
+ if env_key:
+ value = eget(env_key, value)
+ value = type_(value)
+ d.update({k: value for k in keys})
+ def O(settings_key, override_key): # noqa: E743
+ for key in expand_key(settings_key):
+ d[key] = d[key] or d[override_key]
+
+ S('debug_all', 'PYRAMID_DEBUG_ALL', asbool)
+ S('debug_authorization', 'PYRAMID_DEBUG_AUTHORIZATION', asbool)
+ O('debug_authorization', 'debug_all')
+ S('debug_notfound', 'PYRAMID_DEBUG_NOTFOUND', asbool)
+ O('debug_notfound', 'debug_all')
+ S('debug_routematch', 'PYRAMID_DEBUG_ROUTEMATCH', asbool)
+ O('debug_routematch', 'debug_all')
+ S('debug_templates', 'PYRAMID_DEBUG_TEMPLATES', asbool)
+ O('debug_templates', 'debug_all')
+
+ S('reload_all', 'PYRAMID_RELOAD_ALL', asbool)
+ S('reload_templates', 'PYRAMID_RELOAD_TEMPLATES', asbool)
+ O('reload_templates', 'reload_all')
+ S('reload_assets', 'PYRAMID_RELOAD_ASSETS', asbool)
+ O('reload_assets', 'reload_all')
+ S('reload_resources', 'PYRAMID_RELOAD_RESOURCES', asbool)
+ O('reload_resources', 'reload_all')
+ # reload_resources is an older alias for reload_assets
+ for k in expand_key('reload_assets') + expand_key('reload_resources'):
+ d[k] = d['reload_assets'] or d['reload_resources']
+
+ S('default_locale_name', 'PYRAMID_DEFAULT_LOCALE_NAME', str, 'en')
+ S('prevent_http_cache', 'PYRAMID_PREVENT_HTTP_CACHE', asbool)
+ S('prevent_cachebust', 'PYRAMID_PREVENT_CACHEBUST', asbool)
+ S('csrf_trusted_origins', 'PYRAMID_CSRF_TRUSTED_ORIGINS', aslist, [])
+
+ return d
diff --git a/src/pyramid/config/testing.py b/src/pyramid/config/testing.py
new file mode 100644
index 000000000..1daf5cdeb
--- /dev/null
+++ b/src/pyramid/config/testing.py
@@ -0,0 +1,167 @@
+from zope.interface import Interface
+
+from pyramid.interfaces import (
+ ITraverser,
+ IAuthorizationPolicy,
+ IAuthenticationPolicy,
+ IRendererFactory,
+ )
+
+from pyramid.renderers import RendererHelper
+
+from pyramid.traversal import (
+ decode_path_info,
+ split_path_info,
+ )
+
+from pyramid.config.util import action_method
+
+class TestingConfiguratorMixin(object):
+ # testing API
+ def testing_securitypolicy(self, userid=None, groupids=(),
+ permissive=True, remember_result=None,
+ forget_result=None):
+ """Unit/integration testing helper: Registers a pair of faux
+ :app:`Pyramid` security policies: a :term:`authentication
+ policy` and a :term:`authorization policy`.
+
+ The behavior of the registered :term:`authorization policy`
+ depends on the ``permissive`` argument. If ``permissive`` is
+ true, a permissive :term:`authorization policy` is registered;
+ this policy allows all access. If ``permissive`` is false, a
+ nonpermissive :term:`authorization policy` is registered; this
+ policy denies all access.
+
+ ``remember_result``, if provided, should be the result returned by
+ the ``remember`` method of the faux authentication policy. If it is
+ not provided (or it is provided, and is ``None``), the default value
+ ``[]`` (the empty list) will be returned by ``remember``.
+
+ ``forget_result``, if provided, should be the result returned by
+ the ``forget`` method of the faux authentication policy. If it is
+ not provided (or it is provided, and is ``None``), the default value
+ ``[]`` (the empty list) will be returned by ``forget``.
+
+ The behavior of the registered :term:`authentication policy`
+ depends on the values provided for the ``userid`` and
+ ``groupids`` argument. The authentication policy will return
+ the userid identifier implied by the ``userid`` argument and
+ the group ids implied by the ``groupids`` argument when the
+ :attr:`pyramid.request.Request.authenticated_userid` or
+ :attr:`pyramid.request.Request.effective_principals` APIs are
+ used.
+
+ This function is most useful when testing code that uses
+ the APIs named :meth:`pyramid.request.Request.has_permission`,
+ :attr:`pyramid.request.Request.authenticated_userid`,
+ :attr:`pyramid.request.Request.effective_principals`, and
+ :func:`pyramid.security.principals_allowed_by_permission`.
+
+ .. versionadded:: 1.4
+ The ``remember_result`` argument.
+
+ .. versionadded:: 1.4
+ The ``forget_result`` argument.
+ """
+ from pyramid.testing import DummySecurityPolicy
+ policy = DummySecurityPolicy(
+ userid, groupids, permissive, remember_result, forget_result
+ )
+ self.registry.registerUtility(policy, IAuthorizationPolicy)
+ self.registry.registerUtility(policy, IAuthenticationPolicy)
+ return policy
+
+ def testing_resources(self, resources):
+ """Unit/integration testing helper: registers a dictionary of
+ :term:`resource` objects that can be resolved via the
+ :func:`pyramid.traversal.find_resource` API.
+
+ The :func:`pyramid.traversal.find_resource` API is called with
+ a path as one of its arguments. If the dictionary you
+ register when calling this method contains that path as a
+ string key (e.g. ``/foo/bar`` or ``foo/bar``), the
+ corresponding value will be returned to ``find_resource`` (and
+ thus to your code) when
+ :func:`pyramid.traversal.find_resource` is called with an
+ equivalent path string or tuple.
+ """
+ class DummyTraverserFactory:
+ def __init__(self, context):
+ self.context = context
+
+ def __call__(self, request):
+ path = decode_path_info(request.environ['PATH_INFO'])
+ ob = resources[path]
+ traversed = split_path_info(path)
+ return {'context':ob, 'view_name':'','subpath':(),
+ 'traversed':traversed, 'virtual_root':ob,
+ 'virtual_root_path':(), 'root':ob}
+ self.registry.registerAdapter(DummyTraverserFactory, (Interface,),
+ ITraverser)
+ return resources
+
+ testing_models = testing_resources # b/w compat
+
+ @action_method
+ def testing_add_subscriber(self, event_iface=None):
+ """Unit/integration testing helper: Registers a
+ :term:`subscriber` which listens for events of the type
+ ``event_iface``. This method returns a list object which is
+ appended to by the subscriber whenever an event is captured.
+
+ When an event is dispatched that matches the value implied by
+ the ``event_iface`` argument, that event will be appended to
+ the list. You can then compare the values in the list to
+ expected event notifications. This method is useful when
+ testing code that wants to call
+ :meth:`pyramid.registry.Registry.notify`,
+ or :func:`zope.component.event.dispatch`.
+
+ The default value of ``event_iface`` (``None``) implies a
+ subscriber registered for *any* kind of event.
+ """
+ event_iface = self.maybe_dotted(event_iface)
+ L = []
+ def subscriber(*event):
+ L.extend(event)
+ self.add_subscriber(subscriber, event_iface)
+ return L
+
+ def testing_add_renderer(self, path, renderer=None):
+ """Unit/integration testing helper: register a renderer at
+ ``path`` (usually a relative filename ala ``templates/foo.pt``
+ or an asset specification) and return the renderer object.
+ If the ``renderer`` argument is None, a 'dummy' renderer will
+ be used. This function is useful when testing code that calls
+ the :func:`pyramid.renderers.render` function or
+ :func:`pyramid.renderers.render_to_response` function or
+ any other ``render_*`` or ``get_*`` API of the
+ :mod:`pyramid.renderers` module.
+
+ Note that calling this method for with a ``path`` argument
+ representing a renderer factory type (e.g. for ``foo.pt``
+ usually implies the ``chameleon_zpt`` renderer factory)
+ clobbers any existing renderer factory registered for that
+ type.
+
+ .. note:: This method is also available under the alias
+ ``testing_add_template`` (an older name for it).
+
+ """
+ from pyramid.testing import DummyRendererFactory
+ helper = RendererHelper(name=path, registry=self.registry)
+ factory = self.registry.queryUtility(IRendererFactory, name=helper.type)
+ if not isinstance(factory, DummyRendererFactory):
+ factory = DummyRendererFactory(helper.type, factory)
+ self.registry.registerUtility(factory, IRendererFactory,
+ name=helper.type)
+
+ from pyramid.testing import DummyTemplateRenderer
+ if renderer is None:
+ renderer = DummyTemplateRenderer()
+ factory.add(path, renderer)
+ return renderer
+
+ testing_add_template = testing_add_renderer
+
+
diff --git a/src/pyramid/config/tweens.py b/src/pyramid/config/tweens.py
new file mode 100644
index 000000000..8bf21cf71
--- /dev/null
+++ b/src/pyramid/config/tweens.py
@@ -0,0 +1,196 @@
+from zope.interface import implementer
+
+from pyramid.interfaces import ITweens
+
+from pyramid.compat import (
+ string_types,
+ is_nonstr_iter,
+ )
+
+from pyramid.exceptions import ConfigurationError
+
+from pyramid.tweens import (
+ MAIN,
+ INGRESS,
+ EXCVIEW,
+ )
+
+from pyramid.util import (
+ is_string_or_iterable,
+ TopologicalSorter,
+ )
+
+from pyramid.config.util import action_method
+
+class TweensConfiguratorMixin(object):
+ def add_tween(self, tween_factory, under=None, over=None):
+ """
+ .. versionadded:: 1.2
+
+ Add a 'tween factory'. A :term:`tween` (a contraction of 'between')
+ is a bit of code that sits between the Pyramid router's main request
+ handling function and the upstream WSGI component that uses
+ :app:`Pyramid` as its 'app'. Tweens are a feature that may be used
+ by Pyramid framework extensions, to provide, for example,
+ Pyramid-specific view timing support, bookkeeping code that examines
+ exceptions before they are returned to the upstream WSGI application,
+ or a variety of other features. Tweens behave a bit like
+ :term:`WSGI` 'middleware' but they have the benefit of running in a
+ context in which they have access to the Pyramid :term:`application
+ registry` as well as the Pyramid rendering machinery.
+
+ .. note:: You can view the tween ordering configured into a given
+ Pyramid application by using the ``ptweens``
+ command. See :ref:`displaying_tweens`.
+
+ The ``tween_factory`` argument must be a :term:`dotted Python name`
+ to a global object representing the tween factory.
+
+ The ``under`` and ``over`` arguments allow the caller of
+ ``add_tween`` to provide a hint about where in the tween chain this
+ tween factory should be placed when an implicit tween chain is used.
+ These hints are only used when an explicit tween chain is not used
+ (when the ``pyramid.tweens`` configuration value is not set).
+ Allowable values for ``under`` or ``over`` (or both) are:
+
+ - ``None`` (the default).
+
+ - A :term:`dotted Python name` to a tween factory: a string
+ representing the dotted name of a tween factory added in a call to
+ ``add_tween`` in the same configuration session.
+
+ - One of the constants :attr:`pyramid.tweens.MAIN`,
+ :attr:`pyramid.tweens.INGRESS`, or :attr:`pyramid.tweens.EXCVIEW`.
+
+ - An iterable of any combination of the above. This allows the user
+ to specify fallbacks if the desired tween is not included, as well
+ as compatibility with multiple other tweens.
+
+ ``under`` means 'closer to the main Pyramid application than',
+ ``over`` means 'closer to the request ingress than'.
+
+ For example, calling ``add_tween('myapp.tfactory',
+ over=pyramid.tweens.MAIN)`` will attempt to place the tween factory
+ represented by the dotted name ``myapp.tfactory`` directly 'above'
+ (in ``ptweens`` order) the main Pyramid request handler.
+ Likewise, calling ``add_tween('myapp.tfactory',
+ over=pyramid.tweens.MAIN, under='mypkg.someothertween')`` will
+ attempt to place this tween factory 'above' the main handler but
+ 'below' (a fictional) 'mypkg.someothertween' tween factory.
+
+ If all options for ``under`` (or ``over``) cannot be found in the
+ current configuration, it is an error. If some options are specified
+ purely for compatibilty with other tweens, just add a fallback of
+ MAIN or INGRESS. For example, ``under=('mypkg.someothertween',
+ 'mypkg.someothertween2', INGRESS)``. This constraint will require
+ the tween to be located under both the 'mypkg.someothertween' tween,
+ the 'mypkg.someothertween2' tween, and INGRESS. If any of these is
+ not in the current configuration, this constraint will only organize
+ itself based on the tweens that are present.
+
+ Specifying neither ``over`` nor ``under`` is equivalent to specifying
+ ``under=INGRESS``.
+
+ Implicit tween ordering is obviously only best-effort. Pyramid will
+ attempt to present an implicit order of tweens as best it can, but
+ the only surefire way to get any particular ordering is to use an
+ explicit tween order. A user may always override the implicit tween
+ ordering by using an explicit ``pyramid.tweens`` configuration value
+ setting.
+
+ ``under``, and ``over`` arguments are ignored when an explicit tween
+ chain is specified using the ``pyramid.tweens`` configuration value.
+
+ For more information, see :ref:`registering_tweens`.
+
+ """
+ return self._add_tween(tween_factory, under=under, over=over,
+ explicit=False)
+
+ def add_default_tweens(self):
+ self.add_tween(EXCVIEW)
+
+ @action_method
+ def _add_tween(self, tween_factory, under=None, over=None, explicit=False):
+
+ if not isinstance(tween_factory, string_types):
+ raise ConfigurationError(
+ 'The "tween_factory" argument to add_tween must be a '
+ 'dotted name to a globally importable object, not %r' %
+ tween_factory)
+
+ name = tween_factory
+
+ if name in (MAIN, INGRESS):
+ raise ConfigurationError('%s is a reserved tween name' % name)
+
+ tween_factory = self.maybe_dotted(tween_factory)
+
+ for t, p in [('over', over), ('under', under)]:
+ if p is not None:
+ if not is_string_or_iterable(p):
+ raise ConfigurationError(
+ '"%s" must be a string or iterable, not %s' % (t, p))
+
+ if over is INGRESS or is_nonstr_iter(over) and INGRESS in over:
+ raise ConfigurationError('%s cannot be over INGRESS' % name)
+
+ if under is MAIN or is_nonstr_iter(under) and MAIN in under:
+ raise ConfigurationError('%s cannot be under MAIN' % name)
+
+ registry = self.registry
+ introspectables = []
+
+ tweens = registry.queryUtility(ITweens)
+ if tweens is None:
+ tweens = Tweens()
+ registry.registerUtility(tweens, ITweens)
+
+ def register():
+ if explicit:
+ tweens.add_explicit(name, tween_factory)
+ else:
+ tweens.add_implicit(name, tween_factory, under=under, over=over)
+
+ discriminator = ('tween', name, explicit)
+ tween_type = explicit and 'explicit' or 'implicit'
+
+ intr = self.introspectable('tweens',
+ discriminator,
+ name,
+ '%s tween' % tween_type)
+ intr['name'] = name
+ intr['factory'] = tween_factory
+ intr['type'] = tween_type
+ intr['under'] = under
+ intr['over'] = over
+ introspectables.append(intr)
+ self.action(discriminator, register, introspectables=introspectables)
+
+@implementer(ITweens)
+class Tweens(object):
+ def __init__(self):
+ self.sorter = TopologicalSorter(
+ default_before=None,
+ default_after=INGRESS,
+ first=INGRESS,
+ last=MAIN)
+ self.explicit = []
+
+ def add_explicit(self, name, factory):
+ self.explicit.append((name, factory))
+
+ def add_implicit(self, name, factory, under=None, over=None):
+ self.sorter.add(name, factory, after=under, before=over)
+
+ def implicit(self):
+ return self.sorter.sorted()
+
+ def __call__(self, handler, registry):
+ if self.explicit:
+ use = self.explicit
+ else:
+ use = self.implicit()
+ for name, factory in use[::-1]:
+ handler = factory(handler, registry)
+ return handler
diff --git a/src/pyramid/config/util.py b/src/pyramid/config/util.py
new file mode 100644
index 000000000..05d810f6f
--- /dev/null
+++ b/src/pyramid/config/util.py
@@ -0,0 +1,281 @@
+import functools
+from hashlib import md5
+import traceback
+from webob.acceptparse import Accept
+from zope.interface import implementer
+
+from pyramid.compat import (
+ bytes_,
+ is_nonstr_iter
+)
+from pyramid.interfaces import IActionInfo
+
+from pyramid.exceptions import ConfigurationError
+from pyramid.predicates import Notted
+from pyramid.registry import predvalseq
+from pyramid.util import (
+ TopologicalSorter,
+ takes_one_arg,
+)
+
+TopologicalSorter = TopologicalSorter # support bw-compat imports
+takes_one_arg = takes_one_arg # support bw-compat imports
+
+@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', 0) + 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=4)
+
+ # Work around a Python 3.5 issue whereby it would insert an
+ # extra stack frame. This should no longer be necessary in
+ # Python 3.5.1
+ last_frame = ActionInfo(*f[-1])
+ if last_frame.function == 'extract_stack': # pragma: no cover
+ f.pop()
+ info = ActionInfo(*f[-backframes])
+ except Exception: # 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
+
+
+MAX_ORDER = 1 << 30
+DEFAULT_PHASH = md5().hexdigest()
+
+
+class not_(object):
+ """
+
+ You can invert the meaning of any predicate value by wrapping it in a call
+ to :class:`pyramid.config.not_`.
+
+ .. code-block:: python
+ :linenos:
+
+ from pyramid.config import not_
+
+ config.add_view(
+ 'mypackage.views.my_view',
+ route_name='ok',
+ request_method=not_('POST')
+ )
+
+ The above example will ensure that the view is called if the request method
+ is *not* ``POST``, at least if no other view is more specific.
+
+ This technique of wrapping a predicate value in ``not_`` can be used
+ anywhere predicate values are accepted:
+
+ - :meth:`pyramid.config.Configurator.add_view`
+
+ - :meth:`pyramid.config.Configurator.add_route`
+
+ - :meth:`pyramid.config.Configurator.add_subscriber`
+
+ - :meth:`pyramid.view.view_config`
+
+ - :meth:`pyramid.events.subscriber`
+
+ .. versionadded:: 1.5
+ """
+ def __init__(self, value):
+ self.value = value
+
+
+# under = after
+# over = before
+
+class PredicateList(object):
+
+ def __init__(self):
+ self.sorter = TopologicalSorter()
+ self.last_added = None
+
+ def add(self, name, factory, weighs_more_than=None, weighs_less_than=None):
+ # Predicates should be added to a predicate list in (presumed)
+ # computation expense order.
+ ## if weighs_more_than is None and weighs_less_than is None:
+ ## 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,
+ )
+
+ def names(self):
+ # Return the list of valid predicate names.
+ return self.sorter.names
+
+ def make(self, config, **kw):
+ # Given a configurator and a list of keywords, a predicate list is
+ # computed. Elsewhere in the code, we evaluate predicates using a
+ # generator expression. All predicates associated with a view or
+ # route must evaluate true for the view or route to "match" during a
+ # request. The fastest predicate should be evaluated first, then the
+ # next fastest, and so on, as if one returns false, the remainder of
+ # the predicates won't need to be evaluated.
+ #
+ # While we compute predicates, we also compute a predicate hash (aka
+ # phash) that can be used by a caller to identify identical predicate
+ # lists.
+ ordered = self.sorter.sorted()
+ phash = md5()
+ weights = []
+ preds = []
+ for n, (name, predicate_factory) in enumerate(ordered):
+ vals = kw.pop(name, None)
+ if vals is None: # XXX should this be a sentinel other than None?
+ continue
+ if not isinstance(vals, predvalseq):
+ vals = (vals,)
+ for val in vals:
+ realval = val
+ notted = False
+ if isinstance(val, not_):
+ realval = val.value
+ notted = True
+ pred = predicate_factory(realval, config)
+ if notted:
+ pred = Notted(pred)
+ hashes = pred.phash()
+ if not is_nonstr_iter(hashes):
+ hashes = [hashes]
+ for h in hashes:
+ phash.update(bytes_(h))
+ weights.append(1 << n + 1)
+ preds.append(pred)
+ if kw:
+ from difflib import get_close_matches
+ closest = []
+ names = [ name for name, _ in ordered ]
+ for name in kw:
+ closest.extend(get_close_matches(name, names, 3))
+
+ raise ConfigurationError(
+ 'Unknown predicate values: %r (did you mean %s)'
+ % (kw, ','.join(closest))
+ )
+ # A "order" is computed for the predicate list. An order is
+ # a scoring.
+ #
+ # Each predicate is associated with a weight value. The weight of a
+ # predicate symbolizes the relative potential "importance" of the
+ # predicate to all other predicates. A larger weight indicates
+ # greater importance.
+ #
+ # All weights for a given predicate list are bitwise ORed together
+ # to create a "score"; this score is then subtracted from
+ # MAX_ORDER and divided by an integer representing the number of
+ # predicates+1 to determine the order.
+ #
+ # For views, the order represents the ordering in which a "multiview"
+ # ( a collection of views that share the same context/request/name
+ # triad but differ in other ways via predicates) will attempt to call
+ # its set of views. Views with lower orders will be tried first.
+ # The intent is to a) ensure that views with more predicates are
+ # always evaluated before views with fewer predicates and b) to
+ # ensure a stable call ordering of views that share the same number
+ # of predicates. Views which do not have any predicates get an order
+ # of MAX_ORDER, meaning that they will be tried very last.
+ score = 0
+ for bit in weights:
+ score = score | bit
+ order = (MAX_ORDER - score) / (len(preds) + 1)
+ return order, preds, phash.hexdigest()
+
+
+def normalize_accept_offer(offer, allow_range=False):
+ if allow_range and '*' in offer:
+ return offer.lower()
+ return str(Accept.parse_offer(offer))
+
+
+def sort_accept_offers(offers, order=None):
+ """
+ Sort a list of offers by preference.
+
+ For a given ``type/subtype`` category of offers, this algorithm will
+ always sort offers with params higher than the bare offer.
+
+ :param offers: A list of offers to be sorted.
+ :param order: A weighted list of offers where items closer to the start of
+ the list will be a preferred over items closer to the end.
+ :return: A list of offers sorted first by specificity (higher to lower)
+ then by ``order``.
+
+ """
+ if order is None:
+ order = []
+
+ max_weight = len(offers)
+
+ def find_order_index(value, default=None):
+ return next((i for i, x in enumerate(order) if x == value), default)
+
+ def offer_sort_key(value):
+ """
+ (type_weight, params_weight)
+
+ type_weight:
+ - index of specific ``type/subtype`` in order list
+ - ``max_weight * 2`` if no match is found
+
+ params_weight:
+ - index of specific ``type/subtype;params`` in order list
+ - ``max_weight`` if not found
+ - ``max_weight + 1`` if no params at all
+
+ """
+ parsed = Accept.parse_offer(value)
+
+ type_w = find_order_index(
+ parsed.type + '/' + parsed.subtype,
+ max_weight,
+ )
+
+ if parsed.params:
+ param_w = find_order_index(value, max_weight)
+
+ else:
+ param_w = max_weight + 1
+
+ return (type_w, param_w)
+
+ return sorted(offers, key=offer_sort_key)
diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py
new file mode 100644
index 000000000..e6baa7c17
--- /dev/null
+++ b/src/pyramid/config/views.py
@@ -0,0 +1,2327 @@
+import functools
+import inspect
+import posixpath
+import operator
+import os
+import warnings
+
+from webob.acceptparse import Accept
+from zope.interface import (
+ Interface,
+ implementedBy,
+ implementer,
+ )
+from zope.interface.interfaces import IInterface
+
+from pyramid.interfaces import (
+ IAcceptOrder,
+ IExceptionViewClassifier,
+ IException,
+ IMultiView,
+ IPackageOverrides,
+ IRendererFactory,
+ IRequest,
+ IResponse,
+ IRouteRequest,
+ ISecuredView,
+ IStaticURLInfo,
+ IView,
+ IViewClassifier,
+ IViewDerivers,
+ IViewDeriverInfo,
+ IViewMapperFactory,
+ PHASE1_CONFIG,
+ )
+
+from pyramid import renderers
+
+from pyramid.asset import resolve_asset_spec
+from pyramid.compat import (
+ string_types,
+ urlparse,
+ url_quote,
+ WIN,
+ is_nonstr_iter,
+ )
+
+from pyramid.decorator import reify
+
+from pyramid.exceptions import (
+ ConfigurationError,
+ PredicateMismatch,
+ )
+
+from pyramid.httpexceptions import (
+ HTTPForbidden,
+ HTTPNotFound,
+ default_exceptionresponse_view,
+ )
+
+from pyramid.registry import Deferred
+
+from pyramid.security import NO_PERMISSION_REQUIRED
+from pyramid.static import static_view
+
+from pyramid.url import parse_url_overrides
+
+from pyramid.view import AppendSlashNotFoundViewFactory
+
+from pyramid.util import (
+ as_sorted_tuple,
+ TopologicalSorter,
+ )
+
+import pyramid.predicates
+import pyramid.viewderivers
+
+from pyramid.viewderivers import (
+ INGRESS,
+ VIEW,
+ preserve_view_attrs,
+ view_description,
+ requestonly,
+ DefaultViewMapper,
+ wraps_view,
+)
+
+from pyramid.config.util import (
+ action_method,
+ DEFAULT_PHASH,
+ MAX_ORDER,
+ normalize_accept_offer,
+ predvalseq,
+ sort_accept_offers,
+ )
+
+urljoin = urlparse.urljoin
+url_parse = urlparse.urlparse
+
+DefaultViewMapper = DefaultViewMapper # bw-compat
+preserve_view_attrs = preserve_view_attrs # bw-compat
+requestonly = requestonly # bw-compat
+view_description = view_description # bw-compat
+
+@implementer(IMultiView)
+class MultiView(object):
+
+ def __init__(self, name):
+ self.name = name
+ self.media_views = {}
+ self.views = []
+ self.accepts = []
+
+ def __discriminator__(self, context, request):
+ # used by introspection systems like so:
+ # view = adapters.lookup(....)
+ # view.__discriminator__(context, request) -> view's discriminator
+ # so that superdynamic systems can feed the discriminator to
+ # the introspection system to get info about it
+ view = self.match(context, request)
+ return view.__discriminator__(context, request)
+
+ def add(self, view, order, phash=None, accept=None, accept_order=None):
+ if phash is not None:
+ for i, (s, v, h) in enumerate(list(self.views)):
+ if phash == h:
+ self.views[i] = (order, view, phash)
+ return
+
+ if accept is None or '*' in accept:
+ self.views.append((order, view, phash))
+ self.views.sort(key=operator.itemgetter(0))
+ else:
+ subset = self.media_views.setdefault(accept, [])
+ for i, (s, v, h) in enumerate(list(subset)):
+ if phash == h:
+ subset[i] = (order, view, phash)
+ return
+ else:
+ subset.append((order, view, phash))
+ subset.sort(key=operator.itemgetter(0))
+ # dedupe accepts and sort appropriately
+ accepts = set(self.accepts)
+ accepts.add(accept)
+ if accept_order:
+ accept_order = [v for _, v in accept_order.sorted()]
+ self.accepts = sort_accept_offers(accepts, accept_order)
+
+ def get_views(self, request):
+ if self.accepts and hasattr(request, 'accept'):
+ views = []
+ for offer, _ in request.accept.acceptable_offers(self.accepts):
+ views.extend(self.media_views[offer])
+ views.extend(self.views)
+ return views
+ return self.views
+
+ def match(self, context, request):
+ for order, view, phash in self.get_views(request):
+ if not hasattr(view, '__predicated__'):
+ return view
+ if view.__predicated__(context, request):
+ return view
+ raise PredicateMismatch(self.name)
+
+ def __permitted__(self, context, request):
+ view = self.match(context, request)
+ if hasattr(view, '__permitted__'):
+ return view.__permitted__(context, request)
+ return True
+
+ def __call_permissive__(self, context, request):
+ view = self.match(context, request)
+ view = getattr(view, '__call_permissive__', view)
+ return view(context, request)
+
+ def __call__(self, context, request):
+ for order, view, phash in self.get_views(request):
+ try:
+ return view(context, request)
+ except PredicateMismatch:
+ continue
+ raise PredicateMismatch(self.name)
+
+def attr_wrapped_view(view, info):
+ accept, order, phash = (info.options.get('accept', None),
+ getattr(info, 'order', MAX_ORDER),
+ getattr(info, 'phash', DEFAULT_PHASH))
+ # this is a little silly but we don't want to decorate the original
+ # function with attributes that indicate accept, order, and phash,
+ # so we use a wrapper
+ if (
+ (accept is None) and
+ (order == MAX_ORDER) and
+ (phash == DEFAULT_PHASH)
+ ):
+ return view # defaults
+ def attr_view(context, request):
+ return view(context, request)
+ attr_view.__accept__ = accept
+ attr_view.__order__ = order
+ attr_view.__phash__ = phash
+ attr_view.__view_attr__ = info.options.get('attr')
+ attr_view.__permission__ = info.options.get('permission')
+ return attr_view
+
+attr_wrapped_view.options = ('accept', 'attr', 'permission')
+
+def predicated_view(view, info):
+ preds = info.predicates
+ if not preds:
+ return view
+ def predicate_wrapper(context, request):
+ for predicate in preds:
+ if not predicate(context, request):
+ view_name = getattr(view, '__name__', view)
+ raise PredicateMismatch(
+ 'predicate mismatch for view %s (%s)' % (
+ view_name, predicate.text()))
+ return view(context, request)
+ def checker(context, request):
+ return all((predicate(context, request) for predicate in
+ preds))
+ predicate_wrapper.__predicated__ = checker
+ predicate_wrapper.__predicates__ = preds
+ return predicate_wrapper
+
+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()
+ if '_backframes' not in kw:
+ kw['_backframes'] = 1 # for action_method
+ defaults.update(kw)
+ return wrapped(self, *arg, **defaults)
+ return functools.wraps(wrapped)(wrapper)
+
+def combine_decorators(*decorators):
+ def decorated(view_callable):
+ # reversed() allows a more natural ordering in the api
+ for decorator in reversed(decorators):
+ view_callable = decorator(view_callable)
+ return view_callable
+ return decorated
+
+class ViewsConfiguratorMixin(object):
+ @viewdefaults
+ @action_method
+ def add_view(
+ self,
+ view=None,
+ name="",
+ for_=None,
+ permission=None,
+ request_type=None,
+ route_name=None,
+ request_method=None,
+ request_param=None,
+ containment=None,
+ attr=None,
+ renderer=None,
+ wrapper=None,
+ xhr=None,
+ accept=None,
+ header=None,
+ path_info=None,
+ custom_predicates=(),
+ context=None,
+ decorator=None,
+ mapper=None,
+ http_cache=None,
+ match_param=None,
+ check_csrf=None,
+ require_csrf=None,
+ exception_only=False,
+ **view_options):
+ """ Add a :term:`view configuration` to the current
+ configuration state. Arguments to ``add_view`` are broken
+ down below into *predicate* arguments and *non-predicate*
+ arguments. Predicate arguments narrow the circumstances in
+ which the view callable will be invoked when a request is
+ presented to :app:`Pyramid`; non-predicate arguments are
+ informational.
+
+ Non-Predicate Arguments
+
+ view
+
+ A :term:`view callable` or a :term:`dotted Python name`
+ which refers to a view callable. This argument is required
+ unless a ``renderer`` argument also exists. If a
+ ``renderer`` argument is passed, and a ``view`` argument is
+ not provided, the view callable defaults to a callable that
+ returns an empty dictionary (see
+ :ref:`views_which_use_a_renderer`).
+
+ permission
+
+ A :term:`permission` that the user must possess in order to invoke
+ the :term:`view callable`. See :ref:`view_security_section` for
+ more information about view security and permissions. This is
+ often a string like ``view`` or ``edit``.
+
+ If ``permission`` is omitted, a *default* permission may be used
+ for this view registration if one was named as the
+ :class:`pyramid.config.Configurator` constructor's
+ ``default_permission`` argument, or if
+ :meth:`pyramid.config.Configurator.set_default_permission` was used
+ prior to this view registration. Pass the value
+ :data:`pyramid.security.NO_PERMISSION_REQUIRED` as the permission
+ argument to explicitly indicate that the view should always be
+ executable by entirely anonymous users, regardless of the default
+ permission, bypassing any :term:`authorization policy` that may be
+ in effect.
+
+ attr
+
+ This knob is most useful when the view definition is a class.
+
+ The view machinery defaults to using the ``__call__`` method
+ of the :term:`view callable` (or the function itself, if the
+ view callable is a function) to obtain a response. The
+ ``attr`` value allows you to vary the method attribute used
+ to obtain the response. For example, if your view was a
+ class, and the class has a method named ``index`` and you
+ wanted to use this method instead of the class' ``__call__``
+ method to return the response, you'd say ``attr="index"`` in the
+ view configuration for the view.
+
+ renderer
+
+ This is either a single string term (e.g. ``json``) or a
+ string implying a path or :term:`asset specification`
+ (e.g. ``templates/views.pt``) naming a :term:`renderer`
+ implementation. If the ``renderer`` value does not contain
+ a dot ``.``, the specified string will be used to look up a
+ renderer implementation, and that renderer implementation
+ will be used to construct a response from the view return
+ value. If the ``renderer`` value contains a dot (``.``),
+ the specified term will be treated as a path, and the
+ filename extension of the last element in the path will be
+ used to look up the renderer implementation, which will be
+ passed the full path. The renderer implementation will be
+ used to construct a :term:`response` from the view return
+ value.
+
+ Note that if the view itself returns a :term:`response` (see
+ :ref:`the_response`), the specified renderer implementation
+ is never called.
+
+ When the renderer is a path, although a path is usually just
+ a simple relative pathname (e.g. ``templates/foo.pt``,
+ implying that a template named "foo.pt" is in the
+ "templates" directory relative to the directory of the
+ current :term:`package` of the Configurator), a path can be
+ absolute, starting with a slash on UNIX or a drive letter
+ prefix on Windows. The path can alternately be a
+ :term:`asset specification` in the form
+ ``some.dotted.package_name:relative/path``, making it
+ possible to address template assets which live in a
+ separate package.
+
+ The ``renderer`` attribute is optional. If it is not
+ defined, the "null" renderer is assumed (no rendering is
+ performed and the value is passed back to the upstream
+ :app:`Pyramid` machinery unmodified).
+
+ http_cache
+
+ .. versionadded:: 1.1
+
+ When you supply an ``http_cache`` value to a view configuration,
+ the ``Expires`` and ``Cache-Control`` headers of a response
+ generated by the associated view callable are modified. The value
+ for ``http_cache`` may be one of the following:
+
+ - A nonzero integer. If it's a nonzero integer, it's treated as a
+ number of seconds. This number of seconds will be used to
+ compute the ``Expires`` header and the ``Cache-Control:
+ max-age`` parameter of responses to requests which call this view.
+ For example: ``http_cache=3600`` instructs the requesting browser
+ to 'cache this response for an hour, please'.
+
+ - A ``datetime.timedelta`` instance. If it's a
+ ``datetime.timedelta`` instance, it will be converted into a
+ number of seconds, and that number of seconds will be used to
+ compute the ``Expires`` header and the ``Cache-Control:
+ max-age`` parameter of responses to requests which call this view.
+ For example: ``http_cache=datetime.timedelta(days=1)`` instructs
+ the requesting browser to 'cache this response for a day, please'.
+
+ - Zero (``0``). If the value is zero, the ``Cache-Control`` and
+ ``Expires`` headers present in all responses from this view will
+ be composed such that client browser cache (and any intermediate
+ caches) are instructed to never cache the response.
+
+ - A two-tuple. If it's a two tuple (e.g. ``http_cache=(1,
+ {'public':True})``), the first value in the tuple may be a
+ nonzero integer or a ``datetime.timedelta`` instance; in either
+ case this value will be used as the number of seconds to cache
+ the response. The second value in the tuple must be a
+ dictionary. The values present in the dictionary will be used as
+ input to the ``Cache-Control`` response header. For example:
+ ``http_cache=(3600, {'public':True})`` means 'cache for an hour,
+ and add ``public`` to the Cache-Control header of the response'.
+ All keys and values supported by the
+ ``webob.cachecontrol.CacheControl`` interface may be added to the
+ dictionary. Supplying ``{'public':True}`` is equivalent to
+ calling ``response.cache_control.public = True``.
+
+ Providing a non-tuple value as ``http_cache`` is equivalent to
+ calling ``response.cache_expires(value)`` within your view's body.
+
+ Providing a two-tuple value as ``http_cache`` is equivalent to
+ calling ``response.cache_expires(value[0], **value[1])`` within your
+ view's body.
+
+ If you wish to avoid influencing, the ``Expires`` header, and
+ instead wish to only influence ``Cache-Control`` headers, pass a
+ tuple as ``http_cache`` with the first element of ``None``, e.g.:
+ ``(None, {'public':True})``.
+
+ If you wish to prevent a view that uses ``http_cache`` in its
+ configuration from having its caching response headers changed by
+ this machinery, set ``response.cache_control.prevent_auto = True``
+ before returning the response from the view. This effectively
+ disables any HTTP caching done by ``http_cache`` for that response.
+
+ require_csrf
+
+ .. versionadded:: 1.7
+
+ A boolean option or ``None``. Default: ``None``.
+
+ If this option is set to ``True`` then CSRF checks will be enabled
+ for requests to this view. The required token or header default to
+ ``csrf_token`` and ``X-CSRF-Token``, respectively.
+
+ CSRF checks only affect "unsafe" methods as defined by RFC2616. By
+ default, these methods are anything except
+ ``GET``, ``HEAD``, ``OPTIONS``, and ``TRACE``.
+
+ The defaults here may be overridden by
+ :meth:`pyramid.config.Configurator.set_default_csrf_options`.
+
+ This feature requires a configured :term:`session factory`.
+
+ If this option is set to ``False`` then CSRF checks will be disabled
+ regardless of the default ``require_csrf`` setting passed
+ to ``set_default_csrf_options``.
+
+ See :ref:`auto_csrf_checking` for more information.
+
+ wrapper
+
+ The :term:`view name` of a different :term:`view
+ configuration` which will receive the response body of this
+ view as the ``request.wrapped_body`` attribute of its own
+ :term:`request`, and the :term:`response` returned by this
+ view as the ``request.wrapped_response`` attribute of its
+ own request. Using a wrapper makes it possible to "chain"
+ views together to form a composite response. The response
+ of the outermost wrapper view will be returned to the user.
+ The wrapper view will be found as any view is found: see
+ :ref:`view_lookup`. The "best" wrapper view will be found
+ based on the lookup ordering: "under the hood" this wrapper
+ view is looked up via
+ ``pyramid.view.render_view_to_response(context, request,
+ 'wrapper_viewname')``. The context and request of a wrapper
+ view is the same context and request of the inner view. If
+ this attribute is unspecified, no view wrapping is done.
+
+ decorator
+
+ 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).
+
+ An important distinction is that each decorator will receive a
+ response object implementing :class:`pyramid.interfaces.IResponse`
+ instead of the raw value returned from the view callable. All
+ decorators in the chain must return a response object or raise an
+ exception:
+
+ .. code-block:: python
+
+ def log_timer(wrapped):
+ def wrapper(context, request):
+ start = time.time()
+ response = wrapped(context, request)
+ duration = time.time() - start
+ response.headers['X-View-Time'] = '%.3f' % (duration,)
+ log.info('view took %.3f seconds', duration)
+ return response
+ return wrapper
+
+ .. versionchanged:: 1.4a4
+ Passing an iterable.
+
+ mapper
+
+ A Python object or :term:`dotted Python name` which refers to a
+ :term:`view mapper`, or ``None``. By default it is ``None``, which
+ indicates that the view should use the default view mapper. This
+ plug-point is useful for Pyramid extension developers, but it's not
+ very useful for 'civilians' who are just developing stock Pyramid
+ applications. Pay no attention to the man behind the curtain.
+
+ accept
+
+ A :term:`media type` that will be matched against the ``Accept``
+ HTTP request header. If this value is specified, it must be a
+ specific media type such as ``text/html`` or ``text/html;level=1``.
+ If the media type is acceptable by the ``Accept`` header of the
+ request, or if the ``Accept`` header isn't set at all in the request,
+ this predicate will match. If this does not match the ``Accept``
+ header of the request, view matching continues.
+
+ If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is
+ not taken into consideration when deciding whether or not to invoke
+ the associated view callable.
+
+ The ``accept`` argument is technically not a predicate and does
+ not support wrapping with :func:`pyramid.config.not_`.
+
+ See :ref:`accept_content_negotiation` for more information.
+
+ .. versionchanged:: 1.10
+
+ Specifying a media range is deprecated and will be removed in
+ :app:`Pyramid` 2.0. Use explicit media types to avoid any
+ ambiguities in content negotiation.
+
+ exception_only
+
+ .. versionadded:: 1.8
+
+ When this value is ``True``, the ``context`` argument must be
+ a subclass of ``Exception``. This flag indicates that only an
+ :term:`exception view` should be created, and that this view should
+ not match if the traversal :term:`context` matches the ``context``
+ argument. If the ``context`` is a subclass of ``Exception`` and
+ this value is ``False`` (the default), then a view will be
+ registered to match the traversal :term:`context` as well.
+
+ Predicate Arguments
+
+ name
+
+ The :term:`view name`. Read :ref:`traversal_chapter` to
+ understand the concept of a view name.
+
+ context
+
+ An object or a :term:`dotted Python name` referring to an
+ interface or class object that the :term:`context` must be
+ an instance of, *or* the :term:`interface` that the
+ :term:`context` must provide in order for this view to be
+ found and called. This predicate is true when the
+ :term:`context` is an instance of the represented class or
+ if the :term:`context` provides the represented interface;
+ it is otherwise false. This argument may also be provided
+ to ``add_view`` as ``for_`` (an older, still-supported
+ spelling). If the view should *only* match when handling
+ exceptions, then set the ``exception_only`` to ``True``.
+
+ route_name
+
+ This value must match the ``name`` of a :term:`route
+ configuration` declaration (see :ref:`urldispatch_chapter`)
+ that must match before this view will be called.
+
+ request_type
+
+ This value should be an :term:`interface` that the
+ :term:`request` must provide in order for this view to be
+ found and called. This value exists only for backwards
+ compatibility purposes.
+
+ request_method
+
+ This value can be either a string (such as ``"GET"``, ``"POST"``,
+ ``"PUT"``, ``"DELETE"``, ``"HEAD"`` or ``"OPTIONS"``) representing
+ an HTTP ``REQUEST_METHOD``, or a tuple containing one or more of
+ these strings. A view declaration with this argument ensures that
+ the view will only be called when the ``method`` attribute of the
+ request (aka the ``REQUEST_METHOD`` of the WSGI environment) matches
+ a supplied value. Note that use of ``GET`` also implies that the
+ view will respond to ``HEAD`` as of Pyramid 1.4.
+
+ .. versionchanged:: 1.2
+ The ability to pass a tuple of items as ``request_method``.
+ Previous versions allowed only a string.
+
+ request_param
+
+ This value can be any string or any sequence of strings. A view
+ declaration with this argument ensures that the view will only be
+ called when the :term:`request` has a key in the ``request.params``
+ dictionary (an HTTP ``GET`` or ``POST`` variable) that has a
+ name which matches the supplied value (if the value is a string)
+ or values (if the value is a tuple). If any value
+ supplied has a ``=`` sign in it,
+ e.g. ``request_param="foo=123"``, then the key (``foo``)
+ must both exist in the ``request.params`` dictionary, *and*
+ the value must match the right hand side of the expression
+ (``123``) for the view to "match" the current request.
+
+ match_param
+
+ .. versionadded:: 1.2
+
+ This value can be a string of the format "key=value" or a tuple
+ containing one or more of these strings.
+
+ A view declaration with this argument ensures that the view will
+ only be called when the :term:`request` has key/value pairs in its
+ :term:`matchdict` that equal those supplied in the predicate.
+ e.g. ``match_param="action=edit"`` would require the ``action``
+ parameter in the :term:`matchdict` match the right hand side of
+ the expression (``edit``) for the view to "match" the current
+ request.
+
+ If the ``match_param`` is a tuple, every key/value pair must match
+ for the predicate to pass.
+
+ containment
+
+ This value should be a Python class or :term:`interface` (or a
+ :term:`dotted Python name`) that an object in the
+ :term:`lineage` of the context must provide in order for this view
+ to be found and called. The nodes in your object graph must be
+ "location-aware" to use this feature. See
+ :ref:`location_aware` for more information about
+ location-awareness.
+
+ xhr
+
+ This value should be either ``True`` or ``False``. If this
+ value is specified and is ``True``, the :term:`request`
+ must possess an ``HTTP_X_REQUESTED_WITH`` (aka
+ ``X-Requested-With``) header that has the value
+ ``XMLHttpRequest`` for this view to be found and called.
+ This is useful for detecting AJAX requests issued from
+ jQuery, Prototype and other Javascript libraries.
+
+ header
+
+ This value represents an HTTP header name or a header
+ name/value pair. If the value contains a ``:`` (colon), it
+ will be considered a name/value pair
+ (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). The
+ value portion should be a regular expression. If the value
+ does not contain a colon, the entire value will be
+ considered to be the header name
+ (e.g. ``If-Modified-Since``). If the value evaluates to a
+ header name only without a value, the header specified by
+ the name must be present in the request for this predicate
+ to be true. If the value evaluates to a header name/value
+ pair, the header specified by the name must be present in
+ the request *and* the regular expression specified as the
+ value must match the header value. Whether or not the value
+ represents a header name or a header name/value pair, the
+ case of the header name is not significant.
+
+ path_info
+
+ This value represents a regular expression pattern that will
+ be tested against the ``PATH_INFO`` WSGI environment
+ variable. If the regex matches, this predicate will be
+ ``True``.
+
+ check_csrf
+
+ .. deprecated:: 1.7
+ Use the ``require_csrf`` option or see :ref:`auto_csrf_checking`
+ instead to have :class:`pyramid.exceptions.BadCSRFToken`
+ exceptions raised.
+
+ If specified, this value should be one of ``None``, ``True``,
+ ``False``, or a string representing the 'check name'. If the value
+ is ``True`` or a string, CSRF checking will be performed. If the
+ value is ``False`` or ``None``, CSRF checking will not be performed.
+
+ If the value provided is a string, that string will be used as the
+ 'check name'. If the value provided is ``True``, ``csrf_token`` will
+ be used as the check name.
+
+ If CSRF checking is performed, the checked value will be the value of
+ ``request.params[check_name]``. This value will be compared against
+ the value of ``policy.get_csrf_token()`` (where ``policy`` is an
+ implementation of :meth:`pyramid.interfaces.ICSRFStoragePolicy`), and the
+ check will pass if these two values are the same. If the check
+ passes, the associated view will be permitted to execute. If the
+ check fails, the associated view will not be permitted to execute.
+
+ .. versionadded:: 1.4a2
+
+ .. versionchanged:: 1.9
+ This feature requires either a :term:`session factory` to have been
+ configured, or a :term:`CSRF storage policy` other than the default
+ to be in use.
+
+
+ physical_path
+
+ If specified, this value should be a string or a tuple representing
+ the :term:`physical path` of the context found via traversal for this
+ predicate to match as true. For example: ``physical_path='/'`` or
+ ``physical_path='/a/b/c'`` or ``physical_path=('', 'a', 'b', 'c')``.
+ This is not a path prefix match or a regex, it's a whole-path match.
+ It's useful when you want to always potentially show a 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. The
+ individual path elements inbetween slash characters or in tuple
+ elements should be the Unicode representation of the name of the
+ resource and should not be encoded in any way.
+
+ .. versionadded:: 1.4a3
+
+ effective_principals
+
+ If specified, this value should be a :term:`principal` identifier or
+ a sequence of principal identifiers. If the
+ :attr:`pyramid.request.Request.effective_principals` property
+ indicates that every principal named in the argument list is present
+ in the current request, this predicate will return True; otherwise it
+ will return False. For example:
+ ``effective_principals=pyramid.security.Authenticated`` or
+ ``effective_principals=('fred', 'group:admins')``.
+
+ .. versionadded:: 1.4a4
+
+ custom_predicates
+
+ .. deprecated:: 1.5
+ This value should be a sequence of references to custom
+ predicate callables. Use custom predicates when no set of
+ predefined predicates do what you need. Custom predicates
+ can be combined with predefined predicates as necessary.
+ Each custom predicate callable should accept two arguments:
+ ``context`` and ``request`` and should return either
+ ``True`` or ``False`` after doing arbitrary evaluation of
+ the context and/or the request. The ``predicates`` argument
+ to this method and the ability to register third-party view
+ predicates via
+ :meth:`pyramid.config.Configurator.add_view_predicate`
+ obsoletes this argument, but it is kept around for backwards
+ compatibility.
+
+ view_options
+
+ Pass a key/value pair here to use a third-party predicate or set a
+ value for a view deriver. See
+ :meth:`pyramid.config.Configurator.add_view_predicate` and
+ :meth:`pyramid.config.Configurator.add_view_deriver`. See
+ :ref:`view_and_route_predicates` for more information about
+ third-party predicates and :ref:`view_derivers` for information
+ about view derivers.
+
+ .. versionadded: 1.4a1
+
+ .. versionchanged: 1.7
+
+ Support setting view deriver options. Previously, only custom
+ view predicate values could be supplied.
+
+ """
+ if custom_predicates:
+ warnings.warn(
+ ('The "custom_predicates" argument to Configurator.add_view '
+ 'is deprecated as of Pyramid 1.5. Use '
+ '"config.add_view_predicate" and use the registered '
+ 'view predicate as a predicate argument to add_view instead. '
+ 'See "Adding A Third Party View, Route, or Subscriber '
+ 'Predicate" in the "Hooks" chapter of the documentation '
+ 'for more information.'),
+ DeprecationWarning,
+ stacklevel=4,
+ )
+
+ if check_csrf is not None:
+ warnings.warn(
+ ('The "check_csrf" argument to Configurator.add_view is '
+ 'deprecated as of Pyramid 1.7. Use the "require_csrf" option '
+ 'instead or see "Checking CSRF Tokens Automatically" in the '
+ '"Sessions" chapter of the documentation for more '
+ 'information.'),
+ DeprecationWarning,
+ stacklevel=4,
+ )
+
+ if accept is not None:
+ if is_nonstr_iter(accept):
+ raise ConfigurationError(
+ 'A list is not supported in the "accept" view predicate.',
+ )
+ if '*' in accept:
+ warnings.warn(
+ ('Passing a media range to the "accept" argument of '
+ 'Configurator.add_view is deprecated as of Pyramid 1.10. '
+ 'Use explicit media types to avoid ambiguities in '
+ 'content negotiation that may impact your users.'),
+ DeprecationWarning,
+ stacklevel=4,
+ )
+ # XXX when media ranges are gone, switch allow_range=False
+ accept = normalize_accept_offer(accept, allow_range=True)
+
+ view = self.maybe_dotted(view)
+ context = self.maybe_dotted(context)
+ for_ = self.maybe_dotted(for_)
+ containment = self.maybe_dotted(containment)
+ mapper = self.maybe_dotted(mapper)
+
+ if is_nonstr_iter(decorator):
+ decorator = combine_decorators(*map(self.maybe_dotted, decorator))
+ else:
+ decorator = self.maybe_dotted(decorator)
+
+ if not view:
+ if renderer:
+ def view(context, request):
+ return {}
+ else:
+ raise ConfigurationError('"view" was not specified and '
+ 'no "renderer" specified')
+
+ if request_type is not None:
+ request_type = self.maybe_dotted(request_type)
+ if not IInterface.providedBy(request_type):
+ raise ConfigurationError(
+ 'request_type must be an interface, not %s' % request_type)
+
+ if context is None:
+ context = for_
+
+ isexc = isexception(context)
+ if exception_only and not isexc:
+ raise ConfigurationError(
+ 'view "context" must be an exception type when '
+ '"exception_only" is True')
+
+ r_context = context
+ if r_context is None:
+ r_context = Interface
+ if not IInterface.providedBy(r_context):
+ r_context = implementedBy(r_context)
+
+ if isinstance(renderer, string_types):
+ renderer = renderers.RendererHelper(
+ name=renderer, package=self.package,
+ registry=self.registry)
+
+ introspectables = []
+ ovals = view_options.copy()
+ ovals.update(dict(
+ xhr=xhr,
+ request_method=request_method,
+ path_info=path_info,
+ request_param=request_param,
+ header=header,
+ accept=accept,
+ containment=containment,
+ request_type=request_type,
+ match_param=match_param,
+ check_csrf=check_csrf,
+ custom=predvalseq(custom_predicates),
+ ))
+
+ def discrim_func():
+ # We need to defer the discriminator until we know what the phash
+ # is. It can't be computed any sooner because thirdparty
+ # predicates/view derivers may not yet exist when add_view is
+ # called.
+ predlist = self.get_predlist('view')
+ valid_predicates = predlist.names()
+ pvals = {}
+ dvals = {}
+
+ for (k, v) in ovals.items():
+ if k in valid_predicates:
+ pvals[k] = v
+ else:
+ dvals[k] = v
+
+ self._check_view_options(**dvals)
+
+ order, preds, phash = predlist.make(self, **pvals)
+
+ view_intr.update({
+ 'phash': phash,
+ 'order': order,
+ 'predicates': preds,
+ })
+ return ('view', context, name, route_name, phash)
+
+ discriminator = Deferred(discrim_func)
+
+ if inspect.isclass(view) and attr:
+ view_desc = 'method %r of %s' % (
+ attr, self.object_description(view))
+ else:
+ view_desc = self.object_description(view)
+
+ tmpl_intr = None
+
+ view_intr = self.introspectable('views',
+ discriminator,
+ view_desc,
+ 'view')
+ view_intr.update(dict(
+ name=name,
+ context=context,
+ exception_only=exception_only,
+ containment=containment,
+ request_param=request_param,
+ request_methods=request_method,
+ route_name=route_name,
+ attr=attr,
+ xhr=xhr,
+ accept=accept,
+ header=header,
+ path_info=path_info,
+ match_param=match_param,
+ check_csrf=check_csrf,
+ http_cache=http_cache,
+ require_csrf=require_csrf,
+ callable=view,
+ mapper=mapper,
+ decorator=decorator,
+ ))
+ view_intr.update(view_options)
+ introspectables.append(view_intr)
+
+ def register(permission=permission, renderer=renderer):
+ request_iface = IRequest
+ if route_name is not None:
+ request_iface = self.registry.queryUtility(IRouteRequest,
+ name=route_name)
+ if request_iface is None:
+ # route configuration should have already happened in
+ # phase 2
+ raise ConfigurationError(
+ 'No route named %s found for view registration' %
+ route_name)
+
+ if renderer is None:
+ # use default renderer if one exists (reg'd in phase 1)
+ if self.registry.queryUtility(IRendererFactory) is not None:
+ renderer = renderers.RendererHelper(
+ name=None,
+ package=self.package,
+ registry=self.registry
+ )
+
+ renderer_type = getattr(renderer, 'type', None)
+ intrspc = self.introspector
+ if (
+ renderer_type is not None and
+ tmpl_intr is not None and
+ intrspc is not None and
+ intrspc.get('renderer factories', renderer_type) is not None
+ ):
+ # allow failure of registered template factories to be deferred
+ # until view execution, like other bad renderer factories; if
+ # we tried to relate this to an existing renderer factory
+ # without checking if the factory actually existed, we'd end
+ # up with a KeyError at startup time, which is inconsistent
+ # with how other bad renderer registrations behave (they throw
+ # a ValueError at view execution time)
+ tmpl_intr.relate('renderer factories', renderer.type)
+
+ # make a new view separately for normal and exception paths
+ if not exception_only:
+ derived_view = derive_view(False, renderer)
+ register_view(IViewClassifier, request_iface, derived_view)
+ if isexc:
+ derived_exc_view = derive_view(True, renderer)
+ register_view(IExceptionViewClassifier, request_iface,
+ derived_exc_view)
+
+ if exception_only:
+ derived_view = derived_exc_view
+
+ # if there are two derived views, combine them into one for
+ # introspection purposes
+ if not exception_only and isexc:
+ derived_view = runtime_exc_view(derived_view, derived_exc_view)
+
+ derived_view.__discriminator__ = lambda *arg: discriminator
+ # __discriminator__ is used by superdynamic systems
+ # that require it for introspection after manual view lookup;
+ # see also MultiView.__discriminator__
+ view_intr['derived_callable'] = derived_view
+
+ self.registry._clear_view_lookup_cache()
+
+ def derive_view(isexc_only, renderer):
+ # added by discrim_func above during conflict resolving
+ preds = view_intr['predicates']
+ order = view_intr['order']
+ phash = view_intr['phash']
+
+ derived_view = self._derive_view(
+ view,
+ route_name=route_name,
+ permission=permission,
+ predicates=preds,
+ attr=attr,
+ context=context,
+ exception_only=isexc_only,
+ renderer=renderer,
+ wrapper_viewname=wrapper,
+ viewname=name,
+ accept=accept,
+ order=order,
+ phash=phash,
+ decorator=decorator,
+ mapper=mapper,
+ http_cache=http_cache,
+ require_csrf=require_csrf,
+ extra_options=ovals,
+ )
+ return derived_view
+
+ def register_view(classifier, request_iface, derived_view):
+ # A multiviews is a set of views which are registered for
+ # exactly the same context type/request type/name triad. Each
+ # constituent view in a multiview differs only by the
+ # predicates which it possesses.
+
+ # To find a previously registered view for a context
+ # type/request type/name triad, we need to use the
+ # ``registered`` method of the adapter registry rather than
+ # ``lookup``. ``registered`` ignores interface inheritance
+ # for the required and provided arguments, returning only a
+ # view registered previously with the *exact* triad we pass
+ # in.
+
+ # We need to do this three times, because we use three
+ # different interfaces as the ``provided`` interface while
+ # doing registrations, and ``registered`` performs exact
+ # matches on all the arguments it receives.
+
+ old_view = None
+ order, phash = view_intr['order'], view_intr['phash']
+ registered = self.registry.adapters.registered
+
+ for view_type in (IView, ISecuredView, IMultiView):
+ old_view = registered(
+ (classifier, request_iface, r_context),
+ view_type, name)
+ if old_view is not None:
+ break
+
+ old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH)
+ is_multiview = IMultiView.providedBy(old_view)
+ want_multiview = (
+ is_multiview
+ # no component was yet registered for exactly this triad
+ # or only one was registered but with the same phash, meaning
+ # that this view is an override
+ or (old_view is not None and old_phash != phash)
+ )
+
+ if not want_multiview:
+ if hasattr(derived_view, '__call_permissive__'):
+ view_iface = ISecuredView
+ else:
+ view_iface = IView
+ self.registry.registerAdapter(
+ derived_view,
+ (classifier, request_iface, context),
+ view_iface,
+ name
+ )
+
+ else:
+ # - A view or multiview was already registered for this
+ # triad, and the new view is not an override.
+
+ # XXX we could try to be more efficient here and register
+ # a non-secured view for a multiview if none of the
+ # multiview's constituent views have a permission
+ # associated with them, but this code is getting pretty
+ # rough already
+ if is_multiview:
+ multiview = old_view
+ else:
+ multiview = MultiView(name)
+ old_accept = getattr(old_view, '__accept__', None)
+ old_order = getattr(old_view, '__order__', MAX_ORDER)
+ # don't bother passing accept_order here as we know we're
+ # adding another one right after which will re-sort
+ multiview.add(old_view, old_order, old_phash, old_accept)
+ accept_order = self.registry.queryUtility(IAcceptOrder)
+ multiview.add(derived_view, order, phash, accept, accept_order)
+ for view_type in (IView, ISecuredView):
+ # unregister any existing views
+ self.registry.adapters.unregister(
+ (classifier, request_iface, r_context),
+ view_type, name=name)
+ self.registry.registerAdapter(
+ multiview,
+ (classifier, request_iface, context),
+ IMultiView, name=name)
+
+ if mapper:
+ mapper_intr = self.introspectable(
+ 'view mappers',
+ discriminator,
+ 'view mapper for %s' % view_desc,
+ 'view mapper'
+ )
+ mapper_intr['mapper'] = mapper
+ mapper_intr.relate('views', discriminator)
+ introspectables.append(mapper_intr)
+ if route_name:
+ view_intr.relate('routes', route_name) # see add_route
+ if renderer is not None and renderer.name and '.' in renderer.name:
+ # the renderer is a template
+ tmpl_intr = self.introspectable(
+ 'templates',
+ discriminator,
+ renderer.name,
+ 'template'
+ )
+ tmpl_intr.relate('views', discriminator)
+ tmpl_intr['name'] = renderer.name
+ tmpl_intr['type'] = renderer.type
+ tmpl_intr['renderer'] = renderer
+ introspectables.append(tmpl_intr)
+ if permission is not None:
+ # if a permission exists, register a permission introspectable
+ perm_intr = self.introspectable(
+ 'permissions',
+ permission,
+ permission,
+ 'permission'
+ )
+ perm_intr['value'] = permission
+ perm_intr.relate('views', discriminator)
+ introspectables.append(perm_intr)
+ self.action(discriminator, register, introspectables=introspectables)
+
+ def _check_view_options(self, **kw):
+ # we only need to validate deriver options because the predicates
+ # were checked by the predlist
+ derivers = self.registry.getUtility(IViewDerivers)
+ for deriver in derivers.values():
+ for opt in getattr(deriver, 'options', []):
+ kw.pop(opt, None)
+ if kw:
+ raise ConfigurationError('Unknown view options: %s' % (kw,))
+
+ def _apply_view_derivers(self, info):
+ # These derivers are not really derivers and so have fixed order
+ outer_derivers = [('attr_wrapped_view', attr_wrapped_view),
+ ('predicated_view', predicated_view)]
+
+ view = info.original_view
+ derivers = self.registry.getUtility(IViewDerivers)
+ for name, deriver in reversed(outer_derivers + derivers.sorted()):
+ view = wraps_view(deriver)(view, info)
+ return view
+
+ @action_method
+ def add_view_predicate(self, name, factory, weighs_more_than=None,
+ weighs_less_than=None):
+ """
+ .. versionadded:: 1.4
+
+ Adds a view predicate factory. The associated view predicate can
+ later be named as a keyword argument to
+ :meth:`pyramid.config.Configurator.add_view` in the
+ ``predicates`` anonyous keyword argument dictionary.
+
+ ``name`` should be the name of the predicate. It must be a valid
+ Python identifier (it will be used as a keyword argument to
+ ``add_view`` by others).
+
+ ``factory`` should be a :term:`predicate factory` or :term:`dotted
+ Python name` which refers to a predicate factory.
+
+ See :ref:`view_and_route_predicates` for more information.
+ """
+ self._add_predicate(
+ 'view',
+ name,
+ factory,
+ weighs_more_than=weighs_more_than,
+ weighs_less_than=weighs_less_than
+ )
+
+ def add_default_view_predicates(self):
+ p = pyramid.predicates
+ for (name, factory) in (
+ ('xhr', p.XHRPredicate),
+ ('request_method', p.RequestMethodPredicate),
+ ('path_info', p.PathInfoPredicate),
+ ('request_param', p.RequestParamPredicate),
+ ('header', p.HeaderPredicate),
+ ('accept', p.AcceptPredicate),
+ ('containment', p.ContainmentPredicate),
+ ('request_type', p.RequestTypePredicate),
+ ('match_param', p.MatchParamPredicate),
+ ('check_csrf', p.CheckCSRFTokenPredicate),
+ ('physical_path', p.PhysicalPathPredicate),
+ ('effective_principals', p.EffectivePrincipalsPredicate),
+ ('custom', p.CustomPredicate),
+ ):
+ self.add_view_predicate(name, factory)
+
+ def add_default_accept_view_order(self):
+ for accept in (
+ 'text/html',
+ 'application/xhtml+xml',
+ 'application/xml',
+ 'text/xml',
+ 'text/plain',
+ 'application/json',
+ ):
+ self.add_accept_view_order(accept)
+
+ @action_method
+ def add_accept_view_order(
+ self,
+ value,
+ weighs_more_than=None,
+ weighs_less_than=None,
+ ):
+ """
+ Specify an ordering preference for the ``accept`` view option used
+ during :term:`view lookup`.
+
+ By default, if two views have different ``accept`` options and a
+ request specifies ``Accept: */*`` or omits the header entirely then
+ it is random which view will be selected. This method provides a way
+ to specify a server-side, relative ordering between accept media types.
+
+ ``value`` should be a :term:`media type` as specified by
+ :rfc:`7231#section-5.3.2`. For example, ``text/plain;charset=utf8``,
+ ``application/json`` or ``text/html``.
+
+ ``weighs_more_than`` and ``weighs_less_than`` control the ordering
+ of media types. Each value may be a string or a list of strings. If
+ all options for ``weighs_more_than`` (or ``weighs_less_than``) cannot
+ be found, it is an error.
+
+ Earlier calls to ``add_accept_view_order`` are given higher priority
+ over later calls, assuming similar constraints but standard conflict
+ resolution mechanisms can be used to override constraints.
+
+ See :ref:`accept_content_negotiation` for more information.
+
+ .. versionadded:: 1.10
+
+ """
+ def check_type(than):
+ than_type, than_subtype, than_params = Accept.parse_offer(than)
+ # text/plain vs text/html;charset=utf8
+ if bool(offer_params) ^ bool(than_params):
+ raise ConfigurationError(
+ 'cannot compare a media type with params to one without '
+ 'params')
+ # text/plain;charset=utf8 vs text/html;charset=utf8
+ if offer_params and (
+ offer_subtype != than_subtype or offer_type != than_type
+ ):
+ raise ConfigurationError(
+ 'cannot compare params across different media types')
+
+ def normalize_types(thans):
+ thans = [normalize_accept_offer(than) for than in thans]
+ for than in thans:
+ check_type(than)
+ return thans
+
+ value = normalize_accept_offer(value)
+ offer_type, offer_subtype, offer_params = Accept.parse_offer(value)
+
+ if weighs_more_than:
+ if not is_nonstr_iter(weighs_more_than):
+ weighs_more_than = [weighs_more_than]
+ weighs_more_than = normalize_types(weighs_more_than)
+
+ if weighs_less_than:
+ if not is_nonstr_iter(weighs_less_than):
+ weighs_less_than = [weighs_less_than]
+ weighs_less_than = normalize_types(weighs_less_than)
+
+ discriminator = ('accept view order', value)
+ intr = self.introspectable(
+ 'accept view order',
+ value,
+ value,
+ 'accept view order')
+ intr['value'] = value
+ intr['weighs_more_than'] = weighs_more_than
+ intr['weighs_less_than'] = weighs_less_than
+ def register():
+ sorter = self.registry.queryUtility(IAcceptOrder)
+ if sorter is None:
+ sorter = TopologicalSorter()
+ self.registry.registerUtility(sorter, IAcceptOrder)
+ sorter.add(
+ value, value,
+ before=weighs_more_than,
+ after=weighs_less_than,
+ )
+ self.action(discriminator, register, introspectables=(intr,),
+ order=PHASE1_CONFIG) # must be registered before add_view
+
+ @action_method
+ def add_view_deriver(self, deriver, name=None, under=None, over=None):
+ """
+ .. versionadded:: 1.7
+
+ Add a :term:`view deriver` to the view pipeline. View derivers are
+ a feature used by extension authors to wrap views in custom code
+ controllable by view-specific options.
+
+ ``deriver`` should be a callable conforming to the
+ :class:`pyramid.interfaces.IViewDeriver` interface.
+
+ ``name`` should be the name of the view deriver. There are no
+ restrictions on the name of a view deriver. If left unspecified, the
+ name will be constructed from the name of the ``deriver``.
+
+ The ``under`` and ``over`` options can be used to control the ordering
+ of view derivers by providing hints about where in the view pipeline
+ the deriver is used. Each option may be a string or a list of strings.
+ At least one view deriver in each, the over and under directions, must
+ exist to fully satisfy the constraints.
+
+ ``under`` means closer to the user-defined :term:`view callable`,
+ and ``over`` means closer to view pipeline ingress.
+
+ The default value for ``over`` is ``rendered_view`` and ``under`` is
+ ``decorated_view``. This places the deriver somewhere between the two
+ in the view pipeline. If the deriver should be placed elsewhere in the
+ pipeline, such as above ``decorated_view``, then you MUST also specify
+ ``under`` to something earlier in the order, or a
+ ``CyclicDependencyError`` will be raised when trying to sort the
+ derivers.
+
+ See :ref:`view_derivers` for more information.
+
+ """
+ deriver = self.maybe_dotted(deriver)
+
+ if name is None:
+ name = deriver.__name__
+
+ if name in (INGRESS, VIEW):
+ raise ConfigurationError('%s is a reserved view deriver name'
+ % name)
+
+ if under is None:
+ under = 'decorated_view'
+
+ if over is None:
+ over = 'rendered_view'
+
+ over = as_sorted_tuple(over)
+ under = as_sorted_tuple(under)
+
+ if INGRESS in over:
+ raise ConfigurationError('%s cannot be over INGRESS' % name)
+
+ # ensure everything is always over mapped_view
+ if VIEW in over and name != 'mapped_view':
+ over = as_sorted_tuple(over + ('mapped_view',))
+
+ if VIEW in under:
+ raise ConfigurationError('%s cannot be under VIEW' % name)
+ if 'mapped_view' in under:
+ raise ConfigurationError('%s cannot be under "mapped_view"' % name)
+
+ discriminator = ('view deriver', name)
+ intr = self.introspectable(
+ 'view derivers',
+ name,
+ name,
+ 'view deriver')
+ intr['name'] = name
+ intr['deriver'] = deriver
+ intr['under'] = under
+ intr['over'] = over
+ def register():
+ derivers = self.registry.queryUtility(IViewDerivers)
+ if derivers is None:
+ derivers = TopologicalSorter(
+ default_before=None,
+ default_after=INGRESS,
+ first=INGRESS,
+ last=VIEW,
+ )
+ self.registry.registerUtility(derivers, IViewDerivers)
+ derivers.add(name, deriver, before=over, after=under)
+ self.action(discriminator, register, introspectables=(intr,),
+ order=PHASE1_CONFIG) # must be registered before add_view
+
+ def add_default_view_derivers(self):
+ d = pyramid.viewderivers
+ derivers = [
+ ('secured_view', d.secured_view),
+ ('owrapped_view', d.owrapped_view),
+ ('http_cached_view', d.http_cached_view),
+ ('decorated_view', d.decorated_view),
+ ('rendered_view', d.rendered_view),
+ ('mapped_view', d.mapped_view),
+ ]
+ last = INGRESS
+ for name, deriver in derivers:
+ self.add_view_deriver(
+ deriver,
+ name=name,
+ under=last,
+ over=VIEW,
+ )
+ last = name
+
+ # leave the csrf_view loosely coupled to the rest of the pipeline
+ # by ensuring nothing in the default pipeline depends on the order
+ # of the csrf_view
+ self.add_view_deriver(
+ d.csrf_view,
+ 'csrf_view',
+ under='secured_view',
+ over='owrapped_view',
+ )
+
+ def derive_view(self, view, attr=None, renderer=None):
+ """
+ Create a :term:`view callable` using the function, instance,
+ or class (or :term:`dotted Python name` referring to the same)
+ provided as ``view`` object.
+
+ .. warning::
+
+ This method is typically only used by :app:`Pyramid` framework
+ extension authors, not by :app:`Pyramid` application developers.
+
+ This is API is useful to framework extenders who create
+ pluggable systems which need to register 'proxy' view
+ callables for functions, instances, or classes which meet the
+ requirements of being a :app:`Pyramid` view callable. For
+ example, a ``some_other_framework`` function in another
+ framework may want to allow a user to supply a view callable,
+ but he may want to wrap the view callable in his own before
+ registering the wrapper as a :app:`Pyramid` view callable.
+ Because a :app:`Pyramid` view callable can be any of a
+ number of valid objects, the framework extender will not know
+ how to call the user-supplied object. Running it through
+ ``derive_view`` normalizes it to a callable which accepts two
+ arguments: ``context`` and ``request``.
+
+ For example:
+
+ .. code-block:: python
+
+ def some_other_framework(user_supplied_view):
+ config = Configurator(reg)
+ proxy_view = config.derive_view(user_supplied_view)
+ def my_wrapper(context, request):
+ do_something_that_mutates(request)
+ return proxy_view(context, request)
+ config.add_view(my_wrapper)
+
+ The ``view`` object provided should be one of the following:
+
+ - A function or another non-class callable object that accepts
+ a :term:`request` as a single positional argument and which
+ returns a :term:`response` object.
+
+ - A function or other non-class callable object that accepts
+ two positional arguments, ``context, request`` and which
+ returns a :term:`response` object.
+
+ - A class which accepts a single positional argument in its
+ constructor named ``request``, and which has a ``__call__``
+ method that accepts no arguments that returns a
+ :term:`response` object.
+
+ - A class which accepts two positional arguments named
+ ``context, request``, and which has a ``__call__`` method
+ that accepts no arguments that returns a :term:`response`
+ object.
+
+ - A :term:`dotted Python name` which refers to any of the
+ kinds of objects above.
+
+ This API returns a callable which accepts the arguments
+ ``context, request`` and which returns the result of calling
+ the provided ``view`` object.
+
+ The ``attr`` keyword argument is most useful when the view
+ object is a class. It names the method that should be used as
+ the callable. If ``attr`` is not provided, the attribute
+ effectively defaults to ``__call__``. See
+ :ref:`class_as_view` for more information.
+
+ The ``renderer`` keyword argument should be a renderer
+ name. If supplied, it will cause the returned callable to use
+ a :term:`renderer` to convert the user-supplied view result to
+ a :term:`response` object. If a ``renderer`` argument is not
+ supplied, the user-supplied view must itself return a
+ :term:`response` object. """
+ return self._derive_view(view, attr=attr, renderer=renderer)
+
+ # b/w compat
+ def _derive_view(self, view, permission=None, predicates=(),
+ attr=None, renderer=None, wrapper_viewname=None,
+ viewname=None, accept=None, order=MAX_ORDER,
+ phash=DEFAULT_PHASH, decorator=None, route_name=None,
+ mapper=None, http_cache=None, context=None,
+ require_csrf=None, exception_only=False,
+ extra_options=None):
+ view = self.maybe_dotted(view)
+ mapper = self.maybe_dotted(mapper)
+ if isinstance(renderer, string_types):
+ renderer = renderers.RendererHelper(
+ name=renderer, package=self.package,
+ registry=self.registry)
+ if renderer is None:
+ # use default renderer if one exists
+ if self.registry.queryUtility(IRendererFactory) is not None:
+ renderer = renderers.RendererHelper(
+ name=None,
+ package=self.package,
+ registry=self.registry)
+
+ options = dict(
+ view=view,
+ context=context,
+ permission=permission,
+ attr=attr,
+ renderer=renderer,
+ wrapper=wrapper_viewname,
+ name=viewname,
+ accept=accept,
+ mapper=mapper,
+ decorator=decorator,
+ http_cache=http_cache,
+ require_csrf=require_csrf,
+ route_name=route_name
+ )
+ if extra_options:
+ options.update(extra_options)
+
+ info = ViewDeriverInfo(
+ view=view,
+ registry=self.registry,
+ package=self.package,
+ predicates=predicates,
+ exception_only=exception_only,
+ options=options,
+ )
+
+ # order and phash are only necessary for the predicated view and
+ # are not really view deriver options
+ info.order = order
+ info.phash = phash
+
+ return self._apply_view_derivers(info)
+
+ @viewdefaults
+ @action_method
+ def add_forbidden_view(
+ self,
+ view=None,
+ attr=None,
+ renderer=None,
+ wrapper=None,
+ route_name=None,
+ request_type=None,
+ request_method=None,
+ request_param=None,
+ containment=None,
+ xhr=None,
+ accept=None,
+ header=None,
+ path_info=None,
+ custom_predicates=(),
+ decorator=None,
+ mapper=None,
+ match_param=None,
+ **view_options
+ ):
+ """ Add a forbidden view to the current configuration state. The
+ view will be called when Pyramid or application code raises a
+ :exc:`pyramid.httpexceptions.HTTPForbidden` exception and the set of
+ circumstances implied by the predicates provided are matched. The
+ simplest example is:
+
+ .. code-block:: python
+
+ def forbidden(request):
+ return Response('Forbidden', status='403 Forbidden')
+
+ config.add_forbidden_view(forbidden)
+
+ If ``view`` argument is not provided, the view callable defaults to
+ :func:`~pyramid.httpexceptions.default_exceptionresponse_view`.
+
+ All arguments have the same meaning as
+ :meth:`pyramid.config.Configurator.add_view` and each predicate
+ argument restricts the set of circumstances under which this forbidden
+ view will be invoked. Unlike
+ :meth:`pyramid.config.Configurator.add_view`, this method will raise
+ an exception if passed ``name``, ``permission``, ``require_csrf``,
+ ``context``, ``for_``, or ``exception_only`` keyword arguments. These
+ argument values make no sense in the context of a forbidden
+ :term:`exception view`.
+
+ .. versionadded:: 1.3
+
+ .. versionchanged:: 1.8
+
+ The view is created using ``exception_only=True``.
+ """
+ for arg in (
+ 'name', 'permission', 'context', 'for_', 'require_csrf',
+ 'exception_only',
+ ):
+ if arg in view_options:
+ raise ConfigurationError(
+ '%s may not be used as an argument to add_forbidden_view'
+ % (arg,))
+
+ if view is None:
+ view = default_exceptionresponse_view
+
+ settings = dict(
+ view=view,
+ context=HTTPForbidden,
+ exception_only=True,
+ wrapper=wrapper,
+ request_type=request_type,
+ request_method=request_method,
+ request_param=request_param,
+ containment=containment,
+ xhr=xhr,
+ accept=accept,
+ header=header,
+ path_info=path_info,
+ custom_predicates=custom_predicates,
+ decorator=decorator,
+ mapper=mapper,
+ match_param=match_param,
+ route_name=route_name,
+ permission=NO_PERMISSION_REQUIRED,
+ require_csrf=False,
+ attr=attr,
+ renderer=renderer,
+ )
+ settings.update(view_options)
+ return self.add_view(**settings)
+
+ set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias
+
+ @viewdefaults
+ @action_method
+ def add_notfound_view(
+ self,
+ view=None,
+ attr=None,
+ renderer=None,
+ wrapper=None,
+ route_name=None,
+ request_type=None,
+ request_method=None,
+ request_param=None,
+ containment=None,
+ xhr=None,
+ accept=None,
+ header=None,
+ path_info=None,
+ custom_predicates=(),
+ decorator=None,
+ mapper=None,
+ match_param=None,
+ append_slash=False,
+ **view_options
+ ):
+ """ Add a default :term:`Not Found View` to the current configuration
+ state. The view will be called when Pyramid or application code raises
+ an :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g., when a
+ view cannot be found for the request). The simplest example is:
+
+ .. code-block:: python
+
+ def notfound(request):
+ return Response('Not Found', status='404 Not Found')
+
+ config.add_notfound_view(notfound)
+
+ If ``view`` argument is not provided, the view callable defaults to
+ :func:`~pyramid.httpexceptions.default_exceptionresponse_view`.
+
+ All arguments except ``append_slash`` have the same meaning as
+ :meth:`pyramid.config.Configurator.add_view` and each predicate
+ argument restricts the set of circumstances under which this notfound
+ view will be invoked. Unlike
+ :meth:`pyramid.config.Configurator.add_view`, this method will raise
+ an exception if passed ``name``, ``permission``, ``require_csrf``,
+ ``context``, ``for_``, or ``exception_only`` keyword arguments. These
+ argument values make no sense in the context of a Not Found View.
+
+ If ``append_slash`` is ``True``, when this Not Found View is invoked,
+ and the current path info does not end in a slash, the notfound logic
+ will attempt to find a :term:`route` that matches the request's path
+ info suffixed with a slash. If such a route exists, Pyramid will
+ issue a redirect to the URL implied by the route; if it does not,
+ Pyramid will return the result of the view callable provided as
+ ``view``, as normal.
+
+ If the argument provided as ``append_slash`` is not a boolean but
+ instead implements :class:`~pyramid.interfaces.IResponse`, the
+ append_slash logic will behave as if ``append_slash=True`` was passed,
+ but the provided class will be used as the response class instead of
+ the default :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect`
+ response class when a redirect is performed. For example:
+
+ .. code-block:: python
+
+ from pyramid.httpexceptions import HTTPMovedPermanently
+ config.add_notfound_view(append_slash=HTTPMovedPermanently)
+
+ The above means that a redirect to a slash-appended route will be
+ attempted, but instead of :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect`
+ being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
+ be used` for the redirect response if a slash-appended route is found.
+
+ :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect` class is used
+ as default response, which is equivalent to
+ :class:`~pyramid.httpexceptions.HTTPFound` with addition of redirecting
+ with the same HTTP method (useful when doing POST requests).
+
+ .. versionadded:: 1.3
+
+ .. versionchanged:: 1.6
+
+ The ``append_slash`` argument was modified to allow any object that
+ implements the ``IResponse`` interface to specify the response class
+ used when a redirect is performed.
+
+ .. versionchanged:: 1.8
+
+ The view is created using ``exception_only=True``.
+
+ .. versionchanged: 1.10
+
+ Default response was changed from :class:`~pyramid.httpexceptions.HTTPFound`
+ to :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect`.
+
+ """
+ for arg in (
+ 'name', 'permission', 'context', 'for_', 'require_csrf',
+ 'exception_only',
+ ):
+ if arg in view_options:
+ raise ConfigurationError(
+ '%s may not be used as an argument to add_notfound_view'
+ % (arg,))
+
+ if view is None:
+ view = default_exceptionresponse_view
+
+ settings = dict(
+ view=view,
+ context=HTTPNotFound,
+ exception_only=True,
+ wrapper=wrapper,
+ request_type=request_type,
+ request_method=request_method,
+ request_param=request_param,
+ containment=containment,
+ xhr=xhr,
+ accept=accept,
+ header=header,
+ path_info=path_info,
+ custom_predicates=custom_predicates,
+ decorator=decorator,
+ mapper=mapper,
+ match_param=match_param,
+ route_name=route_name,
+ permission=NO_PERMISSION_REQUIRED,
+ require_csrf=False,
+ )
+ settings.update(view_options)
+ if append_slash:
+ view = self._derive_view(view, attr=attr, renderer=renderer)
+ if IResponse.implementedBy(append_slash):
+ view = AppendSlashNotFoundViewFactory(
+ view, redirect_class=append_slash,
+ )
+ else:
+ view = AppendSlashNotFoundViewFactory(view)
+ settings['view'] = view
+ else:
+ settings['attr'] = attr
+ settings['renderer'] = renderer
+ return self.add_view(**settings)
+
+ set_notfound_view = add_notfound_view # deprecated sorta-bw-compat alias
+
+ @viewdefaults
+ @action_method
+ def add_exception_view(
+ self,
+ view=None,
+ context=None,
+ # force all other arguments to be specified as key=value
+ **view_options
+ ):
+ """ Add an :term:`exception view` for the specified ``exception`` to
+ the current configuration state. The view will be called when Pyramid
+ or application code raises the given exception.
+
+ This method accepts almost all of the same arguments as
+ :meth:`pyramid.config.Configurator.add_view` except for ``name``,
+ ``permission``, ``for_``, ``require_csrf``, and ``exception_only``.
+
+ By default, this method will set ``context=Exception``, thus
+ registering for most default Python exceptions. Any subclass of
+ ``Exception`` may be specified.
+
+ .. versionadded:: 1.8
+ """
+ for arg in (
+ 'name', 'for_', 'exception_only', 'require_csrf', 'permission',
+ ):
+ if arg in view_options:
+ raise ConfigurationError(
+ '%s may not be used as an argument to add_exception_view'
+ % (arg,))
+ if context is None:
+ context = Exception
+ view_options.update(dict(
+ view=view,
+ context=context,
+ exception_only=True,
+ permission=NO_PERMISSION_REQUIRED,
+ require_csrf=False,
+ ))
+ return self.add_view(**view_options)
+
+ @action_method
+ def set_view_mapper(self, mapper):
+ """
+ Setting a :term:`view mapper` makes it possible to make use of
+ :term:`view callable` objects which implement different call
+ signatures than the ones supported by :app:`Pyramid` as described in
+ its narrative documentation.
+
+ The ``mapper`` argument should be an object implementing
+ :class:`pyramid.interfaces.IViewMapperFactory` or a :term:`dotted
+ Python name` to such an object. The provided ``mapper`` will become
+ the default view mapper to be used by all subsequent :term:`view
+ configuration` registrations.
+
+ .. seealso::
+
+ See also :ref:`using_a_view_mapper`.
+
+ .. note::
+
+ Using the ``default_view_mapper`` argument to the
+ :class:`pyramid.config.Configurator` constructor
+ can be used to achieve the same purpose.
+ """
+ mapper = self.maybe_dotted(mapper)
+ def register():
+ self.registry.registerUtility(mapper, IViewMapperFactory)
+ # IViewMapperFactory is looked up as the result of view config
+ # in phase 3
+ intr = self.introspectable('view mappers',
+ IViewMapperFactory,
+ self.object_description(mapper),
+ 'default view mapper')
+ intr['mapper'] = mapper
+ self.action(IViewMapperFactory, register, order=PHASE1_CONFIG,
+ introspectables=(intr,))
+
+ @action_method
+ def add_static_view(self, name, path, **kw):
+ """ Add a view used to render static assets such as images
+ and CSS files.
+
+ The ``name`` argument is a string representing an
+ application-relative local URL prefix. It may alternately be a full
+ URL.
+
+ The ``path`` argument is the path on disk where the static files
+ reside. This can be an absolute path, a package-relative path, or a
+ :term:`asset specification`.
+
+ The ``cache_max_age`` keyword argument is input to set the
+ ``Expires`` and ``Cache-Control`` headers for static assets served.
+ Note that this argument has no effect when the ``name`` is a *url
+ prefix*. By default, this argument is ``None``, meaning that no
+ particular Expires or Cache-Control headers are set in the response.
+
+ The ``permission`` keyword argument is used to specify the
+ :term:`permission` required by a user to execute the static view. By
+ default, it is the string
+ :data:`pyramid.security.NO_PERMISSION_REQUIRED`, a special sentinel
+ which indicates that, even if a :term:`default permission` exists for
+ the current application, the static view should be renderered to
+ completely anonymous users. This default value is permissive
+ because, in most web apps, static assets seldom need protection from
+ viewing. If ``permission`` is specified, the security checking will
+ be performed against the default root factory ACL.
+
+ Any other keyword arguments sent to ``add_static_view`` are passed on
+ to :meth:`pyramid.config.Configurator.add_route` (e.g. ``factory``,
+ perhaps to define a custom factory with a custom ACL for this static
+ view).
+
+ *Usage*
+
+ The ``add_static_view`` function is typically used in conjunction
+ with the :meth:`pyramid.request.Request.static_url` method.
+ ``add_static_view`` adds a view which renders a static asset when
+ some URL is visited; :meth:`pyramid.request.Request.static_url`
+ generates a URL to that asset.
+
+ The ``name`` argument to ``add_static_view`` is usually a simple URL
+ prefix (e.g. ``'images'``). When this is the case, the
+ :meth:`pyramid.request.Request.static_url` API will generate a URL
+ which points to a Pyramid view, which will serve up a set of assets
+ that live in the package itself. For example:
+
+ .. code-block:: python
+
+ add_static_view('images', 'mypackage:images/')
+
+ Code that registers such a view can generate URLs to the view via
+ :meth:`pyramid.request.Request.static_url`:
+
+ .. code-block:: python
+
+ request.static_url('mypackage:images/logo.png')
+
+ When ``add_static_view`` is called with a ``name`` argument that
+ represents a URL prefix, as it is above, subsequent calls to
+ :meth:`pyramid.request.Request.static_url` with paths that start with
+ the ``path`` argument passed to ``add_static_view`` will generate a
+ URL something like ``http://<Pyramid app URL>/images/logo.png``,
+ which will cause the ``logo.png`` file in the ``images`` subdirectory
+ of the ``mypackage`` package to be served.
+
+ ``add_static_view`` can alternately be used with a ``name`` argument
+ which is a *URL*, causing static assets to be served from an external
+ webserver. This happens when the ``name`` argument is a fully
+ qualified URL (e.g. starts with ``http://`` or similar). In this
+ mode, the ``name`` is used as the prefix of the full URL when
+ generating a URL using :meth:`pyramid.request.Request.static_url`.
+ Furthermore, if a protocol-relative URL (e.g. ``//example.com/images``)
+ is used as the ``name`` argument, the generated URL will use the
+ protocol of the request (http or https, respectively).
+
+ For example, if ``add_static_view`` is called like so:
+
+ .. code-block:: python
+
+ add_static_view('http://example.com/images', 'mypackage:images/')
+
+ Subsequently, the URLs generated by
+ :meth:`pyramid.request.Request.static_url` for that static view will
+ be prefixed with ``http://example.com/images`` (the external webserver
+ listening on ``example.com`` must be itself configured to respond
+ properly to such a request.):
+
+ .. code-block:: python
+
+ static_url('mypackage:images/logo.png', request)
+
+ See :ref:`static_assets_section` for more information.
+ """
+ spec = self._make_spec(path)
+ info = self._get_static_info()
+ info.add(self, name, spec, **kw)
+
+ def add_cache_buster(self, path, cachebust, explicit=False):
+ """
+ Add a cache buster to a set of files on disk.
+
+ The ``path`` should be the path on disk where the static files
+ reside. This can be an absolute path, a package-relative path, or a
+ :term:`asset specification`.
+
+ The ``cachebust`` argument may be set to cause
+ :meth:`~pyramid.request.Request.static_url` to use cache busting when
+ generating URLs. See :ref:`cache_busting` for general information
+ about cache busting. The value of the ``cachebust`` argument must
+ be an object which implements
+ :class:`~pyramid.interfaces.ICacheBuster`.
+
+ If ``explicit`` is set to ``True`` then the ``path`` for the cache
+ buster will be matched based on the ``rawspec`` instead of the
+ ``pathspec`` as defined in the
+ :class:`~pyramid.interfaces.ICacheBuster` interface.
+ Default: ``False``.
+
+ """
+ spec = self._make_spec(path)
+ info = self._get_static_info()
+ info.add_cache_buster(self, spec, cachebust, explicit=explicit)
+
+ def _get_static_info(self):
+ info = self.registry.queryUtility(IStaticURLInfo)
+ if info is None:
+ info = StaticURLInfo()
+ self.registry.registerUtility(info, IStaticURLInfo)
+ return info
+
+def isexception(o):
+ if IInterface.providedBy(o):
+ if IException.isEqualOrExtendedBy(o):
+ return True
+ return (
+ isinstance(o, Exception) or
+ (inspect.isclass(o) and (issubclass(o, Exception)))
+ )
+
+def runtime_exc_view(view, excview):
+ # create a view callable which can pretend to be both a normal view
+ # and an exception view, dispatching to the appropriate one based
+ # on the state of request.exception
+ def wrapper_view(context, request):
+ if getattr(request, 'exception', None):
+ return excview(context, request)
+ return view(context, request)
+
+ # these constants are the same between the two views
+ wrapper_view.__wraps__ = wrapper_view
+ wrapper_view.__original_view__ = getattr(view, '__original_view__', view)
+ wrapper_view.__module__ = view.__module__
+ wrapper_view.__doc__ = view.__doc__
+ wrapper_view.__name__ = view.__name__
+
+ wrapper_view.__accept__ = getattr(view, '__accept__', None)
+ wrapper_view.__order__ = getattr(view, '__order__', MAX_ORDER)
+ wrapper_view.__phash__ = getattr(view, '__phash__', DEFAULT_PHASH)
+ wrapper_view.__view_attr__ = getattr(view, '__view_attr__', None)
+ wrapper_view.__permission__ = getattr(view, '__permission__', None)
+
+ def wrap_fn(attr):
+ def wrapper(context, request):
+ if getattr(request, 'exception', None):
+ selected_view = excview
+ else:
+ selected_view = view
+ fn = getattr(selected_view, attr, None)
+ if fn is not None:
+ return fn(context, request)
+ return wrapper
+
+ # these methods are dynamic per-request and should dispatch to their
+ # respective views based on whether it's an exception or not
+ wrapper_view.__call_permissive__ = wrap_fn('__call_permissive__')
+ wrapper_view.__permitted__ = wrap_fn('__permitted__')
+ wrapper_view.__predicated__ = wrap_fn('__predicated__')
+ wrapper_view.__predicates__ = wrap_fn('__predicates__')
+ return wrapper_view
+
+@implementer(IViewDeriverInfo)
+class ViewDeriverInfo(object):
+ def __init__(self,
+ view,
+ registry,
+ package,
+ predicates,
+ exception_only,
+ options,
+ ):
+ self.original_view = view
+ self.registry = registry
+ self.package = package
+ self.predicates = predicates or []
+ self.options = options or {}
+ self.exception_only = exception_only
+
+ @reify
+ def settings(self):
+ return self.registry.settings
+
+@implementer(IStaticURLInfo)
+class StaticURLInfo(object):
+ def __init__(self):
+ self.registrations = []
+ self.cache_busters = []
+
+ def generate(self, path, request, **kw):
+ for (url, spec, route_name) in self.registrations:
+ if path.startswith(spec):
+ subpath = path[len(spec):]
+ if WIN: # pragma: no cover
+ subpath = subpath.replace('\\', '/') # windows
+ if self.cache_busters:
+ subpath, kw = self._bust_asset_path(
+ request, spec, subpath, kw)
+ if url is None:
+ kw['subpath'] = subpath
+ return request.route_url(route_name, **kw)
+ else:
+ app_url, qs, anchor = parse_url_overrides(request, kw)
+ parsed = url_parse(url)
+ if not parsed.scheme:
+ url = urlparse.urlunparse(parsed._replace(
+ scheme=request.environ['wsgi.url_scheme']))
+ subpath = url_quote(subpath)
+ result = urljoin(url, subpath)
+ return result + qs + anchor
+
+ raise ValueError('No static URL definition matching %s' % path)
+
+ def add(self, config, name, spec, **extra):
+ # This feature only allows for the serving of a directory and
+ # the files contained within, not of a single asset;
+ # appending a slash here if the spec doesn't have one is
+ # required for proper prefix matching done in ``generate``
+ # (``subpath = path[len(spec):]``).
+ if os.path.isabs(spec): # FBO windows
+ sep = os.sep
+ else:
+ sep = '/'
+ if not spec.endswith(sep) and not spec.endswith(':'):
+ spec = spec + sep
+
+ # we also make sure the name ends with a slash, purely as a
+ # convenience: a name that is a url is required to end in a
+ # slash, so that ``urljoin(name, subpath))`` will work above
+ # when the name is a URL, and it doesn't hurt things for it to
+ # have a name that ends in a slash if it's used as a route
+ # name instead of a URL.
+ if not name.endswith('/'):
+ # make sure it ends with a slash
+ name = name + '/'
+
+ if url_parse(name).netloc:
+ # it's a URL
+ # url, spec, route_name
+ url = name
+ route_name = None
+ else:
+ # it's a view name
+ url = None
+ cache_max_age = extra.pop('cache_max_age', None)
+
+ # create a view
+ view = static_view(spec, cache_max_age=cache_max_age,
+ use_subpath=True)
+
+ # Mutate extra to allow factory, etc to be passed through here.
+ # Treat permission specially because we'd like to default to
+ # permissiveness (see docs of config.add_static_view).
+ permission = extra.pop('permission', None)
+ if permission is None:
+ permission = NO_PERMISSION_REQUIRED
+
+ context = extra.pop('context', None)
+ if context is None:
+ context = extra.pop('for_', None)
+
+ renderer = extra.pop('renderer', None)
+
+ # register a route using the computed view, permission, and
+ # pattern, plus any extras passed to us via add_static_view
+ pattern = "%s*subpath" % name # name already ends with slash
+ if config.route_prefix:
+ route_name = '__%s/%s' % (config.route_prefix, name)
+ else:
+ route_name = '__%s' % name
+ config.add_route(route_name, pattern, **extra)
+ config.add_view(
+ route_name=route_name,
+ view=view,
+ permission=permission,
+ context=context,
+ renderer=renderer,
+ )
+
+ def register():
+ registrations = self.registrations
+
+ names = [t[0] for t in registrations]
+
+ if name in names:
+ idx = names.index(name)
+ registrations.pop(idx)
+
+ # url, spec, route_name
+ registrations.append((url, spec, route_name))
+
+ intr = config.introspectable('static views',
+ name,
+ 'static view for %r' % name,
+ 'static view')
+ intr['name'] = name
+ intr['spec'] = spec
+
+ config.action(None, callable=register, introspectables=(intr,))
+
+ def add_cache_buster(self, config, spec, cachebust, explicit=False):
+ # ensure the spec always has a trailing slash as we only support
+ # adding cache busters to folders, not files
+ if os.path.isabs(spec): # FBO windows
+ sep = os.sep
+ else:
+ sep = '/'
+ if not spec.endswith(sep) and not spec.endswith(':'):
+ spec = spec + sep
+
+ def register():
+ if config.registry.settings.get('pyramid.prevent_cachebust'):
+ return
+
+ cache_busters = self.cache_busters
+
+ # find duplicate cache buster (old_idx)
+ # and insertion location (new_idx)
+ new_idx, old_idx = len(cache_busters), None
+ for idx, (spec_, cb_, explicit_) in enumerate(cache_busters):
+ # if we find an identical (spec, explicit) then use it
+ if spec == spec_ and explicit == explicit_:
+ old_idx = new_idx = idx
+ break
+
+ # past all explicit==False specs then add to the end
+ elif not explicit and explicit_:
+ new_idx = idx
+ break
+
+ # explicit matches and spec is shorter
+ elif explicit == explicit_ and len(spec) < len(spec_):
+ new_idx = idx
+ break
+
+ if old_idx is not None:
+ cache_busters.pop(old_idx)
+
+ cache_busters.insert(new_idx, (spec, cachebust, explicit))
+
+ intr = config.introspectable('cache busters',
+ spec,
+ 'cache buster for %r' % spec,
+ 'cache buster')
+ intr['cachebust'] = cachebust
+ intr['path'] = spec
+ intr['explicit'] = explicit
+
+ config.action(None, callable=register, introspectables=(intr,))
+
+ def _bust_asset_path(self, request, spec, subpath, kw):
+ registry = request.registry
+ pkg_name, pkg_subpath = resolve_asset_spec(spec)
+ rawspec = None
+
+ if pkg_name is not None:
+ pathspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath)
+ overrides = registry.queryUtility(IPackageOverrides, name=pkg_name)
+ if overrides is not None:
+ resource_name = posixpath.join(pkg_subpath, subpath)
+ sources = overrides.filtered_sources(resource_name)
+ for source, filtered_path in sources:
+ rawspec = source.get_path(filtered_path)
+ if hasattr(source, 'pkg_name'):
+ rawspec = '{0}:{1}'.format(source.pkg_name, rawspec)
+ break
+
+ else:
+ pathspec = pkg_subpath + subpath
+
+ if rawspec is None:
+ rawspec = pathspec
+
+ kw['pathspec'] = pathspec
+ kw['rawspec'] = rawspec
+ for spec_, cachebust, explicit in reversed(self.cache_busters):
+ if (
+ (explicit and rawspec.startswith(spec_)) or
+ (not explicit and pathspec.startswith(spec_))
+ ):
+ subpath, kw = cachebust(request, subpath, kw)
+ break
+ return subpath, kw
diff --git a/src/pyramid/config/zca.py b/src/pyramid/config/zca.py
new file mode 100644
index 000000000..bcd5c31e3
--- /dev/null
+++ b/src/pyramid/config/zca.py
@@ -0,0 +1,20 @@
+from pyramid.threadlocal import get_current_registry
+
+class ZCAConfiguratorMixin(object):
+ def hook_zca(self):
+ """ Call :func:`zope.component.getSiteManager.sethook` with the
+ argument :data:`pyramid.threadlocal.get_current_registry`, causing
+ the :term:`Zope Component Architecture` 'global' APIs such as
+ :func:`zope.component.getSiteManager`,
+ :func:`zope.component.getAdapter` and others to use the
+ :app:`Pyramid` :term:`application registry` rather than the Zope
+ 'global' registry."""
+ from zope.component import getSiteManager
+ getSiteManager.sethook(get_current_registry)
+
+ def unhook_zca(self):
+ """ Call :func:`zope.component.getSiteManager.reset` to undo the
+ action of :meth:`pyramid.config.Configurator.hook_zca`."""
+ from zope.component import getSiteManager
+ getSiteManager.reset()
+
diff --git a/src/pyramid/csrf.py b/src/pyramid/csrf.py
new file mode 100644
index 000000000..da171d9af
--- /dev/null
+++ b/src/pyramid/csrf.py
@@ -0,0 +1,336 @@
+import uuid
+
+from webob.cookies import CookieProfile
+from zope.interface import implementer
+
+
+from pyramid.compat import (
+ bytes_,
+ urlparse,
+ text_,
+)
+from pyramid.exceptions import (
+ BadCSRFOrigin,
+ BadCSRFToken,
+)
+from pyramid.interfaces import ICSRFStoragePolicy
+from pyramid.settings import aslist
+from pyramid.util import (
+ SimpleSerializer,
+ is_same_domain,
+ strings_differ
+)
+
+
+@implementer(ICSRFStoragePolicy)
+class LegacySessionCSRFStoragePolicy(object):
+ """ A CSRF storage policy that defers control of CSRF storage to the
+ session.
+
+ This policy maintains compatibility with legacy ISession implementations
+ that know how to manage CSRF tokens themselves via
+ ``ISession.new_csrf_token`` and ``ISession.get_csrf_token``.
+
+ Note that using this CSRF implementation requires that
+ a :term:`session factory` is configured.
+
+ .. versionadded:: 1.9
+
+ """
+ def new_csrf_token(self, request):
+ """ Sets a new CSRF token into the session and returns it. """
+ return request.session.new_csrf_token()
+
+ def get_csrf_token(self, request):
+ """ Returns the currently active CSRF token from the session,
+ generating a new one if needed."""
+ return request.session.get_csrf_token()
+
+ def check_csrf_token(self, request, supplied_token):
+ """ Returns ``True`` if the ``supplied_token`` is valid."""
+ expected_token = self.get_csrf_token(request)
+ return not strings_differ(
+ bytes_(expected_token), bytes_(supplied_token))
+
+
+@implementer(ICSRFStoragePolicy)
+class SessionCSRFStoragePolicy(object):
+ """ A CSRF storage policy that persists the CSRF token in the session.
+
+ Note that using this CSRF implementation requires that
+ a :term:`session factory` is configured.
+
+ ``key``
+
+ The session key where the CSRF token will be stored.
+ Default: `_csrft_`.
+
+ .. versionadded:: 1.9
+
+ """
+ _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex))
+
+ def __init__(self, key='_csrft_'):
+ self.key = key
+
+ def new_csrf_token(self, request):
+ """ Sets a new CSRF token into the session and returns it. """
+ token = self._token_factory()
+ request.session[self.key] = token
+ return token
+
+ def get_csrf_token(self, request):
+ """ Returns the currently active CSRF token from the session,
+ generating a new one if needed."""
+ token = request.session.get(self.key, None)
+ if not token:
+ token = self.new_csrf_token(request)
+ return token
+
+ def check_csrf_token(self, request, supplied_token):
+ """ Returns ``True`` if the ``supplied_token`` is valid."""
+ expected_token = self.get_csrf_token(request)
+ return not strings_differ(
+ bytes_(expected_token), bytes_(supplied_token))
+
+
+@implementer(ICSRFStoragePolicy)
+class CookieCSRFStoragePolicy(object):
+ """ An alternative CSRF implementation that stores its information in
+ unauthenticated cookies, known as the 'Double Submit Cookie' method in the
+ `OWASP CSRF guidelines <https://www.owasp.org/index.php/
+ Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#
+ Double_Submit_Cookie>`_. This gives some additional flexibility with
+ regards to scaling as the tokens can be generated and verified by a
+ front-end server.
+
+ .. versionadded:: 1.9
+
+ .. versionchanged: 1.10
+
+ Added the ``samesite`` option and made the default ``'Lax'``.
+
+ """
+ _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex))
+
+ def __init__(self, cookie_name='csrf_token', secure=False, httponly=False,
+ domain=None, max_age=None, path='/', samesite='Lax'):
+ serializer = SimpleSerializer()
+ self.cookie_profile = CookieProfile(
+ cookie_name=cookie_name,
+ secure=secure,
+ max_age=max_age,
+ httponly=httponly,
+ path=path,
+ domains=[domain],
+ serializer=serializer,
+ samesite=samesite,
+ )
+ self.cookie_name = cookie_name
+
+ def new_csrf_token(self, request):
+ """ Sets a new CSRF token into the request and returns it. """
+ token = self._token_factory()
+ request.cookies[self.cookie_name] = token
+ def set_cookie(request, response):
+ self.cookie_profile.set_cookies(
+ response,
+ token,
+ )
+ request.add_response_callback(set_cookie)
+ return token
+
+ def get_csrf_token(self, request):
+ """ Returns the currently active CSRF token by checking the cookies
+ sent with the current request."""
+ bound_cookies = self.cookie_profile.bind(request)
+ token = bound_cookies.get_value()
+ if not token:
+ token = self.new_csrf_token(request)
+ return token
+
+ def check_csrf_token(self, request, supplied_token):
+ """ Returns ``True`` if the ``supplied_token`` is valid."""
+ expected_token = self.get_csrf_token(request)
+ return not strings_differ(
+ bytes_(expected_token), bytes_(supplied_token))
+
+
+def get_csrf_token(request):
+ """ Get the currently active CSRF token for the request passed, generating
+ a new one using ``new_csrf_token(request)`` if one does not exist. This
+ calls the equivalent method in the chosen CSRF protection implementation.
+
+ .. versionadded :: 1.9
+
+ """
+ registry = request.registry
+ csrf = registry.getUtility(ICSRFStoragePolicy)
+ return csrf.get_csrf_token(request)
+
+
+def new_csrf_token(request):
+ """ Generate a new CSRF token for the request passed and persist it in an
+ implementation defined manner. This calls the equivalent method in the
+ chosen CSRF protection implementation.
+
+ .. versionadded :: 1.9
+
+ """
+ registry = request.registry
+ csrf = registry.getUtility(ICSRFStoragePolicy)
+ return csrf.new_csrf_token(request)
+
+
+def check_csrf_token(request,
+ token='csrf_token',
+ header='X-CSRF-Token',
+ raises=True):
+ """ Check the CSRF token returned by the
+ :class:`pyramid.interfaces.ICSRFStoragePolicy` implementation against the
+ value in ``request.POST.get(token)`` (if a POST request) or
+ ``request.headers.get(header)``. If a ``token`` keyword is not supplied to
+ this function, the string ``csrf_token`` will be used to look up the token
+ in ``request.POST``. If a ``header`` keyword is not supplied to this
+ function, the string ``X-CSRF-Token`` will be used to look up the token in
+ ``request.headers``.
+
+ If the value supplied by post or by header cannot be verified by the
+ :class:`pyramid.interfaces.ICSRFStoragePolicy`, and ``raises`` is
+ ``True``, this function will raise an
+ :exc:`pyramid.exceptions.BadCSRFToken` exception. If the values differ
+ and ``raises`` is ``False``, this function will return ``False``. If the
+ CSRF check is successful, this function will return ``True``
+ unconditionally.
+
+ See :ref:`auto_csrf_checking` for information about how to secure your
+ application automatically against CSRF attacks.
+
+ .. versionadded:: 1.4a2
+
+ .. versionchanged:: 1.7a1
+ A CSRF token passed in the query string of the request is no longer
+ considered valid. It must be passed in either the request body or
+ a header.
+
+ .. versionchanged:: 1.9
+ Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf` and updated
+ to use the configured :class:`pyramid.interfaces.ICSRFStoragePolicy` to
+ verify the CSRF token.
+
+ """
+ supplied_token = ""
+ # We first check the headers for a csrf token, as that is significantly
+ # cheaper than checking the POST body
+ if header is not None:
+ supplied_token = request.headers.get(header, "")
+
+ # If this is a POST/PUT/etc request, then we'll check the body to see if it
+ # has a token. We explicitly use request.POST here because CSRF tokens
+ # should never appear in an URL as doing so is a security issue. We also
+ # explicitly check for request.POST here as we do not support sending form
+ # encoded data over anything but a request.POST.
+ if supplied_token == "" and token is not None:
+ supplied_token = request.POST.get(token, "")
+
+ policy = request.registry.getUtility(ICSRFStoragePolicy)
+ if not policy.check_csrf_token(request, text_(supplied_token)):
+ if raises:
+ raise BadCSRFToken('check_csrf_token(): Invalid token')
+ return False
+ return True
+
+
+def check_csrf_origin(request, trusted_origins=None, raises=True):
+ """
+ Check the ``Origin`` of the request to see if it is a cross site request or
+ not.
+
+ If the value supplied by the ``Origin`` or ``Referer`` header isn't one of the
+ trusted origins and ``raises`` is ``True``, this function will raise a
+ :exc:`pyramid.exceptions.BadCSRFOrigin` exception, but if ``raises`` is
+ ``False``, this function will return ``False`` instead. If the CSRF origin
+ checks are successful this function will return ``True`` unconditionally.
+
+ Additional trusted origins may be added by passing a list of domain (and
+ ports if non-standard like ``['example.com', 'dev.example.com:8080']``) in
+ with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None``
+ (the default) this list of additional domains will be pulled from the
+ ``pyramid.csrf_trusted_origins`` setting.
+
+ Note that this function will do nothing if ``request.scheme`` is not
+ ``https``.
+
+ .. versionadded:: 1.7
+
+ .. versionchanged:: 1.9
+ Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf`
+
+ """
+ def _fail(reason):
+ if raises:
+ raise BadCSRFOrigin(reason)
+ else:
+ return False
+
+ if request.scheme == "https":
+ # Suppose user visits http://example.com/
+ # An active network attacker (man-in-the-middle, MITM) sends a
+ # POST form that targets https://example.com/detonate-bomb/ and
+ # submits it via JavaScript.
+ #
+ # The attacker will need to provide a CSRF cookie and token, but
+ # that's no problem for a MITM when we cannot make any assumptions
+ # about what kind of session storage is being used. So the MITM can
+ # circumvent the CSRF protection. This is true for any HTTP connection,
+ # but anyone using HTTPS expects better! For this reason, for
+ # https://example.com/ we need additional protection that treats
+ # http://example.com/ as completely untrusted. Under HTTPS,
+ # Barth et al. found that the Referer header is missing for
+ # same-domain requests in only about 0.2% of cases or less, so
+ # we can use strict Referer checking.
+
+ # Determine the origin of this request
+ origin = request.headers.get("Origin")
+ if origin is None:
+ origin = request.referrer
+
+ # Fail if we were not able to locate an origin at all
+ if not origin:
+ return _fail("Origin checking failed - no Origin or Referer.")
+
+ # Parse our origin so we we can extract the required information from
+ # it.
+ originp = urlparse.urlparse(origin)
+
+ # Ensure that our Referer is also secure.
+ if originp.scheme != "https":
+ return _fail(
+ "Referer checking failed - Referer is insecure while host is "
+ "secure."
+ )
+
+ # Determine which origins we trust, which by default will include the
+ # current origin.
+ if trusted_origins is None:
+ trusted_origins = aslist(
+ request.registry.settings.get(
+ "pyramid.csrf_trusted_origins", [])
+ )
+
+ if request.host_port not in set(["80", "443"]):
+ trusted_origins.append("{0.domain}:{0.host_port}".format(request))
+ else:
+ trusted_origins.append(request.domain)
+
+ # Actually check to see if the request's origin matches any of our
+ # trusted origins.
+ if not any(is_same_domain(originp.netloc, host)
+ for host in trusted_origins):
+ reason = (
+ "Referer checking failed - {0} does not match any trusted "
+ "origins."
+ )
+ return _fail(reason.format(origin))
+
+ return True
diff --git a/src/pyramid/decorator.py b/src/pyramid/decorator.py
new file mode 100644
index 000000000..065a3feed
--- /dev/null
+++ b/src/pyramid/decorator.py
@@ -0,0 +1,45 @@
+from functools import update_wrapper
+
+
+class reify(object):
+ """ Use as a class method decorator. It operates almost exactly like the
+ Python ``@property`` decorator, but it puts the result of the method it
+ decorates into the instance dict after the first call, effectively
+ replacing the function it decorates with an instance variable. It is, in
+ Python parlance, a non-data descriptor. The following is an example and
+ its usage:
+
+ .. doctest::
+
+ >>> from pyramid.decorator import reify
+
+ >>> class Foo(object):
+ ... @reify
+ ... def jammy(self):
+ ... print('jammy called')
+ ... return 1
+
+ >>> f = Foo()
+ >>> v = f.jammy
+ jammy called
+ >>> print(v)
+ 1
+ >>> f.jammy
+ 1
+ >>> # jammy func not called the second time; it replaced itself with 1
+ >>> # Note: reassignment is possible
+ >>> f.jammy = 2
+ >>> f.jammy
+ 2
+ """
+ def __init__(self, wrapped):
+ self.wrapped = wrapped
+ update_wrapper(self, wrapped)
+
+ def __get__(self, inst, objtype=None):
+ if inst is None:
+ return self
+ val = self.wrapped(inst)
+ setattr(inst, self.wrapped.__name__, val)
+ return val
+
diff --git a/src/pyramid/encode.py b/src/pyramid/encode.py
new file mode 100644
index 000000000..73ff14e62
--- /dev/null
+++ b/src/pyramid/encode.py
@@ -0,0 +1,84 @@
+from pyramid.compat import (
+ text_type,
+ binary_type,
+ is_nonstr_iter,
+ url_quote as _url_quote,
+ url_quote_plus as _quote_plus,
+ )
+
+def url_quote(val, safe=''): # bw compat api
+ cls = val.__class__
+ if cls is text_type:
+ val = val.encode('utf-8')
+ elif cls is not binary_type:
+ val = str(val).encode('utf-8')
+ return _url_quote(val, safe=safe)
+
+# bw compat api (dnr)
+def quote_plus(val, safe=''):
+ cls = val.__class__
+ if cls is text_type:
+ val = val.encode('utf-8')
+ elif cls is not binary_type:
+ val = str(val).encode('utf-8')
+ return _quote_plus(val, safe=safe)
+
+def urlencode(query, doseq=True, quote_via=quote_plus):
+ """
+ An alternate implementation of Python's stdlib
+ :func:`urllib.parse.urlencode` function which accepts unicode keys and
+ values within the ``query`` dict/sequence; all Unicode keys and values are
+ first converted to UTF-8 before being used to compose the query string.
+
+ The value of ``query`` must be a sequence of two-tuples
+ representing key/value pairs *or* an object (often a dictionary)
+ with an ``.items()`` method that returns a sequence of two-tuples
+ representing key/value pairs.
+
+ For minimal calling convention backwards compatibility, this
+ version of urlencode accepts *but ignores* a second argument
+ conventionally named ``doseq``. The Python stdlib version behaves
+ differently when ``doseq`` is False and when a sequence is
+ presented as one of the values. This version always behaves in
+ the ``doseq=True`` mode, no matter what the value of the second
+ argument.
+
+ Both the key and value are encoded using the ``quote_via`` function which
+ by default is using a similar algorithm to :func:`urllib.parse.quote_plus`
+ which converts spaces into '+' characters and '/' into '%2F'.
+
+ .. versionchanged:: 1.5
+ In a key/value pair, if the value is ``None`` then it will be
+ dropped from the resulting output.
+
+ .. versionchanged:: 1.9
+ Added the ``quote_via`` argument to allow alternate quoting algorithms
+ to be used.
+
+ """
+ try:
+ # presumed to be a dictionary
+ query = query.items()
+ except AttributeError:
+ pass
+
+ result = ''
+ prefix = ''
+
+ for (k, v) in query:
+ k = quote_via(k)
+
+ if is_nonstr_iter(v):
+ for x in v:
+ x = quote_via(x)
+ result += '%s%s=%s' % (prefix, k, x)
+ prefix = '&'
+ elif v is None:
+ result += '%s%s=' % (prefix, k)
+ else:
+ v = quote_via(v)
+ result += '%s%s=%s' % (prefix, k, v)
+
+ prefix = '&'
+
+ return result
diff --git a/src/pyramid/events.py b/src/pyramid/events.py
new file mode 100644
index 000000000..93fc127a1
--- /dev/null
+++ b/src/pyramid/events.py
@@ -0,0 +1,289 @@
+import venusian
+
+from zope.interface import (
+ implementer,
+ Interface
+ )
+
+from pyramid.interfaces import (
+ IContextFound,
+ INewRequest,
+ INewResponse,
+ IApplicationCreated,
+ IBeforeRender,
+ IBeforeTraversal,
+ )
+
+class subscriber(object):
+ """ Decorator activated via a :term:`scan` which treats the function
+ being decorated as an event subscriber for the set of interfaces passed
+ as ``*ifaces`` and the set of predicate terms passed as ``**predicates``
+ to the decorator constructor.
+
+ For example:
+
+ .. code-block:: python
+
+ from pyramid.events import NewRequest
+ from pyramid.events import subscriber
+
+ @subscriber(NewRequest)
+ def mysubscriber(event):
+ event.request.foo = 1
+
+ More than one event type can be passed as a constructor argument. The
+ decorated subscriber will be called for each event type.
+
+ .. code-block:: python
+
+ from pyramid.events import NewRequest, NewResponse
+ from pyramid.events import subscriber
+
+ @subscriber(NewRequest, NewResponse)
+ def mysubscriber(event):
+ print(event)
+
+ When the ``subscriber`` decorator is used without passing an arguments,
+ the function it decorates is called for every event sent:
+
+ .. code-block:: python
+
+ from pyramid.events import subscriber
+
+ @subscriber()
+ def mysubscriber(event):
+ print(event)
+
+ This method will have no effect until a :term:`scan` is performed
+ against the package or module which contains it, ala:
+
+ .. code-block:: python
+
+ from pyramid.config import Configurator
+ config = Configurator()
+ config.scan('somepackage_containing_subscribers')
+
+ Any ``**predicate`` arguments will be passed along to
+ :meth:`pyramid.config.Configurator.add_subscriber`. See
+ :ref:`subscriber_predicates` for a description of how predicates can
+ narrow the set of circumstances in which a subscriber will be called.
+
+ Two additional keyword arguments which will be passed to the
+ :term:`venusian` ``attach`` function are ``_depth`` and ``_category``.
+
+ ``_depth`` is provided for people who wish to reuse this class from another
+ decorator. The default value is ``0`` and should be specified relative to
+ the ``subscriber`` invocation. 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.
+
+ ``_category`` sets the decorator category name. It can be useful in
+ combination with the ``category`` argument of ``scan`` to control which
+ views should be processed.
+
+ See the :py:func:`venusian.attach` function in Venusian for more
+ information about the ``_depth`` and ``_category`` arguments.
+
+ .. versionchanged:: 1.9.1
+ Added the ``_depth`` and ``_category`` arguments.
+
+ """
+ venusian = venusian # for unit testing
+
+ def __init__(self, *ifaces, **predicates):
+ self.ifaces = ifaces
+ self.predicates = predicates
+ self.depth = predicates.pop('_depth', 0)
+ self.category = predicates.pop('_category', 'pyramid')
+
+ def register(self, scanner, name, wrapped):
+ config = scanner.config
+ for iface in self.ifaces or (Interface,):
+ config.add_subscriber(wrapped, iface, **self.predicates)
+
+ def __call__(self, wrapped):
+ self.venusian.attach(wrapped, self.register, category=self.category,
+ depth=self.depth + 1)
+ return wrapped
+
+@implementer(INewRequest)
+class NewRequest(object):
+ """ An instance of this class is emitted as an :term:`event`
+ whenever :app:`Pyramid` begins to process a new request. The
+ event instance has an attribute, ``request``, which is a
+ :term:`request` object. This event class implements the
+ :class:`pyramid.interfaces.INewRequest` interface."""
+ def __init__(self, request):
+ self.request = request
+
+@implementer(INewResponse)
+class NewResponse(object):
+ """ An instance of this class is emitted as an :term:`event`
+ whenever any :app:`Pyramid` :term:`view` or :term:`exception
+ view` returns a :term:`response`.
+
+ The instance has two attributes:``request``, which is the request
+ which caused the response, and ``response``, which is the response
+ object returned by a view or renderer.
+
+ If the ``response`` was generated by an :term:`exception view`, the
+ request will have an attribute named ``exception``, which is the
+ exception object which caused the exception view to be executed. If the
+ response was generated by a 'normal' view, this attribute of the request
+ will be ``None``.
+
+ This event will not be generated if a response cannot be created due to
+ an exception that is not caught by an exception view (no response is
+ created under this circumstace).
+
+ This class implements the
+ :class:`pyramid.interfaces.INewResponse` interface.
+
+ .. note::
+
+ Postprocessing a response is usually better handled in a WSGI
+ :term:`middleware` component than in subscriber code that is
+ called by a :class:`pyramid.interfaces.INewResponse` event.
+ The :class:`pyramid.interfaces.INewResponse` event exists
+ almost purely for symmetry with the
+ :class:`pyramid.interfaces.INewRequest` event.
+ """
+ def __init__(self, request, response):
+ self.request = request
+ self.response = response
+
+@implementer(IBeforeTraversal)
+class BeforeTraversal(object):
+ """
+ An instance of this class is emitted as an :term:`event` after the
+ :app:`Pyramid` :term:`router` has attempted to find a :term:`route` object
+ but before any traversal or view code is executed. The instance has an
+ attribute, ``request``, which is the request object generated by
+ :app:`Pyramid`.
+
+ Notably, the request object **may** have an attribute named
+ ``matched_route``, which is the matched route if found. If no route
+ matched, this attribute is not available.
+
+ This class implements the :class:`pyramid.interfaces.IBeforeTraversal`
+ interface.
+ """
+
+ def __init__(self, request):
+ self.request = request
+
+@implementer(IContextFound)
+class ContextFound(object):
+ """ An instance of this class is emitted as an :term:`event` after
+ the :app:`Pyramid` :term:`router` finds a :term:`context`
+ object (after it performs traversal) but before any view code is
+ executed. The instance has an attribute, ``request``, which is
+ the request object generated by :app:`Pyramid`.
+
+ Notably, the request object will have an attribute named
+ ``context``, which is the context that will be provided to the
+ view which will eventually be called, as well as other attributes
+ attached by context-finding code.
+
+ This class implements the
+ :class:`pyramid.interfaces.IContextFound` interface.
+
+ .. note::
+
+ As of :app:`Pyramid` 1.0, for backwards compatibility purposes, this
+ event may also be imported as :class:`pyramid.events.AfterTraversal`.
+ """
+ def __init__(self, request):
+ self.request = request
+
+AfterTraversal = ContextFound # b/c as of 1.0
+
+@implementer(IApplicationCreated)
+class ApplicationCreated(object):
+ """ An instance of this class is emitted as an :term:`event` when
+ the :meth:`pyramid.config.Configurator.make_wsgi_app` is
+ called. The instance has an attribute, ``app``, which is an
+ instance of the :term:`router` that will handle WSGI requests.
+ This class implements the
+ :class:`pyramid.interfaces.IApplicationCreated` interface.
+
+ .. note::
+
+ For backwards compatibility purposes, this class can also be imported as
+ :class:`pyramid.events.WSGIApplicationCreatedEvent`. This was the name
+ of the event class before :app:`Pyramid` 1.0.
+ """
+ def __init__(self, app):
+ self.app = app
+ self.object = app
+
+WSGIApplicationCreatedEvent = ApplicationCreated # b/c (as of 1.0)
+
+@implementer(IBeforeRender)
+class BeforeRender(dict):
+ """
+ Subscribers to this event may introspect and modify the set of
+ :term:`renderer globals` before they are passed to a :term:`renderer`.
+ This event object itself has a dictionary-like interface that can be used
+ for this purpose. For example::
+
+ from pyramid.events import subscriber
+ from pyramid.events import BeforeRender
+
+ @subscriber(BeforeRender)
+ def add_global(event):
+ event['mykey'] = 'foo'
+
+ An object of this type is sent as an event just before a :term:`renderer`
+ is invoked.
+
+ If a subscriber adds a key via ``__setitem__`` that already exists in
+ the renderer globals dictionary, it will overwrite the older value there.
+ This can be problematic because event subscribers to the BeforeRender
+ event do not possess any relative ordering. For maximum interoperability
+ with other third-party subscribers, if you write an event subscriber meant
+ to be used as a BeforeRender subscriber, your subscriber code will need to
+ ensure no value already exists in the renderer globals dictionary before
+ setting an overriding value (which can be done using ``.get`` or
+ ``__contains__`` of the event object).
+
+ The dictionary returned from the view is accessible through the
+ :attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender`
+ event.
+
+ Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from
+ your view callable, like so::
+
+ from pyramid.view import view_config
+
+ @view_config(renderer='some_renderer')
+ def myview(request):
+ return {'mykey': 'somevalue', 'mykey2': 'somevalue2'}
+
+ :attr:`rendering_val` can be used to access these values from the
+ :class:`~pyramid.events.BeforeRender` object::
+
+ from pyramid.events import subscriber
+ from pyramid.events import BeforeRender
+
+ @subscriber(BeforeRender)
+ def read_return(event):
+ # {'mykey': 'somevalue'} is returned from the view
+ print(event.rendering_val['mykey'])
+
+ In other words, :attr:`rendering_val` is the (non-system) value returned
+ by a view or passed to ``render*`` as ``value``. This feature is new in
+ Pyramid 1.2.
+
+ For a description of the values present in the renderer globals dictionary,
+ see :ref:`renderer_system_values`.
+
+ .. seealso::
+
+ See also :class:`pyramid.interfaces.IBeforeRender`.
+ """
+ def __init__(self, system, rendering_val=None):
+ dict.__init__(self, system)
+ self.rendering_val = rendering_val
+
diff --git a/src/pyramid/exceptions.py b/src/pyramid/exceptions.py
new file mode 100644
index 000000000..c95922eb0
--- /dev/null
+++ b/src/pyramid/exceptions.py
@@ -0,0 +1,127 @@
+from pyramid.httpexceptions import (
+ HTTPBadRequest,
+ HTTPNotFound,
+ HTTPForbidden,
+ )
+
+NotFound = HTTPNotFound # bw compat
+Forbidden = HTTPForbidden # bw compat
+
+CR = '\n'
+
+
+class BadCSRFOrigin(HTTPBadRequest):
+ """
+ This exception indicates the request has failed cross-site request forgery
+ origin validation.
+ """
+ title = "Bad CSRF Origin"
+ explanation = (
+ "Access is denied. This server can not verify that the origin or "
+ "referrer of your request matches the current site. Either your "
+ "browser supplied the wrong Origin or Referrer or it did not supply "
+ "one at all."
+ )
+
+
+class BadCSRFToken(HTTPBadRequest):
+ """
+ This exception indicates the request has failed cross-site request
+ forgery token validation.
+ """
+ title = 'Bad CSRF Token'
+ explanation = (
+ 'Access is denied. This server can not verify that your cross-site '
+ 'request forgery token belongs to your login session. Either you '
+ 'supplied the wrong cross-site request forgery token or your session '
+ 'no longer exists. This may be due to session timeout or because '
+ 'browser is not supplying the credentials required, as can happen '
+ 'when the browser has cookies turned off.')
+
+class PredicateMismatch(HTTPNotFound):
+ """
+ This exception is raised by multiviews when no view matches
+ all given predicates.
+
+ This exception subclasses the :class:`HTTPNotFound` exception for a
+ specific reason: if it reaches the main exception handler, it should
+ be treated as :class:`HTTPNotFound`` by any exception view
+ registrations. Thus, typically, this exception will not be seen
+ publicly.
+
+ However, this exception will be raised if the predicates of all
+ views configured to handle another exception context cannot be
+ successfully matched. For instance, if a view is configured to
+ handle a context of ``HTTPForbidden`` and the configured with
+ additional predicates, then :class:`PredicateMismatch` will be
+ raised if:
+
+ * An original view callable has raised :class:`HTTPForbidden` (thus
+ invoking an exception view); and
+ * The given request fails to match all predicates for said
+ exception view associated with :class:`HTTPForbidden`.
+
+ The same applies to any type of exception being handled by an
+ exception view.
+ """
+
+class URLDecodeError(UnicodeDecodeError):
+ """
+ This exception is raised when :app:`Pyramid` cannot
+ successfully decode a URL or a URL path segment. This exception
+ behaves just like the Python builtin
+ :exc:`UnicodeDecodeError`. It is a subclass of the builtin
+ :exc:`UnicodeDecodeError` exception only for identity purposes,
+ mostly so an exception view can be registered when a URL cannot be
+ decoded.
+ """
+
+class ConfigurationError(Exception):
+ """ Raised when inappropriate input values are supplied to an API
+ method of a :term:`Configurator`"""
+
+class ConfigurationConflictError(ConfigurationError):
+ """ Raised when a configuration conflict is detected during action
+ processing"""
+
+ def __init__(self, conflicts):
+ self._conflicts = conflicts
+
+ def __str__(self):
+ r = ["Conflicting configuration actions"]
+ items = sorted(self._conflicts.items())
+ for discriminator, infos in items:
+ r.append(" For: %s" % (discriminator, ))
+ for info in infos:
+ for line in str(info).rstrip().split(CR):
+ r.append(" " + line)
+
+ return CR.join(r)
+
+
+class ConfigurationExecutionError(ConfigurationError):
+ """An error occurred during execution of a configuration action
+ """
+
+ def __init__(self, etype, evalue, info):
+ self.etype, self.evalue, self.info = etype, evalue, info
+
+ def __str__(self):
+ return "%s: %s\n in:\n %s" % (self.etype, self.evalue, self.info)
+
+
+class CyclicDependencyError(Exception):
+ """ The exception raised when the Pyramid topological sorter detects a
+ cyclic dependency."""
+ def __init__(self, cycles):
+ self.cycles = cycles
+
+ def __str__(self):
+ L = []
+ cycles = self.cycles
+ for cycle in cycles:
+ dependent = cycle
+ dependees = cycles[cycle]
+ L.append('%r sorts before %r' % (dependent, dependees))
+ msg = 'Implicit ordering cycle:' + '; '.join(L)
+ return msg
diff --git a/src/pyramid/httpexceptions.py b/src/pyramid/httpexceptions.py
new file mode 100644
index 000000000..bef8420b1
--- /dev/null
+++ b/src/pyramid/httpexceptions.py
@@ -0,0 +1,1182 @@
+"""
+HTTP Exceptions
+---------------
+
+This module contains Pyramid HTTP exception classes. Each class relates to a
+single HTTP status code. Each class is a subclass of the
+:class:`~HTTPException`. Each exception class is also a :term:`response`
+object.
+
+Each exception class has a status code according to :rfc:`2068` or :rfc:`7538`:
+codes with 100-300 are not really errors; 400s are client errors,
+and 500s are server errors.
+
+Exception
+ HTTPException
+ HTTPSuccessful
+ * 200 - HTTPOk
+ * 201 - HTTPCreated
+ * 202 - HTTPAccepted
+ * 203 - HTTPNonAuthoritativeInformation
+ * 204 - HTTPNoContent
+ * 205 - HTTPResetContent
+ * 206 - HTTPPartialContent
+ HTTPRedirection
+ * 300 - HTTPMultipleChoices
+ * 301 - HTTPMovedPermanently
+ * 302 - HTTPFound
+ * 303 - HTTPSeeOther
+ * 304 - HTTPNotModified
+ * 305 - HTTPUseProxy
+ * 307 - HTTPTemporaryRedirect
+ * 308 - HTTPPermanentRedirect
+ HTTPError
+ HTTPClientError
+ * 400 - HTTPBadRequest
+ * 401 - HTTPUnauthorized
+ * 402 - HTTPPaymentRequired
+ * 403 - HTTPForbidden
+ * 404 - HTTPNotFound
+ * 405 - HTTPMethodNotAllowed
+ * 406 - HTTPNotAcceptable
+ * 407 - HTTPProxyAuthenticationRequired
+ * 408 - HTTPRequestTimeout
+ * 409 - HTTPConflict
+ * 410 - HTTPGone
+ * 411 - HTTPLengthRequired
+ * 412 - HTTPPreconditionFailed
+ * 413 - HTTPRequestEntityTooLarge
+ * 414 - HTTPRequestURITooLong
+ * 415 - HTTPUnsupportedMediaType
+ * 416 - HTTPRequestRangeNotSatisfiable
+ * 417 - HTTPExpectationFailed
+ * 422 - HTTPUnprocessableEntity
+ * 423 - HTTPLocked
+ * 424 - HTTPFailedDependency
+ * 428 - HTTPPreconditionRequired
+ * 429 - HTTPTooManyRequests
+ * 431 - HTTPRequestHeaderFieldsTooLarge
+ HTTPServerError
+ * 500 - HTTPInternalServerError
+ * 501 - HTTPNotImplemented
+ * 502 - HTTPBadGateway
+ * 503 - HTTPServiceUnavailable
+ * 504 - HTTPGatewayTimeout
+ * 505 - HTTPVersionNotSupported
+ * 507 - HTTPInsufficientStorage
+
+HTTP exceptions are also :term:`response` objects, thus they accept most of
+the same parameters that can be passed to a regular
+:class:`~pyramid.response.Response`. Each HTTP exception also has the
+following attributes:
+
+ ``code``
+ the HTTP status code for the exception
+
+ ``title``
+ remainder of the status line (stuff after the code)
+
+ ``explanation``
+ a plain-text explanation of the error message that is
+ not subject to environment or header substitutions;
+ it is accessible in the template via ${explanation}
+
+ ``detail``
+ a plain-text message customization that is not subject
+ to environment or header substitutions; accessible in
+ the template via ${detail}
+
+ ``body_template``
+ a ``String.template``-format content fragment used for environment
+ and header substitution; the default template includes both
+ the explanation and further detail provided in the
+ message.
+
+Each HTTP exception accepts the following parameters, any others will
+be forwarded to its :class:`~pyramid.response.Response` superclass:
+
+ ``detail``
+ a plain-text override of the default ``detail``
+
+ ``headers``
+ a list of (k,v) header pairs, or a dict, to be added to the
+ response; use the content_type='application/json' kwarg and other
+ similar kwargs to to change properties of the response supported by the
+ :class:`pyramid.response.Response` superclass
+
+ ``comment``
+ a plain-text additional information which is
+ usually stripped/hidden for end-users
+
+ ``body_template``
+ a ``string.Template`` object containing a content fragment in HTML
+ that frames the explanation and further detail
+
+ ``body``
+ a string that will override the ``body_template`` and be used as the
+ body of the response.
+
+Substitution of response headers into template values is always performed.
+Substitution of WSGI environment values is performed if a ``request`` is
+passed to the exception's constructor.
+
+The subclasses of :class:`~_HTTPMove`
+(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`,
+:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy`,
+:class:`~HTTPTemporaryRedirect`, and :class: `~HTTPPermanentRedirect) are
+redirections that require a ``Location`` field. Reflecting this, these
+subclasses have one additional keyword argument: ``location``,
+which indicates the location to which to redirect.
+"""
+import json
+
+from string import Template
+
+from zope.interface import implementer
+
+from webob import html_escape as _html_escape
+from webob.acceptparse import create_accept_header
+
+from pyramid.compat import (
+ class_types,
+ text_type,
+ binary_type,
+ text_,
+ )
+
+from pyramid.interfaces import IExceptionResponse
+from pyramid.response import Response
+
+def _no_escape(value):
+ if value is None:
+ return ''
+ if not isinstance(value, text_type):
+ if hasattr(value, '__unicode__'):
+ value = value.__unicode__()
+ if isinstance(value, binary_type):
+ value = text_(value, 'utf-8')
+ else:
+ value = text_type(value)
+ return value
+
+@implementer(IExceptionResponse)
+class HTTPException(Response, Exception):
+
+ ## You should set in subclasses:
+ # code = 200
+ # title = 'OK'
+ # explanation = 'why this happens'
+ # body_template_obj = Template('response template')
+ #
+ # This class itself uses the error code "520" with the error message/title
+ # of "Unknown Error". This is not an RFC standard, however it is
+ # implemented in practice. Sub-classes should be overriding the default
+ # values and 520 should not be seen in the wild from Pyramid applications.
+ # Due to changes in WebOb, a code of "None" is not valid, and WebOb due to
+ # more strict error checking rejects it now.
+
+ # differences from webob.exc.WSGIHTTPException:
+ #
+ # - doesn't use "strip_tags" (${br} placeholder for <br/>, no other html
+ # in default body template)
+ #
+ # - __call__ never generates a new Response, it always mutates self
+ #
+ # - explicitly sets self.message = detail to prevent whining by Python
+ # 2.6.5+ access of Exception.message
+ #
+ # - its base class of HTTPException is no longer a Python 2.4 compatibility
+ # shim; it's purely a base class that inherits from Exception. This
+ # implies that this class' ``exception`` property always returns
+ # ``self`` (it exists only for bw compat at this point).
+ #
+ # - documentation improvements (Pyramid-specific docstrings where necessary)
+ #
+ code = 520
+ title = 'Unknown Error'
+ explanation = ''
+ body_template_obj = Template('''\
+${explanation}${br}${br}
+${detail}
+${html_comment}
+''')
+
+ plain_template_obj = Template('''\
+${status}
+
+${body}''')
+
+ html_template_obj = Template('''\
+<html>
+ <head>
+ <title>${status}</title>
+ </head>
+ <body>
+ <h1>${status}</h1>
+ ${body}
+ </body>
+</html>''')
+
+ ## Set this to True for responses that should have no request body
+ empty_body = False
+
+ def __init__(self, detail=None, headers=None, comment=None,
+ body_template=None, json_formatter=None, **kw):
+ status = '%s %s' % (self.code, self.title)
+ Response.__init__(self, status=status, **kw)
+ Exception.__init__(self, detail)
+ self.detail = self.message = detail
+ if headers:
+ self.headers.extend(headers)
+ self.comment = comment
+ if body_template is not None:
+ self.body_template = body_template
+ self.body_template_obj = Template(body_template)
+ if json_formatter is not None:
+ self._json_formatter = json_formatter
+
+ if self.empty_body:
+ del self.content_type
+ del self.content_length
+
+ def __str__(self):
+ return str(self.detail) if self.detail else self.explanation
+
+ def _json_formatter(self, status, body, title, environ):
+ return {'message': body,
+ 'code': status,
+ 'title': self.title}
+
+ def prepare(self, environ):
+ if not self.has_body and not self.empty_body:
+ html_comment = ''
+ comment = self.comment or ''
+ accept_value = environ.get('HTTP_ACCEPT', '')
+ accept = create_accept_header(accept_value)
+ # Attempt to match text/html or application/json, if those don't
+ # match, we will fall through to defaulting to text/plain
+ acceptable = accept.acceptable_offers(['text/html', 'application/json'])
+ acceptable = [offer[0] for offer in acceptable] + ['text/plain']
+ match = acceptable[0]
+
+ if match == 'text/html':
+ self.content_type = 'text/html'
+ escape = _html_escape
+ page_template = self.html_template_obj
+ br = '<br/>'
+ if comment:
+ html_comment = '<!-- %s -->' % escape(comment)
+ elif match == 'application/json':
+ self.content_type = 'application/json'
+ self.charset = None
+ escape = _no_escape
+ br = '\n'
+ if comment:
+ html_comment = escape(comment)
+
+ class JsonPageTemplate(object):
+ def __init__(self, excobj):
+ self.excobj = excobj
+
+ def substitute(self, status, body):
+ jsonbody = self.excobj._json_formatter(
+ status=status,
+ body=body, title=self.excobj.title,
+ environ=environ)
+ return json.dumps(jsonbody)
+
+ page_template = JsonPageTemplate(self)
+ else:
+ self.content_type = 'text/plain'
+ escape = _no_escape
+ page_template = self.plain_template_obj
+ br = '\n'
+ if comment:
+ html_comment = escape(comment)
+ args = {
+ 'br': br,
+ 'explanation': escape(self.explanation),
+ 'detail': escape(self.detail or ''),
+ 'comment': escape(comment),
+ 'html_comment': html_comment,
+ }
+ body_tmpl = self.body_template_obj
+ if HTTPException.body_template_obj is not body_tmpl:
+ # Custom template; add headers to args
+ for k, v in environ.items():
+ if (not k.startswith('wsgi.')) and ('.' in k):
+ # omit custom environ variables, stringifying them may
+ # trigger code that should not be executed here; see
+ # https://github.com/Pylons/pyramid/issues/239
+ continue
+ args[k] = escape(v)
+ for k, v in self.headers.items():
+ args[k.lower()] = escape(v)
+ body = body_tmpl.substitute(args)
+ page = page_template.substitute(status=self.status, body=body)
+ if isinstance(page, text_type):
+ page = page.encode(self.charset if self.charset else 'UTF-8')
+ self.app_iter = [page]
+ self.body = page
+
+ @property
+ def wsgi_response(self):
+ # bw compat only
+ return self
+
+ exception = wsgi_response # bw compat only
+
+ def __call__(self, environ, start_response):
+ # differences from webob.exc.WSGIHTTPException
+ #
+ # - does not try to deal with HEAD requests
+ #
+ # - does not manufacture a new response object when generating
+ # the default response
+ #
+ self.prepare(environ)
+ return Response.__call__(self, environ, start_response)
+
+WSGIHTTPException = HTTPException # b/c post 1.5
+
+class HTTPError(HTTPException):
+ """
+ base class for exceptions with status codes in the 400s and 500s
+
+ This is an exception which indicates that an error has occurred,
+ and that any work in progress should not be committed.
+ """
+
+class HTTPRedirection(HTTPException):
+ """
+ base class for exceptions with status codes in the 300s (redirections)
+
+ This is an abstract base class for 3xx redirection. It indicates
+ that further action needs to be taken by the user agent in order
+ to fulfill the request. It does not necessarly signal an error
+ condition.
+ """
+
+class HTTPSuccessful(HTTPException):
+ """
+ Base class for exceptions with status codes in the 200s (successful
+ responses)
+ """
+
+############################################################
+## 2xx success
+############################################################
+
+class HTTPOk(HTTPSuccessful):
+ """
+ subclass of :class:`~HTTPSuccessful`
+
+ Indicates that the request has succeeded.
+
+ code: 200, title: OK
+ """
+ code = 200
+ title = 'OK'
+
+class HTTPCreated(HTTPSuccessful):
+ """
+ subclass of :class:`~HTTPSuccessful`
+
+ This indicates that request has been fulfilled and resulted in a new
+ resource being created.
+
+ code: 201, title: Created
+ """
+ code = 201
+ title = 'Created'
+
+class HTTPAccepted(HTTPSuccessful):
+ """
+ subclass of :class:`~HTTPSuccessful`
+
+ This indicates that the request has been accepted for processing, but the
+ processing has not been completed.
+
+ code: 202, title: Accepted
+ """
+ code = 202
+ title = 'Accepted'
+ explanation = 'The request is accepted for processing.'
+
+class HTTPNonAuthoritativeInformation(HTTPSuccessful):
+ """
+ subclass of :class:`~HTTPSuccessful`
+
+ This indicates that the returned metainformation in the entity-header is
+ not the definitive set as available from the origin server, but is
+ gathered from a local or a third-party copy.
+
+ code: 203, title: Non-Authoritative Information
+ """
+ code = 203
+ title = 'Non-Authoritative Information'
+
+class HTTPNoContent(HTTPSuccessful):
+ """
+ subclass of :class:`~HTTPSuccessful`
+
+ This indicates that the server has fulfilled the request but does
+ not need to return an entity-body, and might want to return updated
+ metainformation.
+
+ code: 204, title: No Content
+ """
+ code = 204
+ title = 'No Content'
+ empty_body = True
+
+class HTTPResetContent(HTTPSuccessful):
+ """
+ subclass of :class:`~HTTPSuccessful`
+
+ This indicates that the server has fulfilled the request and
+ the user agent SHOULD reset the document view which caused the
+ request to be sent.
+
+ code: 205, title: Reset Content
+ """
+ code = 205
+ title = 'Reset Content'
+ empty_body = True
+
+class HTTPPartialContent(HTTPSuccessful):
+ """
+ subclass of :class:`~HTTPSuccessful`
+
+ This indicates that the server has fulfilled the partial GET
+ request for the resource.
+
+ code: 206, title: Partial Content
+ """
+ code = 206
+ title = 'Partial Content'
+
+## FIXME: add 207 Multi-Status (but it's complicated)
+
+############################################################
+## 3xx redirection
+############################################################
+
+class _HTTPMove(HTTPRedirection):
+ """
+ redirections which require a Location field
+
+ Since a 'Location' header is a required attribute of 301, 302, 303,
+ 305 and 307 (but not 304), this base class provides the mechanics to
+ make this easy.
+
+ You must provide a ``location`` keyword argument.
+ """
+ # differences from webob.exc._HTTPMove:
+ #
+ # - ${location} isn't wrapped in an <a> tag in body
+ #
+ # - location keyword arg defaults to ''
+ #
+ # - location isn't prepended with req.path_url when adding it as
+ # a header
+ #
+ # - ``location`` is first keyword (and positional) argument
+ #
+ # - ``add_slash`` argument is no longer accepted: code that passes
+ # add_slash argument to the constructor will receive an exception.
+ explanation = 'The resource has been moved to'
+ body_template_obj = Template('''\
+${explanation} ${location}; you should be redirected automatically.
+${detail}
+${html_comment}''')
+
+ def __init__(self, location='', detail=None, headers=None, comment=None,
+ body_template=None, **kw):
+ if location is None:
+ raise ValueError("HTTP redirects need a location to redirect to.")
+ super(_HTTPMove, self).__init__(
+ detail=detail, headers=headers, comment=comment,
+ body_template=body_template, location=location, **kw)
+
+class HTTPMultipleChoices(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource corresponds to any one
+ of a set of representations, each with its own specific location,
+ and agent-driven negotiation information is being provided so that
+ the user can select a preferred representation and redirect its
+ request to that location.
+
+ code: 300, title: Multiple Choices
+ """
+ code = 300
+ title = 'Multiple Choices'
+
+class HTTPMovedPermanently(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource has been assigned a new
+ permanent URI and any future references to this resource SHOULD use
+ one of the returned URIs.
+
+ code: 301, title: Moved Permanently
+ """
+ code = 301
+ title = 'Moved Permanently'
+
+class HTTPFound(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource resides temporarily under
+ a different URI.
+
+ code: 302, title: Found
+ """
+ code = 302
+ title = 'Found'
+ explanation = 'The resource was found at'
+
+# This one is safe after a POST (the redirected location will be
+# retrieved with GET):
+class HTTPSeeOther(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the response to the request can be found under
+ a different URI and SHOULD be retrieved using a GET method on that
+ resource.
+
+ code: 303, title: See Other
+ """
+ code = 303
+ title = 'See Other'
+
+class HTTPNotModified(HTTPRedirection):
+ """
+ subclass of :class:`~HTTPRedirection`
+
+ This indicates that if the client has performed a conditional GET
+ request and access is allowed, but the document has not been
+ modified, the server SHOULD respond with this status code.
+
+ code: 304, title: Not Modified
+ """
+ # FIXME: this should include a date or etag header
+ code = 304
+ title = 'Not Modified'
+ empty_body = True
+
+class HTTPUseProxy(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource MUST be accessed through
+ the proxy given by the Location field.
+
+ code: 305, title: Use Proxy
+ """
+ # Not a move, but looks a little like one
+ code = 305
+ title = 'Use Proxy'
+ explanation = (
+ 'The resource must be accessed through a proxy located at')
+
+class HTTPTemporaryRedirect(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource resides temporarily
+ under a different URI.
+
+ code: 307, title: Temporary Redirect
+ """
+ code = 307
+ title = 'Temporary Redirect'
+
+class HTTPPermanentRedirect(_HTTPMove):
+ """
+ subclass of :class:`~_HTTPMove`
+
+ This indicates that the requested resource resides permanently
+ under a different URI and that the request method must not be
+ changed.
+
+ code: 308, title: Permanent Redirect
+ """
+ code = 308
+ title = 'Permanent Redirect'
+
+############################################################
+## 4xx client error
+############################################################
+
+class HTTPClientError(HTTPError):
+ """
+ base class for the 400s, where the client is in error
+
+ This is an error condition in which the client is presumed to be
+ in-error. This is an expected problem, and thus is not considered
+ a bug. A server-side traceback is not warranted. Unless specialized,
+ this is a '400 Bad Request'
+ """
+ code = 400
+ title = 'Bad Request'
+
+class HTTPBadRequest(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the body or headers failed validity checks,
+ preventing the server from being able to continue processing.
+
+ code: 400, title: Bad Request
+ """
+ explanation = ('The server could not comply with the request since '
+ 'it is either malformed or otherwise incorrect.')
+
+class HTTPUnauthorized(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the request requires user authentication.
+
+ code: 401, title: Unauthorized
+ """
+ code = 401
+ title = 'Unauthorized'
+ explanation = (
+ 'This server could not verify that you are authorized to '
+ 'access the document you requested. Either you supplied the '
+ 'wrong credentials (e.g., bad password), or your browser '
+ 'does not understand how to supply the credentials required.')
+
+class HTTPPaymentRequired(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ code: 402, title: Payment Required
+ """
+ code = 402
+ title = 'Payment Required'
+ explanation = ('Access was denied for financial reasons.')
+
+class HTTPForbidden(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server understood the request, but is
+ refusing to fulfill it.
+
+ code: 403, title: Forbidden
+
+ Raise this exception within :term:`view` code to immediately return the
+ :term:`forbidden view` to the invoking user. Usually this is a basic
+ ``403`` page, but the forbidden view can be customized as necessary. See
+ :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be
+ the ``context`` of a :term:`Forbidden View`.
+
+ This exception's constructor treats two arguments specially. The first
+ argument, ``detail``, should be a string. The value of this string will
+ be used as the ``message`` attribute of the exception object. The second
+ special keyword argument, ``result`` is usually an instance of
+ :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied`
+ each of which indicates a reason for the forbidden error. However,
+ ``result`` is also permitted to be just a plain boolean ``False`` object
+ or ``None``. The ``result`` value will be used as the ``result``
+ attribute of the exception object. It defaults to ``None``.
+
+ The :term:`Forbidden View` can use the attributes of a Forbidden
+ exception as necessary to provide extended information in an error
+ report shown to a user.
+ """
+ # differences from webob.exc.HTTPForbidden:
+ #
+ # - accepts a ``result`` keyword argument
+ #
+ # - overrides constructor to set ``self.result``
+ #
+ # differences from older ``pyramid.exceptions.Forbidden``:
+ #
+ # - ``result`` must be passed as a keyword argument.
+ #
+ code = 403
+ title = 'Forbidden'
+ explanation = ('Access was denied to this resource.')
+ def __init__(self, detail=None, headers=None, comment=None,
+ body_template=None, result=None, **kw):
+ HTTPClientError.__init__(self, detail=detail, headers=headers,
+ comment=comment, body_template=body_template,
+ **kw)
+ self.result = result
+
+class HTTPNotFound(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server did not find anything matching the
+ Request-URI.
+
+ code: 404, title: Not Found
+
+ Raise this exception within :term:`view` code to immediately
+ return the :term:`Not Found View` to the invoking user. Usually
+ this is a basic ``404`` page, but the Not Found View can be
+ customized as necessary. See :ref:`changing_the_notfound_view`.
+
+ This exception's constructor accepts a ``detail`` argument
+ (the first argument), which should be a string. The value of this
+ string will be available as the ``message`` attribute of this exception,
+ for availability to the :term:`Not Found View`.
+ """
+ code = 404
+ title = 'Not Found'
+ explanation = ('The resource could not be found.')
+
+class HTTPMethodNotAllowed(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the method specified in the Request-Line is
+ not allowed for the resource identified by the Request-URI.
+
+ code: 405, title: Method Not Allowed
+ """
+ # differences from webob.exc.HTTPMethodNotAllowed:
+ #
+ # - body_template_obj uses ${br} instead of <br />
+ code = 405
+ title = 'Method Not Allowed'
+ body_template_obj = Template('''\
+The method ${REQUEST_METHOD} is not allowed for this resource. ${br}${br}
+${detail}''')
+
+class HTTPNotAcceptable(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates the resource identified by the request is only
+ capable of generating response entities which have content
+ characteristics not acceptable according to the accept headers
+ sent in the request.
+
+ code: 406, title: Not Acceptable
+ """
+ # differences from webob.exc.HTTPNotAcceptable:
+ #
+ # - "template" attribute left off (useless, bug in webob?)
+ code = 406
+ title = 'Not Acceptable'
+
+class HTTPProxyAuthenticationRequired(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This is similar to 401, but indicates that the client must first
+ authenticate itself with the proxy.
+
+ code: 407, title: Proxy Authentication Required
+ """
+ code = 407
+ title = 'Proxy Authentication Required'
+ explanation = ('Authentication with a local proxy is needed.')
+
+class HTTPRequestTimeout(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the client did not produce a request within
+ the time that the server was prepared to wait.
+
+ code: 408, title: Request Timeout
+ """
+ code = 408
+ title = 'Request Timeout'
+ explanation = ('The server has waited too long for the request to '
+ 'be sent by the client.')
+
+class HTTPConflict(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the request could not be completed due to a
+ conflict with the current state of the resource.
+
+ code: 409, title: Conflict
+ """
+ code = 409
+ title = 'Conflict'
+ explanation = ('There was a conflict when trying to complete '
+ 'your request.')
+
+class HTTPGone(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the requested resource is no longer available
+ at the server and no forwarding address is known.
+
+ code: 410, title: Gone
+ """
+ code = 410
+ title = 'Gone'
+ explanation = ('This resource is no longer available. No forwarding '
+ 'address is given.')
+
+class HTTPLengthRequired(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server refuses to accept the request
+ without a defined Content-Length.
+
+ code: 411, title: Length Required
+ """
+ code = 411
+ title = 'Length Required'
+ explanation = ('Content-Length header required.')
+
+class HTTPPreconditionFailed(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the precondition given in one or more of the
+ request-header fields evaluated to false when it was tested on the
+ server.
+
+ code: 412, title: Precondition Failed
+ """
+ code = 412
+ title = 'Precondition Failed'
+ explanation = ('Request precondition failed.')
+
+class HTTPRequestEntityTooLarge(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is refusing to process a request
+ because the request entity is larger than the server is willing or
+ able to process.
+
+ code: 413, title: Request Entity Too Large
+ """
+ code = 413
+ title = 'Request Entity Too Large'
+ explanation = ('The body of your request was too large for this server.')
+
+class HTTPRequestURITooLong(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is refusing to service the request
+ because the Request-URI is longer than the server is willing to
+ interpret.
+
+ code: 414, title: Request-URI Too Long
+ """
+ code = 414
+ title = 'Request-URI Too Long'
+ explanation = ('The request URI was too long for this server.')
+
+class HTTPUnsupportedMediaType(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is refusing to service the request
+ because the entity of the request is in a format not supported by
+ the requested resource for the requested method.
+
+ code: 415, title: Unsupported Media Type
+ """
+ # differences from webob.exc.HTTPUnsupportedMediaType:
+ #
+ # - "template_obj" attribute left off (useless, bug in webob?)
+ code = 415
+ title = 'Unsupported Media Type'
+
+class HTTPRequestRangeNotSatisfiable(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ The server SHOULD return a response with this status code if a
+ request included a Range request-header field, and none of the
+ range-specifier values in this field overlap the current extent
+ of the selected resource, and the request did not include an
+ If-Range request-header field.
+
+ code: 416, title: Request Range Not Satisfiable
+ """
+ code = 416
+ title = 'Request Range Not Satisfiable'
+ explanation = ('The Range requested is not available.')
+
+class HTTPExpectationFailed(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indidcates that the expectation given in an Expect
+ request-header field could not be met by this server.
+
+ code: 417, title: Expectation Failed
+ """
+ code = 417
+ title = 'Expectation Failed'
+ explanation = ('Expectation failed.')
+
+class HTTPUnprocessableEntity(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is unable to process the contained
+ instructions.
+
+ May be used to notify the client that their JSON/XML is well formed, but
+ not correct for the current request.
+
+ See RFC4918 section 11 for more information.
+
+ code: 422, title: Unprocessable Entity
+ """
+ ## Note: from WebDAV
+ code = 422
+ title = 'Unprocessable Entity'
+ explanation = 'Unable to process the contained instructions'
+
+class HTTPLocked(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the resource is locked.
+
+ code: 423, title: Locked
+ """
+ ## Note: from WebDAV
+ code = 423
+ title = 'Locked'
+ explanation = ('The resource is locked')
+
+class HTTPFailedDependency(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the method could not be performed because the
+ requested action depended on another action and that action failed.
+
+ code: 424, title: Failed Dependency
+ """
+ ## Note: from WebDAV
+ code = 424
+ title = 'Failed Dependency'
+ explanation = (
+ 'The method could not be performed because the requested '
+ 'action dependended on another action and that action failed')
+
+class HTTPPreconditionRequired(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the origin server requires the
+ request to be conditional.
+
+ Its typical use is to avoid the "lost update" problem, where a client
+ GETs a resource's state, modifies it, and PUTs it back to the server,
+ when meanwhile a third party has modified the state on the server,
+ leading to a conflict. By requiring requests to be conditional, the
+ server can assure that clients are working with the correct copies.
+
+ RFC 6585.3
+
+ code: 428, title: Precondition Required
+ """
+ code = 428
+ title = 'Precondition Required'
+ explanation = (
+ 'The origin server requires the request to be conditional.')
+
+class HTTPTooManyRequests(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the user has sent too many
+ requests in a given amount of time ("rate limiting").
+
+ RFC 6585.4
+
+ code: 429, title: Too Many Requests
+ """
+ code = 429
+ title = 'Too Many Requests'
+ explanation = (
+ 'The action could not be performed because there were too '
+ 'many requests by the client.')
+
+class HTTPRequestHeaderFieldsTooLarge(HTTPClientError):
+ """
+ subclass of :class:`~HTTPClientError`
+
+ This indicates that the server is unwilling to process
+ the request because its header fields are too large. The request MAY
+ be resubmitted after reducing the size of the request header fields.
+
+ RFC 6585.5
+
+ code: 431, title: Request Header Fields Too Large
+ """
+ code = 431
+ title = 'Request Header Fields Too Large'
+ explanation = (
+ 'The requests header fields were too large.')
+
+############################################################
+## 5xx Server Error
+############################################################
+# Response status codes beginning with the digit "5" indicate cases in
+# which the server is aware that it has erred or is incapable of
+# performing the request. Except when responding to a HEAD request, the
+# server SHOULD include an entity containing an explanation of the error
+# situation, and whether it is a temporary or permanent condition. User
+# agents SHOULD display any included entity to the user. These response
+# codes are applicable to any request method.
+
+class HTTPServerError(HTTPError):
+ """
+ base class for the 500s, where the server is in-error
+
+ This is an error condition in which the server is presumed to be
+ in-error. Unless specialized, this is a '500 Internal Server Error'.
+ """
+ code = 500
+ title = 'Internal Server Error'
+
+class HTTPInternalServerError(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server encountered an unexpected condition
+ which prevented it from fulfilling the request.
+
+ code: 500, title: Internal Server Error
+ """
+ explanation = (
+ 'The server has either erred or is incapable of performing '
+ 'the requested operation.')
+
+class HTTPNotImplemented(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server does not support the functionality
+ required to fulfill the request.
+
+ code: 501, title: Not Implemented
+ """
+ # differences from webob.exc.HTTPNotAcceptable:
+ #
+ # - "template" attr left off (useless, bug in webob?)
+ code = 501
+ title = 'Not Implemented'
+
+class HTTPBadGateway(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server, while acting as a gateway or proxy,
+ received an invalid response from the upstream server it accessed
+ in attempting to fulfill the request.
+
+ code: 502, title: Bad Gateway
+ """
+ code = 502
+ title = 'Bad Gateway'
+ explanation = ('Bad gateway.')
+
+class HTTPServiceUnavailable(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server is currently unable to handle the
+ request due to a temporary overloading or maintenance of the server.
+
+ code: 503, title: Service Unavailable
+ """
+ code = 503
+ title = 'Service Unavailable'
+ explanation = ('The server is currently unavailable. '
+ 'Please try again at a later time.')
+
+class HTTPGatewayTimeout(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server, while acting as a gateway or proxy,
+ did not receive a timely response from the upstream server specified
+ by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server
+ (e.g. DNS) it needed to access in attempting to complete the request.
+
+ code: 504, title: Gateway Timeout
+ """
+ code = 504
+ title = 'Gateway Timeout'
+ explanation = ('The gateway has timed out.')
+
+class HTTPVersionNotSupported(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server does not support, or refuses to
+ support, the HTTP protocol version that was used in the request
+ message.
+
+ code: 505, title: HTTP Version Not Supported
+ """
+ code = 505
+ title = 'HTTP Version Not Supported'
+ explanation = ('The HTTP version is not supported.')
+
+class HTTPInsufficientStorage(HTTPServerError):
+ """
+ subclass of :class:`~HTTPServerError`
+
+ This indicates that the server does not have enough space to save
+ the resource.
+
+ code: 507, title: Insufficient Storage
+ """
+ code = 507
+ title = 'Insufficient Storage'
+ explanation = ('There was not enough space to save the resource')
+
+def exception_response(status_code, **kw):
+ """Creates an HTTP exception based on a status code. Example::
+
+ raise exception_response(404) # raises an HTTPNotFound exception.
+
+ The values passed as ``kw`` are provided to the exception's constructor.
+ """
+ exc = status_map[status_code](**kw)
+ return exc
+
+def default_exceptionresponse_view(context, request):
+ if not isinstance(context, Exception):
+ # backwards compat for an exception response view registered via
+ # config.set_notfound_view or config.set_forbidden_view
+ # instead of as a proper exception view
+ context = request.exception or context
+ return context # assumed to be an IResponse
+
+status_map = {}
+code = None
+for name, value in list(globals().items()):
+ if (
+ isinstance(value, class_types) and
+ issubclass(value, HTTPException) and
+ value not in {HTTPClientError, HTTPServerError} and
+ not name.startswith('_')
+ ):
+ code = getattr(value, 'code', None)
+ if code:
+ status_map[code] = value
+del name, value, code
diff --git a/src/pyramid/i18n.py b/src/pyramid/i18n.py
new file mode 100644
index 000000000..1d11adfe3
--- /dev/null
+++ b/src/pyramid/i18n.py
@@ -0,0 +1,397 @@
+import gettext
+import os
+
+from translationstring import (
+ Translator,
+ Pluralizer,
+ TranslationString, # API
+ TranslationStringFactory, # API
+ )
+
+from pyramid.compat import PY2
+from pyramid.decorator import reify
+
+from pyramid.interfaces import (
+ ILocalizer,
+ ITranslationDirectories,
+ ILocaleNegotiator,
+ )
+
+from pyramid.threadlocal import get_current_registry
+
+TranslationString = TranslationString # PyFlakes
+TranslationStringFactory = TranslationStringFactory # PyFlakes
+
+DEFAULT_PLURAL = lambda n: int(n != 1)
+
+class Localizer(object):
+ """
+ An object providing translation and pluralizations related to
+ the current request's locale name. A
+ :class:`pyramid.i18n.Localizer` object is created using the
+ :func:`pyramid.i18n.get_localizer` function.
+ """
+ def __init__(self, locale_name, translations):
+ self.locale_name = locale_name
+ self.translations = translations
+ self.pluralizer = None
+ self.translator = None
+
+ def translate(self, tstring, domain=None, mapping=None):
+ """
+ Translate a :term:`translation string` to the current language
+ and interpolate any *replacement markers* in the result. The
+ ``translate`` method accepts three arguments: ``tstring``
+ (required), ``domain`` (optional) and ``mapping`` (optional).
+ When called, it will translate the ``tstring`` translation
+ string to a ``unicode`` object using the current locale. If
+ the current locale could not be determined, the result of
+ interpolation of the default value is returned. The optional
+ ``domain`` argument can be used to specify or override the
+ domain of the ``tstring`` (useful when ``tstring`` is a normal
+ string rather than a translation string). The optional
+ ``mapping`` argument can specify or override the ``tstring``
+ interpolation mapping, useful when the ``tstring`` argument is
+ a simple string instead of a translation string.
+
+ Example::
+
+ from pyramid.18n import TranslationString
+ ts = TranslationString('Add ${item}', domain='mypackage',
+ mapping={'item':'Item'})
+ translated = localizer.translate(ts)
+
+ Example::
+
+ translated = localizer.translate('Add ${item}', domain='mypackage',
+ mapping={'item':'Item'})
+
+ """
+ if self.translator is None:
+ self.translator = Translator(self.translations)
+ return self.translator(tstring, domain=domain, mapping=mapping)
+
+ def pluralize(self, singular, plural, n, domain=None, mapping=None):
+ """
+ Return a Unicode string translation by using two
+ :term:`message identifier` objects as a singular/plural pair
+ and an ``n`` value representing the number that appears in the
+ message using gettext plural forms support. The ``singular``
+ and ``plural`` objects should be unicode strings. There is no
+ reason to use translation string objects as arguments as all
+ metadata is ignored.
+
+ ``n`` represents the number of elements. ``domain`` is the
+ translation domain to use to do the pluralization, and ``mapping``
+ is the interpolation mapping that should be used on the result. If
+ the ``domain`` is not supplied, a default domain is used (usually
+ ``messages``).
+
+ Example::
+
+ num = 1
+ translated = localizer.pluralize('Add ${num} item',
+ 'Add ${num} items',
+ num,
+ mapping={'num':num})
+
+ If using the gettext plural support, which is required for
+ languages that have pluralisation rules other than n != 1, the
+ ``singular`` argument must be the message_id defined in the
+ translation file. The plural argument is not used in this case.
+
+ Example::
+
+ num = 1
+ translated = localizer.pluralize('item_plural',
+ '',
+ num,
+ mapping={'num':num})
+
+
+ """
+ if self.pluralizer is None:
+ self.pluralizer = Pluralizer(self.translations)
+ return self.pluralizer(singular, plural, n, domain=domain,
+ mapping=mapping)
+
+
+def default_locale_negotiator(request):
+ """ The default :term:`locale negotiator`. Returns a locale name
+ or ``None``.
+
+ - First, the negotiator looks for the ``_LOCALE_`` attribute of
+ the request object (possibly set by a view or a listener for an
+ :term:`event`). If the attribute exists and it is not ``None``,
+ its value will be used.
+
+ - Then it looks for the ``request.params['_LOCALE_']`` value.
+
+ - Then it looks for the ``request.cookies['_LOCALE_']`` value.
+
+ - Finally, the negotiator returns ``None`` if the locale could not
+ be determined via any of the previous checks (when a locale
+ negotiator returns ``None``, it signifies that the
+ :term:`default locale name` should be used.)
+ """
+ name = '_LOCALE_'
+ locale_name = getattr(request, name, None)
+ if locale_name is None:
+ locale_name = request.params.get(name)
+ if locale_name is None:
+ locale_name = request.cookies.get(name)
+ return locale_name
+
+def negotiate_locale_name(request):
+ """ Negotiate and return the :term:`locale name` associated with
+ the current request."""
+ try:
+ registry = request.registry
+ except AttributeError:
+ registry = get_current_registry()
+ negotiator = registry.queryUtility(ILocaleNegotiator,
+ default=default_locale_negotiator)
+ locale_name = negotiator(request)
+
+ if locale_name is None:
+ settings = registry.settings or {}
+ locale_name = settings.get('default_locale_name', 'en')
+
+ return locale_name
+
+def get_locale_name(request):
+ """
+ .. deprecated:: 1.5
+ Use :attr:`pyramid.request.Request.locale_name` directly instead.
+ Return the :term:`locale name` associated with the current request.
+ """
+ return request.locale_name
+
+def make_localizer(current_locale_name, translation_directories):
+ """ Create a :class:`pyramid.i18n.Localizer` object
+ corresponding to the provided locale name from the
+ translations found in the list of translation directories."""
+ translations = Translations()
+ translations._catalog = {}
+
+ locales_to_try = []
+ if '_' in current_locale_name:
+ locales_to_try = [current_locale_name.split('_')[0]]
+ locales_to_try.append(current_locale_name)
+
+ # intent: order locales left to right in least specific to most specific,
+ # e.g. ['de', 'de_DE']. This services the intent of creating a
+ # translations object that returns a "more specific" translation for a
+ # region, but will fall back to a "less specific" translation for the
+ # locale if necessary. Ordering from least specific to most specific
+ # allows us to call translations.add in the below loop to get this
+ # behavior.
+
+ for tdir in translation_directories:
+ locale_dirs = []
+ for lname in locales_to_try:
+ ldir = os.path.realpath(os.path.join(tdir, lname))
+ if os.path.isdir(ldir):
+ locale_dirs.append(ldir)
+
+ for locale_dir in locale_dirs:
+ messages_dir = os.path.join(locale_dir, 'LC_MESSAGES')
+ if not os.path.isdir(os.path.realpath(messages_dir)):
+ continue
+ for mofile in os.listdir(messages_dir):
+ mopath = os.path.realpath(os.path.join(messages_dir,
+ mofile))
+ if mofile.endswith('.mo') and os.path.isfile(mopath):
+ with open(mopath, 'rb') as mofp:
+ domain = mofile[:-3]
+ dtrans = Translations(mofp, domain)
+ translations.add(dtrans)
+
+ return Localizer(locale_name=current_locale_name,
+ translations=translations)
+
+def get_localizer(request):
+ """
+ .. deprecated:: 1.5
+ Use the :attr:`pyramid.request.Request.localizer` attribute directly
+ instead. Retrieve a :class:`pyramid.i18n.Localizer` object
+ corresponding to the current request's locale name.
+ """
+ return request.localizer
+
+class Translations(gettext.GNUTranslations, object):
+ """An extended translation catalog class (ripped off from Babel) """
+
+ DEFAULT_DOMAIN = 'messages'
+
+ def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN):
+ """Initialize the translations catalog.
+
+ :param fileobj: the file-like object the translation should be read
+ from
+ """
+ # germanic plural by default; self.plural will be overwritten by
+ # GNUTranslations._parse (called as a side effect if fileobj is
+ # passed to GNUTranslations.__init__) with a "real" self.plural for
+ # this domain; see https://github.com/Pylons/pyramid/issues/235
+ # It is only overridden the first time a new message file is found
+ # for a given domain, so all message files must have matching plural
+ # rules if they are in the same domain. We keep track of if we have
+ # overridden so we can special case the default domain, which is always
+ # instantiated before a message file is read.
+ # See also https://github.com/Pylons/pyramid/pull/2102
+ self.plural = DEFAULT_PLURAL
+ gettext.GNUTranslations.__init__(self, fp=fileobj)
+ self.files = list(filter(None, [getattr(fileobj, 'name', None)]))
+ self.domain = domain
+ self._domains = {}
+
+ @classmethod
+ def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN):
+ """Load translations from the given directory.
+
+ :param dirname: the directory containing the ``MO`` files
+ :param locales: the list of locales in order of preference (items in
+ this list can be either `Locale` objects or locale
+ strings)
+ :param domain: the message domain
+ :return: the loaded catalog, or a ``NullTranslations`` instance if no
+ matching translations were found
+ :rtype: `Translations`
+ """
+ if locales is not None:
+ if not isinstance(locales, (list, tuple)):
+ locales = [locales]
+ locales = [str(l) for l in locales]
+ if not domain:
+ domain = cls.DEFAULT_DOMAIN
+ filename = gettext.find(domain, dirname, locales)
+ if not filename:
+ return gettext.NullTranslations()
+ with open(filename, 'rb') as fp:
+ return cls(fileobj=fp, domain=domain)
+
+ def __repr__(self):
+ return '<%s: "%s">' % (type(self).__name__,
+ self._info.get('project-id-version'))
+
+ def add(self, translations, merge=True):
+ """Add the given translations to the catalog.
+
+ If the domain of the translations is different than that of the
+ current catalog, they are added as a catalog that is only accessible
+ by the various ``d*gettext`` functions.
+
+ :param translations: the `Translations` instance with the messages to
+ add
+ :param merge: whether translations for message domains that have
+ already been added should be merged with the existing
+ translations
+ :return: the `Translations` instance (``self``) so that `merge` calls
+ can be easily chained
+ :rtype: `Translations`
+ """
+ domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
+ if domain == self.DEFAULT_DOMAIN and self.plural is DEFAULT_PLURAL:
+ self.plural = translations.plural
+
+ if merge and domain == self.domain:
+ return self.merge(translations)
+
+ existing = self._domains.get(domain)
+ if merge and existing is not None:
+ existing.merge(translations)
+ else:
+ translations.add_fallback(self)
+ self._domains[domain] = translations
+
+ return self
+
+ def merge(self, translations):
+ """Merge the given translations into the catalog.
+
+ Message translations in the specified catalog override any messages
+ with the same identifier in the existing catalog.
+
+ :param translations: the `Translations` instance with the messages to
+ merge
+ :return: the `Translations` instance (``self``) so that `merge` calls
+ can be easily chained
+ :rtype: `Translations`
+ """
+ if isinstance(translations, gettext.GNUTranslations):
+ self._catalog.update(translations._catalog)
+ if isinstance(translations, Translations):
+ self.files.extend(translations.files)
+
+ return self
+
+ def dgettext(self, domain, message):
+ """Like ``gettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).gettext(message)
+
+ def ldgettext(self, domain, message):
+ """Like ``lgettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).lgettext(message)
+
+ def dugettext(self, domain, message):
+ """Like ``ugettext()``, but look the message up in the specified
+ domain.
+ """
+ if PY2:
+ return self._domains.get(domain, self).ugettext(message)
+ else:
+ return self._domains.get(domain, self).gettext(message)
+
+ def dngettext(self, domain, singular, plural, num):
+ """Like ``ngettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).ngettext(singular, plural, num)
+
+ def ldngettext(self, domain, singular, plural, num):
+ """Like ``lngettext()``, but look the message up in the specified
+ domain.
+ """
+ return self._domains.get(domain, self).lngettext(singular, plural, num)
+
+ def dungettext(self, domain, singular, plural, num):
+ """Like ``ungettext()`` but look the message up in the specified
+ domain.
+ """
+ if PY2:
+ return self._domains.get(domain, self).ungettext(
+ singular, plural, num)
+ else:
+ return self._domains.get(domain, self).ngettext(
+ singular, plural, num)
+
+class LocalizerRequestMixin(object):
+ @reify
+ def localizer(self):
+ """ Convenience property to return a localizer """
+ registry = self.registry
+
+ current_locale_name = self.locale_name
+ localizer = registry.queryUtility(ILocalizer, name=current_locale_name)
+
+ if localizer is None:
+ # no localizer utility registered yet
+ tdirs = registry.queryUtility(ITranslationDirectories, default=[])
+ localizer = make_localizer(current_locale_name, tdirs)
+
+ registry.registerUtility(localizer, ILocalizer,
+ name=current_locale_name)
+
+ return localizer
+
+ @reify
+ def locale_name(self):
+ locale_name = negotiate_locale_name(self)
+ return locale_name
+
+
diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py
new file mode 100644
index 000000000..4df5593f8
--- /dev/null
+++ b/src/pyramid/interfaces.py
@@ -0,0 +1,1354 @@
+from zope.deprecation import deprecated
+
+from zope.interface import (
+ Attribute,
+ Interface,
+ )
+
+from pyramid.compat import PY2
+
+# public API interfaces
+
+class IContextFound(Interface):
+ """ An event type that is emitted after :app:`Pyramid` finds a
+ :term:`context` object but before it calls any view code. See the
+ documentation attached to :class:`pyramid.events.ContextFound`
+ for more information.
+
+ .. note::
+
+ For backwards compatibility with versions of
+ :app:`Pyramid` before 1.0, this event interface can also be
+ imported as :class:`pyramid.interfaces.IAfterTraversal`.
+ """
+ request = Attribute('The request object')
+
+IAfterTraversal = IContextFound
+
+class IBeforeTraversal(Interface):
+ """
+ An event type that is emitted after :app:`Pyramid` attempted to find a
+ route but before it calls any traversal or view code. See the documentation
+ attached to :class:`pyramid.events.Routefound` for more information.
+ """
+ request = Attribute('The request object')
+
+class INewRequest(Interface):
+ """ An event type that is emitted whenever :app:`Pyramid`
+ begins to process a new request. See the documentation attached
+ to :class:`pyramid.events.NewRequest` for more information."""
+ request = Attribute('The request object')
+
+class INewResponse(Interface):
+ """ An event type that is emitted whenever any :app:`Pyramid`
+ view returns a response. See the
+ documentation attached to :class:`pyramid.events.NewResponse`
+ for more information."""
+ request = Attribute('The request object')
+ response = Attribute('The response object')
+
+class IApplicationCreated(Interface):
+ """ Event issued when the
+ :meth:`pyramid.config.Configurator.make_wsgi_app` method
+ is called. See the documentation attached to
+ :class:`pyramid.events.ApplicationCreated` for more
+ information.
+
+ .. note::
+
+ For backwards compatibility with :app:`Pyramid`
+ versions before 1.0, this interface can also be imported as
+ :class:`pyramid.interfaces.IWSGIApplicationCreatedEvent`.
+ """
+ app = Attribute("Created application")
+
+IWSGIApplicationCreatedEvent = IApplicationCreated # b /c
+
+class IResponse(Interface):
+ """ Represents a WSGI response using the WebOb response interface.
+ Some attribute and method documentation of this interface references
+ :rfc:`2616`.
+
+ This interface is most famously implemented by
+ :class:`pyramid.response.Response` and the HTTP exception classes in
+ :mod:`pyramid.httpexceptions`."""
+
+ RequestClass = Attribute(
+ """ Alias for :class:`pyramid.request.Request` """)
+
+ def __call__(environ, start_response):
+ """ :term:`WSGI` call interface, should call the start_response
+ callback and should return an iterable"""
+
+ accept_ranges = Attribute(
+ """Gets and sets and deletes the Accept-Ranges header. For more
+ information on Accept-Ranges see RFC 2616, section 14.5""")
+
+ age = Attribute(
+ """Gets and sets and deletes the Age header. Converts using int.
+ For more information on Age see RFC 2616, section 14.6.""")
+
+ allow = Attribute(
+ """Gets and sets and deletes the Allow header. Converts using
+ list. For more information on Allow see RFC 2616, Section 14.7.""")
+
+ app_iter = Attribute(
+ """Returns the app_iter of the response.
+
+ If body was set, this will create an app_iter from that body
+ (a single-item list)""")
+
+ def app_iter_range(start, stop):
+ """ Return a new app_iter built from the response app_iter that
+ serves up only the given start:stop range. """
+
+ body = Attribute(
+ """The body of the response, as a str. This will read in the entire
+ app_iter if necessary.""")
+
+ body_file = Attribute(
+ """A file-like object that can be used to write to the body. If you
+ passed in a list app_iter, that app_iter will be modified by writes.""")
+
+ cache_control = Attribute(
+ """Get/set/modify the Cache-Control header (RFC 2616 section 14.9)""")
+
+ cache_expires = Attribute(
+ """ Get/set the Cache-Control and Expires headers. This sets the
+ response to expire in the number of seconds passed when set. """)
+
+ charset = Attribute(
+ """Get/set the charset (in the Content-Type)""")
+
+ def conditional_response_app(environ, start_response):
+ """ Like the normal __call__ interface, but checks conditional
+ headers:
+
+ - If-Modified-Since (304 Not Modified; only on GET, HEAD)
+
+ - If-None-Match (304 Not Modified; only on GET, HEAD)
+
+ - Range (406 Partial Content; only on GET, HEAD)"""
+
+ content_disposition = Attribute(
+ """Gets and sets and deletes the Content-Disposition header.
+ For more information on Content-Disposition see RFC 2616 section
+ 19.5.1.""")
+
+ content_encoding = Attribute(
+ """Gets and sets and deletes the Content-Encoding header. For more
+ information about Content-Encoding see RFC 2616 section 14.11.""")
+
+ content_language = Attribute(
+ """Gets and sets and deletes the Content-Language header. Converts
+ using list. For more information about Content-Language see RFC 2616
+ section 14.12.""")
+
+ content_length = Attribute(
+ """Gets and sets and deletes the Content-Length header. For more
+ information on Content-Length see RFC 2616 section 14.17.
+ Converts using int. """)
+
+ content_location = Attribute(
+ """Gets and sets and deletes the Content-Location header. For more
+ information on Content-Location see RFC 2616 section 14.14.""")
+
+ content_md5 = Attribute(
+ """Gets and sets and deletes the Content-MD5 header. For more
+ information on Content-MD5 see RFC 2616 section 14.14.""")
+
+ content_range = Attribute(
+ """Gets and sets and deletes the Content-Range header. For more
+ information on Content-Range see section 14.16. Converts using
+ ContentRange object.""")
+
+ content_type = Attribute(
+ """Get/set the Content-Type header (or None), without the charset
+ or any parameters. If you include parameters (or ; at all) when
+ setting the content_type, any existing parameters will be deleted;
+ otherwise they will be preserved.""")
+
+ content_type_params = Attribute(
+ """A dictionary of all the parameters in the content type. This is
+ not a view, set to change, modifications of the dict would not
+ be applied otherwise.""")
+
+ def copy():
+ """ Makes a copy of the response and returns the copy. """
+
+ date = Attribute(
+ """Gets and sets and deletes the Date header. For more information on
+ Date see RFC 2616 section 14.18. Converts using HTTP date.""")
+
+ def delete_cookie(name, path='/', domain=None):
+ """ Delete a cookie from the client. Note that path and domain must
+ match how the cookie was originally set. This sets the cookie to the
+ empty string, and max_age=0 so that it should expire immediately. """
+
+ def encode_content(encoding='gzip', lazy=False):
+ """ Encode the content with the given encoding (only gzip and
+ identity are supported)."""
+
+ environ = Attribute(
+ """Get/set the request environ associated with this response,
+ if any.""")
+
+ etag = Attribute(
+ """ Gets and sets and deletes the ETag header. For more information
+ on ETag see RFC 2616 section 14.19. Converts using Entity tag.""")
+
+ expires = Attribute(
+ """ Gets and sets and deletes the Expires header. For more
+ information on Expires see RFC 2616 section 14.21. Converts using
+ HTTP date.""")
+
+ headerlist = Attribute(
+ """ The list of response headers. """)
+
+ headers = Attribute(
+ """ The headers in a dictionary-like object """)
+
+ last_modified = Attribute(
+ """ Gets and sets and deletes the Last-Modified header. For more
+ information on Last-Modified see RFC 2616 section 14.29. Converts
+ using HTTP date.""")
+
+ location = Attribute(
+ """ Gets and sets and deletes the Location header. For more
+ information on Location see RFC 2616 section 14.30.""")
+
+ def md5_etag(body=None, set_content_md5=False):
+ """ Generate an etag for the response object using an MD5 hash of the
+ body (the body parameter, or self.body if not given). Sets self.etag.
+ If set_content_md5 is True sets self.content_md5 as well """
+
+ def merge_cookies(resp):
+ """ Merge the cookies that were set on this response with the given
+ resp object (which can be any WSGI application). If the resp is a
+ webob.Response object, then the other object will be modified
+ in-place. """
+
+ pragma = Attribute(
+ """ Gets and sets and deletes the Pragma header. For more information
+ on Pragma see RFC 2616 section 14.32. """)
+
+ request = Attribute(
+ """ Return the request associated with this response if any. """)
+
+ retry_after = Attribute(
+ """ Gets and sets and deletes the Retry-After header. For more
+ information on Retry-After see RFC 2616 section 14.37. Converts
+ using HTTP date or delta seconds.""")
+
+ server = Attribute(
+ """ Gets and sets and deletes the Server header. For more information
+ on Server see RFC216 section 14.38. """)
+
+ def set_cookie(name, value='', max_age=None, path='/', domain=None,
+ secure=False, httponly=False, comment=None, expires=None,
+ overwrite=False):
+ """ Set (add) a cookie for the response """
+
+ status = Attribute(
+ """ The status string. """)
+
+ status_int = Attribute(
+ """ The status as an integer """)
+
+ unicode_body = Attribute(
+ """ Get/set the unicode value of the body (using the charset of
+ the Content-Type)""")
+
+ def unset_cookie(name, strict=True):
+ """ Unset a cookie with the given name (remove it from the
+ response)."""
+
+ vary = Attribute(
+ """Gets and sets and deletes the Vary header. For more information
+ on Vary see section 14.44. Converts using list.""")
+
+ www_authenticate = Attribute(
+ """ Gets and sets and deletes the WWW-Authenticate header. For more
+ information on WWW-Authenticate see RFC 2616 section 14.47. Converts
+ using 'parse_auth' and 'serialize_auth'. """)
+
+class IException(Interface): # not an API
+ """ An interface representing a generic exception """
+
+class IExceptionResponse(IException, IResponse):
+ """ An interface representing a WSGI response which is also an exception
+ object. Register an exception view using this interface as a ``context``
+ to apply the registered view for all exception types raised by
+ :app:`Pyramid` internally (any exception that inherits from
+ :class:`pyramid.response.Response`, including
+ :class:`pyramid.httpexceptions.HTTPNotFound` and
+ :class:`pyramid.httpexceptions.HTTPForbidden`)."""
+ def prepare(environ):
+ """ Prepares the response for being called as a WSGI application """
+
+class IDict(Interface):
+ # Documentation-only interface
+
+ def __contains__(k):
+ """ Return ``True`` if key ``k`` exists in the dictionary."""
+
+ def __setitem__(k, value):
+ """ Set a key/value pair into the dictionary"""
+
+ def __delitem__(k):
+ """ Delete an item from the dictionary which is passed to the
+ renderer as the renderer globals dictionary."""
+
+ def __getitem__(k):
+ """ Return the value for key ``k`` from the dictionary or raise a
+ KeyError if the key doesn't exist"""
+
+ def __iter__():
+ """ Return an iterator over the keys of this dictionary """
+
+ def get(k, default=None):
+ """ Return the value for key ``k`` from the renderer dictionary, or
+ the default if no such value exists."""
+
+ def items():
+ """ Return a list of [(k,v)] pairs from the dictionary """
+
+ def keys():
+ """ Return a list of keys from the dictionary """
+
+ def values():
+ """ Return a list of values from the dictionary """
+
+ if PY2:
+
+ def iterkeys():
+ """ Return an iterator of keys from the dictionary """
+
+ def iteritems():
+ """ Return an iterator of (k,v) pairs from the dictionary """
+
+ def itervalues():
+ """ Return an iterator of values from the dictionary """
+
+ has_key = __contains__
+
+ def pop(k, default=None):
+ """ Pop the key k from the dictionary and return its value. If k
+ doesn't exist, and default is provided, return the default. If k
+ doesn't exist and default is not provided, raise a KeyError."""
+
+ def popitem():
+ """ Pop the item with key k from the dictionary and return it as a
+ two-tuple (k, v). If k doesn't exist, raise a KeyError."""
+
+ def setdefault(k, default=None):
+ """ Return the existing value for key ``k`` in the dictionary. If no
+ value with ``k`` exists in the dictionary, set the ``default``
+ value into the dictionary under the k name passed. If a value already
+ existed in the dictionary, return it. If a value did not exist in
+ the dictionary, return the default"""
+
+ def update(d):
+ """ Update the renderer dictionary with another dictionary ``d``."""
+
+ def clear():
+ """ Clear all values from the dictionary """
+
+class IBeforeRender(IDict):
+ """
+ Subscribers to this event may introspect and modify the set of
+ :term:`renderer globals` before they are passed to a :term:`renderer`.
+ The event object itself provides a dictionary-like interface for adding
+ and removing :term:`renderer globals`. The keys and values of the
+ dictionary are those globals. For example::
+
+ from repoze.events import subscriber
+ from pyramid.interfaces import IBeforeRender
+
+ @subscriber(IBeforeRender)
+ def add_global(event):
+ event['mykey'] = 'foo'
+
+ .. seealso::
+
+ See also :ref:`beforerender_event`.
+ """
+ rendering_val = Attribute('The value returned by a view or passed to a '
+ '``render`` method for this rendering. '
+ 'This feature is new in Pyramid 1.2.')
+
+class IRendererInfo(Interface):
+ """ An object implementing this interface is passed to every
+ :term:`renderer factory` constructor as its only argument (conventionally
+ named ``info``)"""
+ name = Attribute('The value passed by the user as the renderer name')
+ package = Attribute('The "current package" when the renderer '
+ 'configuration statement was found')
+ type = Attribute('The renderer type name')
+ registry = Attribute('The "current" application registry when the '
+ 'renderer was created')
+ settings = Attribute('The deployment settings dictionary related '
+ 'to the current application')
+
+ def clone():
+ """ Return a shallow copy that does not share any mutable state."""
+
+class IRendererFactory(Interface):
+ def __call__(info):
+ """ Return an object that implements
+ :class:`pyramid.interfaces.IRenderer`. ``info`` is an
+ object that implements :class:`pyramid.interfaces.IRendererInfo`.
+ """
+
+class IRenderer(Interface):
+ def __call__(value, system):
+ """ Call the renderer with the result of the
+ view (``value``) passed in and return a result (a string or
+ unicode object useful as a response body). Values computed by
+ the system are passed by the system in the ``system``
+ parameter, which is a dictionary. Keys in the dictionary
+ include: ``view`` (the view callable that returned the value),
+ ``renderer_name`` (the template name or simple name of the
+ renderer), ``context`` (the context object passed to the
+ view), and ``request`` (the request object passed to the
+ view)."""
+
+class ITemplateRenderer(IRenderer):
+ def implementation():
+ """ Return the object that the underlying templating system
+ uses to render the template; it is typically a callable that
+ accepts arbitrary keyword arguments and returns a string or
+ unicode object """
+
+deprecated(
+ 'ITemplateRenderer',
+ 'As of Pyramid 1.5 the, "pyramid.interfaces.ITemplateRenderer" interface '
+ 'is scheduled to be removed. It was used by the Mako and Chameleon '
+ 'renderers which have been split into their own packages.'
+ )
+
+class IViewMapper(Interface):
+ def __call__(self, object):
+ """ Provided with an arbitrary object (a function, class, or
+ instance), returns a callable with the call signature ``(context,
+ request)``. The callable returned should itself return a Response
+ object. An IViewMapper is returned by
+ :class:`pyramid.interfaces.IViewMapperFactory`."""
+
+class IViewMapperFactory(Interface):
+ def __call__(self, **kw):
+ """
+ Return an object which implements
+ :class:`pyramid.interfaces.IViewMapper`. ``kw`` will be a dictionary
+ containing view-specific arguments, such as ``permission``,
+ ``predicates``, ``attr``, ``renderer``, and other items. An
+ IViewMapperFactory is used by
+ :meth:`pyramid.config.Configurator.add_view` to provide a plugpoint
+ to extension developers who want to modify potential view callable
+ invocation signatures and response values.
+ """
+
+class IAuthenticationPolicy(Interface):
+ """ An object representing a Pyramid authentication policy. """
+
+ def authenticated_userid(request):
+ """ Return the authenticated :term:`userid` or ``None`` if
+ no authenticated userid can be found. This method of the
+ policy should ensure that a record exists in whatever
+ persistent store is used related to the user (the user
+ should not have been deleted); if a record associated with
+ the current id does not exist in a persistent store, it
+ should return ``None``.
+
+ """
+
+ def unauthenticated_userid(request):
+ """ Return the *unauthenticated* userid. This method
+ performs the same duty as ``authenticated_userid`` but is
+ permitted to return the userid based only on data present
+ in the request; it needn't (and shouldn't) check any
+ persistent store to ensure that the user record related to
+ the request userid exists.
+
+ This method is intended primarily a helper to assist the
+ ``authenticated_userid`` method in pulling credentials out
+ of the request data, abstracting away the specific headers,
+ query strings, etc that are used to authenticate the request.
+
+ """
+
+ def effective_principals(request):
+ """ Return a sequence representing the effective principals
+ typically including the :term:`userid` and any groups belonged
+ to by the current user, always including 'system' groups such
+ as ``pyramid.security.Everyone`` and
+ ``pyramid.security.Authenticated``.
+
+ """
+
+ def remember(request, userid, **kw):
+ """ Return a set of headers suitable for 'remembering' the
+ :term:`userid` named ``userid`` when set in a response. An
+ individual authentication policy and its consumers can
+ decide on the composition and meaning of ``**kw``.
+
+ """
+
+ def forget(request):
+ """ Return a set of headers suitable for 'forgetting' the
+ current user on subsequent requests.
+
+ """
+
+class IAuthorizationPolicy(Interface):
+ """ An object representing a Pyramid authorization policy. """
+ def permits(context, principals, permission):
+ """ Return an instance of :class:`pyramid.security.Allowed` if any
+ of the ``principals`` is allowed the ``permission`` in the current
+ ``context``, else return an instance of
+ :class:`pyramid.security.Denied`.
+ """
+
+ def principals_allowed_by_permission(context, permission):
+ """ Return a set of principal identifiers allowed by the
+ ``permission`` in ``context``. This behavior is optional; if you
+ choose to not implement it you should define this method as
+ something which raises a ``NotImplementedError``. This method
+ will only be called when the
+ ``pyramid.security.principals_allowed_by_permission`` API is
+ used."""
+
+class IMultiDict(IDict): # docs-only interface
+ """
+ An ordered dictionary that can have multiple values for each key. A
+ multidict adds the methods ``getall``, ``getone``, ``mixed``, ``extend``,
+ ``add``, and ``dict_of_lists`` to the normal dictionary interface. A
+ multidict data structure is used as ``request.POST``, ``request.GET``,
+ and ``request.params`` within an :app:`Pyramid` application.
+ """
+
+ def add(key, value):
+ """ Add the key and value, not overwriting any previous value. """
+
+ def dict_of_lists():
+ """
+ Returns a dictionary where each key is associated with a list of
+ values.
+ """
+
+ def extend(other=None, **kwargs):
+ """ Add a set of keys and values, not overwriting any previous
+ values. The ``other`` structure may be a list of two-tuples or a
+ dictionary. If ``**kwargs`` is passed, its value *will* overwrite
+ existing values."""
+
+ def getall(key):
+ """ Return a list of all values matching the key (may be an empty
+ list) """
+
+ def getone(key):
+ """ Get one value matching the key, raising a KeyError if multiple
+ values were found. """
+
+ def mixed():
+ """ Returns a dictionary where the values are either single values,
+ or a list of values when a key/value appears more than once in this
+ dictionary. This is similar to the kind of dictionary often used to
+ represent the variables in a web request. """
+
+# internal interfaces
+
+class IRequest(Interface):
+ """ Request type interface attached to all request objects """
+
+class ITweens(Interface):
+ """ Marker interface for utility registration representing the ordered
+ set of a configuration's tween factories"""
+
+class IRequestHandler(Interface):
+ """ """
+ def __call__(self, request):
+ """ Must return a tuple of IReqest, IResponse or raise an exception.
+ The ``request`` argument will be an instance of an object that
+ provides IRequest."""
+
+IRequest.combined = IRequest # for exception view lookups
+
+class IRequestExtensions(Interface):
+ """ Marker interface for storing request extensions (properties and
+ methods) which will be added to the request object."""
+ descriptors = Attribute(
+ """A list of descriptors that will be added to each request.""")
+ methods = Attribute(
+ """A list of methods to be added to each request.""")
+
+class IRouteRequest(Interface):
+ """ *internal only* interface used as in a utility lookup to find
+ route-specific interfaces. Not an API."""
+
+class IAcceptOrder(Interface):
+ """
+ Marker interface for a list of accept headers with the most important
+ first.
+
+ """
+
+class IStaticURLInfo(Interface):
+ """ A policy for generating URLs to static assets """
+ def add(config, name, spec, **extra):
+ """ Add a new static info registration """
+
+ def generate(path, request, **kw):
+ """ Generate a URL for the given path """
+
+ def add_cache_buster(config, spec, cache_buster):
+ """ Add a new cache buster to a particular set of assets """
+
+class IResponseFactory(Interface):
+ """ A utility which generates a response """
+ def __call__(request):
+ """ Return a response object implementing IResponse,
+ e.g. :class:`pyramid.response.Response`). It should handle the
+ case when ``request`` is ``None``."""
+
+class IRequestFactory(Interface):
+ """ A utility which generates a request """
+ def __call__(environ):
+ """ Return an instance of ``pyramid.request.Request``"""
+
+ def blank(path):
+ """ Return an empty request object (see
+ :meth:`pyramid.request.Request.blank`)"""
+
+class IViewClassifier(Interface):
+ """ *Internal only* marker interface for views."""
+
+class IExceptionViewClassifier(Interface):
+ """ *Internal only* marker interface for exception views."""
+
+class IView(Interface):
+ def __call__(context, request):
+ """ Must return an object that implements IResponse. """
+
+class ISecuredView(IView):
+ """ *Internal only* interface. Not an API. """
+ def __call_permissive__(context, request):
+ """ Guaranteed-permissive version of __call__ """
+
+ def __permitted__(context, request):
+ """ Return True if view execution will be permitted using the
+ context and request, False otherwise"""
+
+class IMultiView(ISecuredView):
+ """ *internal only*. A multiview is a secured view that is a
+ collection of other views. Each of the views is associated with
+ zero or more predicates. Not an API."""
+ def add(view, predicates, order, accept=None, phash=None):
+ """ Add a view to the multiview. """
+
+class IRootFactory(Interface):
+ def __call__(request):
+ """ Return a root object based on the request """
+
+class IDefaultRootFactory(Interface):
+ def __call__(request):
+ """ Return the *default* root object for an application """
+
+class ITraverser(Interface):
+ def __call__(request):
+ """ Return a dictionary with (at least) the keys ``root``,
+ ``context``, ``view_name``, ``subpath``, ``traversed``,
+ ``virtual_root``, and ``virtual_root_path``. These values are
+ typically the result of an object graph traversal. ``root`` is the
+ physical root object, ``context`` will be a model object,
+ ``view_name`` will be the view name used (a Unicode name),
+ ``subpath`` will be a sequence of Unicode names that followed the
+ view name but were not traversed, ``traversed`` will be a sequence of
+ Unicode names that were traversed (including the virtual root path,
+ if any) ``virtual_root`` will be a model object representing the
+ virtual root (or the physical root if traversal was not performed),
+ and ``virtual_root_path`` will be a sequence representing the virtual
+ root path (a sequence of Unicode names) or ``None`` if traversal was
+ not performed.
+
+ Extra keys for special purpose functionality can be returned as
+ necessary.
+
+ All values returned in the dictionary will be made available
+ as attributes of the ``request`` object by the :term:`router`.
+ """
+
+ITraverserFactory = ITraverser # b / c for 1.0 code
+
+class IViewPermission(Interface):
+ def __call__(context, request):
+ """ Return True if the permission allows, return False if it denies.
+ """
+
+class IRouter(Interface):
+ """
+ WSGI application which routes requests to 'view' code based on
+ a view registry.
+
+ """
+ registry = Attribute(
+ """Component architecture registry local to this application.""")
+
+ def request_context(environ):
+ """
+ Create a new request context from a WSGI environ.
+
+ The request context is used to push/pop the threadlocals required
+ when processing the request. It also contains an initialized
+ :class:`pyramid.interfaces.IRequest` instance using the registered
+ :class:`pyramid.interfaces.IRequestFactory`. The context may be
+ used as a context manager to control the threadlocal lifecycle:
+
+ .. code-block:: python
+
+ with router.request_context(environ) as request:
+ ...
+
+ Alternatively, the context may be used without the ``with`` statement
+ by manually invoking its ``begin()`` and ``end()`` methods.
+
+ .. code-block:: python
+
+ ctx = router.request_context(environ)
+ request = ctx.begin()
+ try:
+ ...
+ finally:
+ ctx.end()
+
+ """
+
+ def invoke_request(request):
+ """
+ Invoke the :app:`Pyramid` request pipeline.
+
+ See :ref:`router_chapter` for information on the request pipeline.
+
+ The output should be a :class:`pyramid.interfaces.IResponse` object
+ or a raised exception.
+
+ """
+
+class IExecutionPolicy(Interface):
+ def __call__(environ, router):
+ """
+ This callable triggers the router to process a raw WSGI environ dict
+ into a response and controls the :app:`Pyramid` request pipeline.
+
+ The ``environ`` is the raw WSGI environ.
+
+ The ``router`` is an :class:`pyramid.interfaces.IRouter` object which
+ should be used to create a request object and send it into the
+ processing pipeline.
+
+ The return value should be a :class:`pyramid.interfaces.IResponse`
+ object or an exception that will be handled by WSGI middleware.
+
+ The default execution policy simply creates a request and sends it
+ through the pipeline, attempting to render any exception that escapes:
+
+ .. code-block:: python
+
+ def simple_execution_policy(environ, router):
+ with router.request_context(environ) as request:
+ try:
+ return router.invoke_request(request)
+ except Exception:
+ return request.invoke_exception_view(reraise=True)
+ """
+
+class ISettings(IDict):
+ """ Runtime settings utility for pyramid; represents the
+ deployment settings for the application. Implements a mapping
+ interface."""
+
+# this interface, even if it becomes unused within Pyramid, is
+# imported by other packages (such as traversalwrapper)
+class ILocation(Interface):
+ """Objects that have a structural location"""
+ __parent__ = Attribute("The parent in the location hierarchy")
+ __name__ = Attribute("The name within the parent")
+
+class IDebugLogger(Interface):
+ """ Interface representing a PEP 282 logger """
+
+ILogger = IDebugLogger # b/c
+
+class IRoutePregenerator(Interface):
+ def __call__(request, elements, kw):
+
+ """ A pregenerator is a function associated by a developer with a
+ :term:`route`. The pregenerator for a route is called by
+ :meth:`pyramid.request.Request.route_url` in order to adjust the set
+ of arguments passed to it by the user for special purposes, such as
+ Pylons 'subdomain' support. It will influence the URL returned by
+ ``route_url``.
+
+ A pregenerator should return a two-tuple of ``(elements, kw)``
+ after examining the originals passed to this function, which
+ are the arguments ``(request, elements, kw)``. The simplest
+ pregenerator is::
+
+ def pregenerator(request, elements, kw):
+ return elements, kw
+
+ You can employ a pregenerator by passing a ``pregenerator``
+ argument to the
+ :meth:`pyramid.config.Configurator.add_route`
+ function.
+
+ """
+
+class IRoute(Interface):
+ """ Interface representing the type of object returned from
+ ``IRoutesMapper.get_route``"""
+ name = Attribute('The route name')
+ pattern = Attribute('The route pattern')
+ factory = Attribute(
+ 'The :term:`root factory` used by the :app:`Pyramid` router '
+ 'when this route matches (or ``None``)')
+ predicates = Attribute(
+ 'A sequence of :term:`route predicate` objects used to '
+ 'determine if a request matches this route or not after '
+ 'basic pattern matching has been completed.')
+ pregenerator = Attribute('This attribute should either be ``None`` or '
+ 'a callable object implementing the '
+ '``IRoutePregenerator`` interface')
+
+ def match(path):
+ """
+ If the ``path`` passed to this function can be matched by the
+ ``pattern`` of this route, return a dictionary (the
+ 'matchdict'), which will contain keys representing the dynamic
+ segment markers in the pattern mapped to values extracted from
+ the provided ``path``.
+
+ If the ``path`` passed to this function cannot be matched by
+ the ``pattern`` of this route, return ``None``.
+ """
+ def generate(kw):
+ """
+ Generate a URL based on filling in the dynamic segment markers
+ in the pattern using the ``kw`` dictionary provided.
+ """
+
+class IRoutesMapper(Interface):
+ """ Interface representing a Routes ``Mapper`` object """
+ def get_routes():
+ """ Return a sequence of Route objects registered in the mapper.
+ Static routes will not be returned in this sequence."""
+
+ def has_routes():
+ """ Returns ``True`` if any route has been registered. """
+
+ def get_route(name):
+ """ Returns an ``IRoute`` object if a route with the name ``name``
+ was registered, otherwise return ``None``."""
+
+ def connect(name, pattern, factory=None, predicates=(), pregenerator=None,
+ static=True):
+ """ Add a new route. """
+
+ def generate(name, kw):
+ """ Generate a URL using the route named ``name`` with the
+ keywords implied by kw"""
+
+ def __call__(request):
+ """ Return a dictionary containing matching information for
+ the request; the ``route`` key of this dictionary will either
+ be a Route object or ``None`` if no route matched; the
+ ``match`` key will be the matchdict or ``None`` if no route
+ matched. Static routes will not be considered for matching. """
+
+class IResourceURL(Interface):
+ virtual_path = Attribute(
+ 'The virtual url path of the resource as a string.'
+ )
+ physical_path = Attribute(
+ 'The physical url path of the resource as a string.'
+ )
+ virtual_path_tuple = Attribute(
+ 'The virtual url path of the resource as a tuple. (New in 1.5)'
+ )
+ physical_path_tuple = Attribute(
+ 'The physical url path of the resource as a tuple. (New in 1.5)'
+ )
+
+class IPEP302Loader(Interface):
+ """ See http://www.python.org/dev/peps/pep-0302/#id30.
+ """
+ def get_data(path):
+ """ Retrieve data for and arbitrary "files" from storage backend.
+
+ Raise IOError for not found.
+
+ Data is returned as bytes.
+ """
+
+ def is_package(fullname):
+ """ Return True if the module specified by 'fullname' is a package.
+ """
+
+ def get_code(fullname):
+ """ Return the code object for the module identified by 'fullname'.
+
+ Return 'None' if it's a built-in or extension module.
+
+ If the loader doesn't have the code object but it does have the source
+ code, return the compiled source code.
+
+ Raise ImportError if the module can't be found by the importer at all.
+ """
+
+ def get_source(fullname):
+ """ Return the source code for the module identified by 'fullname'.
+
+ Return a string, using newline characters for line endings, or None
+ if the source is not available.
+
+ Raise ImportError if the module can't be found by the importer at all.
+ """
+
+ def get_filename(fullname):
+ """ Return the value of '__file__' if the named module was loaded.
+
+ If the module is not found, raise ImportError.
+ """
+
+
+class IPackageOverrides(IPEP302Loader):
+ """ Utility for pkg_resources overrides """
+
+# VH_ROOT_KEY is an interface; its imported from other packages (e.g.
+# traversalwrapper)
+VH_ROOT_KEY = 'HTTP_X_VHM_ROOT'
+
+class ILocalizer(Interface):
+ """ Localizer for a specific language """
+
+class ILocaleNegotiator(Interface):
+ def __call__(request):
+ """ Return a locale name """
+
+class ITranslationDirectories(Interface):
+ """ A list object representing all known translation directories
+ for an application"""
+
+class IDefaultPermission(Interface):
+ """ A string object representing the default permission to be used
+ for all view configurations which do not explicitly declare their
+ own."""
+
+class IDefaultCSRFOptions(Interface):
+ """ An object representing the default CSRF settings to be used for
+ all view configurations which do not explicitly declare their own."""
+ require_csrf = Attribute(
+ 'Boolean attribute. If ``True``, then CSRF checks will be enabled by '
+ 'default for the view unless overridden.')
+ token = Attribute('The key to be matched in the body of the request.')
+ header = Attribute('The header to be matched with the CSRF token.')
+ safe_methods = Attribute('A set of safe methods that skip CSRF checks.')
+ callback = Attribute('A callback to disable CSRF checks per-request.')
+
+class ISessionFactory(Interface):
+ """ An interface representing a factory which accepts a request object and
+ returns an ISession object """
+ def __call__(request):
+ """ Return an ISession object """
+
+class ISession(IDict):
+ """ An interface representing a session (a web session object,
+ usually accessed via ``request.session``.
+
+ Keys and values of a session must be pickleable.
+
+ .. warning::
+
+ In :app:`Pyramid` 2.0 the session will only be required to support
+ types that can be serialized using JSON. It's recommended to switch any
+ session implementations to support only JSON and to only store primitive
+ types in sessions. See :ref:`pickle_session_deprecation` for more
+ information about why this change is being made.
+
+ .. versionchanged:: 1.9
+
+ Sessions are no longer required to implement ``get_csrf_token`` and
+ ``new_csrf_token``. CSRF token support was moved to the pluggable
+ :class:`pyramid.interfaces.ICSRFStoragePolicy` configuration hook.
+
+ """
+
+ # attributes
+
+ created = Attribute('Integer representing Epoch time when created.')
+ new = Attribute('Boolean attribute. If ``True``, the session is new.')
+
+ # special methods
+
+ def invalidate():
+ """ Invalidate the session. The action caused by
+ ``invalidate`` is implementation-dependent, but it should have
+ the effect of completely dissociating any data stored in the
+ session with the current request. It might set response
+ values (such as one which clears a cookie), or it might not.
+
+ An invalidated session may be used after the call to ``invalidate``
+ with the effect that a new session is created to store the data. This
+ enables workflows requiring an entirely new session, such as in the
+ case of changing privilege levels or preventing fixation attacks.
+ """
+
+ def changed():
+ """ Mark the session as changed. A user of a session should
+ call this method after he or she mutates a mutable object that
+ is *a value of the session* (it should not be required after
+ mutating the session itself). For example, if the user has
+ stored a dictionary in the session under the key ``foo``, and
+ he or she does ``session['foo'] = {}``, ``changed()`` needn't
+ be called. However, if subsequently he or she does
+ ``session['foo']['a'] = 1``, ``changed()`` must be called for
+ the sessioning machinery to notice the mutation of the
+ internal dictionary."""
+
+ def flash(msg, queue='', allow_duplicate=True):
+ """ Push a flash message onto the end of the flash queue represented
+ by ``queue``. An alternate flash message queue can used by passing
+ an optional ``queue``, which must be a string. If
+ ``allow_duplicate`` is false, if the ``msg`` already exists in the
+ queue, it will not be re-added."""
+
+ def pop_flash(queue=''):
+ """ Pop a queue from the flash storage. The queue is removed from
+ flash storage after this message is called. The queue is returned;
+ it is a list of flash messages added by
+ :meth:`pyramid.interfaces.ISession.flash`"""
+
+ def peek_flash(queue=''):
+ """ Peek at a queue in the flash storage. The queue remains in
+ flash storage after this message is called. The queue is returned;
+ it is a list of flash messages added by
+ :meth:`pyramid.interfaces.ISession.flash`
+ """
+
+
+class ICSRFStoragePolicy(Interface):
+ """ An object that offers the ability to verify CSRF tokens and generate
+ new ones."""
+
+ def new_csrf_token(request):
+ """ Create and return a new, random cross-site request forgery
+ protection token. The token will be an ascii-compatible unicode
+ string.
+
+ """
+
+ def get_csrf_token(request):
+ """ Return a cross-site request forgery protection token. It
+ will be an ascii-compatible unicode string. If a token was previously
+ set for this user via ``new_csrf_token``, that token will be returned.
+ If no CSRF token was previously set, ``new_csrf_token`` will be
+ called, which will create and set a token, and this token will be
+ returned.
+
+ """
+
+ def check_csrf_token(request, token):
+ """ Determine if the supplied ``token`` is valid. Most implementations
+ should simply compare the ``token`` to the current value of
+ ``get_csrf_token`` but it is possible to verify the token using
+ any mechanism necessary using this method.
+
+ Returns ``True`` if the ``token`` is valid, otherwise ``False``.
+
+ """
+
+
+class IIntrospector(Interface):
+ def get(category_name, discriminator, default=None):
+ """ Get the IIntrospectable related to the category_name and the
+ discriminator (or discriminator hash) ``discriminator``. If it does
+ not exist in the introspector, return the value of ``default`` """
+
+ def get_category(category_name, default=None, sort_key=None):
+ """ Get a sequence of dictionaries in the form
+ ``[{'introspectable':IIntrospectable, 'related':[sequence of related
+ IIntrospectables]}, ...]`` where each introspectable is part of the
+ category associated with ``category_name`` .
+
+ If the category named ``category_name`` does not exist in the
+ introspector the value passed as ``default`` will be returned.
+
+ If ``sort_key`` is ``None``, the sequence will be returned in the
+ order the introspectables were added to the introspector. Otherwise,
+ sort_key should be a function that accepts an IIntrospectable and
+ returns a value from it (ala the ``key`` function of Python's
+ ``sorted`` callable)."""
+
+ def categories():
+ """ Return a sorted sequence of category names known by
+ this introspector """
+
+ def categorized(sort_key=None):
+ """ Get a sequence of tuples in the form ``[(category_name,
+ [{'introspectable':IIntrospectable, 'related':[sequence of related
+ IIntrospectables]}, ...])]`` representing all known
+ introspectables. If ``sort_key`` is ``None``, each introspectables
+ sequence will be returned in the order the introspectables were added
+ to the introspector. Otherwise, sort_key should be a function that
+ accepts an IIntrospectable and returns a value from it (ala the
+ ``key`` function of Python's ``sorted`` callable)."""
+
+ def remove(category_name, discriminator):
+ """ Remove the IIntrospectable related to ``category_name`` and
+ ``discriminator`` from the introspector, and fix up any relations
+ that the introspectable participates in. This method will not raise
+ an error if an introspectable related to the category name and
+ discriminator does not exist."""
+
+ def related(intr):
+ """ Return a sequence of IIntrospectables related to the
+ IIntrospectable ``intr``. Return the empty sequence if no relations
+ for exist."""
+
+ def add(intr):
+ """ Add the IIntrospectable ``intr`` (use instead of
+ :meth:`pyramid.interfaces.IIntrospector.add` when you have a custom
+ IIntrospectable). Replaces any existing introspectable registered
+ using the same category/discriminator.
+
+ This method is not typically called directly, instead it's called
+ indirectly by :meth:`pyramid.interfaces.IIntrospector.register`"""
+
+ def relate(*pairs):
+ """ Given any number of ``(category_name, discriminator)`` pairs
+ passed as positional arguments, relate the associated introspectables
+ to each other. The introspectable related to each pair must have
+ already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError`
+ will result if this is not true. An error will not be raised if any
+ pair has already been associated with another.
+
+ This method is not typically called directly, instead it's called
+ indirectly by :meth:`pyramid.interfaces.IIntrospector.register`
+ """
+
+ def unrelate(*pairs):
+ """ Given any number of ``(category_name, discriminator)`` pairs
+ passed as positional arguments, unrelate the associated introspectables
+ from each other. The introspectable related to each pair must have
+ already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError`
+ will result if this is not true. An error will not be raised if any
+ pair is not already related to another.
+
+ This method is not typically called directly, instead it's called
+ indirectly by :meth:`pyramid.interfaces.IIntrospector.register`
+ """
+
+
+class IIntrospectable(Interface):
+ """ An introspectable object used for configuration introspection. In
+ addition to the methods below, objects which implement this interface
+ must also implement all the methods of Python's
+ ``collections.MutableMapping`` (the "dictionary interface"), and must be
+ hashable."""
+
+ title = Attribute('Text title describing this introspectable')
+ type_name = Attribute('Text type name describing this introspectable')
+ order = Attribute('integer order in which registered with introspector '
+ '(managed by introspector, usually)')
+ category_name = Attribute('introspection category name')
+ discriminator = Attribute('introspectable discriminator (within category) '
+ '(must be hashable)')
+ discriminator_hash = Attribute('an integer hash of the discriminator')
+ action_info = Attribute('An IActionInfo object representing the caller '
+ 'that invoked the creation of this introspectable '
+ '(usually a sentinel until updated during '
+ 'self.register)')
+
+ def relate(category_name, discriminator):
+ """ Indicate an intent to relate this IIntrospectable with another
+ IIntrospectable (the one associated with the ``category_name`` and
+ ``discriminator``) during action execution.
+ """
+
+ def unrelate(category_name, discriminator):
+ """ Indicate an intent to break the relationship between this
+ IIntrospectable with another IIntrospectable (the one associated with
+ the ``category_name`` and ``discriminator``) during action execution.
+ """
+
+ def register(introspector, action_info):
+ """ Register this IIntrospectable with an introspector. This method
+ is invoked during action execution. Adds the introspectable and its
+ relations to the introspector. ``introspector`` should be an object
+ implementing IIntrospector. ``action_info`` should be a object
+ implementing the interface :class:`pyramid.interfaces.IActionInfo`
+ representing the call that registered this introspectable.
+ Pseudocode for an implementation of this method:
+
+ .. code-block:: python
+
+ def register(self, introspector, action_info):
+ self.action_info = action_info
+ introspector.add(self)
+ for methodname, category_name, discriminator in self._relations:
+ method = getattr(introspector, methodname)
+ method((i.category_name, i.discriminator),
+ (category_name, discriminator))
+ """
+
+ def __hash__():
+
+ """ Introspectables must be hashable. The typical implementation of
+ an introsepectable's __hash__ is::
+
+ return hash((self.category_name,) + (self.discriminator,))
+ """
+
+class IActionInfo(Interface):
+ """ Class which provides code introspection capability associated with an
+ action. The ParserInfo class used by ZCML implements the same interface."""
+ file = Attribute(
+ 'Filename of action-invoking code as a string')
+ line = Attribute(
+ 'Starting line number in file (as an integer) of action-invoking code.'
+ 'This will be ``None`` if the value could not be determined.')
+
+ def __str__():
+ """ Return a representation of the action information (including
+ source code from file, if possible) """
+
+class IAssetDescriptor(Interface):
+ """
+ Describes an :term:`asset`.
+ """
+
+ def absspec():
+ """
+ Returns the absolute asset specification for this asset
+ (e.g. ``mypackage:templates/foo.pt``).
+ """
+
+ def abspath():
+ """
+ Returns an absolute path in the filesystem to the asset.
+ """
+
+ def stream():
+ """
+ Returns an input stream for reading asset contents. Raises an
+ exception if the asset is a directory or does not exist.
+ """
+
+ def isdir():
+ """
+ Returns True if the asset is a directory, otherwise returns False.
+ """
+
+ def listdir():
+ """
+ Returns iterable of filenames of directory contents. Raises an
+ exception if asset is not a directory.
+ """
+
+ def exists():
+ """
+ Returns True if asset exists, otherwise returns False.
+ """
+
+class IJSONAdapter(Interface):
+ """
+ Marker interface for objects that can convert an arbitrary object
+ into a JSON-serializable primitive.
+ """
+
+class IPredicateList(Interface):
+ """ Interface representing a predicate list """
+
+class IViewDeriver(Interface):
+ options = Attribute('A list of supported options to be passed to '
+ ':meth:`pyramid.config.Configurator.add_view`. '
+ 'This attribute is optional.')
+
+ def __call__(view, info):
+ """
+ Derive a new view from the supplied view.
+
+ View options, package information and registry are available on
+ ``info``, an instance of :class:`pyramid.interfaces.IViewDeriverInfo`.
+
+ The ``view`` is a callable accepting ``(context, request)``.
+
+ """
+
+class IViewDeriverInfo(Interface):
+ """ An object implementing this interface is passed to every
+ :term:`view deriver` during configuration."""
+ registry = Attribute('The "current" application registry where the '
+ 'view was created')
+ package = Attribute('The "current package" where the view '
+ 'configuration statement was found')
+ settings = Attribute('The deployment settings dictionary related '
+ 'to the current application')
+ options = Attribute('The view options passed to the view, including any '
+ 'default values that were not overriden')
+ predicates = Attribute('The list of predicates active on the view')
+ original_view = Attribute('The original view object being wrapped')
+ exception_only = Attribute('The view will only be invoked for exceptions')
+
+class IViewDerivers(Interface):
+ """ Interface for view derivers list """
+
+class ICacheBuster(Interface):
+ """
+ A cache buster modifies the URL generation machinery for
+ :meth:`~pyramid.request.Request.static_url`. See :ref:`cache_busting`.
+
+ .. versionadded:: 1.6
+ """
+ def __call__(request, subpath, kw):
+ """
+ Modifies a subpath and/or keyword arguments from which a static asset
+ URL will be computed during URL generation.
+
+ The ``subpath`` argument is a path of ``/``-delimited segments that
+ represent the portion of the asset URL which is used to find the asset.
+ The ``kw`` argument is a dict of keywords that are to be passed
+ eventually to :meth:`~pyramid.request.Request.static_url` for URL
+ generation. The return value should be a two-tuple of
+ ``(subpath, kw)`` where ``subpath`` is the relative URL from where the
+ file is served and ``kw`` is the same input argument. The return value
+ should be modified to include the cache bust token in the generated
+ URL.
+
+ The ``kw`` dictionary contains extra arguments passed to
+ :meth:`~pyramid.request.Request.static_url` as well as some extra
+ items that may be usful including:
+
+ - ``pathspec`` is the path specification for the resource
+ to be cache busted.
+
+ - ``rawspec`` is the original location of the file, ignoring
+ any calls to :meth:`pyramid.config.Configurator.override_asset`.
+
+ The ``pathspec`` and ``rawspec`` values are only different in cases
+ where an asset has been mounted into a virtual location using
+ :meth:`pyramid.config.Configurator.override_asset`. For example, with
+ a call to ``request.static_url('myapp:static/foo.png'), the
+ ``pathspec`` is ``myapp:static/foo.png`` whereas the ``rawspec`` may
+ be ``themepkg:bar.png``, assuming a call to
+ ``config.override_asset('myapp:static/foo.png', 'themepkg:bar.png')``.
+ """
+
+# configuration phases: a lower phase number means the actions associated
+# with this phase will be executed earlier than those with later phase
+# numbers. The default phase number is 0, FTR.
+
+PHASE0_CONFIG = -30
+PHASE1_CONFIG = -20
+PHASE2_CONFIG = -10
+PHASE3_CONFIG = 0
diff --git a/src/pyramid/location.py b/src/pyramid/location.py
new file mode 100644
index 000000000..4124895a5
--- /dev/null
+++ b/src/pyramid/location.py
@@ -0,0 +1,66 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+def inside(resource1, resource2):
+ """Is ``resource1`` 'inside' ``resource2``? Return ``True`` if so, else
+ ``False``.
+
+ ``resource1`` is 'inside' ``resource2`` if ``resource2`` is a
+ :term:`lineage` ancestor of ``resource1``. It is a lineage ancestor
+ if its parent (or one of its parent's parents, etc.) is an
+ ancestor.
+ """
+ while resource1 is not None:
+ if resource1 is resource2:
+ return True
+ resource1 = resource1.__parent__
+
+ return False
+
+def lineage(resource):
+ """
+ Return a generator representing the :term:`lineage` of the
+ :term:`resource` object implied by the ``resource`` argument. The
+ generator first returns ``resource`` unconditionally. Then, if
+ ``resource`` supplies a ``__parent__`` attribute, return the resource
+ represented by ``resource.__parent__``. If *that* resource has a
+ ``__parent__`` attribute, return that resource's parent, and so on,
+ until the resource being inspected either has no ``__parent__``
+ attribute or which has a ``__parent__`` attribute of ``None``.
+ For example, if the resource tree is::
+
+ thing1 = Thing()
+ thing2 = Thing()
+ thing2.__parent__ = thing1
+
+ Calling ``lineage(thing2)`` will return a generator. When we turn
+ it into a list, we will get::
+
+ list(lineage(thing2))
+ [ <Thing object at thing2>, <Thing object at thing1> ]
+ """
+ while resource is not None:
+ yield resource
+ # The common case is that the AttributeError exception below
+ # is exceptional as long as the developer is a "good citizen"
+ # who has a root object with a __parent__ of None. Using an
+ # exception here instead of a getattr with a default is an
+ # important micro-optimization, because this function is
+ # called in any non-trivial application over and over again to
+ # generate URLs and paths.
+ try:
+ resource = resource.__parent__
+ except AttributeError:
+ resource = None
+
diff --git a/src/pyramid/paster.py b/src/pyramid/paster.py
new file mode 100644
index 000000000..f7544f0c5
--- /dev/null
+++ b/src/pyramid/paster.py
@@ -0,0 +1,111 @@
+from pyramid.scripting import prepare
+from pyramid.scripts.common import get_config_loader
+
+def setup_logging(config_uri, global_conf=None):
+ """
+ Set up Python logging with the filename specified via ``config_uri``
+ (a string in the form ``filename#sectionname``).
+
+ Extra defaults can optionally be specified as a dict in ``global_conf``.
+ """
+ loader = get_config_loader(config_uri)
+ loader.setup_logging(global_conf)
+
+def get_app(config_uri, name=None, options=None):
+ """ Return the WSGI application named ``name`` in the PasteDeploy
+ config file specified by ``config_uri``.
+
+ ``options``, if passed, should be a dictionary used as variable assignments
+ like ``{'http_port': 8080}``. This is useful if e.g. ``%(http_port)s`` is
+ used in the config file.
+
+ If the ``name`` is None, this will attempt to parse the name from
+ the ``config_uri`` string expecting the format ``inifile#name``.
+ If no name is found, the name will default to "main".
+
+ """
+ loader = get_config_loader(config_uri)
+ return loader.get_wsgi_app(name, options)
+
+def get_appsettings(config_uri, name=None, options=None):
+ """ Return a dictionary representing the key/value pairs in an ``app``
+ section within the file represented by ``config_uri``.
+
+ ``options``, if passed, should be a dictionary used as variable assignments
+ like ``{'http_port': 8080}``. This is useful if e.g. ``%(http_port)s`` is
+ used in the config file.
+
+ If the ``name`` is None, this will attempt to parse the name from
+ the ``config_uri`` string expecting the format ``inifile#name``.
+ If no name is found, the name will default to "main".
+
+ """
+ loader = get_config_loader(config_uri)
+ return loader.get_wsgi_app_settings(name, options)
+
+def bootstrap(config_uri, request=None, options=None):
+ """ Load a WSGI application from the PasteDeploy config file specified
+ by ``config_uri``. The environment will be configured as if it is
+ currently serving ``request``, leaving a natural environment in place
+ to write scripts that can generate URLs and utilize renderers.
+
+ This function returns a dictionary with ``app``, ``root``, ``closer``,
+ ``request``, and ``registry`` keys. ``app`` is the WSGI app loaded
+ (based on the ``config_uri``), ``root`` is the traversal root resource
+ of the Pyramid application, and ``closer`` is a parameterless callback
+ that may be called when your script is complete (it pops a threadlocal
+ stack).
+
+ .. note::
+
+ Most operations within :app:`Pyramid` expect to be invoked within the
+ context of a WSGI request, thus it's important when loading your
+ application to anchor it when executing scripts and other code that is
+ not normally invoked during active WSGI requests.
+
+ .. note::
+
+ For a complex config file containing multiple :app:`Pyramid`
+ applications, this function will setup the environment under the context
+ of the last-loaded :app:`Pyramid` application. You may load a specific
+ application yourself by using the lower-level functions
+ :meth:`pyramid.paster.get_app` and :meth:`pyramid.scripting.prepare` in
+ conjunction with :attr:`pyramid.config.global_registries`.
+
+ ``config_uri`` -- specifies the PasteDeploy config file to use for the
+ interactive shell. The format is ``inifile#name``. If the name is left
+ off, ``main`` will be assumed.
+
+ ``request`` -- specified to anchor the script to a given set of WSGI
+ parameters. For example, most people would want to specify the host,
+ scheme and port such that their script will generate URLs in relation
+ to those parameters. A request with default parameters is constructed
+ for you if none is provided. You can mutate the request's ``environ``
+ later to setup a specific host/port/scheme/etc.
+
+ ``options`` Is passed to get_app for use as variable assignments like
+ {'http_port': 8080} and then use %(http_port)s in the
+ config file.
+
+ This function may be used as a context manager to call the ``closer``
+ automatically:
+
+ .. code-block:: python
+
+ with bootstrap('development.ini') as env:
+ request = env['request']
+ # ...
+
+ See :ref:`writing_a_script` for more information about how to use this
+ function.
+
+ .. versionchanged:: 1.8
+
+ Added the ability to use the return value as a context manager.
+
+ """
+ app = get_app(config_uri, options=options)
+ env = prepare(request)
+ env['app'] = app
+ return env
+
diff --git a/src/pyramid/path.py b/src/pyramid/path.py
new file mode 100644
index 000000000..3fac7e940
--- /dev/null
+++ b/src/pyramid/path.py
@@ -0,0 +1,436 @@
+import os
+import pkg_resources
+import sys
+import imp
+
+from zope.interface import implementer
+
+from pyramid.interfaces import IAssetDescriptor
+
+from pyramid.compat import string_types
+
+ignore_types = [ imp.C_EXTENSION, imp.C_BUILTIN ]
+init_names = [ '__init__%s' % x[0] for x in imp.get_suffixes() if
+ x[0] and x[2] not in ignore_types ]
+
+def caller_path(path, level=2):
+ if not os.path.isabs(path):
+ module = caller_module(level + 1)
+ prefix = package_path(module)
+ path = os.path.join(prefix, path)
+ return path
+
+def caller_module(level=2, sys=sys):
+ module_globals = sys._getframe(level).f_globals
+ module_name = module_globals.get('__name__') or '__main__'
+ module = sys.modules[module_name]
+ return module
+
+def package_name(pkg_or_module):
+ """ If this function is passed a module, return the dotted Python
+ package name of the package in which the module lives. If this
+ function is passed a package, return the dotted Python package
+ name of the package itself."""
+ if pkg_or_module is None or pkg_or_module.__name__ == '__main__':
+ return '__main__'
+ pkg_name = pkg_or_module.__name__
+ pkg_filename = getattr(pkg_or_module, '__file__', None)
+ if pkg_filename is None:
+ # Namespace packages do not have __init__.py* files,
+ # and so have no __file__ attribute
+ return pkg_name
+ splitted = os.path.split(pkg_filename)
+ if splitted[-1] in init_names:
+ # it's a package
+ return pkg_name
+ return pkg_name.rsplit('.', 1)[0]
+
+def package_of(pkg_or_module):
+ """ Return the package of a module or return the package itself """
+ pkg_name = package_name(pkg_or_module)
+ __import__(pkg_name)
+ return sys.modules[pkg_name]
+
+def caller_package(level=2, caller_module=caller_module):
+ # caller_module in arglist for tests
+ module = caller_module(level + 1)
+ f = getattr(module, '__file__', '')
+ if (('__init__.py' in f) or ('__init__$py' in f)): # empty at >>>
+ # Module is a package
+ return module
+ # Go up one level to get package
+ package_name = module.__name__.rsplit('.', 1)[0]
+ return sys.modules[package_name]
+
+def package_path(package):
+ # computing the abspath is actually kinda expensive so we memoize
+ # the result
+ prefix = getattr(package, '__abspath__', None)
+ if prefix is None:
+ prefix = pkg_resources.resource_filename(package.__name__, '')
+ # pkg_resources doesn't care whether we feed it a package
+ # name or a module name within the package, the result
+ # will be the same: a directory name to the package itself
+ try:
+ package.__abspath__ = prefix
+ except Exception:
+ # this is only an optimization, ignore any error
+ pass
+ return prefix
+
+class _CALLER_PACKAGE(object):
+ def __repr__(self): # pragma: no cover (for docs)
+ return 'pyramid.path.CALLER_PACKAGE'
+
+CALLER_PACKAGE = _CALLER_PACKAGE()
+
+class Resolver(object):
+ def __init__(self, package=CALLER_PACKAGE):
+ if package in (None, CALLER_PACKAGE):
+ self.package = package
+ else:
+ if isinstance(package, string_types):
+ try:
+ __import__(package)
+ except ImportError:
+ raise ValueError(
+ 'The dotted name %r cannot be imported' % (package,)
+ )
+ package = sys.modules[package]
+ self.package = package_of(package)
+
+ def get_package_name(self):
+ if self.package is CALLER_PACKAGE:
+ package_name = caller_package().__name__
+ else:
+ package_name = self.package.__name__
+ return package_name
+
+ def get_package(self):
+ if self.package is CALLER_PACKAGE:
+ package = caller_package()
+ else:
+ package = self.package
+ return package
+
+
+class AssetResolver(Resolver):
+ """ A class used to resolve an :term:`asset specification` to an
+ :term:`asset descriptor`.
+
+ .. versionadded:: 1.3
+
+ The constructor accepts a single argument named ``package`` which may be
+ any of:
+
+ - A fully qualified (not relative) dotted name to a module or package
+
+ - a Python module or package object
+
+ - The value ``None``
+
+ - The constant value :attr:`pyramid.path.CALLER_PACKAGE`.
+
+ The default value is :attr:`pyramid.path.CALLER_PACKAGE`.
+
+ The ``package`` is used when a relative asset specification is supplied
+ to the :meth:`~pyramid.path.AssetResolver.resolve` method. An asset
+ specification without a colon in it is treated as relative.
+
+ If ``package`` is ``None``, the resolver will
+ only be able to resolve fully qualified (not relative) asset
+ specifications. Any attempt to resolve a relative asset specification
+ will result in an :exc:`ValueError` exception.
+
+ If ``package`` is :attr:`pyramid.path.CALLER_PACKAGE`,
+ the resolver will treat relative asset specifications as
+ relative to the caller of the :meth:`~pyramid.path.AssetResolver.resolve`
+ method.
+
+ If ``package`` is a *module* or *module name* (as opposed to a package or
+ package name), its containing package is computed and this
+ package is used to derive the package name (all names are resolved relative
+ to packages, never to modules). For example, if the ``package`` argument
+ to this type was passed the string ``xml.dom.expatbuilder``, and
+ ``template.pt`` is supplied to the
+ :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting absolute
+ asset spec would be ``xml.minidom:template.pt``, because
+ ``xml.dom.expatbuilder`` is a module object, not a package object.
+
+ If ``package`` is a *package* or *package name* (as opposed to a module or
+ module name), this package will be used to compute relative
+ asset specifications. For example, if the ``package`` argument to this
+ type was passed the string ``xml.dom``, and ``template.pt`` is supplied
+ to the :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting
+ absolute asset spec would be ``xml.minidom:template.pt``.
+ """
+ def resolve(self, spec):
+ """
+ Resolve the asset spec named as ``spec`` to an object that has the
+ attributes and methods described in
+ :class:`pyramid.interfaces.IAssetDescriptor`.
+
+ If ``spec`` is an absolute filename
+ (e.g. ``/path/to/myproject/templates/foo.pt``) or an absolute asset
+ spec (e.g. ``myproject:templates.foo.pt``), an asset descriptor is
+ returned without taking into account the ``package`` passed to this
+ class' constructor.
+
+ If ``spec`` is a *relative* asset specification (an asset
+ specification without a ``:`` in it, e.g. ``templates/foo.pt``), the
+ ``package`` argument of the constructor is used as the package
+ portion of the asset spec. For example:
+
+ .. code-block:: python
+
+ a = AssetResolver('myproject')
+ resolver = a.resolve('templates/foo.pt')
+ print(resolver.abspath())
+ # -> /path/to/myproject/templates/foo.pt
+
+ If the AssetResolver is constructed without a ``package`` argument of
+ ``None``, and a relative asset specification is passed to
+ ``resolve``, an :exc:`ValueError` exception is raised.
+ """
+ if os.path.isabs(spec):
+ return FSAssetDescriptor(spec)
+ path = spec
+ if ':' in path:
+ package_name, path = spec.split(':', 1)
+ else:
+ if self.package is CALLER_PACKAGE:
+ package_name = caller_package().__name__
+ else:
+ package_name = getattr(self.package, '__name__', None)
+ if package_name is None:
+ raise ValueError(
+ 'relative spec %r irresolveable without package' % (spec,)
+ )
+ return PkgResourcesAssetDescriptor(package_name, path)
+
+class DottedNameResolver(Resolver):
+ """ A class used to resolve a :term:`dotted Python name` to a package or
+ module object.
+
+ .. versionadded:: 1.3
+
+ The constructor accepts a single argument named ``package`` which may be
+ any of:
+
+ - A fully qualified (not relative) dotted name to a module or package
+
+ - a Python module or package object
+
+ - The value ``None``
+
+ - The constant value :attr:`pyramid.path.CALLER_PACKAGE`.
+
+ The default value is :attr:`pyramid.path.CALLER_PACKAGE`.
+
+ The ``package`` is used when a relative dotted name is supplied to the
+ :meth:`~pyramid.path.DottedNameResolver.resolve` method. A dotted name
+ which has a ``.`` (dot) or ``:`` (colon) as its first character is
+ treated as relative.
+
+ If ``package`` is ``None``, the resolver will only be able to resolve
+ fully qualified (not relative) names. Any attempt to resolve a
+ relative name will result in an :exc:`ValueError` exception.
+
+ If ``package`` is :attr:`pyramid.path.CALLER_PACKAGE`,
+ the resolver will treat relative dotted names as relative to
+ the caller of the :meth:`~pyramid.path.DottedNameResolver.resolve`
+ method.
+
+ If ``package`` is a *module* or *module name* (as opposed to a package or
+ package name), its containing package is computed and this
+ package used to derive the package name (all names are resolved relative
+ to packages, never to modules). For example, if the ``package`` argument
+ to this type was passed the string ``xml.dom.expatbuilder``, and
+ ``.mindom`` is supplied to the
+ :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting
+ import would be for ``xml.minidom``, because ``xml.dom.expatbuilder`` is
+ a module object, not a package object.
+
+ If ``package`` is a *package* or *package name* (as opposed to a module or
+ module name), this package will be used to relative compute
+ dotted names. For example, if the ``package`` argument to this type was
+ passed the string ``xml.dom``, and ``.minidom`` is supplied to the
+ :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting
+ import would be for ``xml.minidom``.
+ """
+ def resolve(self, dotted):
+ """
+ This method resolves a dotted name reference to a global Python
+ object (an object which can be imported) to the object itself.
+
+ Two dotted name styles are supported:
+
+ - ``pkg_resources``-style dotted names where non-module attributes
+ of a package are separated from the rest of the path using a ``:``
+ e.g. ``package.module:attr``.
+
+ - ``zope.dottedname``-style dotted names where non-module
+ attributes of a package are separated from the rest of the path
+ using a ``.`` e.g. ``package.module.attr``.
+
+ These styles can be used interchangeably. If the supplied name
+ contains a ``:`` (colon), the ``pkg_resources`` resolution
+ mechanism will be chosen, otherwise the ``zope.dottedname``
+ resolution mechanism will be chosen.
+
+ If the ``dotted`` argument passed to this method is not a string, a
+ :exc:`ValueError` will be raised.
+
+ When a dotted name cannot be resolved, a :exc:`ValueError` error is
+ raised.
+
+ Example:
+
+ .. code-block:: python
+
+ r = DottedNameResolver()
+ v = r.resolve('xml') # v is the xml module
+
+ """
+ if not isinstance(dotted, string_types):
+ raise ValueError('%r is not a string' % (dotted,))
+ package = self.package
+ if package is CALLER_PACKAGE:
+ package = caller_package()
+ return self._resolve(dotted, package)
+
+ def maybe_resolve(self, dotted):
+ """
+ This method behaves just like
+ :meth:`~pyramid.path.DottedNameResolver.resolve`, except if the
+ ``dotted`` value passed is not a string, it is simply returned. For
+ example:
+
+ .. code-block:: python
+
+ import xml
+ r = DottedNameResolver()
+ v = r.maybe_resolve(xml)
+ # v is the xml module; no exception raised
+ """
+ if isinstance(dotted, string_types):
+ package = self.package
+ if package is CALLER_PACKAGE:
+ package = caller_package()
+ return self._resolve(dotted, package)
+ return dotted
+
+ def _resolve(self, dotted, package):
+ if ':' in dotted:
+ return self._pkg_resources_style(dotted, package)
+ else:
+ return self._zope_dottedname_style(dotted, package)
+
+ def _pkg_resources_style(self, value, package):
+ """ package.module:attr style """
+ if value.startswith(('.', ':')):
+ if not package:
+ raise ValueError(
+ 'relative name %r irresolveable without package' % (value,)
+ )
+ if value in ['.', ':']:
+ value = package.__name__
+ else:
+ value = package.__name__ + value
+ # Calling EntryPoint.load with an argument is deprecated.
+ # See https://pythonhosted.org/setuptools/history.html#id8
+ ep = pkg_resources.EntryPoint.parse('x=%s' % value)
+ if hasattr(ep, 'resolve'):
+ # setuptools>=10.2
+ return ep.resolve() # pragma: NO COVER
+ else:
+ return ep.load(False) # pragma: NO COVER
+
+ def _zope_dottedname_style(self, value, package):
+ """ package.module.attr style """
+ module = getattr(package, '__name__', None) # package may be None
+ if not module:
+ module = None
+ if value == '.':
+ if module is None:
+ raise ValueError(
+ 'relative name %r irresolveable without package' % (value,)
+ )
+ name = module.split('.')
+ else:
+ name = value.split('.')
+ if not name[0]:
+ if module is None:
+ raise ValueError(
+ 'relative name %r irresolveable without '
+ 'package' % (value,)
+ )
+ module = module.split('.')
+ name.pop(0)
+ while not name[0]:
+ module.pop()
+ name.pop(0)
+ name = module + name
+
+ used = name.pop(0)
+ found = __import__(used)
+ for n in name:
+ used += '.' + n
+ try:
+ found = getattr(found, n)
+ except AttributeError:
+ __import__(used)
+ found = getattr(found, n) # pragma: no cover
+
+ return found
+
+@implementer(IAssetDescriptor)
+class PkgResourcesAssetDescriptor(object):
+ pkg_resources = pkg_resources
+
+ def __init__(self, pkg_name, path):
+ self.pkg_name = pkg_name
+ self.path = path
+
+ def absspec(self):
+ return '%s:%s' % (self.pkg_name, self.path)
+
+ def abspath(self):
+ return os.path.abspath(
+ self.pkg_resources.resource_filename(self.pkg_name, self.path))
+
+ def stream(self):
+ return self.pkg_resources.resource_stream(self.pkg_name, self.path)
+
+ def isdir(self):
+ return self.pkg_resources.resource_isdir(self.pkg_name, self.path)
+
+ def listdir(self):
+ return self.pkg_resources.resource_listdir(self.pkg_name, self.path)
+
+ def exists(self):
+ return self.pkg_resources.resource_exists(self.pkg_name, self.path)
+
+@implementer(IAssetDescriptor)
+class FSAssetDescriptor(object):
+
+ def __init__(self, path):
+ self.path = os.path.abspath(path)
+
+ def absspec(self):
+ raise NotImplementedError
+
+ def abspath(self):
+ return self.path
+
+ def stream(self):
+ return open(self.path, 'rb')
+
+ def isdir(self):
+ return os.path.isdir(self.path)
+
+ def listdir(self):
+ return os.listdir(self.path)
+
+ def exists(self):
+ return os.path.exists(self.path)
diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py
new file mode 100644
index 000000000..97edae8a0
--- /dev/null
+++ b/src/pyramid/predicates.py
@@ -0,0 +1,336 @@
+import re
+
+from pyramid.exceptions import ConfigurationError
+
+from pyramid.compat import is_nonstr_iter
+
+from pyramid.csrf import check_csrf_token
+from pyramid.traversal import (
+ find_interface,
+ traversal_path,
+ resource_path_tuple
+ )
+
+from pyramid.urldispatch import _compile_route
+from pyramid.util import (
+ as_sorted_tuple,
+ object_description,
+)
+
+_marker = object()
+
+class XHRPredicate(object):
+ def __init__(self, val, config):
+ self.val = bool(val)
+
+ def text(self):
+ return 'xhr = %s' % self.val
+
+ phash = text
+
+ def __call__(self, context, request):
+ return bool(request.is_xhr) is self.val
+
+class RequestMethodPredicate(object):
+ def __init__(self, val, config):
+ request_method = as_sorted_tuple(val)
+ if 'GET' in request_method and 'HEAD' not in request_method:
+ # GET implies HEAD too
+ request_method = as_sorted_tuple(request_method + ('HEAD',))
+ self.val = request_method
+
+ def text(self):
+ return 'request_method = %s' % (','.join(self.val))
+
+ phash = text
+
+ def __call__(self, context, request):
+ return request.method in self.val
+
+class PathInfoPredicate(object):
+ def __init__(self, val, config):
+ self.orig = val
+ try:
+ val = re.compile(val)
+ except re.error as why:
+ raise ConfigurationError(why.args[0])
+ self.val = val
+
+ def text(self):
+ return 'path_info = %s' % (self.orig,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return self.val.match(request.upath_info) is not None
+
+class RequestParamPredicate(object):
+ def __init__(self, val, config):
+ val = as_sorted_tuple(val)
+ reqs = []
+ for p in val:
+ k = p
+ v = None
+ if p.startswith('='):
+ if '=' in p[1:]:
+ k, v = p[1:].split('=', 1)
+ k = '=' + k
+ k, v = k.strip(), v.strip()
+ elif '=' in p:
+ k, v = p.split('=', 1)
+ k, v = k.strip(), v.strip()
+ reqs.append((k, v))
+ self.val = val
+ self.reqs = reqs
+
+ def text(self):
+ return 'request_param %s' % ','.join(
+ ['%s=%s' % (x,y) if y else x for x, y in self.reqs]
+ )
+
+ phash = text
+
+ def __call__(self, context, request):
+ for k, v in self.reqs:
+ actual = request.params.get(k)
+ if actual is None:
+ return False
+ if v is not None and actual != v:
+ return False
+ return True
+
+class HeaderPredicate(object):
+ def __init__(self, val, config):
+ name = val
+ v = None
+ if ':' in name:
+ name, val_str = name.split(':', 1)
+ try:
+ v = re.compile(val_str)
+ except re.error as why:
+ raise ConfigurationError(why.args[0])
+ if v is None:
+ self._text = 'header %s' % (name,)
+ else:
+ self._text = 'header %s=%s' % (name, val_str)
+ self.name = name
+ self.val = v
+
+ def text(self):
+ return self._text
+
+ phash = text
+
+ def __call__(self, context, request):
+ if self.val is None:
+ return self.name in request.headers
+ val = request.headers.get(self.name)
+ if val is None:
+ return False
+ return self.val.match(val) is not None
+
+class AcceptPredicate(object):
+ _is_using_deprecated_ranges = False
+
+ def __init__(self, values, config):
+ if not is_nonstr_iter(values):
+ values = (values,)
+ # deprecated media ranges were only supported in versions of the
+ # predicate that didn't support lists, so check it here
+ if len(values) == 1 and '*' in values[0]:
+ self._is_using_deprecated_ranges = True
+ self.values = values
+
+ def text(self):
+ return 'accept = %s' % (', '.join(self.values),)
+
+ phash = text
+
+ def __call__(self, context, request):
+ if self._is_using_deprecated_ranges:
+ return self.values[0] in request.accept
+ return bool(request.accept.acceptable_offers(self.values))
+
+class ContainmentPredicate(object):
+ def __init__(self, val, config):
+ self.val = config.maybe_dotted(val)
+
+ def text(self):
+ return 'containment = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ ctx = getattr(request, 'context', context)
+ return find_interface(ctx, self.val) is not None
+
+class RequestTypePredicate(object):
+ def __init__(self, val, config):
+ self.val = val
+
+ def text(self):
+ return 'request_type = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ return self.val.providedBy(request)
+
+class MatchParamPredicate(object):
+ def __init__(self, val, config):
+ val = as_sorted_tuple(val)
+ self.val = val
+ reqs = [ p.split('=', 1) for p in val ]
+ self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ]
+
+ def text(self):
+ return 'match_param %s' % ','.join(
+ ['%s=%s' % (x,y) for x, y in self.reqs]
+ )
+
+ 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
+ return True
+
+class CustomPredicate(object):
+ def __init__(self, func, config):
+ self.func = func
+
+ def text(self):
+ return getattr(
+ self.func,
+ '__text__',
+ 'custom predicate: %s' % object_description(self.func)
+ )
+
+ def phash(self):
+ # using hash() here rather than id() is intentional: we
+ # want to allow custom predicates that are part of
+ # frameworks to be able to define custom __hash__
+ # functions for custom predicates, so that the hash output
+ # of predicate instances which are "logically the same"
+ # may compare equal.
+ return 'custom:%r' % hash(self.func)
+
+ def __call__(self, context, request):
+ return self.func(context, request)
+
+
+class TraversePredicate(object):
+ # Can only be used as a *route* "predicate"; it adds 'traverse' to the
+ # matchdict if it's specified in the routing args. This causes the
+ # ResourceTreeTraverser to use the resolved traverse pattern as the
+ # traversal path.
+ def __init__(self, val, config):
+ _, self.tgenerate = _compile_route(val)
+ self.val = val
+
+ def text(self):
+ return 'traverse matchdict pseudo-predicate'
+
+ def phash(self):
+ # This isn't actually a predicate, it's just a infodict modifier that
+ # injects ``traverse`` into the matchdict. As a result, we don't
+ # need to update the hash.
+ return ''
+
+ def __call__(self, context, request):
+ if 'traverse' in context:
+ return True
+ m = context['match']
+ tvalue = self.tgenerate(m) # tvalue will be urlquoted string
+ m['traverse'] = traversal_path(tvalue)
+ # This isn't actually a predicate, it's just a infodict modifier that
+ # injects ``traverse`` into the matchdict. As a result, we just
+ # return True.
+ return True
+
+class CheckCSRFTokenPredicate(object):
+
+ check_csrf_token = staticmethod(check_csrf_token) # testing
+
+ def __init__(self, val, config):
+ self.val = val
+
+ def text(self):
+ return 'check_csrf = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ val = self.val
+ if val:
+ if val is True:
+ val = 'csrf_token'
+ return self.check_csrf_token(request, val, raises=False)
+ return True
+
+class PhysicalPathPredicate(object):
+ def __init__(self, val, config):
+ if is_nonstr_iter(val):
+ self.val = tuple(val)
+ else:
+ val = tuple(filter(None, val.split('/')))
+ self.val = ('',) + val
+
+ def text(self):
+ return 'physical_path = %s' % (self.val,)
+
+ phash = text
+
+ def __call__(self, context, request):
+ 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):
+ if is_nonstr_iter(val):
+ self.val = set(val)
+ else:
+ self.val = set((val,))
+
+ def text(self):
+ return 'effective_principals = %s' % sorted(list(self.val))
+
+ phash = text
+
+ def __call__(self, context, request):
+ req_principals = request.effective_principals
+ if is_nonstr_iter(req_principals):
+ rpset = set(req_principals)
+ if self.val.issubset(rpset):
+ return True
+ return False
+
+class Notted(object):
+ def __init__(self, predicate):
+ self.predicate = predicate
+
+ def _notted_text(self, val):
+ # if the underlying predicate doesnt return a value, it's not really
+ # a predicate, it's just something pretending to be a predicate,
+ # so dont update the hash
+ if val:
+ val = '!' + val
+ return val
+
+ def text(self):
+ return self._notted_text(self.predicate.text())
+
+ def phash(self):
+ return self._notted_text(self.predicate.phash())
+
+ def __call__(self, context, request):
+ result = self.predicate(context, request)
+ phash = self.phash()
+ if phash:
+ result = not result
+ return result
diff --git a/src/pyramid/registry.py b/src/pyramid/registry.py
new file mode 100644
index 000000000..a741c495e
--- /dev/null
+++ b/src/pyramid/registry.py
@@ -0,0 +1,297 @@
+import operator
+import threading
+
+from zope.interface import implementer
+from zope.interface.registry import Components
+
+from pyramid.compat import text_
+from pyramid.decorator import reify
+
+from pyramid.interfaces import (
+ IIntrospector,
+ IIntrospectable,
+ ISettings,
+)
+
+from pyramid.path import (
+ CALLER_PACKAGE,
+ caller_package,
+)
+
+empty = text_('')
+
+class Registry(Components, dict):
+ """ A registry object is an :term:`application registry`.
+
+ It is used by the framework itself to perform mappings of URLs to view
+ callables, as well as servicing other various framework duties. A registry
+ has its own internal API, but this API is rarely used by Pyramid
+ application developers (it's usually only used by developers of the
+ Pyramid framework and Pyramid addons). But it has a number of attributes
+ that may be useful to application developers within application code,
+ such as ``settings``, which is a dictionary containing application
+ deployment settings.
+
+ For information about the purpose and usage of the application registry,
+ see :ref:`zca_chapter`.
+
+ The registry may be used both as an :class:`pyramid.interfaces.IDict` and
+ as a Zope component registry.
+ These two ways of storing configuration are independent.
+ Applications will tend to prefer to store information as key-values
+ whereas addons may prefer to use the component registry to avoid naming
+ conflicts and to provide more complex lookup mechanisms.
+
+ The application registry is usually accessed as ``request.registry`` in
+ application code. By the time a registry is used to handle requests it
+ should be considered frozen and read-only. Any changes to its internal
+ state should be done with caution and concern for thread-safety.
+
+ """
+
+ # for optimization purposes, if no listeners are listening, don't try
+ # to notify them
+ has_listeners = False
+
+ _settings = None
+
+ def __init__(self, package_name=CALLER_PACKAGE, *args, **kw):
+ # add a registry-instance-specific lock, which is used when the lookup
+ # cache is mutated
+ self._lock = threading.Lock()
+ # add a view lookup cache
+ self._clear_view_lookup_cache()
+ if package_name is CALLER_PACKAGE:
+ package_name = caller_package().__name__
+ Components.__init__(self, package_name, *args, **kw)
+ dict.__init__(self)
+
+ def _clear_view_lookup_cache(self):
+ self._view_lookup_cache = {}
+
+ def __nonzero__(self):
+ # defeat bool determination via dict.__len__
+ return True
+
+ @reify
+ def package_name(self):
+ return self.__name__
+
+ def registerSubscriptionAdapter(self, *arg, **kw):
+ result = Components.registerSubscriptionAdapter(self, *arg, **kw)
+ self.has_listeners = True
+ return result
+
+ def registerSelfAdapter(self, required=None, provided=None, name=empty,
+ info=empty, event=True):
+ # registerAdapter analogue which always returns the object itself
+ # when required is matched
+ return self.registerAdapter(lambda x: x, required=required,
+ provided=provided, name=name,
+ info=info, event=event)
+
+ def queryAdapterOrSelf(self, object, interface, default=None):
+ # queryAdapter analogue which returns the object if it implements
+ # the interface, otherwise it will return an adaptation to the
+ # interface
+ if not interface.providedBy(object):
+ return self.queryAdapter(object, interface, default=default)
+ return object
+
+ def registerHandler(self, *arg, **kw):
+ result = Components.registerHandler(self, *arg, **kw)
+ self.has_listeners = True
+ return result
+
+ def notify(self, *events):
+ if self.has_listeners:
+ # iterating over subscribers assures they get executed
+ [ _ for _ in self.subscribers(events, None) ]
+
+ # backwards compatibility for code that wants to look up a settings
+ # object via ``registry.getUtility(ISettings)``
+ def _get_settings(self):
+ return self._settings
+
+ def _set_settings(self, settings):
+ self.registerUtility(settings, ISettings)
+ self._settings = settings
+
+ settings = property(_get_settings, _set_settings)
+
+@implementer(IIntrospector)
+class Introspector(object):
+ def __init__(self):
+ self._refs = {}
+ self._categories = {}
+ self._counter = 0
+
+ def add(self, intr):
+ category = self._categories.setdefault(intr.category_name, {})
+ category[intr.discriminator] = intr
+ category[intr.discriminator_hash] = intr
+ intr.order = self._counter
+ self._counter += 1
+
+ def get(self, category_name, discriminator, default=None):
+ category = self._categories.setdefault(category_name, {})
+ intr = category.get(discriminator, default)
+ return intr
+
+ def get_category(self, category_name, default=None, sort_key=None):
+ if sort_key is None:
+ sort_key = operator.attrgetter('order')
+ category = self._categories.get(category_name)
+ if category is None:
+ return default
+ values = category.values()
+ values = sorted(set(values), key=sort_key)
+ return [
+ {'introspectable': intr,
+ 'related': self.related(intr)}
+ for intr in values
+ ]
+
+ def categorized(self, sort_key=None):
+ L = []
+ for category_name in self.categories():
+ L.append((category_name, self.get_category(category_name,
+ sort_key=sort_key)))
+ return L
+
+ def categories(self):
+ return sorted(self._categories.keys())
+
+ def remove(self, category_name, discriminator):
+ intr = self.get(category_name, discriminator)
+ if intr is None:
+ return
+ L = self._refs.pop(intr, [])
+ for d in L:
+ L2 = self._refs[d]
+ L2.remove(intr)
+ category = self._categories[intr.category_name]
+ del category[intr.discriminator]
+ del category[intr.discriminator_hash]
+
+ def _get_intrs_by_pairs(self, pairs):
+ introspectables = []
+ for pair in pairs:
+ category_name, discriminator = pair
+ intr = self._categories.get(category_name, {}).get(discriminator)
+ if intr is None:
+ raise KeyError((category_name, discriminator))
+ introspectables.append(intr)
+ return introspectables
+
+ def relate(self, *pairs):
+ introspectables = self._get_intrs_by_pairs(pairs)
+ relatable = ((x,y) for x in introspectables for y in introspectables)
+ for x, y in relatable:
+ L = self._refs.setdefault(x, [])
+ if x is not y and y not in L:
+ L.append(y)
+
+ def unrelate(self, *pairs):
+ introspectables = self._get_intrs_by_pairs(pairs)
+ relatable = ((x,y) for x in introspectables for y in introspectables)
+ for x, y in relatable:
+ L = self._refs.get(x, [])
+ if y in L:
+ L.remove(y)
+
+ def related(self, intr):
+ category_name, discriminator = intr.category_name, intr.discriminator
+ intr = self._categories.get(category_name, {}).get(discriminator)
+ if intr is None:
+ raise KeyError((category_name, discriminator))
+ return self._refs.get(intr, [])
+
+@implementer(IIntrospectable)
+class Introspectable(dict):
+
+ order = 0 # mutated by introspector.add
+ action_info = None # mutated by self.register
+
+ def __init__(self, category_name, discriminator, title, type_name):
+ self.category_name = category_name
+ self.discriminator = discriminator
+ self.title = title
+ self.type_name = type_name
+ self._relations = []
+
+ def relate(self, category_name, discriminator):
+ self._relations.append((True, category_name, discriminator))
+
+ def unrelate(self, category_name, discriminator):
+ self._relations.append((False, category_name, discriminator))
+
+ def _assert_resolved(self):
+ assert undefer(self.discriminator) is self.discriminator
+
+ @property
+ def discriminator_hash(self):
+ self._assert_resolved()
+ return hash(self.discriminator)
+
+ def __hash__(self):
+ self._assert_resolved()
+ return hash((self.category_name,) + (self.discriminator,))
+
+ def __repr__(self):
+ self._assert_resolved()
+ return '<%s category %r, discriminator %r>' % (self.__class__.__name__,
+ self.category_name,
+ self.discriminator)
+
+ def __nonzero__(self):
+ return True
+
+ __bool__ = __nonzero__ # py3
+
+ def register(self, introspector, action_info):
+ self.discriminator = undefer(self.discriminator)
+ self.action_info = action_info
+ introspector.add(self)
+ for relate, category_name, discriminator in self._relations:
+ discriminator = undefer(discriminator)
+ if relate:
+ method = introspector.relate
+ else:
+ method = introspector.unrelate
+ method(
+ (self.category_name, self.discriminator),
+ (category_name, discriminator)
+ )
+
+class Deferred(object):
+ """ Can be used by a third-party configuration extender to wrap a
+ :term:`discriminator` during configuration if an immediately hashable
+ discriminator cannot be computed because it relies on unresolved values.
+ The function should accept no arguments and should return a hashable
+ discriminator."""
+ def __init__(self, func):
+ self.func = func
+
+ @reify
+ def value(self):
+ result = self.func()
+ del self.func
+ return result
+
+ def resolve(self):
+ return self.value
+
+def undefer(v):
+ """ Function which accepts an object and returns it unless it is a
+ :class:`pyramid.registry.Deferred` instance. If it is an instance of
+ that class, its ``resolve`` method is called, and the result of the
+ method is returned."""
+ if isinstance(v, Deferred):
+ v = v.resolve()
+ return v
+
+class predvalseq(tuple):
+ """ A subtype of tuple used to represent a sequence of predicate values """
+
+global_registry = Registry('global')
diff --git a/src/pyramid/renderers.py b/src/pyramid/renderers.py
new file mode 100644
index 000000000..d1c85b371
--- /dev/null
+++ b/src/pyramid/renderers.py
@@ -0,0 +1,529 @@
+from functools import partial
+import json
+import os
+import re
+
+from zope.interface import (
+ implementer,
+ providedBy,
+ )
+from zope.interface.registry import Components
+
+from pyramid.interfaces import (
+ IJSONAdapter,
+ IRendererFactory,
+ IRendererInfo,
+ )
+
+from pyramid.compat import (
+ string_types,
+ text_type,
+ )
+
+from pyramid.csrf import get_csrf_token
+from pyramid.decorator import reify
+
+from pyramid.events import BeforeRender
+
+from pyramid.httpexceptions import HTTPBadRequest
+
+from pyramid.path import caller_package
+
+from pyramid.response import _get_response_factory
+from pyramid.threadlocal import get_current_registry
+from pyramid.util import hide_attrs
+
+# API
+
+def render(renderer_name, value, request=None, package=None):
+ """ Using the renderer ``renderer_name`` (a template
+ or a static renderer), render the value (or set of values) present
+ in ``value``. Return the result of the renderer's ``__call__``
+ method (usually a string or Unicode).
+
+ If the ``renderer_name`` refers to a file on disk, such as when the
+ renderer is a template, it's usually best to supply the name as an
+ :term:`asset specification`
+ (e.g. ``packagename:path/to/template.pt``).
+
+ You may supply a relative asset spec as ``renderer_name``. If
+ the ``package`` argument is supplied, a relative renderer path
+ will be converted to an absolute asset specification by
+ combining the package ``package`` with the relative
+ asset specification ``renderer_name``. If ``package``
+ is ``None`` (the default), the package name of the *caller* of
+ this function will be used as the package.
+
+ The ``value`` provided will be supplied as the input to the
+ renderer. Usually, for template renderings, this should be a
+ dictionary. For other renderers, this will need to be whatever
+ sort of value the renderer expects.
+
+ The 'system' values supplied to the renderer will include a basic set of
+ top-level system names, such as ``request``, ``context``,
+ ``renderer_name``, and ``view``. See :ref:`renderer_system_values` for
+ the full list. If :term:`renderer globals` have been specified, these
+ will also be used to augment the value.
+
+ Supply a ``request`` parameter in order to provide the renderer
+ with the most correct 'system' values (``request`` and ``context``
+ in particular).
+
+ """
+ try:
+ registry = request.registry
+ except AttributeError:
+ registry = None
+ if package is None:
+ package = caller_package()
+ helper = RendererHelper(name=renderer_name, package=package,
+ registry=registry)
+
+ with hide_attrs(request, 'response'):
+ result = helper.render(value, None, request=request)
+
+ return result
+
+def render_to_response(renderer_name,
+ value,
+ request=None,
+ package=None,
+ response=None):
+ """ Using the renderer ``renderer_name`` (a template
+ or a static renderer), render the value (or set of values) using
+ the result of the renderer's ``__call__`` method (usually a string
+ or Unicode) as the response body.
+
+ If the renderer name refers to a file on disk (such as when the
+ renderer is a template), it's usually best to supply the name as a
+ :term:`asset specification`.
+
+ You may supply a relative asset spec as ``renderer_name``. If
+ the ``package`` argument is supplied, a relative renderer name
+ will be converted to an absolute asset specification by
+ combining the package ``package`` with the relative
+ asset specification ``renderer_name``. If you do
+ not supply a ``package`` (or ``package`` is ``None``) the package
+ name of the *caller* of this function will be used as the package.
+
+ The ``value`` provided will be supplied as the input to the
+ renderer. Usually, for template renderings, this should be a
+ dictionary. For other renderers, this will need to be whatever
+ sort of value the renderer expects.
+
+ The 'system' values supplied to the renderer will include a basic set of
+ top-level system names, such as ``request``, ``context``,
+ ``renderer_name``, and ``view``. See :ref:`renderer_system_values` for
+ the full list. If :term:`renderer globals` have been specified, these
+ will also be used to argument the value.
+
+ Supply a ``request`` parameter in order to provide the renderer
+ with the most correct 'system' values (``request`` and ``context``
+ in particular). Keep in mind that any changes made to ``request.response``
+ prior to calling this function will not be reflected in the resulting
+ response object. A new response object will be created for each call
+ unless one is passed as the ``response`` argument.
+
+ .. versionchanged:: 1.6
+ In previous versions, any changes made to ``request.response`` outside
+ of this function call would affect the returned response. This is no
+ longer the case. If you wish to send in a pre-initialized response
+ then you may pass one in the ``response`` argument.
+
+ """
+ try:
+ registry = request.registry
+ except AttributeError:
+ registry = None
+ if package is None:
+ package = caller_package()
+ helper = RendererHelper(name=renderer_name, package=package,
+ registry=registry)
+
+ with hide_attrs(request, 'response'):
+ if response is not None:
+ request.response = response
+ result = helper.render_to_response(value, None, request=request)
+
+ return result
+
+def get_renderer(renderer_name, package=None, registry=None):
+ """ Return the renderer object for the renderer ``renderer_name``.
+
+ You may supply a relative asset spec as ``renderer_name``. If
+ the ``package`` argument is supplied, a relative renderer name
+ will be converted to an absolute asset specification by
+ combining the package ``package`` with the relative
+ asset specification ``renderer_name``. If ``package`` is ``None``
+ (the default), the package name of the *caller* of this function
+ will be used as the package.
+
+ You may directly supply an :term:`application registry` using the
+ ``registry`` argument, and it will be used to look up the renderer.
+ Otherwise, the current thread-local registry (obtained via
+ :func:`~pyramid.threadlocal.get_current_registry`) will be used.
+ """
+ if package is None:
+ package = caller_package()
+ helper = RendererHelper(name=renderer_name, package=package,
+ registry=registry)
+ return helper.renderer
+
+# concrete renderer factory implementations (also API)
+
+def string_renderer_factory(info):
+ def _render(value, system):
+ if not isinstance(value, string_types):
+ value = str(value)
+ request = system.get('request')
+ if request is not None:
+ response = request.response
+ ct = response.content_type
+ if ct == response.default_content_type:
+ response.content_type = 'text/plain'
+ return value
+ return _render
+
+_marker = object()
+
+class JSON(object):
+ """ Renderer that returns a JSON-encoded string.
+
+ Configure a custom JSON renderer using the
+ :meth:`~pyramid.config.Configurator.add_renderer` API at application
+ startup time:
+
+ .. code-block:: python
+
+ from pyramid.config import Configurator
+
+ config = Configurator()
+ config.add_renderer('myjson', JSON(indent=4))
+
+ Once this renderer is registered as above, you can use
+ ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or
+ :meth:`~pyramid.config.Configurator.add_view`:
+
+ .. code-block:: python
+
+ from pyramid.view import view_config
+
+ @view_config(renderer='myjson')
+ def myview(request):
+ return {'greeting':'Hello world'}
+
+ Custom objects can be serialized using the renderer by either
+ implementing the ``__json__`` magic method, or by registering
+ adapters with the renderer. See
+ :ref:`json_serializing_custom_objects` for more information.
+
+ .. note::
+
+ The default serializer uses ``json.JSONEncoder``. A different
+ serializer can be specified via the ``serializer`` argument. Custom
+ serializers should accept the object, a callback ``default``, and any
+ extra ``kw`` keyword arguments passed during renderer construction.
+ This feature isn't widely used but it can be used to replace the
+ stock JSON serializer with, say, simplejson. If all you want to
+ do, however, is serialize custom objects, you should use the method
+ explained in :ref:`json_serializing_custom_objects` instead
+ of replacing the serializer.
+
+ .. versionadded:: 1.4
+ Prior to this version, there was no public API for supplying options
+ to the underlying serializer without defining a custom renderer.
+ """
+
+ def __init__(self, serializer=json.dumps, adapters=(), **kw):
+ """ Any keyword arguments will be passed to the ``serializer``
+ function."""
+ self.serializer = serializer
+ self.kw = kw
+ self.components = Components()
+ for type, adapter in adapters:
+ self.add_adapter(type, adapter)
+
+ def add_adapter(self, type_or_iface, adapter):
+ """ When an object of the type (or interface) ``type_or_iface`` fails
+ to automatically encode using the serializer, the renderer will use
+ the adapter ``adapter`` to convert it into a JSON-serializable
+ object. The adapter must accept two arguments: the object and the
+ currently active request.
+
+ .. code-block:: python
+
+ class Foo(object):
+ x = 5
+
+ def foo_adapter(obj, request):
+ return obj.x
+
+ renderer = JSON(indent=4)
+ renderer.add_adapter(Foo, foo_adapter)
+
+ When you've done this, the JSON renderer will be able to serialize
+ instances of the ``Foo`` class when they're encountered in your view
+ results."""
+
+ self.components.registerAdapter(adapter, (type_or_iface,),
+ IJSONAdapter)
+
+ def __call__(self, info):
+ """ Returns a plain JSON-encoded string with content-type
+ ``application/json``. The content-type may be overridden by
+ setting ``request.response.content_type``."""
+ def _render(value, system):
+ request = system.get('request')
+ if request is not None:
+ response = request.response
+ ct = response.content_type
+ if ct == response.default_content_type:
+ response.content_type = 'application/json'
+ default = self._make_default(request)
+ return self.serializer(value, default=default, **self.kw)
+
+ return _render
+
+ def _make_default(self, request):
+ def default(obj):
+ if hasattr(obj, '__json__'):
+ return obj.__json__(request)
+ obj_iface = providedBy(obj)
+ adapters = self.components.adapters
+ result = adapters.lookup((obj_iface,), IJSONAdapter,
+ default=_marker)
+ if result is _marker:
+ raise TypeError('%r is not JSON serializable' % (obj,))
+ return result(obj, request)
+ return default
+
+json_renderer_factory = JSON() # bw compat
+
+JSONP_VALID_CALLBACK = re.compile(r"^[$a-z_][$0-9a-z_\.\[\]]+[^.]$", re.I)
+
+class JSONP(JSON):
+ """ `JSONP <https://en.wikipedia.org/wiki/JSONP>`_ renderer factory helper
+ which implements a hybrid json/jsonp renderer. JSONP is useful for
+ making cross-domain AJAX requests.
+
+ Configure a JSONP renderer using the
+ :meth:`pyramid.config.Configurator.add_renderer` API at application
+ startup time:
+
+ .. code-block:: python
+
+ from pyramid.config import Configurator
+
+ config = Configurator()
+ config.add_renderer('jsonp', JSONP(param_name='callback'))
+
+ The class' constructor also accepts arbitrary keyword arguments. All
+ keyword arguments except ``param_name`` are passed to the ``json.dumps``
+ function as its keyword arguments.
+
+ .. code-block:: python
+
+ from pyramid.config import Configurator
+
+ config = Configurator()
+ config.add_renderer('jsonp', JSONP(param_name='callback', indent=4))
+
+ .. versionchanged:: 1.4
+ The ability of this class to accept a ``**kw`` in its constructor.
+
+ The arguments passed to this class' constructor mean the same thing as
+ the arguments passed to :class:`pyramid.renderers.JSON` (including
+ ``serializer`` and ``adapters``).
+
+ Once this renderer is registered via
+ :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use
+ ``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or
+ :meth:`pyramid.config.Configurator.add_view``:
+
+ .. code-block:: python
+
+ from pyramid.view import view_config
+
+ @view_config(renderer='jsonp')
+ def myview(request):
+ return {'greeting':'Hello world'}
+
+ When a view is called that uses the JSONP renderer:
+
+ - If there is a parameter in the request's HTTP query string that matches
+ the ``param_name`` of the registered JSONP renderer (by default,
+ ``callback``), the renderer will return a JSONP response.
+
+ - If there is no callback parameter in the request's query string, the
+ renderer will return a 'plain' JSON response.
+
+ .. versionadded:: 1.1
+
+ .. seealso::
+
+ See also :ref:`jsonp_renderer`.
+ """
+
+ def __init__(self, param_name='callback', **kw):
+ self.param_name = param_name
+ JSON.__init__(self, **kw)
+
+ def __call__(self, info):
+ """ Returns JSONP-encoded string with content-type
+ ``application/javascript`` if query parameter matching
+ ``self.param_name`` is present in request.GET; otherwise returns
+ plain-JSON encoded string with content-type ``application/json``"""
+ def _render(value, system):
+ request = system.get('request')
+ default = self._make_default(request)
+ val = self.serializer(value, default=default, **self.kw)
+ ct = 'application/json'
+ body = val
+ if request is not None:
+ callback = request.GET.get(self.param_name)
+
+ if callback is not None:
+ if not JSONP_VALID_CALLBACK.match(callback):
+ raise HTTPBadRequest('Invalid JSONP callback function name.')
+
+ ct = 'application/javascript'
+ body = '/**/{0}({1});'.format(callback, val)
+ response = request.response
+ if response.content_type == response.default_content_type:
+ response.content_type = ct
+ return body
+ return _render
+
+@implementer(IRendererInfo)
+class RendererHelper(object):
+ def __init__(self, name=None, package=None, registry=None):
+ if name and '.' in name:
+ rtype = os.path.splitext(name)[1]
+ else:
+ # important.. must be a string; cannot be None; see issue 249
+ rtype = name or ''
+
+ if registry is None:
+ registry = get_current_registry()
+
+ self.name = name
+ self.package = package
+ self.type = rtype
+ self.registry = registry
+
+ @reify
+ def settings(self):
+ settings = self.registry.settings
+ if settings is None:
+ settings = {}
+ return settings
+
+ @reify
+ def renderer(self):
+ factory = self.registry.queryUtility(IRendererFactory, name=self.type)
+ if factory is None:
+ raise ValueError(
+ 'No such renderer factory %s' % str(self.type))
+ return factory(self)
+
+ def get_renderer(self):
+ return self.renderer
+
+ def render_view(self, request, response, view, context):
+ system = {'view':view,
+ 'renderer_name':self.name, # b/c
+ 'renderer_info':self,
+ 'context':context,
+ 'request':request,
+ 'req':request,
+ 'get_csrf_token':partial(get_csrf_token, request),
+ }
+ return self.render_to_response(response, system, request=request)
+
+ def render(self, value, system_values, request=None):
+ renderer = self.renderer
+ if system_values is None:
+ system_values = {
+ 'view':None,
+ 'renderer_name':self.name, # b/c
+ 'renderer_info':self,
+ 'context':getattr(request, 'context', None),
+ 'request':request,
+ 'req':request,
+ 'get_csrf_token':partial(get_csrf_token, request),
+ }
+
+ system_values = BeforeRender(system_values, value)
+
+ registry = self.registry
+ registry.notify(system_values)
+ result = renderer(value, system_values)
+ return result
+
+ def render_to_response(self, value, system_values, request=None):
+ result = self.render(value, system_values, request=request)
+ return self._make_response(result, request)
+
+ def _make_response(self, result, request):
+ # broken out of render_to_response as a separate method for testing
+ # purposes
+ response = getattr(request, 'response', None)
+ if response is None:
+ # request is None or request is not a pyramid.response.Response
+ registry = self.registry
+ response_factory = _get_response_factory(registry)
+ response = response_factory(request)
+
+ if result is not None:
+ if isinstance(result, text_type):
+ response.text = result
+ elif isinstance(result, bytes):
+ response.body = result
+ elif hasattr(result, '__iter__'):
+ response.app_iter = result
+ else:
+ response.body = result
+
+ return response
+
+ def clone(self, name=None, package=None, registry=None):
+ if name is None:
+ name = self.name
+ if package is None:
+ package = self.package
+ if registry is None:
+ registry = self.registry
+ return self.__class__(name=name, package=package, registry=registry)
+
+class NullRendererHelper(RendererHelper):
+ """ Special renderer helper that has render_* methods which simply return
+ the value they are fed rather than converting them to response objects;
+ useful for testing purposes and special case view configuration
+ registrations that want to use the view configuration machinery but do
+ not want actual rendering to happen ."""
+ def __init__(self, name=None, package=None, registry=None):
+ # we override the initializer to avoid calling get_current_registry
+ # (it will return a reference to the global registry when this
+ # thing is called at module scope; we don't want that).
+ self.name = None
+ self.package = None
+ self.type = ''
+ self.registry = None
+
+ @property
+ def settings(self):
+ return {}
+
+ def render_view(self, request, value, view, context):
+ return value
+
+ def render(self, value, system_values, request=None):
+ return value
+
+ def render_to_response(self, value, system_values, request=None):
+ return value
+
+ def clone(self, name=None, package=None, registry=None):
+ return self
+
+null_renderer = NullRendererHelper()
diff --git a/src/pyramid/request.py b/src/pyramid/request.py
new file mode 100644
index 000000000..201f1d648
--- /dev/null
+++ b/src/pyramid/request.py
@@ -0,0 +1,334 @@
+from collections import deque
+import json
+
+from zope.interface import implementer
+from zope.interface.interface import InterfaceClass
+
+from webob import BaseRequest
+
+from pyramid.interfaces import (
+ IRequest,
+ IRequestExtensions,
+ IResponse,
+ ISessionFactory,
+ )
+
+from pyramid.compat import (
+ text_,
+ bytes_,
+ native_,
+ iteritems_,
+ )
+
+from pyramid.decorator import reify
+from pyramid.i18n import LocalizerRequestMixin
+from pyramid.response import Response, _get_response_factory
+from pyramid.security import (
+ AuthenticationAPIMixin,
+ AuthorizationAPIMixin,
+ )
+from pyramid.url import URLMethodsMixin
+from pyramid.util import (
+ InstancePropertyHelper,
+ InstancePropertyMixin,
+)
+from pyramid.view import ViewMethodsMixin
+
+class TemplateContext(object):
+ pass
+
+class CallbackMethodsMixin(object):
+ @reify
+ def finished_callbacks(self):
+ return deque()
+
+ @reify
+ def response_callbacks(self):
+ return deque()
+
+ def add_response_callback(self, callback):
+ """
+ Add a callback to the set of callbacks to be called by the
+ :term:`router` at a point after a :term:`response` object is
+ successfully created. :app:`Pyramid` does not have a
+ global response object: this functionality allows an
+ application to register an action to be performed against the
+ response once one is created.
+
+ A 'callback' is a callable which accepts two positional
+ parameters: ``request`` and ``response``. For example:
+
+ .. code-block:: python
+ :linenos:
+
+ def cache_callback(request, response):
+ 'Set the cache_control max_age for the response'
+ response.cache_control.max_age = 360
+ request.add_response_callback(cache_callback)
+
+ Response callbacks are called in the order they're added
+ (first-to-most-recently-added). No response callback is
+ called if an exception happens in application code, or if the
+ response object returned by :term:`view` code is invalid.
+
+ All response callbacks are called *after* the tweens and
+ *before* the :class:`pyramid.events.NewResponse` event is sent.
+
+ Errors raised by callbacks are not handled specially. They
+ will be propagated to the caller of the :app:`Pyramid`
+ router application.
+
+ .. seealso::
+
+ See also :ref:`using_response_callbacks`.
+ """
+
+ self.response_callbacks.append(callback)
+
+ def _process_response_callbacks(self, response):
+ callbacks = self.response_callbacks
+ while callbacks:
+ callback = callbacks.popleft()
+ callback(self, response)
+
+ def add_finished_callback(self, callback):
+ """
+ Add a callback to the set of callbacks to be called
+ unconditionally by the :term:`router` at the very end of
+ request processing.
+
+ ``callback`` is a callable which accepts a single positional
+ parameter: ``request``. For example:
+
+ .. code-block:: python
+ :linenos:
+
+ import transaction
+
+ def commit_callback(request):
+ '''commit or abort the transaction associated with request'''
+ if request.exception is not None:
+ transaction.abort()
+ else:
+ transaction.commit()
+ request.add_finished_callback(commit_callback)
+
+ Finished callbacks are called in the order they're added (
+ first- to most-recently- added). Finished callbacks (unlike
+ response callbacks) are *always* called, even if an exception
+ happens in application code that prevents a response from
+ being generated.
+
+ The set of finished callbacks associated with a request are
+ called *very late* in the processing of that request; they are
+ essentially the last thing called by the :term:`router`. They
+ are called after response processing has already occurred in a
+ top-level ``finally:`` block within the router request
+ processing code. As a result, mutations performed to the
+ ``request`` provided to a finished callback will have no
+ meaningful effect, because response processing will have
+ already occurred, and the request's scope will expire almost
+ immediately after all finished callbacks have been processed.
+
+ Errors raised by finished callbacks are not handled specially.
+ They will be propagated to the caller of the :app:`Pyramid`
+ router application.
+
+ .. seealso::
+
+ See also :ref:`using_finished_callbacks`.
+ """
+ self.finished_callbacks.append(callback)
+
+ def _process_finished_callbacks(self):
+ callbacks = self.finished_callbacks
+ while callbacks:
+ callback = callbacks.popleft()
+ callback(self)
+
+@implementer(IRequest)
+class Request(
+ BaseRequest,
+ URLMethodsMixin,
+ CallbackMethodsMixin,
+ InstancePropertyMixin,
+ LocalizerRequestMixin,
+ AuthenticationAPIMixin,
+ AuthorizationAPIMixin,
+ ViewMethodsMixin,
+ ):
+ """
+ A subclass of the :term:`WebOb` Request class. An instance of
+ this class is created by the :term:`router` and is provided to a
+ view callable (and to other subsystems) as the ``request``
+ argument.
+
+ The documentation below (save for the ``add_response_callback`` and
+ ``add_finished_callback`` methods, which are defined in this subclass
+ itself, and the attributes ``context``, ``registry``, ``root``,
+ ``subpath``, ``traversed``, ``view_name``, ``virtual_root`` , and
+ ``virtual_root_path``, each of which is added to the request by the
+ :term:`router` at request ingress time) are autogenerated from the WebOb
+ source code used when this documentation was generated.
+
+ Due to technical constraints, we can't yet display the WebOb
+ version number from which this documentation is autogenerated, but
+ it will be the 'prevailing WebOb version' at the time of the
+ release of this :app:`Pyramid` version. See
+ https://webob.org/ for further information.
+ """
+ exception = None
+ exc_info = None
+ matchdict = None
+ matched_route = None
+ request_iface = IRequest
+
+ ResponseClass = Response
+
+ @reify
+ def tmpl_context(self):
+ # docs-deprecated template context for Pylons-like apps; do not
+ # remove.
+ return TemplateContext()
+
+ @reify
+ def session(self):
+ """ Obtain the :term:`session` object associated with this
+ request. If a :term:`session factory` has not been registered
+ during application configuration, a
+ :class:`pyramid.exceptions.ConfigurationError` will be raised"""
+ factory = self.registry.queryUtility(ISessionFactory)
+ if factory is None:
+ raise AttributeError(
+ 'No session factory registered '
+ '(see the Sessions chapter of the Pyramid documentation)')
+ return factory(self)
+
+ @reify
+ def response(self):
+ """This attribute is actually a "reified" property which returns an
+ instance of the :class:`pyramid.response.Response`. class. The
+ response object returned does not exist until this attribute is
+ accessed. Subsequent accesses will return the same Response object.
+
+ The ``request.response`` API is used by renderers. A render obtains
+ the response object it will return from a view that uses that renderer
+ by accessing ``request.response``. Therefore, it's possible to use the
+ ``request.response`` API to set up a response object with "the
+ right" attributes (e.g. by calling ``request.response.set_cookie()``)
+ within a view that uses a renderer. Mutations to this response object
+ will be preserved in the response sent to the client."""
+ response_factory = _get_response_factory(self.registry)
+ return response_factory(self)
+
+ def is_response(self, ob):
+ """ Return ``True`` if the object passed as ``ob`` is a valid
+ response object, ``False`` otherwise."""
+ if ob.__class__ is Response:
+ return True
+ registry = self.registry
+ adapted = registry.queryAdapterOrSelf(ob, IResponse)
+ if adapted is None:
+ return False
+ return adapted is ob
+
+ @property
+ def json_body(self):
+ return json.loads(text_(self.body, self.charset))
+
+
+def route_request_iface(name, bases=()):
+ # zope.interface treats the __name__ as the __doc__ and changes __name__
+ # to None for interfaces that contain spaces if you do not pass a
+ # nonempty __doc__ (insane); see
+ # zope.interface.interface.Element.__init__ and
+ # https://github.com/Pylons/pyramid/issues/232; as a result, always pass
+ # __doc__ to the InterfaceClass constructor.
+ iface = InterfaceClass('%s_IRequest' % name, bases=bases,
+ __doc__="route_request_iface-generated interface")
+ # for exception view lookups
+ iface.combined = InterfaceClass(
+ '%s_combined_IRequest' % name,
+ bases=(iface, IRequest),
+ __doc__='route_request_iface-generated combined interface')
+ return iface
+
+
+def add_global_response_headers(request, headerlist):
+ def add_headers(request, response):
+ for k, v in headerlist:
+ response.headerlist.append((k, v))
+ request.add_response_callback(add_headers)
+
+def call_app_with_subpath_as_path_info(request, app):
+ # Copy the request. Use the source request's subpath (if it exists) as
+ # the new request's PATH_INFO. Set the request copy's SCRIPT_NAME to the
+ # prefix before the subpath. Call the application with the new request
+ # and return a response.
+ #
+ # Postconditions:
+ # - SCRIPT_NAME and PATH_INFO are empty or start with /
+ # - At least one of SCRIPT_NAME or PATH_INFO are set.
+ # - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should
+ # be '/').
+
+ environ = request.environ
+ script_name = environ.get('SCRIPT_NAME', '')
+ path_info = environ.get('PATH_INFO', '/')
+ subpath = list(getattr(request, 'subpath', ()))
+
+ new_script_name = ''
+
+ # compute new_path_info
+ new_path_info = '/' + '/'.join([native_(x.encode('utf-8'), 'latin-1')
+ for x in subpath])
+
+ if new_path_info != '/': # don't want a sole double-slash
+ if path_info != '/': # if orig path_info is '/', we're already done
+ if path_info.endswith('/'):
+ # readd trailing slash stripped by subpath (traversal)
+ # conversion
+ new_path_info += '/'
+
+ # compute new_script_name
+ workback = (script_name + path_info).split('/')
+
+ tmp = []
+ while workback:
+ if tmp == subpath:
+ break
+ el = workback.pop()
+ if el:
+ tmp.insert(0, text_(bytes_(el, 'latin-1'), 'utf-8'))
+
+ # strip all trailing slashes from workback to avoid appending undue slashes
+ # to end of script_name
+ while workback and (workback[-1] == ''):
+ workback = workback[:-1]
+
+ new_script_name = '/'.join(workback)
+
+ new_request = request.copy()
+ new_request.environ['SCRIPT_NAME'] = new_script_name
+ new_request.environ['PATH_INFO'] = new_path_info
+
+ return new_request.get_response(app)
+
+def apply_request_extensions(request, extensions=None):
+ """Apply request extensions (methods and properties) to an instance of
+ :class:`pyramid.interfaces.IRequest`. This method is dependent on the
+ ``request`` containing a properly initialized registry.
+
+ After invoking this method, the ``request`` should have the methods
+ and properties that were defined using
+ :meth:`pyramid.config.Configurator.add_request_method`.
+ """
+ if extensions is None:
+ extensions = request.registry.queryUtility(IRequestExtensions)
+ if extensions is not None:
+ for name, fn in iteritems_(extensions.methods):
+ method = fn.__get__(request, request.__class__)
+ setattr(request, name, method)
+
+ InstancePropertyHelper.apply_properties(
+ request, extensions.descriptors)
diff --git a/src/pyramid/resource.py b/src/pyramid/resource.py
new file mode 100644
index 000000000..986c75e37
--- /dev/null
+++ b/src/pyramid/resource.py
@@ -0,0 +1,5 @@
+""" Backwards compatibility shim module (forever). """
+from pyramid.asset import * # b/w compat
+resolve_resource_spec = resolve_asset_spec
+resource_spec_from_abspath = asset_spec_from_abspath
+abspath_from_resource_spec = abspath_from_asset_spec
diff --git a/src/pyramid/response.py b/src/pyramid/response.py
new file mode 100644
index 000000000..1e2546ed0
--- /dev/null
+++ b/src/pyramid/response.py
@@ -0,0 +1,211 @@
+import mimetypes
+from os.path import (
+ getmtime,
+ getsize,
+ )
+
+import venusian
+
+from webob import Response as _Response
+from zope.interface import implementer
+from pyramid.interfaces import IResponse, IResponseFactory
+
+
+def init_mimetypes(mimetypes):
+ # this is a function so it can be unittested
+ if hasattr(mimetypes, 'init'):
+ mimetypes.init()
+ return True
+ return False
+
+# See http://bugs.python.org/issue5853 which is a recursion bug
+# that seems to effect Python 2.6, Python 2.6.1, and 2.6.2 (a fix
+# has been applied on the Python 2 trunk).
+init_mimetypes(mimetypes)
+
+_BLOCK_SIZE = 4096 * 64 # 256K
+
+@implementer(IResponse)
+class Response(_Response):
+ pass
+
+class FileResponse(Response):
+ """
+ A Response object that can be used to serve a static file from disk
+ simply.
+
+ ``path`` is a file path on disk.
+
+ ``request`` must be a Pyramid :term:`request` object. Note
+ that a request *must* be passed if the response is meant to attempt to
+ use the ``wsgi.file_wrapper`` feature of the web server that you're using
+ to serve your Pyramid application.
+
+ ``cache_max_age`` is the number of seconds that should be used
+ to HTTP cache this response.
+
+ ``content_type`` is the content_type of the response.
+
+ ``content_encoding`` is the content_encoding of the response.
+ It's generally safe to leave this set to ``None`` if you're serving a
+ binary file. This argument will be ignored if you also leave
+ ``content-type`` as ``None``.
+ """
+ def __init__(self, path, request=None, cache_max_age=None,
+ content_type=None, content_encoding=None):
+ if content_type is None:
+ content_type, content_encoding = _guess_type(path)
+ super(FileResponse, self).__init__(
+ conditional_response=True,
+ content_type=content_type,
+ content_encoding=content_encoding
+ )
+ self.last_modified = getmtime(path)
+ content_length = getsize(path)
+ f = open(path, 'rb')
+ app_iter = None
+ if request is not None:
+ environ = request.environ
+ if 'wsgi.file_wrapper' in environ:
+ app_iter = environ['wsgi.file_wrapper'](f, _BLOCK_SIZE)
+ if app_iter is None:
+ app_iter = FileIter(f, _BLOCK_SIZE)
+ self.app_iter = app_iter
+ # assignment of content_length must come after assignment of app_iter
+ self.content_length = content_length
+ if cache_max_age is not None:
+ self.cache_expires = cache_max_age
+
+class FileIter(object):
+ """ A fixed-block-size iterator for use as a WSGI app_iter.
+
+ ``file`` is a Python file pointer (or at least an object with a ``read``
+ method that takes a size hint).
+
+ ``block_size`` is an optional block size for iteration.
+ """
+ def __init__(self, file, block_size=_BLOCK_SIZE):
+ self.file = file
+ self.block_size = block_size
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ val = self.file.read(self.block_size)
+ if not val:
+ raise StopIteration
+ return val
+
+ __next__ = next # py3
+
+ def close(self):
+ self.file.close()
+
+
+class response_adapter(object):
+ """ Decorator activated via a :term:`scan` which treats the function
+ being decorated as a :term:`response adapter` for the set of types or
+ interfaces passed as ``*types_or_ifaces`` to the decorator constructor.
+
+ For example, if you scan the following response adapter:
+
+ .. code-block:: python
+
+ from pyramid.response import Response
+ from pyramid.response import response_adapter
+
+ @response_adapter(int)
+ def myadapter(i):
+ return Response(status=i)
+
+ You can then return an integer from your view callables, and it will be
+ converted into a response with the integer as the status code.
+
+ More than one type or interface can be passed as a constructor argument.
+ The decorated response adapter will be called for each type or interface.
+
+ .. code-block:: python
+
+ import json
+
+ from pyramid.response import Response
+ from pyramid.response import response_adapter
+
+ @response_adapter(dict, list)
+ def myadapter(ob):
+ return Response(json.dumps(ob))
+
+ This method will have no effect until a :term:`scan` is performed
+ agains the package or module which contains it, ala:
+
+ .. code-block:: python
+
+ from pyramid.config import Configurator
+ config = Configurator()
+ config.scan('somepackage_containing_adapters')
+
+ Two additional keyword arguments which will be passed to the
+ :term:`venusian` ``attach`` function are ``_depth`` and ``_category``.
+
+ ``_depth`` is provided for people who wish to reuse this class from another
+ decorator. The default value is ``0`` and should be specified relative to
+ the ``response_adapter`` invocation. 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.
+
+ ``_category`` sets the decorator category name. It can be useful in
+ combination with the ``category`` argument of ``scan`` to control which
+ views should be processed.
+
+ See the :py:func:`venusian.attach` function in Venusian for more
+ information about the ``_depth`` and ``_category`` arguments.
+
+ .. versionchanged:: 1.9.1
+ Added the ``_depth`` and ``_category`` arguments.
+
+ """
+ venusian = venusian # for unit testing
+
+ def __init__(self, *types_or_ifaces, **kwargs):
+ self.types_or_ifaces = types_or_ifaces
+ self.depth = kwargs.pop('_depth', 0)
+ self.category = kwargs.pop('_category', 'pyramid')
+ self.kwargs = kwargs
+
+ def register(self, scanner, name, wrapped):
+ config = scanner.config
+ for type_or_iface in self.types_or_ifaces:
+ config.add_response_adapter(wrapped, type_or_iface, **self.kwargs)
+
+ def __call__(self, wrapped):
+ self.venusian.attach(wrapped, self.register, category=self.category,
+ depth=self.depth + 1)
+ return wrapped
+
+
+def _get_response_factory(registry):
+ """ Obtain a :class: `pyramid.response.Response` using the
+ `pyramid.interfaces.IResponseFactory`.
+ """
+ response_factory = registry.queryUtility(
+ IResponseFactory,
+ default=lambda r: Response()
+ )
+
+ return response_factory
+
+
+def _guess_type(path):
+ content_type, content_encoding = mimetypes.guess_type(
+ path,
+ strict=False
+ )
+ if content_type is None:
+ content_type = 'application/octet-stream'
+ # str-ifying content_type is a workaround for a bug in Python 2.7.7
+ # on Windows where mimetypes.guess_type returns unicode for the
+ # content_type.
+ content_type = str(content_type)
+ return content_type, content_encoding
diff --git a/src/pyramid/router.py b/src/pyramid/router.py
new file mode 100644
index 000000000..49b7b601b
--- /dev/null
+++ b/src/pyramid/router.py
@@ -0,0 +1,278 @@
+from zope.interface import (
+ implementer,
+ providedBy,
+ )
+
+from pyramid.interfaces import (
+ IDebugLogger,
+ IExecutionPolicy,
+ IRequest,
+ IRequestExtensions,
+ IRootFactory,
+ IRouteRequest,
+ IRouter,
+ IRequestFactory,
+ IRoutesMapper,
+ ITraverser,
+ ITweens,
+ )
+
+from pyramid.events import (
+ ContextFound,
+ NewRequest,
+ NewResponse,
+ BeforeTraversal,
+ )
+
+from pyramid.httpexceptions import HTTPNotFound
+from pyramid.request import Request
+from pyramid.view import _call_view
+from pyramid.request import apply_request_extensions
+from pyramid.threadlocal import RequestContext
+
+from pyramid.traversal import (
+ DefaultRootFactory,
+ ResourceTreeTraverser,
+ )
+
+@implementer(IRouter)
+class Router(object):
+
+ debug_notfound = False
+ debug_routematch = False
+
+ def __init__(self, registry):
+ q = registry.queryUtility
+ self.logger = q(IDebugLogger)
+ self.root_factory = q(IRootFactory, default=DefaultRootFactory)
+ self.routes_mapper = q(IRoutesMapper)
+ self.request_factory = q(IRequestFactory, default=Request)
+ self.request_extensions = q(IRequestExtensions)
+ self.execution_policy = q(
+ IExecutionPolicy, default=default_execution_policy)
+ self.orig_handle_request = self.handle_request
+ tweens = q(ITweens)
+ if tweens is not None:
+ self.handle_request = tweens(self.handle_request, registry)
+ self.root_policy = self.root_factory # b/w compat
+ self.registry = registry
+ settings = registry.settings
+ if settings is not None:
+ self.debug_notfound = settings['debug_notfound']
+ self.debug_routematch = settings['debug_routematch']
+
+ def handle_request(self, request):
+ attrs = request.__dict__
+ registry = attrs['registry']
+
+ request.request_iface = IRequest
+ context = None
+ routes_mapper = self.routes_mapper
+ debug_routematch = self.debug_routematch
+ adapters = registry.adapters
+ has_listeners = registry.has_listeners
+ notify = registry.notify
+ logger = self.logger
+
+ has_listeners and notify(NewRequest(request))
+ # find the root object
+ root_factory = self.root_factory
+ if routes_mapper is not None:
+ info = routes_mapper(request)
+ match, route = info['match'], info['route']
+ if route is None:
+ if debug_routematch:
+ msg = ('no route matched for url %s' %
+ request.url)
+ logger and logger.debug(msg)
+ else:
+ attrs['matchdict'] = match
+ attrs['matched_route'] = route
+
+ if debug_routematch:
+ msg = (
+ 'route matched for url %s; '
+ 'route_name: %r, '
+ 'path_info: %r, '
+ 'pattern: %r, '
+ 'matchdict: %r, '
+ 'predicates: %r' % (
+ request.url,
+ route.name,
+ request.path_info,
+ route.pattern,
+ match,
+ ', '.join([p.text() for p in route.predicates]))
+ )
+ logger and logger.debug(msg)
+
+ request.request_iface = registry.queryUtility(
+ IRouteRequest,
+ name=route.name,
+ default=IRequest)
+
+ root_factory = route.factory or self.root_factory
+
+ # Notify anyone listening that we are about to start traversal
+ #
+ # Notify before creating root_factory in case we want to do something
+ # special on a route we may have matched. See
+ # https://github.com/Pylons/pyramid/pull/1876 for ideas of what is
+ # possible.
+ has_listeners and notify(BeforeTraversal(request))
+
+ # Create the root factory
+ root = root_factory(request)
+ attrs['root'] = root
+
+ # We are about to traverse and find a context
+ traverser = adapters.queryAdapter(root, ITraverser)
+ if traverser is None:
+ traverser = ResourceTreeTraverser(root)
+ tdict = traverser(request)
+
+ context, view_name, subpath, traversed, vroot, vroot_path = (
+ tdict['context'],
+ tdict['view_name'],
+ tdict['subpath'],
+ tdict['traversed'],
+ tdict['virtual_root'],
+ tdict['virtual_root_path']
+ )
+
+ attrs.update(tdict)
+
+ # Notify anyone listening that we have a context and traversal is
+ # complete
+ has_listeners and notify(ContextFound(request))
+
+ # find a view callable
+ context_iface = providedBy(context)
+ response = _call_view(
+ registry,
+ request,
+ context,
+ context_iface,
+ view_name
+ )
+
+ if response is None:
+ if self.debug_notfound:
+ msg = (
+ 'debug_notfound of url %s; path_info: %r, '
+ 'context: %r, view_name: %r, subpath: %r, '
+ 'traversed: %r, root: %r, vroot: %r, '
+ 'vroot_path: %r' % (
+ request.url, request.path_info, context,
+ view_name, subpath, traversed, root, vroot,
+ vroot_path)
+ )
+ logger and logger.debug(msg)
+ else:
+ msg = request.path_info
+ raise HTTPNotFound(msg)
+
+ return response
+
+ def invoke_subrequest(self, request, use_tweens=False):
+ """Obtain a response object from the Pyramid application based on
+ information in the ``request`` object provided. The ``request``
+ object must be an object that implements the Pyramid request
+ interface (such as a :class:`pyramid.request.Request` instance). If
+ ``use_tweens`` is ``True``, the request will be sent to the
+ :term:`tween` in the tween stack closest to the request ingress. If
+ ``use_tweens`` is ``False``, the request will be sent to the main
+ router handler, and no tweens will be invoked.
+
+ See the API for pyramid.request for complete documentation.
+ """
+ request.registry = self.registry
+ request.invoke_subrequest = self.invoke_subrequest
+ extensions = self.request_extensions
+ if extensions is not None:
+ apply_request_extensions(request, extensions=extensions)
+ with RequestContext(request):
+ return self.invoke_request(request, _use_tweens=use_tweens)
+
+ def request_context(self, environ):
+ """
+ Create a new request context from a WSGI environ.
+
+ The request context is used to push/pop the threadlocals required
+ when processing the request. It also contains an initialized
+ :class:`pyramid.interfaces.IRequest` instance using the registered
+ :class:`pyramid.interfaces.IRequestFactory`. The context may be
+ used as a context manager to control the threadlocal lifecycle:
+
+ .. code-block:: python
+
+ with router.request_context(environ) as request:
+ ...
+
+ Alternatively, the context may be used without the ``with`` statement
+ by manually invoking its ``begin()`` and ``end()`` methods.
+
+ .. code-block:: python
+
+ ctx = router.request_context(environ)
+ request = ctx.begin()
+ try:
+ ...
+ finally:
+ ctx.end()
+
+ """
+ request = self.request_factory(environ)
+ request.registry = self.registry
+ request.invoke_subrequest = self.invoke_subrequest
+ extensions = self.request_extensions
+ if extensions is not None:
+ apply_request_extensions(request, extensions=extensions)
+ return RequestContext(request)
+
+ def invoke_request(self, request, _use_tweens=True):
+ """
+ Execute a request through the request processing pipeline and
+ return the generated response.
+
+ """
+ registry = self.registry
+ has_listeners = registry.has_listeners
+ notify = registry.notify
+
+ if _use_tweens:
+ handle_request = self.handle_request
+ else:
+ handle_request = self.orig_handle_request
+
+ try:
+ response = handle_request(request)
+
+ if request.response_callbacks:
+ request._process_response_callbacks(response)
+
+ has_listeners and notify(NewResponse(request, response))
+
+ return response
+
+ finally:
+ if request.finished_callbacks:
+ request._process_finished_callbacks()
+
+ def __call__(self, environ, start_response):
+ """
+ Accept ``environ`` and ``start_response``; create a
+ :term:`request` and route the request to a :app:`Pyramid`
+ view based on introspection of :term:`view configuration`
+ within the application registry; call ``start_response`` and
+ return an iterable.
+ """
+ response = self.execution_policy(environ, self)
+ return response(environ, start_response)
+
+def default_execution_policy(environ, router):
+ with router.request_context(environ) as request:
+ try:
+ return router.invoke_request(request)
+ except Exception:
+ return request.invoke_exception_view(reraise=True)
diff --git a/src/pyramid/scaffolds/__init__.py b/src/pyramid/scaffolds/__init__.py
new file mode 100644
index 000000000..71a220e22
--- /dev/null
+++ b/src/pyramid/scaffolds/__init__.py
@@ -0,0 +1,65 @@
+import binascii
+import os
+from textwrap import dedent
+
+from pyramid.compat import native_
+
+from pyramid.scaffolds.template import Template # API
+
+class PyramidTemplate(Template):
+ """
+ A class that can be used as a base class for Pyramid scaffolding
+ templates.
+ """
+ def pre(self, command, output_dir, vars):
+ """ Overrides :meth:`pyramid.scaffolds.template.Template.pre`, adding
+ several variables to the default variables list (including
+ ``random_string``, and ``package_logger``). It also prevents common
+ misnamings (such as naming a package "site" or naming a package
+ logger "root".
+ """
+ vars['random_string'] = native_(binascii.hexlify(os.urandom(20)))
+ package_logger = vars['package']
+ if package_logger == 'root':
+ # Rename the app logger in the rare case a project is named 'root'
+ package_logger = 'app'
+ vars['package_logger'] = package_logger
+ return Template.pre(self, command, output_dir, vars)
+
+ def post(self, command, output_dir, vars): # pragma: no cover
+ """ Overrides :meth:`pyramid.scaffolds.template.Template.post`, to
+ print "Welcome to Pyramid. Sorry for the convenience." after a
+ successful scaffolding rendering."""
+
+ separator = "=" * 79
+ msg = dedent(
+ """
+ %(separator)s
+ Tutorials: https://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/
+ Documentation: https://docs.pylonsproject.org/projects/pyramid/en/latest/
+ Twitter: https://twitter.com/PylonsProject
+ Mailing List: https://groups.google.com/forum/#!forum/pylons-discuss
+
+ Welcome to Pyramid. Sorry for the convenience.
+ %(separator)s
+ """ % {'separator': separator})
+
+ self.out(msg)
+ return Template.post(self, command, output_dir, vars)
+
+ def out(self, msg): # pragma: no cover (replaceable testing hook)
+ print(msg)
+
+class StarterProjectTemplate(PyramidTemplate):
+ _template_dir = 'starter'
+ summary = 'Pyramid starter project using URL dispatch and Jinja2'
+
+class ZODBProjectTemplate(PyramidTemplate):
+ _template_dir = 'zodb'
+ summary = 'Pyramid project using ZODB, traversal, and Chameleon'
+
+class AlchemyProjectTemplate(PyramidTemplate):
+ _template_dir = 'alchemy'
+ summary = (
+ 'Pyramid project using SQLAlchemy, SQLite, URL dispatch, and '
+ 'Jinja2')
diff --git a/src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl b/src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl
new file mode 100644
index 000000000..273a4a580
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl
@@ -0,0 +1,3 @@
+[run]
+source = {{package}}
+omit = {{package}}/test*
diff --git a/src/pyramid/scaffolds/alchemy/+package+/__init__.py b/src/pyramid/scaffolds/alchemy/+package+/__init__.py
new file mode 100644
index 000000000..4dab44823
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/__init__.py
@@ -0,0 +1,12 @@
+from pyramid.config import Configurator
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.include('.models')
+ config.include('.routes')
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/src/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/src/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl
new file mode 100644
index 000000000..521816ce7
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl
@@ -0,0 +1,74 @@
+from sqlalchemy import engine_from_config
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import configure_mappers
+import zope.sqlalchemy
+
+# import or define all models here to ensure they are attached to the
+# Base.metadata prior to any initialization routines
+from .mymodel import MyModel # noqa
+
+# run configure_mappers after defining all of the models to ensure
+# all relationships can be setup
+configure_mappers()
+
+
+def get_engine(settings, prefix='sqlalchemy.'):
+ return engine_from_config(settings, prefix)
+
+
+def get_session_factory(engine):
+ factory = sessionmaker()
+ factory.configure(bind=engine)
+ return factory
+
+
+def get_tm_session(session_factory, transaction_manager):
+ """
+ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction.
+
+ This function will hook the session to the transaction manager which
+ will take care of committing any changes.
+
+ - When using pyramid_tm it will automatically be committed or aborted
+ depending on whether an exception is raised.
+
+ - When using scripts you should wrap the session in a manager yourself.
+ For example::
+
+ import transaction
+
+ engine = get_engine(settings)
+ session_factory = get_session_factory(engine)
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ """
+ dbsession = session_factory()
+ zope.sqlalchemy.register(
+ dbsession, transaction_manager=transaction_manager)
+ return dbsession
+
+
+def includeme(config):
+ """
+ Initialize the model for a Pyramid app.
+
+ Activate this setup using ``config.include('{{project}}.models')``.
+
+ """
+ settings = config.get_settings()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+
+ # use pyramid_tm to hook the transaction lifecycle to the request
+ config.include('pyramid_tm')
+
+ session_factory = get_session_factory(get_engine(settings))
+ config.registry['dbsession_factory'] = session_factory
+
+ # make request.dbsession available for use in Pyramid
+ config.add_request_method(
+ # r.tm is the transaction manager used by pyramid_tm
+ lambda r: get_tm_session(session_factory, r.tm),
+ 'dbsession',
+ reify=True
+ )
diff --git a/src/pyramid/scaffolds/alchemy/+package+/models/meta.py b/src/pyramid/scaffolds/alchemy/+package+/models/meta.py
new file mode 100644
index 000000000..0682247b5
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/models/meta.py
@@ -0,0 +1,16 @@
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.schema import MetaData
+
+# Recommended naming convention used by Alembic, as various different database
+# providers will autogenerate vastly different names making migrations more
+# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html
+NAMING_CONVENTION = {
+ "ix": 'ix_%(column_0_label)s',
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
+ "ck": "ck_%(table_name)s_%(constraint_name)s",
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
+ "pk": "pk_%(table_name)s"
+}
+
+metadata = MetaData(naming_convention=NAMING_CONVENTION)
+Base = declarative_base(metadata=metadata)
diff --git a/src/pyramid/scaffolds/alchemy/+package+/models/mymodel.py b/src/pyramid/scaffolds/alchemy/+package+/models/mymodel.py
new file mode 100644
index 000000000..d65a01a42
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/models/mymodel.py
@@ -0,0 +1,18 @@
+from sqlalchemy import (
+ Column,
+ Index,
+ Integer,
+ Text,
+)
+
+from .meta import Base
+
+
+class MyModel(Base):
+ __tablename__ = 'models'
+ id = Column(Integer, primary_key=True)
+ name = Column(Text)
+ value = Column(Integer)
+
+
+Index('my_index', MyModel.name, unique=True, mysql_length=255)
diff --git a/src/pyramid/scaffolds/alchemy/+package+/routes.py b/src/pyramid/scaffolds/alchemy/+package+/routes.py
new file mode 100644
index 000000000..25504ad4d
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/routes.py
@@ -0,0 +1,3 @@
+def includeme(config):
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
diff --git a/src/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py b/src/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py
new file mode 100644
index 000000000..5bb534f79
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/src/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/src/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py
new file mode 100644
index 000000000..7307ecc5c
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py
@@ -0,0 +1,45 @@
+import os
+import sys
+import transaction
+
+from pyramid.paster import (
+ get_appsettings,
+ setup_logging,
+ )
+
+from pyramid.scripts.common import parse_vars
+
+from ..models.meta import Base
+from ..models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+from ..models import MyModel
+
+
+def usage(argv):
+ cmd = os.path.basename(argv[0])
+ print('usage: %s <config_uri> [var=value]\n'
+ '(example: "%s development.ini")' % (cmd, cmd))
+ sys.exit(1)
+
+
+def main(argv=sys.argv):
+ if len(argv) < 2:
+ usage(argv)
+ config_uri = argv[1]
+ options = parse_vars(argv[2:])
+ setup_logging(config_uri)
+ settings = get_appsettings(config_uri, options=options)
+
+ engine = get_engine(settings)
+ Base.metadata.create_all(engine)
+
+ session_factory = get_session_factory(engine)
+
+ with transaction.manager:
+ dbsession = get_tm_session(session_factory, transaction.manager)
+
+ model = MyModel(name='one', value=1)
+ dbsession.add(model)
diff --git a/src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.png b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.png
Binary files differ
diff --git a/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png
new file mode 100644
index 000000000..4ab837be9
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png
Binary files differ
diff --git a/src/pyramid/scaffolds/alchemy/+package+/static/theme.css b/src/pyramid/scaffolds/alchemy/+package+/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/static/theme.css
@@ -0,0 +1,154 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+body {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #ffffff;
+ background: #bc2131;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+}
+p {
+ font-weight: 300;
+}
+.font-normal {
+ font-weight: 400;
+}
+.font-semi-bold {
+ font-weight: 600;
+}
+.font-bold {
+ font-weight: 700;
+}
+.starter-template {
+ margin-top: 250px;
+}
+.starter-template .content {
+ margin-left: 10px;
+}
+.starter-template .content h1 {
+ margin-top: 10px;
+ font-size: 60px;
+}
+.starter-template .content h1 .smaller {
+ font-size: 40px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead {
+ font-size: 25px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead .font-normal {
+ color: #ffffff;
+}
+.starter-template .links {
+ float: right;
+ right: 0;
+ margin-top: 125px;
+}
+.starter-template .links ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+}
+.starter-template .links ul li {
+ list-style: none;
+ display: inline;
+ margin: 0 10px;
+}
+.starter-template .links ul li:first-child {
+ margin-left: 0;
+}
+.starter-template .links ul li:last-child {
+ margin-right: 0;
+}
+.starter-template .links ul li.current-version {
+ color: #f2b7bd;
+ font-weight: 400;
+}
+.starter-template .links ul li a, a {
+ color: #f2b7bd;
+ text-decoration: underline;
+}
+.starter-template .links ul li a:hover, a:hover {
+ color: #ffffff;
+ text-decoration: underline;
+}
+.starter-template .links ul li .icon-muted {
+ color: #eb8b95;
+ margin-right: 5px;
+}
+.starter-template .links ul li:hover .icon-muted {
+ color: #ffffff;
+}
+.starter-template .copyright {
+ margin-top: 10px;
+ font-size: 0.9em;
+ color: #f2b7bd;
+ text-transform: lowercase;
+ float: right;
+ right: 0;
+}
+@media (max-width: 1199px) {
+ .starter-template .content h1 {
+ font-size: 45px;
+ }
+ .starter-template .content h1 .smaller {
+ font-size: 30px;
+ }
+ .starter-template .content .lead {
+ font-size: 20px;
+ }
+}
+@media (max-width: 991px) {
+ .starter-template {
+ margin-top: 0;
+ }
+ .starter-template .logo {
+ margin: 40px auto;
+ }
+ .starter-template .content {
+ margin-left: 0;
+ text-align: center;
+ }
+ .starter-template .content h1 {
+ margin-bottom: 20px;
+ }
+ .starter-template .links {
+ float: none;
+ text-align: center;
+ margin-top: 60px;
+ }
+ .starter-template .copyright {
+ float: none;
+ text-align: center;
+ }
+}
+@media (max-width: 767px) {
+ .starter-template .content h1 .smaller {
+ font-size: 25px;
+ display: block;
+ }
+ .starter-template .content .lead {
+ font-size: 16px;
+ }
+ .starter-template .links {
+ margin-top: 40px;
+ }
+ .starter-template .links ul li {
+ display: block;
+ margin: 0;
+ }
+ .starter-template .links ul li .icon-muted {
+ display: none;
+ }
+ .starter-template .copyright {
+ margin-top: 20px;
+ }
+}
diff --git a/src/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl b/src/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl
new file mode 100644
index 000000000..1917f83c7
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead"><span class="font-semi-bold">404</span> Page Not Found</p>
+</div>
+{% endblock content %}
diff --git a/src/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl b/src/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl
new file mode 100644
index 000000000..d6b3ca9c6
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="\{\{request.locale_name\}\}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="\{\{request.static_url('{{package}}:static/pyramid-16x16.png')\}\}">
+
+ <title>Alchemy Scaffold for The Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="\{\{request.static_url('{{package}}:static/theme.css')\}\}" rel="stylesheet">
+
+ <!-- HTML5 shiv and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!--[if lt IE 9]>
+ <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js" integrity="sha384-f1r2UzjsxZ9T4V1f2zBO/evUqSEOpeaUUZcMTz1Up63bl4ruYnFYeM+BxI4NhyI0" crossorigin="anonymous"></script>
+ <![endif]-->
+ </head>
+
+ <body>
+
+ <div class="starter-template">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-2">
+ <img class="logo img-responsive" src="\{\{request.static_url('{{package}}:static/pyramid.png')\}\}" alt="pyramid web framework">
+ </div>
+ <div class="col-md-10">
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
+ </div>
+ </div>
+ <div class="row">
+ <div class="links">
+ <ul>
+ <li class="current-version">Generated by v{{pyramid_version}}</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/">Docs</a></li>
+ <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
+ <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js" integrity="sha384-aBL3Lzi6c9LNDGvpHkZrrm3ZVsIwohDD7CDozL0pk8FwCrfmV7H9w8j3L7ikEv6h" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js" integrity="sha384-s1ITto93iSMDxlp/79qhWHi+LsIi9Gx6yL+cOKDuymvihkfol83TYbLbOw+W/wv4" crossorigin="anonymous"></script>
+ </body>
+</html>
diff --git a/src/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl b/src/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl
new file mode 100644
index 000000000..01fe5b8e3
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">\{\{project\}\}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework {{pyramid_version}}</span>.</p>
+</div>
+{% endblock content %}
diff --git a/src/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/src/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl
new file mode 100644
index 000000000..072eab5b2
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl
@@ -0,0 +1,65 @@
+import unittest
+import transaction
+
+from pyramid import testing
+
+
+def dummy_request(dbsession):
+ return testing.DummyRequest(dbsession=dbsession)
+
+
+class BaseTest(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp(settings={
+ 'sqlalchemy.url': 'sqlite:///:memory:'
+ })
+ self.config.include('.models')
+ settings = self.config.get_settings()
+
+ from .models import (
+ get_engine,
+ get_session_factory,
+ get_tm_session,
+ )
+
+ self.engine = get_engine(settings)
+ session_factory = get_session_factory(self.engine)
+
+ self.session = get_tm_session(session_factory, transaction.manager)
+
+ def init_database(self):
+ from .models.meta import Base
+ Base.metadata.create_all(self.engine)
+
+ def tearDown(self):
+ from .models.meta import Base
+
+ testing.tearDown()
+ transaction.abort()
+ Base.metadata.drop_all(self.engine)
+
+
+class TestMyViewSuccessCondition(BaseTest):
+
+ def setUp(self):
+ super(TestMyViewSuccessCondition, self).setUp()
+ self.init_database()
+
+ from .models import MyModel
+
+ model = MyModel(name='one', value=55)
+ self.session.add(model)
+
+ def test_passing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info['one'].name, 'one')
+ self.assertEqual(info['project'], '{{project}}')
+
+
+class TestMyViewFailureCondition(BaseTest):
+
+ def test_failing_view(self):
+ from .views.default import my_view
+ info = my_view(dummy_request(self.session))
+ self.assertEqual(info.status_int, 500)
diff --git a/src/pyramid/scaffolds/alchemy/+package+/views/__init__.py b/src/pyramid/scaffolds/alchemy/+package+/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/views/__init__.py
diff --git a/src/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl b/src/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl
new file mode 100644
index 000000000..7bf0026e5
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl
@@ -0,0 +1,33 @@
+from pyramid.response import Response
+from pyramid.view import view_config
+
+from sqlalchemy.exc import DBAPIError
+
+from ..models import MyModel
+
+
+@view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
+def my_view(request):
+ try:
+ query = request.dbsession.query(MyModel)
+ one = query.filter(MyModel.name == 'one').first()
+ except DBAPIError:
+ return Response(db_err_msg, content_type='text/plain', status=500)
+ return {'one': one, 'project': '{{project}}'}
+
+
+db_err_msg = """\
+Pyramid is having a problem using your SQL database. The problem
+might be caused by one of the following things:
+
+1. You may need to run the "initialize_{{project}}_db" script
+ to initialize your database tables. Check your virtual
+ environment's "bin" directory for this script and try to run it.
+
+2. Your database server may not be running. Check that the
+ database server referred to by the "sqlalchemy.url" setting in
+ your "development.ini" file is running.
+
+After you fix the problem, please restart the Pyramid application to
+try it again.
+"""
diff --git a/src/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl b/src/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl
new file mode 100644
index 000000000..69d6e2804
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl
@@ -0,0 +1,7 @@
+from pyramid.view import notfound_view_config
+
+
+@notfound_view_config(renderer='../templates/404.jinja2')
+def notfound_view(request):
+ request.response.status = 404
+ return {}
diff --git a/src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl b/src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl b/src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl
new file mode 100644
index 000000000..f93f45544
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/src/pyramid/scaffolds/alchemy/README.txt_tmpl b/src/pyramid/scaffolds/alchemy/README.txt_tmpl
new file mode 100644
index 000000000..83c37edea
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/README.txt_tmpl
@@ -0,0 +1,14 @@
+{{project}} README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/initialize_{{project}}_db development.ini
+
+- $VENV/bin/pserve development.ini
+
diff --git a/src/pyramid/scaffolds/alchemy/development.ini_tmpl b/src/pyramid/scaffolds/alchemy/development.ini_tmpl
new file mode 100644
index 000000000..3cfb3996d
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/development.ini_tmpl
@@ -0,0 +1,69 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html
+###
+
+[app:main]
+use = egg:{{project}}
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+
+sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html
+###
+
+[loggers]
+keys = root, {{package_logger}}, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_{{package_logger}}]
+level = DEBUG
+handlers =
+qualname = {{package}}
+
+[logger_sqlalchemy]
+level = INFO
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/src/pyramid/scaffolds/alchemy/production.ini_tmpl b/src/pyramid/scaffolds/alchemy/production.ini_tmpl
new file mode 100644
index 000000000..043229a71
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/production.ini_tmpl
@@ -0,0 +1,59 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html
+###
+
+[app:main]
+use = egg:{{project}}
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite
+
+[server:main]
+use = egg:waitress#main
+listen = *:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html
+###
+
+[loggers]
+keys = root, {{package_logger}}, sqlalchemy
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_{{package_logger}}]
+level = WARN
+handlers =
+qualname = {{package}}
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither. (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/src/pyramid/scaffolds/alchemy/pytest.ini_tmpl b/src/pyramid/scaffolds/alchemy/pytest.ini_tmpl
new file mode 100644
index 000000000..a30c8bcad
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/pytest.ini_tmpl
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = {{package}}
+python_files = *.py
diff --git a/src/pyramid/scaffolds/alchemy/setup.py_tmpl b/src/pyramid/scaffolds/alchemy/setup.py_tmpl
new file mode 100644
index 000000000..9318817dc
--- /dev/null
+++ b/src/pyramid/scaffolds/alchemy/setup.py_tmpl
@@ -0,0 +1,55 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.txt')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'pyramid',
+ 'pyramid_jinja2',
+ 'pyramid_debugtoolbar',
+ 'pyramid_tm',
+ 'SQLAlchemy',
+ 'transaction',
+ 'zope.sqlalchemy',
+ 'waitress',
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
+setup(name='{{project}}',
+ version='0.0',
+ description='{{project}}',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web wsgi bfg pylons pyramid',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
+ install_requires=requires,
+ entry_points="""\
+ [paste.app_factory]
+ main = {{package}}:main
+ [console_scripts]
+ initialize_{{project}}_db = {{package}}.scripts.initializedb:main
+ """,
+ )
diff --git a/src/pyramid/scaffolds/copydir.py b/src/pyramid/scaffolds/copydir.py
new file mode 100644
index 000000000..0914bb0d4
--- /dev/null
+++ b/src/pyramid/scaffolds/copydir.py
@@ -0,0 +1,301 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste
+# (http://pythonpaste.org) Licensed under the MIT license:
+# http://www.opensource.org/licenses/mit-license.php
+
+import os
+import sys
+import pkg_resources
+
+from pyramid.compat import (
+ input_,
+ native_,
+ url_quote as compat_url_quote,
+ escape,
+ )
+
+fsenc = sys.getfilesystemencoding()
+
+
+class SkipTemplate(Exception):
+ """
+ Raised to indicate that the template should not be copied over.
+ Raise this exception during the substitution of your template
+ """
+
+def copy_dir(source, dest, vars, verbosity, simulate, indent=0,
+ sub_vars=True, interactive=False, overwrite=True,
+ template_renderer=None, out_=sys.stdout):
+ """
+ Copies the ``source`` directory to the ``dest`` directory.
+
+ ``vars``: A dictionary of variables to use in any substitutions.
+
+ ``verbosity``: Higher numbers will show more about what is happening.
+
+ ``simulate``: If true, then don't actually *do* anything.
+
+ ``indent``: Indent any messages by this amount.
+
+ ``sub_vars``: If true, variables in ``_tmpl`` files and ``+var+``
+ in filenames will be substituted.
+
+ ``overwrite``: If false, then don't every overwrite anything.
+
+ ``interactive``: If you are overwriting a file and interactive is
+ true, then ask before overwriting.
+
+ ``template_renderer``: This is a function for rendering templates (if you
+ don't want to use string.Template). It should have the signature
+ ``template_renderer(content_as_string, vars_as_dict,
+ filename=filename)``.
+ """
+ def out(msg):
+ out_.write(msg)
+ out_.write('\n')
+ out_.flush()
+ # This allows you to use a leading +dot+ in filenames which would
+ # otherwise be skipped because leading dots make the file hidden:
+ vars.setdefault('dot', '.')
+ vars.setdefault('plus', '+')
+ use_pkg_resources = isinstance(source, tuple)
+ if use_pkg_resources:
+ names = sorted(pkg_resources.resource_listdir(source[0], source[1]))
+ else:
+ names = sorted(os.listdir(source))
+ pad = ' ' * (indent * 2)
+ if not os.path.exists(dest):
+ if verbosity >= 1:
+ out('%sCreating %s/' % (pad, dest))
+ if not simulate:
+ makedirs(dest, verbosity=verbosity, pad=pad)
+ elif verbosity >= 2:
+ out('%sDirectory %s exists' % (pad, dest))
+ for name in names:
+ if use_pkg_resources:
+ full = '/'.join([source[1], name])
+ else:
+ full = os.path.join(source, name)
+ reason = should_skip_file(name)
+ if reason:
+ if verbosity >= 2:
+ reason = pad + reason % {'filename': full}
+ out(reason)
+ continue # pragma: no cover
+ if sub_vars:
+ dest_full = os.path.join(dest, substitute_filename(name, vars))
+ sub_file = False
+ if dest_full.endswith('_tmpl'):
+ dest_full = dest_full[:-5]
+ sub_file = sub_vars
+ if use_pkg_resources and pkg_resources.resource_isdir(source[0], full):
+ if verbosity:
+ out('%sRecursing into %s' % (pad, os.path.basename(full)))
+ copy_dir((source[0], full), dest_full, vars, verbosity, simulate,
+ indent=indent + 1, sub_vars=sub_vars,
+ interactive=interactive, overwrite=overwrite,
+ template_renderer=template_renderer, out_=out_)
+ continue
+ elif not use_pkg_resources and os.path.isdir(full):
+ if verbosity:
+ out('%sRecursing into %s' % (pad, os.path.basename(full)))
+ copy_dir(full, dest_full, vars, verbosity, simulate,
+ indent=indent + 1, sub_vars=sub_vars,
+ interactive=interactive, overwrite=overwrite,
+ template_renderer=template_renderer, out_=out_)
+ continue
+ elif use_pkg_resources:
+ content = pkg_resources.resource_string(source[0], full)
+ else:
+ with open(full, 'rb') as f:
+ content = f.read()
+ if sub_file:
+ try:
+ content = substitute_content(
+ content, vars, filename=full,
+ template_renderer=template_renderer
+ )
+ except SkipTemplate:
+ continue # pragma: no cover
+ if content is None:
+ continue # pragma: no cover
+ already_exists = os.path.exists(dest_full)
+ if already_exists:
+ with open(dest_full, 'rb') as f:
+ old_content = f.read()
+ if old_content == content:
+ if verbosity:
+ out('%s%s already exists (same content)' %
+ (pad, dest_full))
+ continue # pragma: no cover
+ if interactive:
+ if not query_interactive(
+ native_(full, fsenc), native_(dest_full, fsenc),
+ native_(content, fsenc), native_(old_content, fsenc),
+ simulate=simulate, out_=out_):
+ continue
+ elif not overwrite:
+ continue # pragma: no cover
+ if verbosity and use_pkg_resources:
+ out('%sCopying %s to %s' % (pad, full, dest_full))
+ elif verbosity:
+ out(
+ '%sCopying %s to %s' % (pad, os.path.basename(full),
+ dest_full))
+ if not simulate:
+ with open(dest_full, 'wb') as f:
+ f.write(content)
+
+def should_skip_file(name):
+ """
+ Checks if a file should be skipped based on its name.
+
+ If it should be skipped, returns the reason, otherwise returns
+ None.
+ """
+ if name.startswith('.'):
+ return 'Skipping hidden file %(filename)s'
+ if name.endswith(('~', '.bak')):
+ return 'Skipping backup file %(filename)s'
+ if name.endswith(('.pyc', '.pyo')):
+ return 'Skipping %s file ' % os.path.splitext(name)[1] + '%(filename)s'
+ if name.endswith('$py.class'):
+ return 'Skipping $py.class file %(filename)s'
+ if name in ('CVS', '_darcs'):
+ return 'Skipping version control directory %(filename)s'
+ return None
+
+# Overridden on user's request:
+all_answer = None
+
+def query_interactive(src_fn, dest_fn, src_content, dest_content,
+ simulate, out_=sys.stdout):
+ def out(msg):
+ out_.write(msg)
+ out_.write('\n')
+ out_.flush()
+ global all_answer
+ from difflib import unified_diff, context_diff
+ u_diff = list(unified_diff(
+ dest_content.splitlines(),
+ src_content.splitlines(),
+ dest_fn, src_fn))
+ c_diff = list(context_diff(
+ dest_content.splitlines(),
+ src_content.splitlines(),
+ dest_fn, src_fn))
+ added = len([l for l in u_diff if l.startswith('+') and
+ not l.startswith('+++')])
+ removed = len([l for l in u_diff if l.startswith('-') and
+ not l.startswith('---')])
+ if added > removed:
+ msg = '; %i lines added' % (added - removed)
+ elif removed > added:
+ msg = '; %i lines removed' % (removed - added)
+ else:
+ msg = ''
+ out('Replace %i bytes with %i bytes (%i/%i lines changed%s)' % (
+ len(dest_content), len(src_content),
+ removed, len(dest_content.splitlines()), msg))
+ prompt = 'Overwrite %s [y/n/d/B/?] ' % dest_fn
+ while 1:
+ if all_answer is None:
+ response = input_(prompt).strip().lower()
+ else:
+ response = all_answer
+ if not response or response[0] == 'b':
+ import shutil
+ new_dest_fn = dest_fn + '.bak'
+ n = 0
+ while os.path.exists(new_dest_fn):
+ n += 1
+ new_dest_fn = dest_fn + '.bak' + str(n)
+ out('Backing up %s to %s' % (dest_fn, new_dest_fn))
+ if not simulate:
+ shutil.copyfile(dest_fn, new_dest_fn)
+ return True
+ elif response.startswith('all '):
+ rest = response[4:].strip()
+ if not rest or rest[0] not in ('y', 'n', 'b'):
+ out(query_usage)
+ continue
+ response = all_answer = rest[0]
+ if response[0] == 'y':
+ return True
+ elif response[0] == 'n':
+ return False
+ elif response == 'dc':
+ out('\n'.join(c_diff))
+ elif response[0] == 'd':
+ out('\n'.join(u_diff))
+ else:
+ out(query_usage)
+
+query_usage = """\
+Responses:
+ Y(es): Overwrite the file with the new content.
+ N(o): Do not overwrite the file.
+ D(iff): Show a unified diff of the proposed changes (dc=context diff)
+ B(ackup): Save the current file contents to a .bak file
+ (and overwrite)
+ Type "all Y/N/B" to use Y/N/B for answer to all future questions
+"""
+
+def makedirs(dir, verbosity, pad):
+ parent = os.path.dirname(os.path.abspath(dir))
+ if not os.path.exists(parent):
+ makedirs(parent, verbosity, pad) # pragma: no cover
+ os.mkdir(dir)
+
+def substitute_filename(fn, vars):
+ for var, value in vars.items():
+ fn = fn.replace('+%s+' % var, str(value))
+ return fn
+
+def substitute_content(content, vars, filename='<string>',
+ template_renderer=None):
+ v = standard_vars.copy()
+ v.update(vars)
+ return template_renderer(content, v, filename=filename)
+
+def html_quote(s):
+ if s is None:
+ return ''
+ return escape(str(s), 1)
+
+def url_quote(s):
+ if s is None:
+ return ''
+ return compat_url_quote(str(s))
+
+def test(conf, true_cond, false_cond=None):
+ if conf:
+ return true_cond
+ else:
+ return false_cond
+
+def skip_template(condition=True, *args):
+ """
+ Raise SkipTemplate, which causes copydir to skip the template
+ being processed. If you pass in a condition, only raise if that
+ condition is true (allows you to use this with string.Template)
+
+ If you pass any additional arguments, they will be used to
+ instantiate SkipTemplate (generally use like
+ ``skip_template(license=='GPL', 'Skipping file; not using GPL')``)
+ """
+ if condition:
+ raise SkipTemplate(*args)
+
+standard_vars = {
+ 'nothing': None,
+ 'html_quote': html_quote,
+ 'url_quote': url_quote,
+ 'empty': '""',
+ 'test': test,
+ 'repr': repr,
+ 'str': str,
+ 'bool': bool,
+ 'SkipTemplate': SkipTemplate,
+ 'skip_template': skip_template,
+ }
+
diff --git a/src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl b/src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl
new file mode 100644
index 000000000..273a4a580
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl
@@ -0,0 +1,3 @@
+[run]
+source = {{package}}
+omit = {{package}}/test*
diff --git a/src/pyramid/scaffolds/starter/+package+/__init__.py b/src/pyramid/scaffolds/starter/+package+/__init__.py
new file mode 100644
index 000000000..49dde36d4
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+package+/__init__.py
@@ -0,0 +1,12 @@
+from pyramid.config import Configurator
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(settings=settings)
+ config.include('pyramid_jinja2')
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('home', '/')
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.png b/src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.png
Binary files differ
diff --git a/src/pyramid/scaffolds/starter/+package+/static/pyramid.png b/src/pyramid/scaffolds/starter/+package+/static/pyramid.png
new file mode 100644
index 000000000..4ab837be9
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+package+/static/pyramid.png
Binary files differ
diff --git a/src/pyramid/scaffolds/starter/+package+/static/theme.css b/src/pyramid/scaffolds/starter/+package+/static/theme.css
new file mode 100644
index 000000000..be50ad420
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+package+/static/theme.css
@@ -0,0 +1,152 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+body {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #ffffff;
+ background: #bc2131;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+}
+p {
+ font-weight: 300;
+}
+.font-normal {
+ font-weight: 400;
+}
+.font-semi-bold {
+ font-weight: 600;
+}
+.font-bold {
+ font-weight: 700;
+}
+.starter-template {
+ margin-top: 250px;
+}
+.starter-template .content {
+ margin-left: 10px;
+}
+.starter-template .content h1 {
+ margin-top: 10px;
+ font-size: 60px;
+}
+.starter-template .content h1 .smaller {
+ font-size: 40px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead {
+ font-size: 25px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead .font-normal {
+ color: #ffffff;
+}
+.starter-template .links {
+ float: right;
+ right: 0;
+ margin-top: 125px;
+}
+.starter-template .links ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+}
+.starter-template .links ul li {
+ list-style: none;
+ display: inline;
+ margin: 0 10px;
+}
+.starter-template .links ul li:first-child {
+ margin-left: 0;
+}
+.starter-template .links ul li:last-child {
+ margin-right: 0;
+}
+.starter-template .links ul li.current-version {
+ color: #f2b7bd;
+ font-weight: 400;
+}
+.starter-template .links ul li a {
+ color: #ffffff;
+}
+.starter-template .links ul li a:hover {
+ text-decoration: underline;
+}
+.starter-template .links ul li .icon-muted {
+ color: #eb8b95;
+ margin-right: 5px;
+}
+.starter-template .links ul li:hover .icon-muted {
+ color: #ffffff;
+}
+.starter-template .copyright {
+ margin-top: 10px;
+ font-size: 0.9em;
+ color: #f2b7bd;
+ text-transform: lowercase;
+ float: right;
+ right: 0;
+}
+@media (max-width: 1199px) {
+ .starter-template .content h1 {
+ font-size: 45px;
+ }
+ .starter-template .content h1 .smaller {
+ font-size: 30px;
+ }
+ .starter-template .content .lead {
+ font-size: 20px;
+ }
+}
+@media (max-width: 991px) {
+ .starter-template {
+ margin-top: 0;
+ }
+ .starter-template .logo {
+ margin: 40px auto;
+ }
+ .starter-template .content {
+ margin-left: 0;
+ text-align: center;
+ }
+ .starter-template .content h1 {
+ margin-bottom: 20px;
+ }
+ .starter-template .links {
+ float: none;
+ text-align: center;
+ margin-top: 60px;
+ }
+ .starter-template .copyright {
+ float: none;
+ text-align: center;
+ }
+}
+@media (max-width: 767px) {
+ .starter-template .content h1 .smaller {
+ font-size: 25px;
+ display: block;
+ }
+ .starter-template .content .lead {
+ font-size: 16px;
+ }
+ .starter-template .links {
+ margin-top: 40px;
+ }
+ .starter-template .links ul li {
+ display: block;
+ margin: 0;
+ }
+ .starter-template .links ul li .icon-muted {
+ display: none;
+ }
+ .starter-template .copyright {
+ margin-top: 20px;
+ }
+}
diff --git a/src/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl b/src/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl
new file mode 100644
index 000000000..54baf7a2a
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="\{\{request.locale_name\}\}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="\{\{request.static_url('{{package}}:static/pyramid-16x16.png')\}\}">
+
+ <title>Starter Scaffold for The Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="\{\{request.static_url('{{package}}:static/theme.css')\}\}" rel="stylesheet">
+
+ <!-- HTML5 shiv and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!--[if lt IE 9]>
+ <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js" integrity="sha384-f1r2UzjsxZ9T4V1f2zBO/evUqSEOpeaUUZcMTz1Up63bl4ruYnFYeM+BxI4NhyI0" crossorigin="anonymous"></script>
+ <![endif]-->
+ </head>
+
+ <body>
+
+ <div class="starter-template">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-2">
+ <img class="logo img-responsive" src="\{\{request.static_url('{{package}}:static/pyramid.png')\}\}" alt="pyramid web framework">
+ </div>
+ <div class="col-md-10">
+ {% block content %}
+ <p>No content</p>
+ {% endblock content %}
+ </div>
+ </div>
+ <div class="row">
+ <div class="links">
+ <ul>
+ <li class="current-version">Generated by v{{pyramid_version}}</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/">Docs</a></li>
+ <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
+ <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js" integrity="sha384-aBL3Lzi6c9LNDGvpHkZrrm3ZVsIwohDD7CDozL0pk8FwCrfmV7H9w8j3L7ikEv6h" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js" integrity="sha384-s1ITto93iSMDxlp/79qhWHi+LsIi9Gx6yL+cOKDuymvihkfol83TYbLbOw+W/wv4" crossorigin="anonymous"></script>
+ </body>
+</html>
diff --git a/src/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl b/src/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl
new file mode 100644
index 000000000..f826ff9e7
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl
@@ -0,0 +1,8 @@
+{% extends "layout.jinja2" %}
+
+{% block content%}
+<div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">\{\{project\}\}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework {{pyramid_version}}</span>.</p>
+</div>
+{% endblock content %}
diff --git a/src/pyramid/scaffolds/starter/+package+/tests.py_tmpl b/src/pyramid/scaffolds/starter/+package+/tests.py_tmpl
new file mode 100644
index 000000000..30f3f0430
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+package+/tests.py_tmpl
@@ -0,0 +1,29 @@
+import unittest
+
+from pyramid import testing
+
+
+class ViewTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_my_view(self):
+ from .views import my_view
+ request = testing.DummyRequest()
+ info = my_view(request)
+ self.assertEqual(info['project'], '{{project}}')
+
+
+class FunctionalTests(unittest.TestCase):
+ def setUp(self):
+ from {{package}} import main
+ app = main({})
+ from webtest import TestApp
+ self.testapp = TestApp(app)
+
+ def test_root(self):
+ res = self.testapp.get('/', status=200)
+ self.assertTrue(b'Pyramid' in res.body)
diff --git a/src/pyramid/scaffolds/starter/+package+/views.py_tmpl b/src/pyramid/scaffolds/starter/+package+/views.py_tmpl
new file mode 100644
index 000000000..01b9d0130
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/+package+/views.py_tmpl
@@ -0,0 +1,6 @@
+from pyramid.view import view_config
+
+
+@view_config(route_name='home', renderer='templates/mytemplate.jinja2')
+def my_view(request):
+ return {'project': '{{project}}'}
diff --git a/src/pyramid/scaffolds/starter/CHANGES.txt_tmpl b/src/pyramid/scaffolds/starter/CHANGES.txt_tmpl
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/CHANGES.txt_tmpl
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/src/pyramid/scaffolds/starter/MANIFEST.in_tmpl b/src/pyramid/scaffolds/starter/MANIFEST.in_tmpl
new file mode 100644
index 000000000..4d1c86b44
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/MANIFEST.in_tmpl
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/src/pyramid/scaffolds/starter/README.txt_tmpl b/src/pyramid/scaffolds/starter/README.txt_tmpl
new file mode 100644
index 000000000..127ad7595
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/README.txt_tmpl
@@ -0,0 +1,12 @@
+{{project}} README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
+
diff --git a/src/pyramid/scaffolds/starter/development.ini_tmpl b/src/pyramid/scaffolds/starter/development.ini_tmpl
new file mode 100644
index 000000000..c6e42d97c
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/development.ini_tmpl
@@ -0,0 +1,59 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html
+###
+
+[app:main]
+use = egg:{{project}}
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html
+###
+
+[loggers]
+keys = root, {{package_logger}}
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_{{package_logger}}]
+level = DEBUG
+handlers =
+qualname = {{package}}
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/src/pyramid/scaffolds/starter/production.ini_tmpl b/src/pyramid/scaffolds/starter/production.ini_tmpl
new file mode 100644
index 000000000..1107a6b2f
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/production.ini_tmpl
@@ -0,0 +1,53 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html
+###
+
+[app:main]
+use = egg:{{project}}
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = *:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html
+###
+
+[loggers]
+keys = root, {{package_logger}}
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_{{package_logger}}]
+level = WARN
+handlers =
+qualname = {{package}}
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/src/pyramid/scaffolds/starter/pytest.ini_tmpl b/src/pyramid/scaffolds/starter/pytest.ini_tmpl
new file mode 100644
index 000000000..a30c8bcad
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/pytest.ini_tmpl
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = {{package}}
+python_files = *.py
diff --git a/src/pyramid/scaffolds/starter/setup.py_tmpl b/src/pyramid/scaffolds/starter/setup.py_tmpl
new file mode 100644
index 000000000..7f50bbbc2
--- /dev/null
+++ b/src/pyramid/scaffolds/starter/setup.py_tmpl
@@ -0,0 +1,49 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.txt')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'pyramid',
+ 'pyramid_jinja2',
+ 'pyramid_debugtoolbar',
+ 'waitress',
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
+setup(name='{{project}}',
+ version='0.0',
+ description='{{project}}',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web pyramid pylons',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
+ install_requires=requires,
+ entry_points="""\
+ [paste.app_factory]
+ main = {{package}}:main
+ """,
+ )
diff --git a/src/pyramid/scaffolds/template.py b/src/pyramid/scaffolds/template.py
new file mode 100644
index 000000000..e5098e815
--- /dev/null
+++ b/src/pyramid/scaffolds/template.py
@@ -0,0 +1,172 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste
+# (http://pythonpaste.org) Licensed under the MIT license:
+# http://www.opensource.org/licenses/mit-license.php
+
+import re
+import sys
+import os
+
+from pyramid.compat import (
+ native_,
+ bytes_,
+ )
+
+from pyramid.scaffolds import copydir
+
+fsenc = sys.getfilesystemencoding()
+
+class Template(object):
+ """ Inherit from this base class and override methods to use the Pyramid
+ scaffolding system."""
+ copydir = copydir # for testing
+ _template_dir = None
+
+ def __init__(self, name):
+ self.name = name
+
+ def render_template(self, content, vars, filename=None):
+ """ Return a bytestring representing a templated file based on the
+ input (content) and the variable names defined (vars). ``filename``
+ is used for exception reporting."""
+ # this method must not be named "template_renderer" fbo of extension
+ # scaffolds that need to work under pyramid 1.2 and 1.3, and which
+ # need to do "template_renderer =
+ # staticmethod(paste_script_template_renderer)"
+ content = native_(content, fsenc)
+ try:
+ return bytes_(
+ substitute_escaped_double_braces(
+ substitute_double_braces(content, TypeMapper(vars))), fsenc)
+ except Exception as e:
+ _add_except(e, ' in file %s' % filename)
+ raise
+
+ def module_dir(self):
+ mod = sys.modules[self.__class__.__module__]
+ return os.path.dirname(mod.__file__)
+
+ def template_dir(self):
+ """ Return the template directory of the scaffold. By default, it
+ returns the value of ``os.path.join(self.module_dir(),
+ self._template_dir)`` (``self.module_dir()`` returns the module in
+ which your subclass has been defined). If ``self._template_dir`` is
+ a tuple this method just returns the value instead of trying to
+ construct a path. If _template_dir is a tuple, it should be a
+ 2-element tuple: ``(package_name, package_relative_path)``."""
+ assert self._template_dir is not None, (
+ "Template %r didn't set _template_dir" % self)
+ if isinstance(self._template_dir, tuple):
+ return self._template_dir
+ else:
+ return os.path.join(self.module_dir(), self._template_dir)
+
+ def run(self, command, output_dir, vars):
+ self.pre(command, output_dir, vars)
+ self.write_files(command, output_dir, vars)
+ self.post(command, output_dir, vars)
+
+ def pre(self, command, output_dir, vars): # pragma: no cover
+ """
+ Called before template is applied.
+ """
+ pass
+
+ def post(self, command, output_dir, vars): # pragma: no cover
+ """
+ Called after template is applied.
+ """
+ pass
+
+ def write_files(self, command, output_dir, vars):
+ template_dir = self.template_dir()
+ if not self.exists(output_dir):
+ self.out("Creating directory %s" % output_dir)
+ if not command.args.simulate:
+ # Don't let copydir create this top-level directory,
+ # since copydir will svn add it sometimes:
+ self.makedirs(output_dir)
+ self.copydir.copy_dir(
+ template_dir,
+ output_dir,
+ vars,
+ verbosity=command.verbosity,
+ simulate=command.args.simulate,
+ interactive=command.args.interactive,
+ overwrite=command.args.overwrite,
+ indent=1,
+ template_renderer=self.render_template,
+ )
+
+ def makedirs(self, dir): # pragma: no cover
+ return os.makedirs(dir)
+
+ def exists(self, path): # pragma: no cover
+ return os.path.exists(path)
+
+ def out(self, msg): # pragma: no cover
+ print(msg)
+
+ # hair for exit with usage when paster create is used under 1.3 instead
+ # of pcreate for extension scaffolds which need to support multiple
+ # versions of pyramid; the check_vars method is called by pastescript
+ # only as the result of "paster create"; pyramid doesn't use it. the
+ # required_templates tuple is required to allow it to get as far as
+ # calling check_vars.
+ required_templates = ()
+ def check_vars(self, vars, other):
+ raise RuntimeError(
+ 'Under Pyramid 1.3, you should use the "pcreate" command rather '
+ 'than "paster create"')
+
+class TypeMapper(dict):
+
+ def __getitem__(self, item):
+ options = item.split('|')
+ for op in options[:-1]:
+ try:
+ value = eval_with_catch(op, dict(self.items()))
+ break
+ except (NameError, KeyError):
+ pass
+ else:
+ value = eval(options[-1], dict(self.items()))
+ if value is None:
+ return ''
+ else:
+ return str(value)
+
+def eval_with_catch(expr, vars):
+ try:
+ return eval(expr, vars)
+ except Exception as e:
+ _add_except(e, 'in expression %r' % expr)
+ raise
+
+double_brace_pattern = re.compile(r'{{(?P<braced>.*?)}}')
+
+def substitute_double_braces(content, values):
+ def double_bracerepl(match):
+ value = match.group('braced').strip()
+ return values[value]
+ return double_brace_pattern.sub(double_bracerepl, content)
+
+escaped_double_brace_pattern = re.compile(r'\\{\\{(?P<escape_braced>[^\\]*?)\\}\\}')
+
+def substitute_escaped_double_braces(content):
+ def escaped_double_bracerepl(match):
+ value = match.group('escape_braced').strip()
+ return "{{%(value)s}}" % locals()
+ return escaped_double_brace_pattern.sub(escaped_double_bracerepl, content)
+
+def _add_except(exc, info): # pragma: no cover
+ if not hasattr(exc, 'args') or exc.args is None:
+ return
+ args = list(exc.args)
+ if args:
+ args[0] += ' ' + info
+ else:
+ args = [info]
+ exc.args = tuple(args)
+ return
+
+
diff --git a/src/pyramid/scaffolds/tests.py b/src/pyramid/scaffolds/tests.py
new file mode 100644
index 000000000..44680a464
--- /dev/null
+++ b/src/pyramid/scaffolds/tests.py
@@ -0,0 +1,75 @@
+import sys
+import os
+import shutil
+import subprocess
+import tempfile
+import time
+
+try:
+ import http.client as httplib
+except ImportError:
+ import httplib
+
+
+class TemplateTest(object):
+ def make_venv(self, directory): # pragma: no cover
+ import virtualenv
+ from virtualenv import Logger
+ logger = Logger([(Logger.level_for_integer(2), sys.stdout)])
+ virtualenv.logger = logger
+ virtualenv.create_environment(directory,
+ site_packages=False,
+ clear=False,
+ unzip_setuptools=True)
+
+ def install(self, tmpl_name): # pragma: no cover
+ try:
+ self.old_cwd = os.getcwd()
+ self.directory = tempfile.mkdtemp()
+ self.make_venv(self.directory)
+ here = os.path.abspath(os.path.dirname(__file__))
+ os.chdir(os.path.dirname(os.path.dirname(here)))
+ pip = os.path.join(self.directory, 'bin', 'pip')
+ subprocess.check_call([pip, 'install', '-e', '.'])
+ os.chdir(self.directory)
+ subprocess.check_call(['bin/pcreate', '-s', tmpl_name, 'Dingle'])
+ os.chdir('Dingle')
+ subprocess.check_call([pip, 'install', '.[testing]'])
+ if tmpl_name == 'alchemy':
+ populate = os.path.join(self.directory, 'bin',
+ 'initialize_Dingle_db')
+ subprocess.check_call([populate, 'development.ini'])
+ subprocess.check_call([
+ os.path.join(self.directory, 'bin', 'py.test')])
+ pserve = os.path.join(self.directory, 'bin', 'pserve')
+ for ininame, hastoolbar in (('development.ini', True),
+ ('production.ini', False)):
+ proc = subprocess.Popen([pserve, ininame])
+ try:
+ time.sleep(5)
+ proc.poll()
+ if proc.returncode is not None:
+ raise RuntimeError('%s didnt start' % ininame)
+ conn = httplib.HTTPConnection('localhost:6543')
+ conn.request('GET', '/')
+ resp = conn.getresponse()
+ assert resp.status == 200, ininame
+ data = resp.read()
+ toolbarchunk = b'<div id="pDebug"'
+ if hastoolbar:
+ assert toolbarchunk in data, ininame
+ else:
+ assert toolbarchunk not in data, ininame
+ finally:
+ proc.terminate()
+ finally:
+ shutil.rmtree(self.directory)
+ os.chdir(self.old_cwd)
+
+if __name__ == '__main__': # pragma: no cover
+ templates = ['starter', 'alchemy', 'zodb']
+
+ for name in templates:
+ test = TemplateTest()
+ test.install(name)
+
diff --git a/src/pyramid/scaffolds/zodb/+dot+coveragerc_tmpl b/src/pyramid/scaffolds/zodb/+dot+coveragerc_tmpl
new file mode 100644
index 000000000..273a4a580
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+dot+coveragerc_tmpl
@@ -0,0 +1,3 @@
+[run]
+source = {{package}}
+omit = {{package}}/test*
diff --git a/src/pyramid/scaffolds/zodb/+package+/__init__.py b/src/pyramid/scaffolds/zodb/+package+/__init__.py
new file mode 100644
index 000000000..a956d0faf
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+package+/__init__.py
@@ -0,0 +1,20 @@
+from pyramid.config import Configurator
+from pyramid_zodbconn import get_connection
+from .models import appmaker
+
+
+def root_factory(request):
+ conn = get_connection(request)
+ return appmaker(conn.root())
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(root_factory=root_factory, settings=settings)
+ settings = config.get_settings()
+ settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager'
+ config.include('pyramid_chameleon')
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/src/pyramid/scaffolds/zodb/+package+/models.py b/src/pyramid/scaffolds/zodb/+package+/models.py
new file mode 100644
index 000000000..e5aa3e9f7
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+package+/models.py
@@ -0,0 +1,14 @@
+from persistent.mapping import PersistentMapping
+
+
+class MyModel(PersistentMapping):
+ __parent__ = __name__ = None
+
+
+def appmaker(zodb_root):
+ if 'app_root' not in zodb_root:
+ app_root = MyModel()
+ zodb_root['app_root'] = app_root
+ import transaction
+ transaction.commit()
+ return zodb_root['app_root']
diff --git a/src/pyramid/scaffolds/zodb/+package+/static/pyramid-16x16.png b/src/pyramid/scaffolds/zodb/+package+/static/pyramid-16x16.png
new file mode 100644
index 000000000..979203112
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+package+/static/pyramid-16x16.png
Binary files differ
diff --git a/src/pyramid/scaffolds/zodb/+package+/static/pyramid.png b/src/pyramid/scaffolds/zodb/+package+/static/pyramid.png
new file mode 100644
index 000000000..4ab837be9
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+package+/static/pyramid.png
Binary files differ
diff --git a/src/pyramid/scaffolds/zodb/+package+/static/theme.css b/src/pyramid/scaffolds/zodb/+package+/static/theme.css
new file mode 100644
index 000000000..0f4b1a4d4
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+package+/static/theme.css
@@ -0,0 +1,154 @@
+@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700);
+body {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+ color: #ffffff;
+ background: #bc2131;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-weight: 300;
+}
+p {
+ font-weight: 300;
+}
+.font-normal {
+ font-weight: 400;
+}
+.font-semi-bold {
+ font-weight: 600;
+}
+.font-bold {
+ font-weight: 700;
+}
+.starter-template {
+ margin-top: 250px;
+}
+.starter-template .content {
+ margin-left: 10px;
+}
+.starter-template .content h1 {
+ margin-top: 10px;
+ font-size: 60px;
+}
+.starter-template .content h1 .smaller {
+ font-size: 40px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead {
+ font-size: 25px;
+ color: #f2b7bd;
+}
+.starter-template .content .lead .font-normal {
+ color: #ffffff;
+}
+.starter-template .links {
+ float: right;
+ right: 0;
+ margin-top: 125px;
+}
+.starter-template .links ul {
+ display: block;
+ padding: 0;
+ margin: 0;
+}
+.starter-template .links ul li {
+ list-style: none;
+ display: inline;
+ margin: 0 10px;
+}
+.starter-template .links ul li:first-child {
+ margin-left: 0;
+}
+.starter-template .links ul li:last-child {
+ margin-right: 0;
+}
+.starter-template .links ul li.current-version {
+ color: #f2b7bd;
+ font-weight: 400;
+}
+.starter-template .links ul li a, a {
+ color: #f2b7bd;
+ text-decoration: underline;
+}
+.starter-template .links ul li a:hover, a:hover {
+ color: #ffffff;
+ text-decoration: underline;
+}
+.starter-template .links ul li .icon-muted {
+ color: #eb8b95;
+ margin-right: 5px;
+}
+.starter-template .links ul li:hover .icon-muted {
+ color: #ffffff;
+}
+.starter-template .copyright {
+ margin-top: 10px;
+ font-size: 0.9em;
+ color: #f2b7bd;
+ text-transform: lowercase;
+ float: right;
+ right: 0;
+}
+@media (max-width: 1199px) {
+ .starter-template .content h1 {
+ font-size: 45px;
+ }
+ .starter-template .content h1 .smaller {
+ font-size: 30px;
+ }
+ .starter-template .content .lead {
+ font-size: 20px;
+ }
+}
+@media (max-width: 991px) {
+ .starter-template {
+ margin-top: 0;
+ }
+ .starter-template .logo {
+ margin: 40px auto;
+ }
+ .starter-template .content {
+ margin-left: 0;
+ text-align: center;
+ }
+ .starter-template .content h1 {
+ margin-bottom: 20px;
+ }
+ .starter-template .links {
+ float: none;
+ text-align: center;
+ margin-top: 60px;
+ }
+ .starter-template .copyright {
+ float: none;
+ text-align: center;
+ }
+}
+@media (max-width: 767px) {
+ .starter-template .content h1 .smaller {
+ font-size: 25px;
+ display: block;
+ }
+ .starter-template .content .lead {
+ font-size: 16px;
+ }
+ .starter-template .links {
+ margin-top: 40px;
+ }
+ .starter-template .links ul li {
+ display: block;
+ margin: 0;
+ }
+ .starter-template .links ul li .icon-muted {
+ display: none;
+ }
+ .starter-template .copyright {
+ margin-top: 20px;
+ }
+}
diff --git a/src/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt_tmpl b/src/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt_tmpl
new file mode 100644
index 000000000..f66effa41
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt_tmpl
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<html lang="${request.locale_name}">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="pyramid web application">
+ <meta name="author" content="Pylons Project">
+ <link rel="shortcut icon" href="${request.static_url('{{package}}:static/pyramid-16x16.png')}">
+
+ <title>ZODB Scaffold for The Pyramid Web Framework</title>
+
+ <!-- Bootstrap core CSS -->
+ <link href="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
+
+ <!-- Custom styles for this scaffold -->
+ <link href="${request.static_url('{{package}}:static/theme.css')}" rel="stylesheet">
+
+ <!-- HTML5 shiv and Respond.js IE8 support of HTML5 elements and media queries -->
+ <!--[if lt IE 9]>
+ <script src="//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js" integrity="sha384-0s5Pv64cNZJieYFkXYOTId2HMA2Lfb6q2nAcx2n0RTLUnCAoTTsS0nKEO27XyKcY" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js" integrity="sha384-f1r2UzjsxZ9T4V1f2zBO/evUqSEOpeaUUZcMTz1Up63bl4ruYnFYeM+BxI4NhyI0" crossorigin="anonymous"></script>
+ <![endif]-->
+ </head>
+
+ <body>
+
+ <div class="starter-template">
+ <div class="container">
+ <div class="row">
+ <div class="col-md-2">
+ <img class="logo img-responsive" src="${request.static_url('{{package}}:static/pyramid.png')}" alt="pyramid web framework">
+ </div>
+ <div class="col-md-10">
+ <div class="content">
+ <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">ZODB scaffold</span></h1>
+ <p class="lead">Welcome to <span class="font-normal">${project}</span>, an&nbsp;application generated&nbsp;by<br>the <span class="font-normal">Pyramid Web Framework {{pyramid_version}}</span>.</p>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="links">
+ <ul>
+ <li class="current-version">Generated by v{{pyramid_version}}</li>
+ <li><i class="glyphicon glyphicon-bookmark icon-muted"></i><a href="https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/">Docs</a></li>
+ <li><i class="glyphicon glyphicon-cog icon-muted"></i><a href="https://github.com/Pylons/pyramid">Github Project</a></li>
+ <li><i class="glyphicon glyphicon-globe icon-muted"></i><a href="https://webchat.freenode.net/?channels=pyramid">IRC Channel</a></li>
+ <li><i class="glyphicon glyphicon-home icon-muted"></i><a href="https://pylonsproject.org">Pylons Project</a></li>
+ </ul>
+ </div>
+ </div>
+ <div class="row">
+ <div class="copyright">
+ Copyright &copy; Pylons Project
+ </div>
+ </div>
+ </div>
+ </div>
+
+
+ <!-- Bootstrap core JavaScript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="//oss.maxcdn.com/libs/jquery/1.10.2/jquery.min.js" integrity="sha384-aBL3Lzi6c9LNDGvpHkZrrm3ZVsIwohDD7CDozL0pk8FwCrfmV7H9w8j3L7ikEv6h" crossorigin="anonymous"></script>
+ <script src="//oss.maxcdn.com/libs/twitter-bootstrap/3.0.3/js/bootstrap.min.js" integrity="sha384-s1ITto93iSMDxlp/79qhWHi+LsIi9Gx6yL+cOKDuymvihkfol83TYbLbOw+W/wv4" crossorigin="anonymous"></script>
+ </body>
+</html>
diff --git a/src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl b/src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl
new file mode 100644
index 000000000..94912a850
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl
@@ -0,0 +1,17 @@
+import unittest
+
+from pyramid import testing
+
+
+class ViewTests(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_my_view(self):
+ from .views import my_view
+ request = testing.DummyRequest()
+ info = my_view(request)
+ self.assertEqual(info['project'], '{{project}}')
diff --git a/src/pyramid/scaffolds/zodb/+package+/views.py_tmpl b/src/pyramid/scaffolds/zodb/+package+/views.py_tmpl
new file mode 100644
index 000000000..1e8a9b65a
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/+package+/views.py_tmpl
@@ -0,0 +1,7 @@
+from pyramid.view import view_config
+from .models import MyModel
+
+
+@view_config(context=MyModel, renderer='templates/mytemplate.pt')
+def my_view(request):
+ return {'project': '{{project}}'}
diff --git a/src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl b/src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl
new file mode 100644
index 000000000..35a34f332
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version
diff --git a/src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl b/src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl
new file mode 100644
index 000000000..0ff6eb7a0
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml
diff --git a/src/pyramid/scaffolds/zodb/README.txt_tmpl b/src/pyramid/scaffolds/zodb/README.txt_tmpl
new file mode 100644
index 000000000..127ad7595
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/README.txt_tmpl
@@ -0,0 +1,12 @@
+{{project}} README
+==================
+
+Getting Started
+---------------
+
+- cd <directory containing this file>
+
+- $VENV/bin/pip install -e .
+
+- $VENV/bin/pserve development.ini
+
diff --git a/src/pyramid/scaffolds/zodb/development.ini_tmpl b/src/pyramid/scaffolds/zodb/development.ini_tmpl
new file mode 100644
index 000000000..7d898bcd4
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/development.ini_tmpl
@@ -0,0 +1,64 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html
+###
+
+[app:main]
+use = egg:{{project}}
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+ pyramid_zodbconn
+ pyramid_tm
+
+tm.attempts = 3
+zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = localhost:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html
+###
+
+[loggers]
+keys = root, {{package_logger}}
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_{{package_logger}}]
+level = DEBUG
+handlers =
+qualname = {{package}}
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/src/pyramid/scaffolds/zodb/production.ini_tmpl b/src/pyramid/scaffolds/zodb/production.ini_tmpl
new file mode 100644
index 000000000..7c2e90c2e
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/production.ini_tmpl
@@ -0,0 +1,59 @@
+###
+# app configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html
+###
+
+[app:main]
+use = egg:{{project}}
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_tm
+ pyramid_zodbconn
+
+tm.attempts = 3
+zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = *:6543
+
+###
+# logging configuration
+# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html
+###
+
+[loggers]
+keys = root, {{package_logger}}
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_{{package_logger}}]
+level = WARN
+handlers =
+qualname = {{package}}
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/src/pyramid/scaffolds/zodb/pytest.ini_tmpl b/src/pyramid/scaffolds/zodb/pytest.ini_tmpl
new file mode 100644
index 000000000..a30c8bcad
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/pytest.ini_tmpl
@@ -0,0 +1,3 @@
+[pytest]
+testpaths = {{package}}
+python_files = *.py
diff --git a/src/pyramid/scaffolds/zodb/setup.py_tmpl b/src/pyramid/scaffolds/zodb/setup.py_tmpl
new file mode 100644
index 000000000..19771d756
--- /dev/null
+++ b/src/pyramid/scaffolds/zodb/setup.py_tmpl
@@ -0,0 +1,53 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.txt')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'pyramid',
+ 'pyramid_chameleon',
+ 'pyramid_debugtoolbar',
+ 'pyramid_tm',
+ 'pyramid_zodbconn',
+ 'transaction',
+ 'ZODB3',
+ 'waitress',
+ ]
+
+tests_require = [
+ 'WebTest >= 1.3.1', # py3 compat
+ 'pytest', # includes virtualenv
+ 'pytest-cov',
+ ]
+
+setup(name='{{project}}',
+ version='0.0',
+ description='{{project}}',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Internet :: WWW/HTTP",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web pylons pyramid',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ extras_require={
+ 'testing': tests_require,
+ },
+ install_requires=requires,
+ entry_points="""\
+ [paste.app_factory]
+ main = {{package}}:main
+ """,
+ )
diff --git a/src/pyramid/scripting.py b/src/pyramid/scripting.py
new file mode 100644
index 000000000..087b55ccb
--- /dev/null
+++ b/src/pyramid/scripting.py
@@ -0,0 +1,141 @@
+from pyramid.config import global_registries
+from pyramid.exceptions import ConfigurationError
+
+from pyramid.interfaces import (
+ IRequestFactory,
+ IRootFactory,
+ )
+from pyramid.request import Request
+from pyramid.request import apply_request_extensions
+
+from pyramid.threadlocal import RequestContext
+from pyramid.traversal import DefaultRootFactory
+
+def get_root(app, request=None):
+ """ Return a tuple composed of ``(root, closer)`` when provided a
+ :term:`router` instance as the ``app`` argument. The ``root``
+ returned is the application root object. The ``closer`` returned
+ is a callable (accepting no arguments) that should be called when
+ your scripting application is finished using the root.
+
+ ``request`` is passed to the :app:`Pyramid` application root
+ factory to compute the root. If ``request`` is None, a default
+ will be constructed using the registry's :term:`Request Factory`
+ via the :meth:`pyramid.interfaces.IRequestFactory.blank` method.
+ """
+ registry = app.registry
+ if request is None:
+ request = _make_request('/', registry)
+ request.registry = registry
+ ctx = RequestContext(request)
+ ctx.begin()
+ def closer():
+ ctx.end()
+ root = app.root_factory(request)
+ return root, closer
+
+def prepare(request=None, registry=None):
+ """ This function pushes data onto the Pyramid threadlocal stack
+ (request and registry), making those objects 'current'. It
+ returns a dictionary useful for bootstrapping a Pyramid
+ application in a scripting environment.
+
+ ``request`` is passed to the :app:`Pyramid` application root
+ factory to compute the root. If ``request`` is None, a default
+ will be constructed using the registry's :term:`Request Factory`
+ via the :meth:`pyramid.interfaces.IRequestFactory.blank` method.
+
+ If ``registry`` is not supplied, the last registry loaded from
+ :attr:`pyramid.config.global_registries` will be used. If you
+ have loaded more than one :app:`Pyramid` application in the
+ current process, you may not want to use the last registry
+ loaded, thus you can search the ``global_registries`` and supply
+ the appropriate one based on your own criteria.
+
+ The function returns a dictionary composed of ``root``,
+ ``closer``, ``registry``, ``request`` and ``root_factory``. The
+ ``root`` returned is the application's root resource object. The
+ ``closer`` returned is a callable (accepting no arguments) that
+ should be called when your scripting application is finished
+ using the root. ``registry`` is the resolved registry object.
+ ``request`` is the request object passed or the constructed request
+ if no request is passed. ``root_factory`` is the root factory used
+ to construct the root.
+
+ This function may be used as a context manager to call the ``closer``
+ automatically:
+
+ .. code-block:: python
+
+ registry = config.registry
+ with prepare(registry) as env:
+ request = env['request']
+ # ...
+
+ .. versionchanged:: 1.8
+
+ Added the ability to use the return value as a context manager.
+
+ """
+ if registry is None:
+ registry = getattr(request, 'registry', global_registries.last)
+ if registry is None:
+ raise ConfigurationError('No valid Pyramid applications could be '
+ 'found, make sure one has been created '
+ 'before trying to activate it.')
+ if request is None:
+ request = _make_request('/', registry)
+ # NB: even though _make_request might have already set registry on
+ # request, we reset it in case someone has passed in their own
+ # request.
+ request.registry = registry
+ ctx = RequestContext(request)
+ ctx.begin()
+ apply_request_extensions(request)
+ def closer():
+ ctx.end()
+ root_factory = registry.queryUtility(IRootFactory,
+ default=DefaultRootFactory)
+ root = root_factory(request)
+ if getattr(request, 'context', None) is None:
+ request.context = root
+ return AppEnvironment(
+ root=root,
+ closer=closer,
+ registry=registry,
+ request=request,
+ root_factory=root_factory,
+ )
+
+class AppEnvironment(dict):
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self['closer']()
+
+def _make_request(path, registry=None):
+ """ Return a :meth:`pyramid.request.Request` object anchored at a
+ given path. The object returned will be generated from the supplied
+ registry's :term:`Request Factory` using the
+ :meth:`pyramid.interfaces.IRequestFactory.blank` method.
+
+ This request object can be passed to :meth:`pyramid.scripting.get_root`
+ or :meth:`pyramid.scripting.prepare` to initialize an application in
+ preparation for executing a script with a proper environment setup.
+ URLs can then be generated with the object, as well as rendering
+ templates.
+
+ If ``registry`` is not supplied, the last registry loaded from
+ :attr:`pyramid.config.global_registries` will be used. If you have
+ loaded more than one :app:`Pyramid` application in the current
+ process, you may not want to use the last registry loaded, thus
+ you can search the ``global_registries`` and supply the appropriate
+ one based on your own criteria.
+ """
+ if registry is None:
+ registry = global_registries.last
+ request_factory = registry.queryUtility(IRequestFactory, default=Request)
+ request = request_factory.blank(path)
+ request.registry = registry
+ return request
diff --git a/src/pyramid/scripts/__init__.py b/src/pyramid/scripts/__init__.py
new file mode 100644
index 000000000..5bb534f79
--- /dev/null
+++ b/src/pyramid/scripts/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/src/pyramid/scripts/common.py b/src/pyramid/scripts/common.py
new file mode 100644
index 000000000..f4b8027db
--- /dev/null
+++ b/src/pyramid/scripts/common.py
@@ -0,0 +1,23 @@
+import plaster
+
+def parse_vars(args):
+ """
+ Given variables like ``['a=b', 'c=d']`` turns it into ``{'a':
+ 'b', 'c': 'd'}``
+ """
+ result = {}
+ for arg in args:
+ if '=' not in arg:
+ raise ValueError(
+ 'Variable assignment %r invalid (no "=")'
+ % arg)
+ name, value = arg.split('=', 1)
+ result[name] = value
+ return result
+
+def get_config_loader(config_uri):
+ """
+ Find a ``plaster.ILoader`` object supporting the "wsgi" protocol.
+
+ """
+ return plaster.get_loader(config_uri, protocols=['wsgi'])
diff --git a/src/pyramid/scripts/pcreate.py b/src/pyramid/scripts/pcreate.py
new file mode 100644
index 000000000..a6db520ce
--- /dev/null
+++ b/src/pyramid/scripts/pcreate.py
@@ -0,0 +1,251 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste
+# (http://pythonpaste.org) Licensed under the MIT license:
+# http://www.opensource.org/licenses/mit-license.php
+
+import argparse
+import os
+import os.path
+import pkg_resources
+import re
+import sys
+from pyramid.compat import input_
+
+_bad_chars_re = re.compile('[^a-zA-Z0-9_]')
+
+
+def main(argv=sys.argv, quiet=False):
+ command = PCreateCommand(argv, quiet)
+ try:
+ return command.run()
+ except KeyboardInterrupt: # pragma: no cover
+ return 1
+
+
+class PCreateCommand(object):
+ verbosity = 1 # required
+ parser = argparse.ArgumentParser(
+ description="""\
+Render Pyramid scaffolding to an output directory.
+
+Note: As of Pyramid 1.8, this command is deprecated. Use a specific
+cookiecutter instead:
+https://github.com/Pylons/?q=cookiecutter
+""",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument('-s', '--scaffold',
+ dest='scaffold_name',
+ action='append',
+ help=("Add a scaffold to the create process "
+ "(multiple -s args accepted)"))
+ parser.add_argument('-t', '--template',
+ dest='scaffold_name',
+ action='append',
+ help=('A backwards compatibility alias for '
+ '-s/--scaffold. Add a scaffold to the '
+ 'create process (multiple -t args accepted)'))
+ parser.add_argument('-l', '--list',
+ dest='list',
+ action='store_true',
+ help="List all available scaffold names")
+ parser.add_argument('--list-templates',
+ dest='list',
+ action='store_true',
+ help=("A backwards compatibility alias for -l/--list. "
+ "List all available scaffold names."))
+ parser.add_argument('--package-name',
+ dest='package_name',
+ action='store',
+ help='Package name to use. The name provided is '
+ 'assumed to be a valid Python package name, and '
+ 'will not be validated. By default the package '
+ 'name is derived from the value of '
+ 'output_directory.')
+ parser.add_argument('--simulate',
+ dest='simulate',
+ action='store_true',
+ help='Simulate but do no work')
+ parser.add_argument('--overwrite',
+ dest='overwrite',
+ action='store_true',
+ help='Always overwrite')
+ parser.add_argument('--interactive',
+ dest='interactive',
+ action='store_true',
+ help='When a file would be overwritten, interrogate '
+ '(this is the default, but you may specify it to '
+ 'override --overwrite)')
+ parser.add_argument('--ignore-conflicting-name',
+ dest='force_bad_name',
+ action='store_true',
+ default=False,
+ help='Do create a project even if the chosen name '
+ 'is the name of an already existing / importable '
+ 'package.')
+ parser.add_argument('output_directory',
+ nargs='?',
+ default=None,
+ help='The directory where the project will be '
+ 'created.')
+
+ pyramid_dist = pkg_resources.get_distribution("pyramid")
+
+ def __init__(self, argv, quiet=False):
+ self.quiet = quiet
+ self.args = self.parser.parse_args(argv[1:])
+ if not self.args.interactive and not self.args.overwrite:
+ self.args.interactive = True
+ self.scaffolds = self.all_scaffolds()
+
+ def run(self):
+ if self.args.list:
+ return self.show_scaffolds()
+ if not self.args.scaffold_name and not self.args.output_directory:
+ if not self.quiet: # pragma: no cover
+ self.parser.print_help()
+ self.out('')
+ self.show_scaffolds()
+ return 2
+
+ if not self.validate_input():
+ return 2
+ self._warn_pcreate_deprecated()
+
+ return self.render_scaffolds()
+
+ @property
+ def output_path(self):
+ return os.path.abspath(os.path.normpath(self.args.output_directory))
+
+ @property
+ def project_vars(self):
+ output_dir = self.output_path
+ project_name = os.path.basename(os.path.split(output_dir)[1])
+ if self.args.package_name is None:
+ pkg_name = _bad_chars_re.sub(
+ '', project_name.lower().replace('-', '_'))
+ safe_name = pkg_resources.safe_name(project_name)
+ else:
+ pkg_name = self.args.package_name
+ safe_name = pkg_name
+ egg_name = pkg_resources.to_filename(safe_name)
+
+ # get pyramid package version
+ pyramid_version = self.pyramid_dist.version
+
+ # map pyramid package version of the documentation branch ##
+ # if version ends with 'dev' then docs version is 'master'
+ if self.pyramid_dist.version[-3:] == 'dev':
+ pyramid_docs_branch = 'master'
+ else:
+ # if not version is not 'dev' find the version.major_version string
+ # and combine it with '-branch'
+ version_match = re.match(r'(\d+\.\d+)', self.pyramid_dist.version)
+ if version_match is not None:
+ pyramid_docs_branch = "%s-branch" % version_match.group()
+ # if can not parse the version then default to 'latest'
+ else:
+ pyramid_docs_branch = 'latest'
+
+ return {
+ 'project': project_name,
+ 'package': pkg_name,
+ 'egg': egg_name,
+ 'pyramid_version': pyramid_version,
+ 'pyramid_docs_branch': pyramid_docs_branch,
+ }
+
+ def render_scaffolds(self):
+ props = self.project_vars
+ output_dir = self.output_path
+ for scaffold_name in self.args.scaffold_name:
+ for scaffold in self.scaffolds:
+ if scaffold.name == scaffold_name:
+ scaffold.run(self, output_dir, props)
+ return 0
+
+ def show_scaffolds(self):
+ scaffolds = sorted(self.scaffolds, key=lambda x: x.name)
+ if scaffolds:
+ max_name = max([len(t.name) for t in scaffolds])
+ self.out('Available scaffolds:')
+ for scaffold in scaffolds:
+ self.out(' %s:%s %s' % (
+ scaffold.name,
+ ' ' * (max_name - len(scaffold.name)), scaffold.summary))
+ else:
+ self.out('No scaffolds available')
+ return 0
+
+ def all_scaffolds(self):
+ scaffolds = []
+ eps = list(pkg_resources.iter_entry_points('pyramid.scaffold'))
+ for entry in eps:
+ try:
+ scaffold_class = entry.load()
+ scaffold = scaffold_class(entry.name)
+ scaffolds.append(scaffold)
+ except Exception as e: # pragma: no cover
+ self.out('Warning: could not load entry point %s (%s: %s)' % (
+ entry.name, e.__class__.__name__, e))
+ return scaffolds
+
+ def out(self, msg): # pragma: no cover
+ if not self.quiet:
+ print(msg)
+
+ def validate_input(self):
+ if not self.args.scaffold_name:
+ self.out('You must provide at least one scaffold name: '
+ '-s <scaffold name>')
+ self.out('')
+ self.show_scaffolds()
+ return False
+ if not self.args.output_directory:
+ self.out('You must provide a project name')
+ return False
+ available = [x.name for x in self.scaffolds]
+ diff = set(self.args.scaffold_name).difference(available)
+ if diff:
+ self.out('Unavailable scaffolds: %s' % ", ".join(sorted(diff)))
+ return False
+
+ pkg_name = self.project_vars['package']
+
+ if pkg_name == 'site' and not self.args.force_bad_name:
+ self.out('The package name "site" has a special meaning in '
+ 'Python. Are you sure you want to use it as your '
+ 'project\'s name?')
+ return self.confirm_bad_name('Really use "{0}"?: '.format(
+ pkg_name))
+
+ # check if pkg_name can be imported (i.e. already exists in current
+ # $PYTHON_PATH, if so - let the user confirm
+ pkg_exists = True
+ try:
+ # use absolute imports
+ __import__(pkg_name, globals(), locals(), [], 0)
+ except ImportError as error:
+ pkg_exists = False
+ if not pkg_exists:
+ return True
+
+ if self.args.force_bad_name:
+ return True
+ self.out('A package named "{0}" already exists, are you sure you want '
+ 'to use it as your project\'s name?'.format(pkg_name))
+ return self.confirm_bad_name('Really use "{0}"?: '.format(pkg_name))
+
+ def confirm_bad_name(self, prompt): # pragma: no cover
+ answer = input_('{0} [y|N]: '.format(prompt))
+ return answer.strip().lower() == 'y'
+
+ def _warn_pcreate_deprecated(self):
+ self.out('''\
+Note: As of Pyramid 1.8, this command is deprecated. Use a specific
+cookiecutter instead:
+https://github.com/pylons/?query=cookiecutter
+''')
+
+if __name__ == '__main__': # pragma: no cover
+ sys.exit(main() or 0)
diff --git a/src/pyramid/scripts/pdistreport.py b/src/pyramid/scripts/pdistreport.py
new file mode 100644
index 000000000..1952e5d39
--- /dev/null
+++ b/src/pyramid/scripts/pdistreport.py
@@ -0,0 +1,43 @@
+import sys
+import platform
+import pkg_resources
+import argparse
+from operator import itemgetter
+
+def out(*args): # pragma: no cover
+ for arg in args:
+ sys.stdout.write(arg)
+ sys.stdout.write(' ')
+ sys.stdout.write('\n')
+
+def get_parser():
+ parser = argparse.ArgumentParser(
+ description="Show Python distribution versions and locations in use")
+ return parser
+
+def main(argv=sys.argv, pkg_resources=pkg_resources, platform=platform.platform,
+ out=out):
+ # all args except argv are for unit testing purposes only
+ parser = get_parser()
+ parser.parse_args(argv[1:])
+ packages = []
+ for distribution in pkg_resources.working_set:
+ name = distribution.project_name
+ packages.append(
+ {'version': distribution.version,
+ 'lowername': name.lower(),
+ 'name': name,
+ 'location':distribution.location}
+ )
+ packages = sorted(packages, key=itemgetter('lowername'))
+ pyramid_version = pkg_resources.get_distribution('pyramid').version
+ plat = platform()
+ out('Pyramid version:', pyramid_version)
+ out('Platform:', plat)
+ out('Packages:')
+ for package in packages:
+ out(' ', package['name'], package['version'])
+ out(' ', package['location'])
+
+if __name__ == '__main__': # pragma: no cover
+ sys.exit(main() or 0)
diff --git a/src/pyramid/scripts/prequest.py b/src/pyramid/scripts/prequest.py
new file mode 100644
index 000000000..f0681afd7
--- /dev/null
+++ b/src/pyramid/scripts/prequest.py
@@ -0,0 +1,207 @@
+import base64
+import argparse
+import sys
+import textwrap
+
+from pyramid.compat import url_unquote
+from pyramid.request import Request
+from pyramid.scripts.common import get_config_loader
+from pyramid.scripts.common import parse_vars
+
+def main(argv=sys.argv, quiet=False):
+ command = PRequestCommand(argv, quiet)
+ return command.run()
+
+class PRequestCommand(object):
+ description = """\
+ Submit a HTTP request to a web application.
+
+ This command makes an artifical request to a web application that uses a
+ PasteDeploy (.ini) configuration file for the server and application.
+
+ Use "prequest config.ini /path" to request "/path".
+
+ Use "prequest --method=POST config.ini /path < data" to do a POST with
+ the given request body.
+
+ Use "prequest --method=PUT config.ini /path < data" to do a
+ PUT with the given request body.
+
+ Use "prequest --method=PATCH config.ini /path < data" to do a
+ PATCH with the given request body.
+
+ Use "prequest --method=OPTIONS config.ini /path" to do an
+ OPTIONS request.
+
+ Use "prequest --method=PROPFIND config.ini /path" to do a
+ PROPFIND request.
+
+ If the path is relative (doesn't begin with "/") it is interpreted as
+ relative to "/". The path passed to this script should be URL-quoted.
+ The path can be succeeded with a query string (e.g. '/path?a=1&=b2').
+
+ The variable "environ['paste.command_request']" will be set to "True" in
+ the request's WSGI environment, so your application can distinguish these
+ calls from normal requests.
+ """
+
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(description),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ '-n', '--app-name',
+ dest='app_name',
+ metavar='NAME',
+ help=(
+ "Load the named application from the config file (default 'main')"
+ ),
+ )
+ parser.add_argument(
+ '--header',
+ dest='headers',
+ metavar='NAME:VALUE',
+ action='append',
+ help=(
+ "Header to add to request (you can use this option multiple times)"
+ ),
+ )
+ parser.add_argument(
+ '-d', '--display-headers',
+ dest='display_headers',
+ action='store_true',
+ help='Display status and headers before the response body'
+ )
+ parser.add_argument(
+ '-m', '--method',
+ dest='method',
+ choices=['GET', 'HEAD', 'POST', 'PUT', 'PATCH','DELETE',
+ 'PROPFIND', 'OPTIONS'],
+ help='Request method type (GET, POST, PUT, PATCH, DELETE, '
+ 'PROPFIND, OPTIONS)',
+ )
+ parser.add_argument(
+ '-l', '--login',
+ dest='login',
+ help='HTTP basic auth username:password pair',
+ )
+
+ parser.add_argument(
+ 'config_uri',
+ nargs='?',
+ default=None,
+ help='The URI to the configuration file.',
+ )
+
+ parser.add_argument(
+ 'path_info',
+ nargs='?',
+ default=None,
+ help='The path of the request.',
+ )
+
+ parser.add_argument(
+ 'config_vars',
+ nargs='*',
+ default=(),
+ help="Variables required by the config file. For example, "
+ "`http_port=%%(http_port)s` would expect `http_port=8080` to be "
+ "passed here.",
+ )
+
+ _get_config_loader = staticmethod(get_config_loader)
+ stdin = sys.stdin
+
+ def __init__(self, argv, quiet=False):
+ self.quiet = quiet
+ self.args = self.parser.parse_args(argv[1:])
+
+ def out(self, msg): # pragma: no cover
+ if not self.quiet:
+ print(msg)
+
+ def run(self):
+ if not self.args.config_uri or not self.args.path_info:
+ self.out('You must provide at least two arguments')
+ return 2
+ config_uri = self.args.config_uri
+ config_vars = parse_vars(self.args.config_vars)
+ path = self.args.path_info
+
+ loader = self._get_config_loader(config_uri)
+ loader.setup_logging(config_vars)
+
+ app = loader.get_wsgi_app(self.args.app_name, config_vars)
+
+ if not path.startswith('/'):
+ path = '/' + path
+
+ try:
+ path, qs = path.split('?', 1)
+ except ValueError:
+ qs = ''
+
+ path = url_unquote(path)
+
+ headers = {}
+ if self.args.login:
+ enc = base64.b64encode(self.args.login.encode('ascii'))
+ headers['Authorization'] = 'Basic ' + enc.decode('ascii')
+
+ if self.args.headers:
+ for item in self.args.headers:
+ if ':' not in item:
+ self.out(
+ "Bad --header=%s option, value must be in the form "
+ "'name:value'" % item)
+ return 2
+ name, value = item.split(':', 1)
+ headers[name] = value.strip()
+
+ request_method = (self.args.method or 'GET').upper()
+
+ environ = {
+ 'REQUEST_METHOD': request_method,
+ 'SCRIPT_NAME': '', # may be empty if app is at the root
+ 'PATH_INFO': path,
+ 'SERVER_NAME': 'localhost', # always mandatory
+ 'SERVER_PORT': '80', # always mandatory
+ 'SERVER_PROTOCOL': 'HTTP/1.0',
+ 'CONTENT_TYPE': 'text/plain',
+ 'REMOTE_ADDR':'127.0.0.1',
+ 'wsgi.run_once': True,
+ 'wsgi.multithread': False,
+ 'wsgi.multiprocess': False,
+ 'wsgi.errors': sys.stderr,
+ 'wsgi.url_scheme': 'http',
+ 'wsgi.version': (1, 0),
+ 'QUERY_STRING': qs,
+ 'HTTP_ACCEPT': 'text/plain;q=1.0, */*;q=0.1',
+ 'paste.command_request': True,
+ }
+
+ if request_method in ('POST', 'PUT', 'PATCH'):
+ environ['wsgi.input'] = self.stdin
+ environ['CONTENT_LENGTH'] = '-1'
+
+ for name, value in headers.items():
+ if name.lower() == 'content-type':
+ name = 'CONTENT_TYPE'
+ else:
+ name = 'HTTP_' + name.upper().replace('-', '_')
+ environ[name] = value
+
+ request = Request.blank(path, environ=environ)
+ response = request.get_response(app)
+ if self.args.display_headers:
+ self.out(response.status)
+ for name, value in response.headerlist:
+ self.out('%s: %s' % (name, value))
+ if response.charset:
+ self.out(response.ubody)
+ else:
+ self.out(response.body)
+ return 0
+
+if __name__ == '__main__': # pragma: no cover
+ sys.exit(main() or 0)
diff --git a/src/pyramid/scripts/proutes.py b/src/pyramid/scripts/proutes.py
new file mode 100644
index 000000000..69d61ae8f
--- /dev/null
+++ b/src/pyramid/scripts/proutes.py
@@ -0,0 +1,416 @@
+import fnmatch
+import argparse
+import sys
+import textwrap
+import re
+
+from zope.interface import Interface
+
+from pyramid.paster import bootstrap
+from pyramid.compat import string_types
+from pyramid.interfaces import IRouteRequest
+from pyramid.config import not_
+
+from pyramid.scripts.common import get_config_loader
+from pyramid.scripts.common import parse_vars
+from pyramid.static import static_view
+from pyramid.view import _find_views
+
+
+PAD = 3
+ANY_KEY = '*'
+UNKNOWN_KEY = '<unknown>'
+
+
+def main(argv=sys.argv, quiet=False):
+ command = PRoutesCommand(argv, quiet)
+ return command.run()
+
+
+def _get_pattern(route):
+ pattern = route.pattern
+
+ if not pattern.startswith('/'):
+ pattern = '/%s' % pattern
+ return pattern
+
+
+def _get_print_format(fmt, max_name, max_pattern, max_view, max_method):
+ print_fmt = ''
+ max_map = {
+ 'name': max_name,
+ 'pattern': max_pattern,
+ 'view': max_view,
+ 'method': max_method,
+ }
+ sizes = []
+
+ for index, col in enumerate(fmt):
+ size = max_map[col] + PAD
+ print_fmt += '{{%s: <{%s}}} ' % (col, index)
+ sizes.append(size)
+
+ return print_fmt.format(*sizes)
+
+
+def _get_request_methods(route_request_methods, view_request_methods):
+ excludes = set()
+
+ if route_request_methods:
+ route_request_methods = set(route_request_methods)
+
+ if view_request_methods:
+ view_request_methods = set(view_request_methods)
+
+ for method in view_request_methods.copy():
+ if method.startswith('!'):
+ view_request_methods.remove(method)
+ excludes.add(method[1:])
+
+ has_route_methods = route_request_methods is not None
+ has_view_methods = len(view_request_methods) > 0
+ has_methods = has_route_methods or has_view_methods
+
+ if has_route_methods is False and has_view_methods is False:
+ request_methods = [ANY_KEY]
+ elif has_route_methods is False and has_view_methods is True:
+ request_methods = view_request_methods
+ elif has_route_methods is True and has_view_methods is False:
+ request_methods = route_request_methods
+ else:
+ request_methods = route_request_methods.intersection(
+ view_request_methods
+ )
+
+ request_methods = set(request_methods).difference(excludes)
+
+ if has_methods and not request_methods:
+ request_methods = '<route mismatch>'
+ elif request_methods:
+ if excludes and request_methods == set([ANY_KEY]):
+ for exclude in excludes:
+ request_methods.add('!%s' % exclude)
+
+ request_methods = ','.join(sorted(request_methods))
+
+ return request_methods
+
+
+def _get_view_module(view_callable):
+ if view_callable is None:
+ return UNKNOWN_KEY
+
+ if hasattr(view_callable, '__name__'):
+ if hasattr(view_callable, '__original_view__'):
+ original_view = view_callable.__original_view__
+ else:
+ original_view = None
+
+ if isinstance(original_view, static_view):
+ if original_view.package_name is not None:
+ return '%s:%s' % (
+ original_view.package_name,
+ original_view.docroot
+ )
+ else:
+ return original_view.docroot
+ else:
+ view_name = view_callable.__name__
+ else:
+ # Currently only MultiView hits this,
+ # we could just not run _get_view_module
+ # for them and remove this logic
+ view_name = str(view_callable)
+
+ view_module = '%s.%s' % (
+ view_callable.__module__,
+ view_name,
+ )
+
+ # If pyramid wraps something in wsgiapp or wsgiapp2 decorators
+ # that is currently returned as pyramid.router.decorator, lets
+ # hack a nice name in:
+ if view_module == 'pyramid.router.decorator':
+ view_module = '<wsgiapp>'
+
+ return view_module
+
+
+def get_route_data(route, registry):
+ pattern = _get_pattern(route)
+
+ request_iface = registry.queryUtility(
+ IRouteRequest,
+ name=route.name
+ )
+
+ route_request_methods = None
+ view_request_methods_order = []
+ view_request_methods = {}
+ view_callable = None
+
+ route_intr = registry.introspector.get(
+ 'routes', route.name
+ )
+
+ if request_iface is None:
+ return [
+ (route.name, _get_pattern(route), UNKNOWN_KEY, ANY_KEY)
+ ]
+
+ view_callables = _find_views(registry, request_iface, Interface, '')
+ if view_callables:
+ view_callable = view_callables[0]
+ else:
+ view_callable = None
+ view_module = _get_view_module(view_callable)
+
+ # Introspectables can be turned off, so there could be a chance
+ # that we have no `route_intr` but we do have a route + callable
+ if route_intr is None:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+ else:
+ if route_intr.get('static', False) is True:
+ return [
+ (route.name, route_intr['external_url'], UNKNOWN_KEY, ANY_KEY)
+ ]
+
+ route_request_methods = route_intr['request_methods']
+ view_intr = registry.introspector.related(route_intr)
+
+ if view_intr:
+ for view in view_intr:
+ request_method = view.get('request_methods')
+
+ if request_method is not None:
+ if view.get('attr') is not None:
+ view_callable = getattr(view['callable'], view['attr'])
+ view_module = '%s.%s' % (
+ _get_view_module(view['callable']),
+ view['attr']
+ )
+ else:
+ view_callable = view['callable']
+ view_module = _get_view_module(view_callable)
+
+ if view_module not in view_request_methods:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ if isinstance(request_method, string_types):
+ request_method = (request_method,)
+ elif isinstance(request_method, not_):
+ request_method = ('!%s' % request_method.value,)
+
+ view_request_methods[view_module].extend(request_method)
+ else:
+ if view_module not in view_request_methods:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ else:
+ view_request_methods[view_module] = []
+ view_request_methods_order.append(view_module)
+
+ final_routes = []
+
+ for view_module in view_request_methods_order:
+ methods = view_request_methods[view_module]
+ request_methods = _get_request_methods(
+ route_request_methods,
+ methods
+ )
+
+ final_routes.append((
+ route.name,
+ pattern,
+ view_module,
+ request_methods,
+ ))
+
+ return final_routes
+
+
+class PRoutesCommand(object):
+ description = """\
+ Print all URL dispatch routes used by a Pyramid application in the
+ order in which they are evaluated. Each route includes the name of the
+ route, the pattern of the route, and the view callable which will be
+ invoked when the route is matched.
+
+ This command accepts one positional argument named 'config_uri'. It
+ specifies the PasteDeploy config file to use for the interactive
+ shell. The format is 'inifile#name'. If the name is left off, 'main'
+ will be assumed. Example: 'proutes myapp.ini'.
+
+ """
+ bootstrap = staticmethod(bootstrap) # testing
+ get_config_loader = staticmethod(get_config_loader) # testing
+ stdout = sys.stdout
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(description),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument('-g', '--glob',
+ action='store',
+ dest='glob',
+ default='',
+ help='Display routes matching glob pattern')
+
+ parser.add_argument('-f', '--format',
+ action='store',
+ dest='format',
+ default='',
+ help=('Choose which columns to display, this will '
+ 'override the format key in the [proutes] ini '
+ 'section'))
+
+ parser.add_argument(
+ 'config_uri',
+ nargs='?',
+ default=None,
+ help='The URI to the configuration file.',
+ )
+
+ parser.add_argument(
+ 'config_vars',
+ nargs='*',
+ default=(),
+ help="Variables required by the config file. For example, "
+ "`http_port=%%(http_port)s` would expect `http_port=8080` to be "
+ "passed here.",
+ )
+
+ def __init__(self, argv, quiet=False):
+ self.args = self.parser.parse_args(argv[1:])
+ self.quiet = quiet
+ self.available_formats = [
+ 'name', 'pattern', 'view', 'method'
+ ]
+ self.column_format = self.available_formats
+
+ def validate_formats(self, formats):
+ invalid_formats = []
+ for fmt in formats:
+ if fmt not in self.available_formats:
+ invalid_formats.append(fmt)
+
+ msg = (
+ 'You provided invalid formats %s, '
+ 'Available formats are %s'
+ )
+
+ if invalid_formats:
+ msg = msg % (invalid_formats, self.available_formats)
+ self.out(msg)
+ return False
+
+ return True
+
+ def proutes_file_config(self, loader, global_conf=None):
+ settings = loader.get_settings('proutes', global_conf)
+ format = settings.get('format')
+ if format:
+ cols = re.split(r'[,|\s\n]+', format)
+ self.column_format = [x.strip() for x in cols]
+
+ def out(self, msg): # pragma: no cover
+ if not self.quiet:
+ print(msg)
+
+ def _get_mapper(self, registry):
+ from pyramid.config import Configurator
+ config = Configurator(registry=registry)
+ return config.get_routes_mapper()
+
+ def run(self, quiet=False):
+ if not self.args.config_uri:
+ self.out('requires a config file argument')
+ return 2
+
+ config_uri = self.args.config_uri
+ config_vars = parse_vars(self.args.config_vars)
+ loader = self.get_config_loader(config_uri)
+ loader.setup_logging(config_vars)
+ self.proutes_file_config(loader, config_vars)
+
+ env = self.bootstrap(config_uri, options=config_vars)
+ registry = env['registry']
+ mapper = self._get_mapper(registry)
+
+ if self.args.format:
+ columns = self.args.format.split(',')
+ self.column_format = [x.strip() for x in columns]
+
+ is_valid = self.validate_formats(self.column_format)
+
+ if is_valid is False:
+ return 2
+
+ if mapper is None:
+ return 0
+
+ max_name = len('Name')
+ max_pattern = len('Pattern')
+ max_view = len('View')
+ max_method = len('Method')
+
+ routes = mapper.get_routes(include_static=True)
+
+ if len(routes) == 0:
+ return 0
+
+ mapped_routes = [{
+ 'name': 'Name',
+ 'pattern': 'Pattern',
+ 'view': 'View',
+ 'method': 'Method'
+ },{
+ 'name': '----',
+ 'pattern': '-------',
+ 'view': '----',
+ 'method': '------'
+ }]
+
+ for route in routes:
+ route_data = get_route_data(route, registry)
+
+ for name, pattern, view, method in route_data:
+ if self.args.glob:
+ match = (fnmatch.fnmatch(name, self.args.glob) or
+ fnmatch.fnmatch(pattern, self.args.glob))
+ if not match:
+ continue
+
+ if len(name) > max_name:
+ max_name = len(name)
+
+ if len(pattern) > max_pattern:
+ max_pattern = len(pattern)
+
+ if len(view) > max_view:
+ max_view = len(view)
+
+ if len(method) > max_method:
+ max_method = len(method)
+
+ mapped_routes.append({
+ 'name': name,
+ 'pattern': pattern,
+ 'view': view,
+ 'method': method
+ })
+
+ fmt = _get_print_format(
+ self.column_format, max_name, max_pattern, max_view, max_method
+ )
+
+ for route in mapped_routes:
+ self.out(fmt.format(**route))
+
+ return 0
+
+
+if __name__ == '__main__': # pragma: no cover
+ sys.exit(main() or 0)
diff --git a/src/pyramid/scripts/pserve.py b/src/pyramid/scripts/pserve.py
new file mode 100644
index 000000000..8ee6e1467
--- /dev/null
+++ b/src/pyramid/scripts/pserve.py
@@ -0,0 +1,383 @@
+# (c) 2005 Ian Bicking and contributors; written for Paste
+# (http://pythonpaste.org) Licensed under the MIT license:
+# http://www.opensource.org/licenses/mit-license.php
+#
+# For discussion of daemonizing:
+# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731
+#
+# Code taken also from QP: http://www.mems-exchange.org/software/qp/ From
+# lib/site.py
+
+import argparse
+import os
+import re
+import sys
+import textwrap
+import threading
+import time
+import webbrowser
+
+import hupper
+
+from pyramid.compat import PY2
+
+from pyramid.scripts.common import get_config_loader
+from pyramid.scripts.common import parse_vars
+from pyramid.path import AssetResolver
+from pyramid.settings import aslist
+
+
+def main(argv=sys.argv, quiet=False):
+ command = PServeCommand(argv, quiet=quiet)
+ return command.run()
+
+
+class PServeCommand(object):
+
+ description = """\
+ This command serves a web application that uses a PasteDeploy
+ configuration file for the server and application.
+
+ You can also include variable assignments like 'http_port=8080'
+ and then use %(http_port)s in your config files.
+ """
+ default_verbosity = 1
+
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(description),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ '-n', '--app-name',
+ dest='app_name',
+ metavar='NAME',
+ help="Load the named application (default main)")
+ parser.add_argument(
+ '-s', '--server',
+ dest='server',
+ metavar='SERVER_TYPE',
+ help="Use the named server.")
+ parser.add_argument(
+ '--server-name',
+ dest='server_name',
+ metavar='SECTION_NAME',
+ help=("Use the named server as defined in the configuration file "
+ "(default: main)"))
+ parser.add_argument(
+ '--reload',
+ dest='reload',
+ action='store_true',
+ help="Use auto-restart file monitor")
+ parser.add_argument(
+ '--reload-interval',
+ dest='reload_interval',
+ default=1,
+ help=("Seconds between checking files (low number can cause "
+ "significant CPU usage)"))
+ parser.add_argument(
+ '-b', '--browser',
+ dest='browser',
+ action='store_true',
+ help=("Open a web browser to the server url. The server url is "
+ "determined from the 'open_url' setting in the 'pserve' "
+ "section of the configuration file."))
+ parser.add_argument(
+ '-v', '--verbose',
+ default=default_verbosity,
+ dest='verbose',
+ action='count',
+ help="Set verbose level (default " + str(default_verbosity) + ")")
+ parser.add_argument(
+ '-q', '--quiet',
+ action='store_const',
+ const=0,
+ dest='verbose',
+ help="Suppress verbose output")
+ parser.add_argument(
+ 'config_uri',
+ nargs='?',
+ default=None,
+ help='The URI to the configuration file.',
+ )
+ parser.add_argument(
+ 'config_vars',
+ nargs='*',
+ default=(),
+ help="Variables required by the config file. For example, "
+ "`http_port=%%(http_port)s` would expect `http_port=8080` to be "
+ "passed here.",
+ )
+
+ _get_config_loader = staticmethod(get_config_loader) # for testing
+
+ open_url = None
+
+ _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)
+
+ def __init__(self, argv, quiet=False):
+ self.args = self.parser.parse_args(argv[1:])
+ if quiet:
+ self.args.verbose = 0
+ if self.args.reload:
+ self.worker_kwargs = {'argv': argv, "quiet": quiet}
+ self.watch_files = set()
+
+ def out(self, msg): # pragma: no cover
+ if self.args.verbose > 0:
+ print(msg)
+
+ def get_config_path(self, loader):
+ return os.path.abspath(loader.uri.path)
+
+ def pserve_file_config(self, loader, global_conf=None):
+ settings = loader.get_settings('pserve', global_conf)
+ config_path = self.get_config_path(loader)
+ here = os.path.dirname(config_path)
+ watch_files = aslist(settings.get('watch_files', ''), flatten=False)
+
+ # track file paths relative to the ini file
+ resolver = AssetResolver(package=None)
+ for file in watch_files:
+ if ':' in file:
+ file = resolver.resolve(file).abspath()
+ elif not os.path.isabs(file):
+ file = os.path.join(here, file)
+ self.watch_files.add(os.path.abspath(file))
+
+ # attempt to determine the url of the server
+ open_url = settings.get('open_url')
+ if open_url:
+ self.open_url = open_url
+
+ def guess_server_url(self, loader, server_name, global_conf=None):
+ server_name = server_name or 'main'
+ settings = loader.get_settings('server:' + server_name, global_conf)
+ if 'port' in settings:
+ return 'http://127.0.0.1:{port}'.format(**settings)
+
+ def run(self): # pragma: no cover
+ if not self.args.config_uri:
+ self.out('You must give a config file')
+ return 2
+ config_uri = self.args.config_uri
+ config_vars = parse_vars(self.args.config_vars)
+ app_spec = self.args.config_uri
+ app_name = self.args.app_name
+
+ loader = self._get_config_loader(config_uri)
+ loader.setup_logging(config_vars)
+
+ self.pserve_file_config(loader, global_conf=config_vars)
+
+ server_name = self.args.server_name
+ if self.args.server:
+ server_spec = 'egg:pyramid'
+ assert server_name is None
+ server_name = self.args.server
+ else:
+ server_spec = app_spec
+
+ server_loader = loader
+ if server_spec != app_spec:
+ server_loader = self.get_config_loader(server_spec)
+
+ # do not open the browser on each reload so check hupper first
+ if self.args.browser and not hupper.is_active():
+ url = self.open_url
+
+ if not url:
+ url = self.guess_server_url(
+ server_loader, server_name, config_vars)
+
+ if not url:
+ self.out('WARNING: could not determine the server\'s url to '
+ 'open the browser. To fix this set the "open_url" '
+ 'setting in the [pserve] section of the '
+ 'configuration file.')
+
+ else:
+ def open_browser():
+ time.sleep(1)
+ webbrowser.open(url)
+ t = threading.Thread(target=open_browser)
+ t.setDaemon(True)
+ t.start()
+
+ if self.args.reload and not hupper.is_active():
+ if self.args.verbose > 1:
+ self.out('Running reloading file monitor')
+ hupper.start_reloader(
+ 'pyramid.scripts.pserve.main',
+ reload_interval=int(self.args.reload_interval),
+ verbose=self.args.verbose,
+ worker_kwargs=self.worker_kwargs
+ )
+ return 0
+
+ config_path = self.get_config_path(loader)
+ self.watch_files.add(config_path)
+
+ server_path = self.get_config_path(server_loader)
+ self.watch_files.add(server_path)
+
+ if hupper.is_active():
+ reloader = hupper.get_reloader()
+ reloader.watch_files(list(self.watch_files))
+
+ server = server_loader.get_wsgi_server(server_name, config_vars)
+
+ app = loader.get_wsgi_app(app_name, config_vars)
+
+ if self.args.verbose > 0:
+ if hasattr(os, 'getpid'):
+ msg = 'Starting server in PID %i.' % os.getpid()
+ else:
+ msg = 'Starting server.'
+ self.out(msg)
+
+ try:
+ server(app)
+ except (SystemExit, KeyboardInterrupt) as e:
+ if self.args.verbose > 1:
+ raise
+ if str(e):
+ msg = ' ' + str(e)
+ else:
+ msg = ''
+ self.out('Exiting%s (-v to see traceback)' % msg)
+
+
+# For paste.deploy server instantiation (egg:pyramid#wsgiref)
+def wsgiref_server_runner(wsgi_app, global_conf, **kw): # pragma: no cover
+ from wsgiref.simple_server import make_server
+ host = kw.get('host', '0.0.0.0')
+ port = int(kw.get('port', 8080))
+ server = make_server(host, port, wsgi_app)
+ print('Starting HTTP server on http://%s:%s' % (host, port))
+ server.serve_forever()
+
+
+# For paste.deploy server instantiation (egg:pyramid#cherrypy)
+def cherrypy_server_runner(
+ app, global_conf=None, host='127.0.0.1', port=None,
+ ssl_pem=None, protocol_version=None, numthreads=None,
+ server_name=None, max=None, request_queue_size=None,
+ timeout=None
+ ): # pragma: no cover
+ """
+ Entry point for CherryPy's WSGI server
+
+ Serves the specified WSGI app via CherryPyWSGIServer.
+
+ ``app``
+
+ The WSGI 'application callable'; multiple WSGI applications
+ may be passed as (script_name, callable) pairs.
+
+ ``host``
+
+ This is the ipaddress to bind to (or a hostname if your
+ nameserver is properly configured). This defaults to
+ 127.0.0.1, which is not a public interface.
+
+ ``port``
+
+ The port to run on, defaults to 8080 for HTTP, or 4443 for
+ HTTPS. This can be a string or an integer value.
+
+ ``ssl_pem``
+
+ This an optional SSL certificate file (via OpenSSL) You can
+ generate a self-signed test PEM certificate file as follows:
+
+ $ openssl genrsa 1024 > host.key
+ $ chmod 400 host.key
+ $ openssl req -new -x509 -nodes -sha1 -days 365 \\
+ -key host.key > host.cert
+ $ cat host.cert host.key > host.pem
+ $ chmod 400 host.pem
+
+ ``protocol_version``
+
+ The protocol used by the server, by default ``HTTP/1.1``.
+
+ ``numthreads``
+
+ The number of worker threads to create.
+
+ ``server_name``
+
+ The string to set for WSGI's SERVER_NAME environ entry.
+
+ ``max``
+
+ The maximum number of queued requests. (defaults to -1 = no
+ limit).
+
+ ``request_queue_size``
+
+ The 'backlog' argument to socket.listen(); specifies the
+ maximum number of queued connections.
+
+ ``timeout``
+
+ The timeout in seconds for accepted connections.
+ """
+ is_ssl = False
+ if ssl_pem:
+ port = port or 4443
+ is_ssl = True
+
+ if not port:
+ if ':' in host:
+ host, port = host.split(':', 1)
+ else:
+ port = 8080
+ bind_addr = (host, int(port))
+
+ kwargs = {}
+ for var_name in ('numthreads', 'max', 'request_queue_size', 'timeout'):
+ var = locals()[var_name]
+ if var is not None:
+ kwargs[var_name] = int(var)
+
+ try:
+ from cheroot.wsgi import Server as WSGIServer
+ except ImportError:
+ from cherrypy.wsgiserver import CherryPyWSGIServer as WSGIServer
+
+ server = WSGIServer(bind_addr, app,
+ server_name=server_name, **kwargs)
+ if ssl_pem is not None:
+ if PY2:
+ server.ssl_certificate = server.ssl_private_key = ssl_pem
+ else:
+ # creates wsgiserver.ssl_builtin as side-effect
+ try:
+ from cheroot.server import get_ssl_adapter_class
+ from cheroot.ssl.builtin import BuiltinSSLAdapter
+ except ImportError:
+ from cherrypy.wsgiserver import get_ssl_adapter_class
+ from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter
+ get_ssl_adapter_class()
+ server.ssl_adapter = BuiltinSSLAdapter(ssl_pem, ssl_pem)
+
+ if protocol_version:
+ server.protocol = protocol_version
+
+ try:
+ protocol = is_ssl and 'https' or 'http'
+ if host == '0.0.0.0':
+ print('serving on 0.0.0.0:%s view at %s://127.0.0.1:%s' %
+ (port, protocol, port))
+ else:
+ print('serving on %s://%s:%s' % (protocol, host, port))
+ server.start()
+ except (KeyboardInterrupt, SystemExit):
+ server.stop()
+
+ return server
+
+
+if __name__ == '__main__': # pragma: no cover
+ sys.exit(main() or 0)
diff --git a/src/pyramid/scripts/pshell.py b/src/pyramid/scripts/pshell.py
new file mode 100644
index 000000000..4898eb39f
--- /dev/null
+++ b/src/pyramid/scripts/pshell.py
@@ -0,0 +1,270 @@
+from code import interact
+from contextlib import contextmanager
+import argparse
+import os
+import sys
+import textwrap
+import pkg_resources
+
+from pyramid.compat import exec_
+from pyramid.util import DottedNameResolver
+from pyramid.util import make_contextmanager
+from pyramid.paster import bootstrap
+
+from pyramid.settings import aslist
+
+from pyramid.scripts.common import get_config_loader
+from pyramid.scripts.common import parse_vars
+
+def main(argv=sys.argv, quiet=False):
+ command = PShellCommand(argv, quiet)
+ return command.run()
+
+
+def python_shell_runner(env, help, interact=interact):
+ cprt = 'Type "help" for more information.'
+ banner = "Python %s on %s\n%s" % (sys.version, sys.platform, cprt)
+ banner += '\n\n' + help + '\n'
+ interact(banner, local=env)
+
+
+class PShellCommand(object):
+ description = """\
+ Open an interactive shell with a Pyramid app loaded. This command
+ accepts one positional argument named "config_uri" which specifies the
+ PasteDeploy config file to use for the interactive shell. The format is
+ "inifile#name". If the name is left off, the Pyramid default application
+ will be assumed. Example: "pshell myapp.ini#main".
+
+ If you do not point the loader directly at the section of the ini file
+ containing your Pyramid application, the command will attempt to
+ find the app for you. If you are loading a pipeline that contains more
+ than one Pyramid application within it, the loader will use the
+ last one.
+ """
+ bootstrap = staticmethod(bootstrap) # for testing
+ get_config_loader = staticmethod(get_config_loader) # for testing
+ pkg_resources = pkg_resources # for testing
+
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(description),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument('-p', '--python-shell',
+ action='store',
+ dest='python_shell',
+ default='',
+ help=('Select the shell to use. A list of possible '
+ 'shells is available using the --list-shells '
+ 'option.'))
+ parser.add_argument('-l', '--list-shells',
+ dest='list',
+ action='store_true',
+ help='List all available shells.')
+ parser.add_argument('--setup',
+ dest='setup',
+ help=("A callable that will be passed the environment "
+ "before it is made available to the shell. This "
+ "option will override the 'setup' key in the "
+ "[pshell] ini section."))
+ parser.add_argument('config_uri',
+ nargs='?',
+ default=None,
+ help='The URI to the configuration file.')
+ parser.add_argument(
+ 'config_vars',
+ nargs='*',
+ default=(),
+ help="Variables required by the config file. For example, "
+ "`http_port=%%(http_port)s` would expect `http_port=8080` to be "
+ "passed here.",
+ )
+
+ default_runner = python_shell_runner # testing
+
+ loaded_objects = {}
+ object_help = {}
+ preferred_shells = []
+ setup = None
+ pystartup = os.environ.get('PYTHONSTARTUP')
+ resolver = DottedNameResolver(None)
+
+ def __init__(self, argv, quiet=False):
+ self.quiet = quiet
+ self.args = self.parser.parse_args(argv[1:])
+
+ def pshell_file_config(self, loader, defaults):
+ settings = loader.get_settings('pshell', defaults)
+ self.loaded_objects = {}
+ self.object_help = {}
+ self.setup = None
+ for k, v in settings.items():
+ if k == 'setup':
+ self.setup = v
+ elif k == 'default_shell':
+ self.preferred_shells = [x.lower() for x in aslist(v)]
+ else:
+ self.loaded_objects[k] = self.resolver.maybe_resolve(v)
+ self.object_help[k] = v
+
+ def out(self, msg): # pragma: no cover
+ if not self.quiet:
+ print(msg)
+
+ def run(self, shell=None):
+ if self.args.list:
+ return self.show_shells()
+ if not self.args.config_uri:
+ self.out('Requires a config file argument')
+ return 2
+
+ config_uri = self.args.config_uri
+ config_vars = parse_vars(self.args.config_vars)
+ loader = self.get_config_loader(config_uri)
+ loader.setup_logging(config_vars)
+ self.pshell_file_config(loader, config_vars)
+
+ self.env = self.bootstrap(config_uri, options=config_vars)
+
+ # remove the closer from the env
+ self.closer = self.env.pop('closer')
+
+ try:
+ if shell is None:
+ try:
+ shell = self.make_shell()
+ except ValueError as e:
+ self.out(str(e))
+ return 1
+
+ with self.setup_env():
+ shell(self.env, self.help)
+
+ finally:
+ self.closer()
+
+ @contextmanager
+ def setup_env(self):
+ # setup help text for default environment
+ env = self.env
+ env_help = dict(env)
+ env_help['app'] = 'The WSGI application.'
+ env_help['root'] = 'Root of the default resource tree.'
+ env_help['registry'] = 'Active Pyramid registry.'
+ env_help['request'] = 'Active request object.'
+ env_help['root_factory'] = (
+ 'Default root factory used to create `root`.')
+
+ # load the pshell section of the ini file
+ env.update(self.loaded_objects)
+
+ # eliminate duplicates from env, allowing custom vars to override
+ for k in self.loaded_objects:
+ if k in env_help:
+ del env_help[k]
+
+ # override use_script with command-line options
+ if self.args.setup:
+ self.setup = self.args.setup
+
+ if self.setup:
+ # call the setup callable
+ self.setup = self.resolver.maybe_resolve(self.setup)
+
+ # store the env before muddling it with the script
+ orig_env = env.copy()
+ setup_manager = make_contextmanager(self.setup)
+ with setup_manager(env):
+ # remove any objects from default help that were overidden
+ for k, v in env.items():
+ if k not in orig_env or v != orig_env[k]:
+ if getattr(v, '__doc__', False):
+ env_help[k] = v.__doc__.replace("\n", " ")
+ else:
+ env_help[k] = v
+ del orig_env
+
+ # generate help text
+ help = ''
+ if env_help:
+ help += 'Environment:'
+ for var in sorted(env_help.keys()):
+ help += '\n %-12s %s' % (var, env_help[var])
+
+ if self.object_help:
+ help += '\n\nCustom Variables:'
+ for var in sorted(self.object_help.keys()):
+ help += '\n %-12s %s' % (var, self.object_help[var])
+
+ if self.pystartup and os.path.isfile(self.pystartup):
+ with open(self.pystartup, 'rb') as fp:
+ exec_(fp.read().decode('utf-8'), env)
+ if '__builtins__' in env:
+ del env['__builtins__']
+
+ self.help = help.strip()
+ yield
+
+ def show_shells(self):
+ shells = self.find_all_shells()
+ sorted_names = sorted(shells.keys(), key=lambda x: x.lower())
+
+ self.out('Available shells:')
+ for name in sorted_names:
+ self.out(' %s' % (name,))
+ return 0
+
+ def find_all_shells(self):
+ pkg_resources = self.pkg_resources
+
+ shells = {}
+ for ep in pkg_resources.iter_entry_points('pyramid.pshell_runner'):
+ name = ep.name
+ shell_factory = ep.load()
+ shells[name] = shell_factory
+ return shells
+
+ def make_shell(self):
+ shells = self.find_all_shells()
+
+ shell = None
+ user_shell = self.args.python_shell.lower()
+
+ if not user_shell:
+ preferred_shells = self.preferred_shells
+ if not preferred_shells:
+ # by default prioritize all shells above python
+ preferred_shells = [k for k in shells.keys() if k != 'python']
+ max_weight = len(preferred_shells)
+ def order(x):
+ # invert weight to reverse sort the list
+ # (closer to the front is higher priority)
+ try:
+ return preferred_shells.index(x[0].lower()) - max_weight
+ except ValueError:
+ return 1
+ sorted_shells = sorted(shells.items(), key=order)
+
+ if len(sorted_shells) > 0:
+ shell = sorted_shells[0][1]
+
+ else:
+ runner = shells.get(user_shell)
+
+ if runner is not None:
+ shell = runner
+
+ if shell is None:
+ raise ValueError(
+ 'could not find a shell named "%s"' % user_shell
+ )
+
+ if shell is None:
+ # should never happen, but just incase entry points are borked
+ shell = self.default_runner
+
+ return shell
+
+
+if __name__ == '__main__': # pragma: no cover
+ sys.exit(main() or 0)
diff --git a/src/pyramid/scripts/ptweens.py b/src/pyramid/scripts/ptweens.py
new file mode 100644
index 000000000..d5cbebe12
--- /dev/null
+++ b/src/pyramid/scripts/ptweens.py
@@ -0,0 +1,109 @@
+import argparse
+import sys
+import textwrap
+
+from pyramid.interfaces import ITweens
+
+from pyramid.tweens import MAIN
+from pyramid.tweens import INGRESS
+from pyramid.paster import bootstrap
+from pyramid.paster import setup_logging
+from pyramid.scripts.common import parse_vars
+
+def main(argv=sys.argv, quiet=False):
+ command = PTweensCommand(argv, quiet)
+ return command.run()
+
+class PTweensCommand(object):
+ description = """\
+ Print all implicit and explicit tween objects used by a Pyramid
+ application. The handler output includes whether the system is using an
+ explicit tweens ordering (will be true when the "pyramid.tweens"
+ deployment setting is used) or an implicit tweens ordering (will be true
+ when the "pyramid.tweens" deployment setting is *not* used).
+
+ This command accepts one positional argument named "config_uri" which
+ specifies the PasteDeploy config file to use for the interactive
+ shell. The format is "inifile#name". If the name is left off, "main"
+ will be assumed. Example: "ptweens myapp.ini#main".
+
+ """
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(description),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ parser.add_argument('config_uri',
+ nargs='?',
+ default=None,
+ help='The URI to the configuration file.')
+
+ parser.add_argument(
+ 'config_vars',
+ nargs='*',
+ default=(),
+ help="Variables required by the config file. For example, "
+ "`http_port=%%(http_port)s` would expect `http_port=8080` to be "
+ "passed here.",
+ )
+
+ stdout = sys.stdout
+ bootstrap = staticmethod(bootstrap) # testing
+ setup_logging = staticmethod(setup_logging) # testing
+
+ def __init__(self, argv, quiet=False):
+ self.quiet = quiet
+ self.args = self.parser.parse_args(argv[1:])
+
+ def _get_tweens(self, registry):
+ from pyramid.config import Configurator
+ config = Configurator(registry=registry)
+ return config.registry.queryUtility(ITweens)
+
+ def out(self, msg): # pragma: no cover
+ if not self.quiet:
+ print(msg)
+
+ def show_chain(self, chain):
+ fmt = '%-10s %-65s'
+ self.out(fmt % ('Position', 'Name'))
+ self.out(fmt % ('-' * len('Position'), '-' * len('Name')))
+ self.out(fmt % ('-', INGRESS))
+ for pos, (name, _) in enumerate(chain):
+ self.out(fmt % (pos, name))
+ self.out(fmt % ('-', MAIN))
+
+ def run(self):
+ if not self.args.config_uri:
+ self.out('Requires a config file argument')
+ return 2
+ config_uri = self.args.config_uri
+ config_vars = parse_vars(self.args.config_vars)
+ self.setup_logging(config_uri, global_conf=config_vars)
+ env = self.bootstrap(config_uri, options=config_vars)
+ registry = env['registry']
+ tweens = self._get_tweens(registry)
+ if tweens is not None:
+ explicit = tweens.explicit
+ if explicit:
+ self.out('"pyramid.tweens" config value set '
+ '(explicitly ordered tweens used)')
+ self.out('')
+ self.out('Explicit Tween Chain (used)')
+ self.out('')
+ self.show_chain(tweens.explicit)
+ self.out('')
+ self.out('Implicit Tween Chain (not used)')
+ self.out('')
+ self.show_chain(tweens.implicit())
+ else:
+ self.out('"pyramid.tweens" config value NOT set '
+ '(implicitly ordered tweens used)')
+ self.out('')
+ self.out('Implicit Tween Chain')
+ self.out('')
+ self.show_chain(tweens.implicit())
+ return 0
+
+if __name__ == '__main__': # pragma: no cover
+ sys.exit(main() or 0)
diff --git a/src/pyramid/scripts/pviews.py b/src/pyramid/scripts/pviews.py
new file mode 100644
index 000000000..c0df2f078
--- /dev/null
+++ b/src/pyramid/scripts/pviews.py
@@ -0,0 +1,289 @@
+import argparse
+import sys
+import textwrap
+
+from pyramid.interfaces import IMultiView
+from pyramid.paster import bootstrap
+from pyramid.paster import setup_logging
+from pyramid.request import Request
+from pyramid.scripts.common import parse_vars
+from pyramid.view import _find_views
+
+def main(argv=sys.argv, quiet=False):
+ command = PViewsCommand(argv, quiet)
+ return command.run()
+
+class PViewsCommand(object):
+ description = """\
+ Print, for a given URL, the views that might match. Underneath each
+ potentially matching route, list the predicates required. Underneath
+ each route+predicate set, print each view that might match and its
+ predicates.
+
+ This command accepts two positional arguments: 'config_uri' specifies the
+ PasteDeploy config file to use for the interactive shell. The format is
+ 'inifile#name'. If the name is left off, 'main' will be assumed. 'url'
+ specifies the path info portion of a URL that will be used to find
+ matching views. Example: 'proutes myapp.ini#main /url'
+ """
+ stdout = sys.stdout
+
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(description),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ parser.add_argument('config_uri',
+ nargs='?',
+ default=None,
+ help='The URI to the configuration file.')
+
+ parser.add_argument('url',
+ nargs='?',
+ default=None,
+ help='The path info portion of the URL.')
+ parser.add_argument(
+ 'config_vars',
+ nargs='*',
+ default=(),
+ help="Variables required by the config file. For example, "
+ "`http_port=%%(http_port)s` would expect `http_port=8080` to be "
+ "passed here.",
+ )
+
+
+ bootstrap = staticmethod(bootstrap) # testing
+ setup_logging = staticmethod(setup_logging) # testing
+
+ def __init__(self, argv, quiet=False):
+ self.quiet = quiet
+ self.args = self.parser.parse_args(argv[1:])
+
+ def out(self, msg): # pragma: no cover
+ if not self.quiet:
+ print(msg)
+
+ def _find_multi_routes(self, mapper, request):
+ infos = []
+ path = request.environ['PATH_INFO']
+ # find all routes that match path, regardless of predicates
+ for route in mapper.get_routes():
+ match = route.match(path)
+ if match is not None:
+ info = {'match':match, 'route':route}
+ infos.append(info)
+ return infos
+
+ def _find_view(self, request):
+ """
+ Accept ``url`` and ``registry``; create a :term:`request` and
+ find a :app:`Pyramid` view based on introspection of :term:`view
+ configuration` within the application registry; return the view.
+ """
+ from zope.interface import providedBy
+ from zope.interface import implementer
+ from pyramid.interfaces import IRequest
+ from pyramid.interfaces import IRootFactory
+ from pyramid.interfaces import IRouteRequest
+ from pyramid.interfaces import IRoutesMapper
+ from pyramid.interfaces import ITraverser
+ from pyramid.traversal import DefaultRootFactory
+ from pyramid.traversal import ResourceTreeTraverser
+
+ registry = request.registry
+ q = registry.queryUtility
+ root_factory = q(IRootFactory, default=DefaultRootFactory)
+ routes_mapper = q(IRoutesMapper)
+
+ adapters = registry.adapters
+
+ @implementer(IMultiView)
+ class RoutesMultiView(object):
+
+ def __init__(self, infos, context_iface, root_factory, request):
+ self.views = []
+ for info in infos:
+ match, route = info['match'], info['route']
+ if route is not None:
+ request_iface = registry.queryUtility(
+ IRouteRequest,
+ name=route.name,
+ default=IRequest)
+ views = _find_views(
+ request.registry,
+ request_iface,
+ context_iface,
+ ''
+ )
+ if not views:
+ continue
+ view = views[0]
+ view.__request_attrs__ = {}
+ view.__request_attrs__['matchdict'] = match
+ view.__request_attrs__['matched_route'] = route
+ root_factory = route.factory or root_factory
+ root = root_factory(request)
+ traverser = adapters.queryAdapter(root, ITraverser)
+ if traverser is None:
+ traverser = ResourceTreeTraverser(root)
+ tdict = traverser(request)
+ view.__request_attrs__.update(tdict)
+ if not hasattr(view, '__view_attr__'):
+ view.__view_attr__ = ''
+ self.views.append((None, view, None))
+
+ context = None
+ routes_multiview = None
+ attrs = request.__dict__
+ request_iface = IRequest
+
+ # find the root object
+ if routes_mapper is not None:
+ infos = self._find_multi_routes(routes_mapper, request)
+ if len(infos) == 1:
+ info = infos[0]
+ match, route = info['match'], info['route']
+ if route is not None:
+ attrs['matchdict'] = match
+ attrs['matched_route'] = route
+ request.environ['bfg.routes.matchdict'] = match
+ request_iface = registry.queryUtility(
+ IRouteRequest,
+ name=route.name,
+ default=IRequest)
+ root_factory = route.factory or root_factory
+ if len(infos) > 1:
+ routes_multiview = infos
+
+ root = root_factory(request)
+ attrs['root'] = root
+
+ # find a context
+ traverser = adapters.queryAdapter(root, ITraverser)
+ if traverser is None:
+ traverser = ResourceTreeTraverser(root)
+ tdict = traverser(request)
+ context, view_name = (tdict['context'], tdict['view_name'])
+
+ attrs.update(tdict)
+
+ # find a view callable
+ context_iface = providedBy(context)
+ if routes_multiview is None:
+ views = _find_views(
+ request.registry,
+ request_iface,
+ context_iface,
+ view_name,
+ )
+ if views:
+ view = views[0]
+ else:
+ view = None
+ else:
+ view = RoutesMultiView(infos, context_iface, root_factory, request)
+
+ # routes are not registered with a view name
+ if view is None:
+ views = _find_views(
+ request.registry,
+ request_iface,
+ context_iface,
+ '',
+ )
+ if views:
+ view = views[0]
+ else:
+ view = None
+ # we don't want a multiview here
+ if IMultiView.providedBy(view):
+ view = None
+
+ if view is not None:
+ view.__request_attrs__ = attrs
+
+ return view
+
+ def output_route_attrs(self, attrs, indent):
+ route = attrs['matched_route']
+ self.out("%sroute name: %s" % (indent, route.name))
+ 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])
+ if predicates != '':
+ self.out("%sroute predicates (%s)" % (indent, predicates))
+
+ def output_view_info(self, view_wrapper, level=1):
+ indent = " " * level
+ name = getattr(view_wrapper, '__name__', '')
+ module = getattr(view_wrapper, '__module__', '')
+ attr = getattr(view_wrapper, '__view_attr__', None)
+ request_attrs = getattr(view_wrapper, '__request_attrs__', {})
+ if attr is not None:
+ view_callable = "%s.%s.%s" % (module, name, attr)
+ else:
+ attr = view_wrapper.__class__.__name__
+ if attr == 'function':
+ attr = name
+ view_callable = "%s.%s" % (module, attr)
+ self.out('')
+ if 'matched_route' in request_attrs:
+ self.out("%sRoute:" % indent)
+ self.out("%s------" % indent)
+ self.output_route_attrs(request_attrs, indent)
+ permission = getattr(view_wrapper, '__permission__', None)
+ if not IMultiView.providedBy(view_wrapper):
+ # single view for this route, so repeat call without route data
+ del request_attrs['matched_route']
+ self.output_view_info(view_wrapper, level + 1)
+ else:
+ self.out("%sView:" % indent)
+ self.out("%s-----" % indent)
+ self.out("%s%s" % (indent, view_callable))
+ permission = getattr(view_wrapper, '__permission__', None)
+ if permission is not None:
+ self.out("%srequired permission = %s" % (indent, permission))
+ predicates = getattr(view_wrapper, '__predicates__', None)
+ if predicates is not None:
+ predicate_text = ', '.join([p.text() for p in predicates])
+ self.out("%sview predicates (%s)" % (indent, predicate_text))
+
+ def run(self):
+ if not self.args.config_uri or not self.args.url:
+ self.out('Command requires a config file arg and a url arg')
+ return 2
+ config_uri = self.args.config_uri
+ config_vars = parse_vars(self.args.config_vars)
+ url = self.args.url
+
+ self.setup_logging(config_uri, global_conf=config_vars)
+
+ if not url.startswith('/'):
+ url = '/%s' % url
+ request = Request.blank(url)
+ env = self.bootstrap(config_uri, options=config_vars, request=request)
+ view = self._find_view(request)
+ self.out('')
+ self.out("URL = %s" % url)
+ self.out('')
+ if view is not None:
+ self.out(" context: %s" % view.__request_attrs__['context'])
+ self.out(" view name: %s" % view.__request_attrs__['view_name'])
+ if IMultiView.providedBy(view):
+ for dummy, view_wrapper, dummy in view.views:
+ self.output_view_info(view_wrapper)
+ if IMultiView.providedBy(view_wrapper):
+ for dummy, mv_view_wrapper, dummy in view_wrapper.views:
+ self.output_view_info(mv_view_wrapper, level=2)
+ else:
+ if view is not None:
+ self.output_view_info(view)
+ else:
+ self.out(" Not found.")
+ self.out('')
+ env['closer']()
+ return 0
+
+if __name__ == '__main__': # pragma: no cover
+ sys.exit(main() or 0)
diff --git a/src/pyramid/security.py b/src/pyramid/security.py
new file mode 100644
index 000000000..0bdca090b
--- /dev/null
+++ b/src/pyramid/security.py
@@ -0,0 +1,427 @@
+from zope.deprecation import deprecated
+from zope.interface import providedBy
+
+from pyramid.interfaces import (
+ IAuthenticationPolicy,
+ IAuthorizationPolicy,
+ ISecuredView,
+ IView,
+ IViewClassifier,
+ )
+
+from pyramid.compat import map_
+from pyramid.threadlocal import get_current_registry
+
+Everyone = 'system.Everyone'
+Authenticated = 'system.Authenticated'
+Allow = 'Allow'
+Deny = 'Deny'
+
+class AllPermissionsList(object):
+ """ Stand in 'permission list' to represent all permissions """
+
+ def __iter__(self):
+ return iter(())
+
+ def __contains__(self, other):
+ return True
+
+ def __eq__(self, other):
+ return isinstance(other, self.__class__)
+
+ALL_PERMISSIONS = AllPermissionsList()
+DENY_ALL = (Deny, Everyone, ALL_PERMISSIONS)
+
+NO_PERMISSION_REQUIRED = '__no_permission_required__'
+
+def _get_registry(request):
+ try:
+ reg = request.registry
+ except AttributeError:
+ reg = get_current_registry() # b/c
+ return reg
+
+def _get_authentication_policy(request):
+ registry = _get_registry(request)
+ return registry.queryUtility(IAuthenticationPolicy)
+
+def has_permission(permission, context, request):
+ """
+ A function that calls :meth:`pyramid.request.Request.has_permission`
+ and returns its result.
+
+ .. deprecated:: 1.5
+ Use :meth:`pyramid.request.Request.has_permission` instead.
+
+ .. versionchanged:: 1.5a3
+ If context is None, then attempt to use the context attribute of self;
+ if not set, then the AttributeError is propagated.
+ """
+ return request.has_permission(permission, context)
+
+deprecated(
+ 'has_permission',
+ 'As of Pyramid 1.5 the "pyramid.security.has_permission" API is now '
+ 'deprecated. It will be removed in Pyramid 1.8. Use the '
+ '"has_permission" method of the Pyramid request instead.'
+ )
+
+
+def authenticated_userid(request):
+ """
+ A function that returns the value of the property
+ :attr:`pyramid.request.Request.authenticated_userid`.
+
+ .. deprecated:: 1.5
+ Use :attr:`pyramid.request.Request.authenticated_userid` instead.
+ """
+ return request.authenticated_userid
+
+deprecated(
+ 'authenticated_userid',
+ 'As of Pyramid 1.5 the "pyramid.security.authenticated_userid" API is now '
+ 'deprecated. It will be removed in Pyramid 1.8. Use the '
+ '"authenticated_userid" attribute of the Pyramid request instead.'
+ )
+
+def unauthenticated_userid(request):
+ """
+ A function that returns the value of the property
+ :attr:`pyramid.request.Request.unauthenticated_userid`.
+
+ .. deprecated:: 1.5
+ Use :attr:`pyramid.request.Request.unauthenticated_userid` instead.
+ """
+ return request.unauthenticated_userid
+
+deprecated(
+ 'unauthenticated_userid',
+ 'As of Pyramid 1.5 the "pyramid.security.unauthenticated_userid" API is '
+ 'now deprecated. It will be removed in Pyramid 1.8. Use the '
+ '"unauthenticated_userid" attribute of the Pyramid request instead.'
+ )
+
+def effective_principals(request):
+ """
+ A function that returns the value of the property
+ :attr:`pyramid.request.Request.effective_principals`.
+
+ .. deprecated:: 1.5
+ Use :attr:`pyramid.request.Request.effective_principals` instead.
+ """
+ return request.effective_principals
+
+deprecated(
+ 'effective_principals',
+ 'As of Pyramid 1.5 the "pyramid.security.effective_principals" API is '
+ 'now deprecated. It will be removed in Pyramid 1.8. Use the '
+ '"effective_principals" attribute of the Pyramid request instead.'
+ )
+
+def remember(request, userid, **kw):
+ """
+ Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``)
+ on this request's response.
+ These headers are suitable for 'remembering' a set of credentials
+ implied by the data passed as ``userid`` and ``*kw`` using the
+ current :term:`authentication policy`. Common usage might look
+ like so within the body of a view function (``response`` is
+ assumed to be a :term:`WebOb` -style :term:`response` object
+ computed previously by the view code):
+
+ .. code-block:: python
+
+ from pyramid.security import remember
+ headers = remember(request, 'chrism', password='123', max_age='86400')
+ response = request.response
+ response.headerlist.extend(headers)
+ return response
+
+ If no :term:`authentication policy` is in use, this function will
+ always return an empty sequence. If used, the composition and
+ meaning of ``**kw`` must be agreed upon by the calling code and
+ the effective authentication policy.
+
+ .. versionchanged:: 1.6
+ Deprecated the ``principal`` argument in favor of ``userid`` to clarify
+ its relationship to the authentication policy.
+
+ .. versionchanged:: 1.10
+ Removed the deprecated ``principal`` argument.
+ """
+ policy = _get_authentication_policy(request)
+ if policy is None:
+ return []
+ return policy.remember(request, userid, **kw)
+
+def forget(request):
+ """
+ Return a sequence of header tuples (e.g. ``[('Set-Cookie',
+ 'foo=abc')]``) suitable for 'forgetting' the set of credentials
+ possessed by the currently authenticated user. A common usage
+ might look like so within the body of a view function
+ (``response`` is assumed to be an :term:`WebOb` -style
+ :term:`response` object computed previously by the view code):
+
+ .. code-block:: python
+
+ from pyramid.security import forget
+ headers = forget(request)
+ response.headerlist.extend(headers)
+ return response
+
+ If no :term:`authentication policy` is in use, this function will
+ always return an empty sequence.
+ """
+ policy = _get_authentication_policy(request)
+ if policy is None:
+ return []
+ return policy.forget(request)
+
+def principals_allowed_by_permission(context, permission):
+ """ Provided a ``context`` (a resource object), and a ``permission``
+ (a string or unicode object), if an :term:`authorization policy` is
+ in effect, return a sequence of :term:`principal` ids that possess
+ the permission in the ``context``. If no authorization policy is
+ in effect, this will return a sequence with the single value
+ :mod:`pyramid.security.Everyone` (the special principal
+ identifier representing all principals).
+
+ .. note::
+
+ Even if an :term:`authorization policy` is in effect,
+ some (exotic) authorization policies may not implement the
+ required machinery for this function; those will cause a
+ :exc:`NotImplementedError` exception to be raised when this
+ function is invoked.
+ """
+ reg = get_current_registry()
+ policy = reg.queryUtility(IAuthorizationPolicy)
+ if policy is None:
+ return [Everyone]
+ return policy.principals_allowed_by_permission(context, permission)
+
+def view_execution_permitted(context, request, name=''):
+ """ If the view specified by ``context`` and ``name`` is protected
+ by a :term:`permission`, check the permission associated with the
+ view using the effective authentication/authorization policies and
+ the ``request``. Return a boolean result. If no
+ :term:`authorization policy` is in effect, or if the view is not
+ protected by a permission, return ``True``. If no view can view found,
+ an exception will be raised.
+
+ .. versionchanged:: 1.4a4
+ An exception is raised if no view is found.
+
+ """
+ reg = _get_registry(request)
+ provides = [IViewClassifier] + map_(providedBy, (request, context))
+ # XXX not sure what to do here about using _find_views or analogue;
+ # for now let's just keep it as-is
+ view = reg.adapters.lookup(provides, ISecuredView, name=name)
+ if view is None:
+ view = reg.adapters.lookup(provides, IView, name=name)
+ if view is None:
+ raise TypeError('No registered view satisfies the constraints. '
+ 'It would not make sense to claim that this view '
+ '"is" or "is not" permitted.')
+ return Allowed(
+ 'Allowed: view name %r in context %r (no permission defined)' %
+ (name, context))
+ return view.__permitted__(context, request)
+
+
+class PermitsResult(int):
+ def __new__(cls, s, *args):
+ """
+ Create a new instance.
+
+ :param fmt: A format string explaining the reason for denial.
+ :param args: Arguments are stored and used with the format string
+ to generate the ``msg``.
+
+ """
+ inst = int.__new__(cls, cls.boolval)
+ inst.s = s
+ inst.args = args
+ return inst
+
+ @property
+ def msg(self):
+ """ A string indicating why the result was generated."""
+ return self.s % self.args
+
+ def __str__(self):
+ return self.msg
+
+ def __repr__(self):
+ return '<%s instance at %s with msg %r>' % (self.__class__.__name__,
+ id(self),
+ self.msg)
+
+class Denied(PermitsResult):
+ """
+ An instance of ``Denied`` is returned when a security-related
+ API or other :app:`Pyramid` code denies an action unrelated to
+ an ACL check. It evaluates equal to all boolean false types. It
+ has an attribute named ``msg`` describing the circumstances for
+ the deny.
+
+ """
+ boolval = 0
+
+class Allowed(PermitsResult):
+ """
+ An instance of ``Allowed`` is returned when a security-related
+ API or other :app:`Pyramid` code allows an action unrelated to
+ an ACL check. It evaluates equal to all boolean true types. It
+ has an attribute named ``msg`` describing the circumstances for
+ the allow.
+
+ """
+ boolval = 1
+
+class ACLPermitsResult(PermitsResult):
+ def __new__(cls, ace, acl, permission, principals, context):
+ """
+ Create a new instance.
+
+ :param ace: The :term:`ACE` that matched, triggering the result.
+ :param acl: The :term:`ACL` containing ``ace``.
+ :param permission: The required :term:`permission`.
+ :param principals: The list of :term:`principals <principal>` provided.
+ :param context: The :term:`context` providing the :term:`lineage`
+ searched.
+
+ """
+ fmt = ('%s permission %r via ACE %r in ACL %r on context %r for '
+ 'principals %r')
+ inst = PermitsResult.__new__(
+ cls,
+ fmt,
+ cls.__name__,
+ permission,
+ ace,
+ acl,
+ context,
+ principals,
+ )
+ inst.permission = permission
+ inst.ace = ace
+ inst.acl = acl
+ inst.principals = principals
+ inst.context = context
+ return inst
+
+class ACLDenied(ACLPermitsResult, Denied):
+ """
+ An instance of ``ACLDenied`` is a specialization of
+ :class:`pyramid.security.Denied` that represents that a security check
+ made explicitly against ACL was denied. It evaluates equal to all
+ boolean false types. It also has the following attributes: ``acl``,
+ ``ace``, ``permission``, ``principals``, and ``context``. These
+ attributes indicate the security values involved in the request. Its
+ ``__str__`` method prints a summary of these attributes for debugging
+ purposes. The same summary is available as the ``msg`` attribute.
+
+ """
+
+class ACLAllowed(ACLPermitsResult, Allowed):
+ """
+ An instance of ``ACLAllowed`` is a specialization of
+ :class:`pyramid.security.Allowed` that represents that a security check
+ made explicitly against ACL was allowed. It evaluates equal to all
+ boolean true types. It also has the following attributes: ``acl``,
+ ``ace``, ``permission``, ``principals``, and ``context``. These
+ attributes indicate the security values involved in the request. Its
+ ``__str__`` method prints a summary of these attributes for debugging
+ purposes. The same summary is available as the ``msg`` attribute.
+
+ """
+
+class AuthenticationAPIMixin(object):
+
+ def _get_authentication_policy(self):
+ reg = _get_registry(self)
+ return reg.queryUtility(IAuthenticationPolicy)
+
+ @property
+ def authenticated_userid(self):
+ """ Return the userid of the currently authenticated user or
+ ``None`` if there is no :term:`authentication policy` in effect or
+ there is no currently authenticated user.
+
+ .. versionadded:: 1.5
+ """
+ policy = self._get_authentication_policy()
+ if policy is None:
+ return None
+ return policy.authenticated_userid(self)
+
+ @property
+ def unauthenticated_userid(self):
+ """ Return an object which represents the *claimed* (not verified) user
+ id of the credentials present in the request. ``None`` if there is no
+ :term:`authentication policy` in effect or there is no user data
+ associated with the current request. This differs from
+ :attr:`~pyramid.request.Request.authenticated_userid`, because the
+ effective authentication policy will not ensure that a record
+ associated with the userid exists in persistent storage.
+
+ .. versionadded:: 1.5
+ """
+ policy = self._get_authentication_policy()
+ if policy is None:
+ return None
+ return policy.unauthenticated_userid(self)
+
+ @property
+ def effective_principals(self):
+ """ Return the list of 'effective' :term:`principal` identifiers
+ for the ``request``. If no :term:`authentication policy` is in effect,
+ this will return a one-element list containing the
+ :data:`pyramid.security.Everyone` principal.
+
+ .. versionadded:: 1.5
+ """
+ policy = self._get_authentication_policy()
+ if policy is None:
+ return [Everyone]
+ return policy.effective_principals(self)
+
+class AuthorizationAPIMixin(object):
+
+ def has_permission(self, permission, context=None):
+ """ Given a permission and an optional context, returns an instance of
+ :data:`pyramid.security.Allowed` if the permission is granted to this
+ request with the provided context, or the context already associated
+ with the request. Otherwise, returns an instance of
+ :data:`pyramid.security.Denied`. This method delegates to the current
+ authentication and authorization policies. Returns
+ :data:`pyramid.security.Allowed` unconditionally if no authentication
+ policy has been registered for this request. If ``context`` is not
+ supplied or is supplied as ``None``, the context used is the
+ ``request.context`` attribute.
+
+ :param permission: Does this request have the given permission?
+ :type permission: unicode, str
+ :param context: A resource object or ``None``
+ :type context: object
+ :returns: Either :class:`pyramid.security.Allowed` or
+ :class:`pyramid.security.Denied`.
+
+ .. versionadded:: 1.5
+
+ """
+ if context is None:
+ context = self.context
+ reg = _get_registry(self)
+ authn_policy = reg.queryUtility(IAuthenticationPolicy)
+ if authn_policy is None:
+ return Allowed('No authentication policy in use.')
+ authz_policy = reg.queryUtility(IAuthorizationPolicy)
+ if authz_policy is None:
+ raise ValueError('Authentication policy registered without '
+ 'authorization policy') # should never happen
+ principals = authn_policy.effective_principals(self)
+ return authz_policy.permits(context, principals, permission)
diff --git a/src/pyramid/session.py b/src/pyramid/session.py
new file mode 100644
index 000000000..b953fa184
--- /dev/null
+++ b/src/pyramid/session.py
@@ -0,0 +1,712 @@
+import base64
+import binascii
+import hashlib
+import hmac
+import os
+import time
+import warnings
+
+from zope.deprecation import deprecated
+from zope.interface import implementer
+
+from webob.cookies import (
+ JSONSerializer,
+ SignedSerializer,
+)
+
+from pyramid.compat import (
+ pickle,
+ PY2,
+ text_,
+ bytes_,
+ native_,
+ )
+from pyramid.csrf import (
+ check_csrf_origin,
+ check_csrf_token,
+)
+
+from pyramid.interfaces import ISession
+from pyramid.util import strings_differ
+
+
+def manage_accessed(wrapped):
+ """ Decorator which causes a cookie to be renewed when an accessor
+ method is called."""
+ def accessed(session, *arg, **kw):
+ session.accessed = now = int(time.time())
+ if session._reissue_time is not None:
+ if now - session.renewed > session._reissue_time:
+ session.changed()
+ return wrapped(session, *arg, **kw)
+ accessed.__doc__ = wrapped.__doc__
+ return accessed
+
+def manage_changed(wrapped):
+ """ Decorator which causes a cookie to be set when a setter method
+ is called."""
+ def changed(session, *arg, **kw):
+ session.accessed = int(time.time())
+ session.changed()
+ return wrapped(session, *arg, **kw)
+ changed.__doc__ = wrapped.__doc__
+ return changed
+
+def signed_serialize(data, secret):
+ """ Serialize any pickleable structure (``data``) and sign it
+ using the ``secret`` (must be a string). Return the
+ serialization, which includes the signature as its first 40 bytes.
+ The ``signed_deserialize`` method will deserialize such a value.
+
+ This function is useful for creating signed cookies. For example:
+
+ .. code-block:: python
+
+ cookieval = signed_serialize({'a':1}, 'secret')
+ response.set_cookie('signed_cookie', cookieval)
+
+ .. deprecated:: 1.10
+
+ This function will be removed in :app:`Pyramid` 2.0. It is using
+ pickle-based serialization, which is considered vulnerable to remote
+ code execution attacks and will no longer be used by the default
+ session factories at that time.
+
+ """
+ pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
+ try:
+ # bw-compat with pyramid <= 1.5b1 where latin1 is the default
+ secret = bytes_(secret)
+ except UnicodeEncodeError:
+ secret = bytes_(secret, 'utf-8')
+ sig = hmac.new(secret, pickled, hashlib.sha1).hexdigest()
+ return sig + native_(base64.b64encode(pickled))
+
+deprecated(
+ 'signed_serialize',
+ 'This function will be removed in Pyramid 2.0. It is using pickle-based '
+ 'serialization, which is considered vulnerable to remote code execution '
+ 'attacks.',
+)
+
+def signed_deserialize(serialized, secret, hmac=hmac):
+ """ Deserialize the value returned from ``signed_serialize``. If
+ the value cannot be deserialized for any reason, a
+ :exc:`ValueError` exception will be raised.
+
+ This function is useful for deserializing a signed cookie value
+ created by ``signed_serialize``. For example:
+
+ .. code-block:: python
+
+ cookieval = request.cookies['signed_cookie']
+ data = signed_deserialize(cookieval, 'secret')
+
+ .. deprecated:: 1.10
+
+ This function will be removed in :app:`Pyramid` 2.0. It is using
+ pickle-based serialization, which is considered vulnerable to remote
+ code execution attacks and will no longer be used by the default
+ session factories at that time.
+ """
+ # hmac parameterized only for unit tests
+ try:
+ input_sig, pickled = (bytes_(serialized[:40]),
+ base64.b64decode(bytes_(serialized[40:])))
+ except (binascii.Error, TypeError) as e:
+ # Badly formed data can make base64 die
+ raise ValueError('Badly formed base64 data: %s' % e)
+
+ try:
+ # bw-compat with pyramid <= 1.5b1 where latin1 is the default
+ secret = bytes_(secret)
+ except UnicodeEncodeError:
+ secret = bytes_(secret, 'utf-8')
+ sig = bytes_(hmac.new(secret, pickled, hashlib.sha1).hexdigest())
+
+ # Avoid timing attacks (see
+ # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf)
+ if strings_differ(sig, input_sig):
+ raise ValueError('Invalid signature')
+
+ return pickle.loads(pickled)
+
+deprecated(
+ 'signed_deserialize',
+ 'This function will be removed in Pyramid 2.0. It is using pickle-based '
+ 'serialization, which is considered vulnerable to remote code execution '
+ 'attacks.',
+)
+
+
+class PickleSerializer(object):
+ """ A serializer that uses the pickle protocol to dump Python
+ data to bytes.
+
+ This is the default serializer used by Pyramid.
+
+ ``protocol`` may be specified to control the version of pickle used.
+ Defaults to :attr:`pickle.HIGHEST_PROTOCOL`.
+
+ """
+ def __init__(self, protocol=pickle.HIGHEST_PROTOCOL):
+ self.protocol = protocol
+
+ def loads(self, bstruct):
+ """Accept bytes and return a Python object."""
+ try:
+ return pickle.loads(bstruct)
+ # at least ValueError, AttributeError, ImportError but more to be safe
+ except Exception:
+ raise ValueError
+
+ def dumps(self, appstruct):
+ """Accept a Python object and return bytes."""
+ return pickle.dumps(appstruct, self.protocol)
+
+
+JSONSerializer = JSONSerializer # api
+
+
+def BaseCookieSessionFactory(
+ serializer,
+ cookie_name='session',
+ max_age=None,
+ path='/',
+ domain=None,
+ secure=False,
+ httponly=False,
+ samesite='Lax',
+ timeout=1200,
+ reissue_time=0,
+ set_on_exception=True,
+ ):
+ """
+ Configure a :term:`session factory` which will provide cookie-based
+ sessions. The return value of this function is a :term:`session factory`,
+ which may be provided as the ``session_factory`` argument of a
+ :class:`pyramid.config.Configurator` constructor, or used as the
+ ``session_factory`` argument of the
+ :meth:`pyramid.config.Configurator.set_session_factory` method.
+
+ The session factory returned by this function will create sessions
+ which are limited to storing fewer than 4000 bytes of data (as the
+ payload must fit into a single cookie).
+
+ .. warning:
+
+ This class provides no protection from tampering and is only intended
+ to be used by framework authors to create their own cookie-based
+ session factories.
+
+ Parameters:
+
+ ``serializer``
+ An object with two methods: ``loads`` and ``dumps``. The ``loads``
+ method should accept bytes and return a Python object. The ``dumps``
+ method should accept a Python object and return bytes. A ``ValueError``
+ should be raised for malformed inputs.
+
+ ``cookie_name``
+ The name of the cookie used for sessioning. Default: ``'session'``.
+
+ ``max_age``
+ The maximum age of the cookie used for sessioning (in seconds).
+ Default: ``None`` (browser scope).
+
+ ``path``
+ The path used for the session cookie. Default: ``'/'``.
+
+ ``domain``
+ The domain used for the session cookie. Default: ``None`` (no domain).
+
+ ``secure``
+ The 'secure' flag of the session cookie. Default: ``False``.
+
+ ``httponly``
+ Hide the cookie from Javascript by setting the 'HttpOnly' flag of the
+ session cookie. Default: ``False``.
+
+ ``samesite``
+ The 'samesite' option of the session cookie. Set the value to ``None``
+ to turn off the samesite option. Default: ``'Lax'``.
+
+ ``timeout``
+ A number of seconds of inactivity before a session times out. If
+ ``None`` then the cookie never expires. This lifetime only applies
+ to the *value* within the cookie. Meaning that if the cookie expires
+ due to a lower ``max_age``, then this setting has no effect.
+ Default: ``1200``.
+
+ ``reissue_time``
+ The number of seconds that must pass before the cookie is automatically
+ reissued as the result of a request which accesses the session. The
+ duration is measured as the number of seconds since the last session
+ cookie was issued and 'now'. If this value is ``0``, a new cookie
+ will be reissued on every request accessing the session. If ``None``
+ then the cookie's lifetime will never be extended.
+
+ A good rule of thumb: if you want auto-expired cookies based on
+ inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
+ ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
+ (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
+ than the ``reissue_time`` value, as the ticket will never be reissued.
+ However, such a configuration is not explicitly prevented.
+
+ Default: ``0``.
+
+ ``set_on_exception``
+ If ``True``, set a session cookie even if an exception occurs
+ while rendering a view. Default: ``True``.
+
+ .. versionadded: 1.5a3
+
+ .. versionchanged: 1.10
+
+ Added the ``samesite`` option and made the default ``'Lax'``.
+ """
+
+ @implementer(ISession)
+ class CookieSession(dict):
+ """ Dictionary-like session object """
+
+ # configuration parameters
+ _cookie_name = cookie_name
+ _cookie_max_age = max_age if max_age is None else int(max_age)
+ _cookie_path = path
+ _cookie_domain = domain
+ _cookie_secure = secure
+ _cookie_httponly = httponly
+ _cookie_samesite = samesite
+ _cookie_on_exception = set_on_exception
+ _timeout = timeout if timeout is None else int(timeout)
+ _reissue_time = reissue_time if reissue_time is None else int(reissue_time)
+
+ # dirty flag
+ _dirty = False
+
+ def __init__(self, request):
+ self.request = request
+ now = time.time()
+ created = renewed = now
+ new = True
+ value = None
+ state = {}
+ cookieval = request.cookies.get(self._cookie_name)
+ if cookieval is not None:
+ try:
+ value = serializer.loads(bytes_(cookieval))
+ except ValueError:
+ # the cookie failed to deserialize, dropped
+ value = None
+
+ if value is not None:
+ try:
+ # since the value is not necessarily signed, we have
+ # to unpack it a little carefully
+ rval, cval, sval = value
+ renewed = float(rval)
+ created = float(cval)
+ state = sval
+ new = False
+ except (TypeError, ValueError):
+ # value failed to unpack properly or renewed was not
+ # a numeric type so we'll fail deserialization here
+ state = {}
+
+ if self._timeout is not None:
+ if now - renewed > self._timeout:
+ # expire the session because it was not renewed
+ # before the timeout threshold
+ state = {}
+
+ self.created = created
+ self.accessed = renewed
+ self.renewed = renewed
+ self.new = new
+ dict.__init__(self, state)
+
+ # ISession methods
+ def changed(self):
+ if not self._dirty:
+ self._dirty = True
+ def set_cookie_callback(request, response):
+ self._set_cookie(response)
+ self.request = None # explicitly break cycle for gc
+ self.request.add_response_callback(set_cookie_callback)
+
+ def invalidate(self):
+ self.clear() # XXX probably needs to unset cookie
+
+ # non-modifying dictionary methods
+ get = manage_accessed(dict.get)
+ __getitem__ = manage_accessed(dict.__getitem__)
+ items = manage_accessed(dict.items)
+ values = manage_accessed(dict.values)
+ keys = manage_accessed(dict.keys)
+ __contains__ = manage_accessed(dict.__contains__)
+ __len__ = manage_accessed(dict.__len__)
+ __iter__ = manage_accessed(dict.__iter__)
+
+ if PY2:
+ iteritems = manage_accessed(dict.iteritems)
+ itervalues = manage_accessed(dict.itervalues)
+ iterkeys = manage_accessed(dict.iterkeys)
+ has_key = manage_accessed(dict.has_key)
+
+ # modifying dictionary methods
+ clear = manage_changed(dict.clear)
+ update = manage_changed(dict.update)
+ setdefault = manage_changed(dict.setdefault)
+ pop = manage_changed(dict.pop)
+ popitem = manage_changed(dict.popitem)
+ __setitem__ = manage_changed(dict.__setitem__)
+ __delitem__ = manage_changed(dict.__delitem__)
+
+ # flash API methods
+ @manage_changed
+ def flash(self, msg, queue='', allow_duplicate=True):
+ storage = self.setdefault('_f_' + queue, [])
+ if allow_duplicate or (msg not in storage):
+ storage.append(msg)
+
+ @manage_changed
+ def pop_flash(self, queue=''):
+ storage = self.pop('_f_' + queue, [])
+ return storage
+
+ @manage_accessed
+ def peek_flash(self, queue=''):
+ storage = self.get('_f_' + queue, [])
+ return storage
+
+ # CSRF API methods
+ @manage_changed
+ def new_csrf_token(self):
+ token = text_(binascii.hexlify(os.urandom(20)))
+ self['_csrft_'] = token
+ return token
+
+ @manage_accessed
+ def get_csrf_token(self):
+ token = self.get('_csrft_', None)
+ if token is None:
+ token = self.new_csrf_token()
+ return token
+
+ # non-API methods
+ def _set_cookie(self, response):
+ if not self._cookie_on_exception:
+ exception = getattr(self.request, 'exception', None)
+ if exception is not None: # dont set a cookie during exceptions
+ return False
+ cookieval = native_(serializer.dumps(
+ (self.accessed, self.created, dict(self))
+ ))
+ if len(cookieval) > 4064:
+ raise ValueError(
+ 'Cookie value is too long to store (%s bytes)' %
+ len(cookieval)
+ )
+ response.set_cookie(
+ self._cookie_name,
+ value=cookieval,
+ max_age=self._cookie_max_age,
+ path=self._cookie_path,
+ domain=self._cookie_domain,
+ secure=self._cookie_secure,
+ httponly=self._cookie_httponly,
+ samesite=self._cookie_samesite,
+ )
+ return True
+
+ return CookieSession
+
+
+def UnencryptedCookieSessionFactoryConfig(
+ secret,
+ timeout=1200,
+ cookie_name='session',
+ cookie_max_age=None,
+ cookie_path='/',
+ cookie_domain=None,
+ cookie_secure=False,
+ cookie_httponly=False,
+ cookie_samesite='Lax',
+ cookie_on_exception=True,
+ signed_serialize=signed_serialize,
+ signed_deserialize=signed_deserialize,
+ ):
+ """
+ .. deprecated:: 1.5
+ Use :func:`pyramid.session.SignedCookieSessionFactory` instead.
+ Caveat: Cookies generated using ``SignedCookieSessionFactory`` are not
+ compatible with cookies generated using
+ ``UnencryptedCookieSessionFactory``, so existing user session data
+ will be destroyed if you switch to it.
+
+ Configure a :term:`session factory` which will provide unencrypted
+ (but signed) cookie-based sessions. The return value of this
+ function is a :term:`session factory`, which may be provided as
+ the ``session_factory`` argument of a
+ :class:`pyramid.config.Configurator` constructor, or used
+ as the ``session_factory`` argument of the
+ :meth:`pyramid.config.Configurator.set_session_factory`
+ method.
+
+ The session factory returned by this function will create sessions
+ which are limited to storing fewer than 4000 bytes of data (as the
+ payload must fit into a single cookie).
+
+ Parameters:
+
+ ``secret``
+ A string which is used to sign the cookie.
+
+ ``timeout``
+ A number of seconds of inactivity before a session times out.
+
+ ``cookie_name``
+ The name of the cookie used for sessioning.
+
+ ``cookie_max_age``
+ The maximum age of the cookie used for sessioning (in seconds).
+ Default: ``None`` (browser scope).
+
+ ``cookie_path``
+ The path used for the session cookie.
+
+ ``cookie_domain``
+ The domain used for the session cookie. Default: ``None`` (no domain).
+
+ ``cookie_secure``
+ The 'secure' flag of the session cookie.
+
+ ``cookie_httponly``
+ The 'httpOnly' flag of the session cookie.
+
+ ``cookie_samesite``
+ The 'samesite' option of the session cookie. Set the value to ``None``
+ to turn off the samesite option. Default: ``'Lax'``.
+
+ ``cookie_on_exception``
+ If ``True``, set a session cookie even if an exception occurs
+ while rendering a view.
+
+ ``signed_serialize``
+ A callable which takes more or less arbitrary Python data structure and
+ a secret and returns a signed serialization in bytes.
+ Default: ``signed_serialize`` (using pickle).
+
+ ``signed_deserialize``
+ A callable which takes a signed and serialized data structure in bytes
+ and a secret and returns the original data structure if the signature
+ is valid. Default: ``signed_deserialize`` (using pickle).
+
+ .. versionchanged: 1.10
+
+ Added the ``samesite`` option and made the default ``'Lax'``.
+ """
+
+ class SerializerWrapper(object):
+ def __init__(self, secret):
+ self.secret = secret
+
+ def loads(self, bstruct):
+ return signed_deserialize(bstruct, secret)
+
+ def dumps(self, appstruct):
+ return signed_serialize(appstruct, secret)
+
+ serializer = SerializerWrapper(secret)
+
+ return BaseCookieSessionFactory(
+ serializer,
+ cookie_name=cookie_name,
+ max_age=cookie_max_age,
+ path=cookie_path,
+ domain=cookie_domain,
+ secure=cookie_secure,
+ httponly=cookie_httponly,
+ samesite=cookie_samesite,
+ timeout=timeout,
+ reissue_time=0, # to keep session.accessed == session.renewed
+ set_on_exception=cookie_on_exception,
+ )
+
+deprecated(
+ 'UnencryptedCookieSessionFactoryConfig',
+ 'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of '
+ 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.'
+ ' Caveat: Cookies generated using SignedCookieSessionFactory are not '
+ 'compatible with cookies generated using UnencryptedCookieSessionFactory, '
+ 'so existing user session data will be destroyed if you switch to it.'
+ )
+
+
+def SignedCookieSessionFactory(
+ secret,
+ cookie_name='session',
+ max_age=None,
+ path='/',
+ domain=None,
+ secure=False,
+ httponly=False,
+ samesite='Lax',
+ set_on_exception=True,
+ timeout=1200,
+ reissue_time=0,
+ hashalg='sha512',
+ salt='pyramid.session.',
+ serializer=None,
+ ):
+ """
+ .. versionadded:: 1.5
+
+ Configure a :term:`session factory` which will provide signed
+ cookie-based sessions. The return value of this
+ function is a :term:`session factory`, which may be provided as
+ the ``session_factory`` argument of a
+ :class:`pyramid.config.Configurator` constructor, or used
+ as the ``session_factory`` argument of the
+ :meth:`pyramid.config.Configurator.set_session_factory`
+ method.
+
+ The session factory returned by this function will create sessions
+ which are limited to storing fewer than 4000 bytes of data (as the
+ payload must fit into a single cookie).
+
+ Parameters:
+
+ ``secret``
+ A string which is used to sign the cookie. The secret should be at
+ least as long as the block size of the selected hash algorithm. For
+ ``sha512`` this would mean a 512 bit (64 character) secret. It should
+ be unique within the set of secret values provided to Pyramid for
+ its various subsystems (see :ref:`admonishment_against_secret_sharing`).
+
+ ``hashalg``
+ The HMAC digest algorithm to use for signing. The algorithm must be
+ supported by the :mod:`hashlib` library. Default: ``'sha512'``.
+
+ ``salt``
+ A namespace to avoid collisions between different uses of a shared
+ secret. Reusing a secret for different parts of an application is
+ strongly discouraged (see :ref:`admonishment_against_secret_sharing`).
+ Default: ``'pyramid.session.'``.
+
+ ``cookie_name``
+ The name of the cookie used for sessioning. Default: ``'session'``.
+
+ ``max_age``
+ The maximum age of the cookie used for sessioning (in seconds).
+ Default: ``None`` (browser scope).
+
+ ``path``
+ The path used for the session cookie. Default: ``'/'``.
+
+ ``domain``
+ The domain used for the session cookie. Default: ``None`` (no domain).
+
+ ``secure``
+ The 'secure' flag of the session cookie. Default: ``False``.
+
+ ``httponly``
+ Hide the cookie from Javascript by setting the 'HttpOnly' flag of the
+ session cookie. Default: ``False``.
+
+ ``samesite``
+ The 'samesite' option of the session cookie. Set the value to ``None``
+ to turn off the samesite option. Default: ``'Lax'``.
+
+ ``timeout``
+ A number of seconds of inactivity before a session times out. If
+ ``None`` then the cookie never expires. This lifetime only applies
+ to the *value* within the cookie. Meaning that if the cookie expires
+ due to a lower ``max_age``, then this setting has no effect.
+ Default: ``1200``.
+
+ ``reissue_time``
+ The number of seconds that must pass before the cookie is automatically
+ reissued as the result of accessing the session. The
+ duration is measured as the number of seconds since the last session
+ cookie was issued and 'now'. If this value is ``0``, a new cookie
+ will be reissued on every request accessing the session. If ``None``
+ then the cookie's lifetime will never be extended.
+
+ A good rule of thumb: if you want auto-expired cookies based on
+ inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
+ ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
+ (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
+ than the ``reissue_time`` value, as the ticket will never be reissued.
+ However, such a configuration is not explicitly prevented.
+
+ Default: ``0``.
+
+ ``set_on_exception``
+ If ``True``, set a session cookie even if an exception occurs
+ while rendering a view. Default: ``True``.
+
+ ``serializer``
+ An object with two methods: ``loads`` and ``dumps``. The ``loads``
+ method should accept bytes and return a Python object. The ``dumps``
+ method should accept a Python object and return bytes. A ``ValueError``
+ should be raised for malformed inputs. If a serializer is not passed,
+ the :class:`pyramid.session.PickleSerializer` serializer will be used.
+
+ .. warning::
+
+ In :app:`Pyramid` 2.0 the default ``serializer`` option will change to
+ use :class:`pyramid.session.JSONSerializer`. See
+ :ref:`pickle_session_deprecation` for more information about why this
+ change is being made.
+
+ .. versionadded: 1.5a3
+
+ .. versionchanged: 1.10
+
+ Added the ``samesite`` option and made the default ``Lax``.
+
+ """
+ if serializer is None:
+ serializer = PickleSerializer()
+ warnings.warn(
+ 'The default pickle serializer is deprecated as of Pyramid 1.9 '
+ 'and it will be changed to use pyramid.session.JSONSerializer in '
+ 'version 2.0. Explicitly set the serializer to avoid future '
+ 'incompatibilities. See "Upcoming Changes to ISession in '
+ 'Pyramid 2.0" for more information about this change.',
+ DeprecationWarning,
+ stacklevel=1,
+ )
+
+ signed_serializer = SignedSerializer(
+ secret,
+ salt,
+ hashalg,
+ serializer=serializer,
+ )
+
+ return BaseCookieSessionFactory(
+ signed_serializer,
+ cookie_name=cookie_name,
+ max_age=max_age,
+ path=path,
+ domain=domain,
+ secure=secure,
+ httponly=httponly,
+ samesite=samesite,
+ timeout=timeout,
+ reissue_time=reissue_time,
+ set_on_exception=set_on_exception,
+ )
+
+check_csrf_origin = check_csrf_origin # api
+deprecated('check_csrf_origin',
+ 'pyramid.session.check_csrf_origin is deprecated as of Pyramid '
+ '1.9. Use pyramid.csrf.check_csrf_origin instead.')
+
+check_csrf_token = check_csrf_token # api
+deprecated('check_csrf_token',
+ 'pyramid.session.check_csrf_token is deprecated as of Pyramid '
+ '1.9. Use pyramid.csrf.check_csrf_token instead.')
diff --git a/src/pyramid/settings.py b/src/pyramid/settings.py
new file mode 100644
index 000000000..8a498d572
--- /dev/null
+++ b/src/pyramid/settings.py
@@ -0,0 +1,33 @@
+from pyramid.compat import string_types
+
+truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
+falsey = frozenset(('f', 'false', 'n', 'no', 'off', '0'))
+
+def asbool(s):
+ """ Return the boolean value ``True`` if the case-lowered value of string
+ input ``s`` is a :term:`truthy string`. If ``s`` is already one of the
+ boolean values ``True`` or ``False``, return it."""
+ if s is None:
+ return False
+ if isinstance(s, bool):
+ return s
+ s = str(s).strip()
+ return s.lower() in truthy
+
+def aslist_cronly(value):
+ if isinstance(value, string_types):
+ value = filter(None, [x.strip() for x in value.splitlines()])
+ return list(value)
+
+def aslist(value, flatten=True):
+ """ Return a list of strings, separating the input based on newlines
+ and, if flatten=True (the default), also split on spaces within
+ each line."""
+ values = aslist_cronly(value)
+ if not flatten:
+ return values
+ result = []
+ for value in values:
+ subvalues = value.split()
+ result.extend(subvalues)
+ return result
diff --git a/src/pyramid/static.py b/src/pyramid/static.py
new file mode 100644
index 000000000..70fdf877b
--- /dev/null
+++ b/src/pyramid/static.py
@@ -0,0 +1,301 @@
+# -*- coding: utf-8 -*-
+import json
+import os
+
+from os.path import (
+ getmtime,
+ normcase,
+ normpath,
+ join,
+ isdir,
+ exists,
+ )
+
+from pkg_resources import (
+ resource_exists,
+ resource_filename,
+ resource_isdir,
+ )
+
+from pyramid.asset import (
+ abspath_from_asset_spec,
+ resolve_asset_spec,
+)
+
+from pyramid.compat import (
+ lru_cache,
+ text_,
+)
+
+from pyramid.httpexceptions import (
+ HTTPNotFound,
+ HTTPMovedPermanently,
+ )
+
+from pyramid.path import caller_package
+
+from pyramid.response import (
+ _guess_type,
+ FileResponse,
+)
+
+from pyramid.traversal import traversal_path_info
+
+slash = text_('/')
+
+class static_view(object):
+ """ An instance of this class is a callable which can act as a
+ :app:`Pyramid` :term:`view callable`; this view will serve
+ static files from a directory on disk based on the ``root_dir``
+ you provide to its constructor.
+
+ The directory may contain subdirectories (recursively); the static
+ view implementation will descend into these directories as
+ necessary based on the components of the URL in order to resolve a
+ path into a response.
+
+ You may pass an absolute or relative filesystem path or a
+ :term:`asset specification` representing the directory
+ containing static files as the ``root_dir`` argument to this
+ class' constructor.
+
+ If the ``root_dir`` path is relative, and the ``package_name``
+ argument is ``None``, ``root_dir`` will be considered relative to
+ the directory in which the Python file which *calls* ``static``
+ resides. If the ``package_name`` name argument is provided, and a
+ relative ``root_dir`` is provided, the ``root_dir`` will be
+ considered relative to the Python :term:`package` specified by
+ ``package_name`` (a dotted path to a Python package).
+
+ ``cache_max_age`` influences the ``Expires`` and ``Max-Age``
+ response headers returned by the view (default is 3600 seconds or
+ one hour).
+
+ ``use_subpath`` influences whether ``request.subpath`` will be used as
+ ``PATH_INFO`` when calling the underlying WSGI application which actually
+ serves the static files. If it is ``True``, the static application will
+ consider ``request.subpath`` as ``PATH_INFO`` input. If it is ``False``,
+ the static application will consider request.environ[``PATH_INFO``] as
+ ``PATH_INFO`` input. By default, this is ``False``.
+
+ .. note::
+
+ If the ``root_dir`` is relative to a :term:`package`, or is a
+ :term:`asset specification` the :app:`Pyramid`
+ :class:`pyramid.config.Configurator` method can be used to override
+ assets within the named ``root_dir`` package-relative directory.
+ However, if the ``root_dir`` is absolute, configuration will not be able
+ to override the assets it contains.
+ """
+
+ def __init__(self, root_dir, cache_max_age=3600, package_name=None,
+ use_subpath=False, index='index.html'):
+ # package_name is for bw compat; it is preferred to pass in a
+ # package-relative path as root_dir
+ # (e.g. ``anotherpackage:foo/static``).
+ self.cache_max_age = cache_max_age
+ if package_name is None:
+ package_name = caller_package().__name__
+ package_name, docroot = resolve_asset_spec(root_dir, package_name)
+ self.use_subpath = use_subpath
+ self.package_name = package_name
+ self.docroot = docroot
+ self.norm_docroot = normcase(normpath(docroot))
+ self.index = index
+
+ def __call__(self, context, request):
+ if self.use_subpath:
+ path_tuple = request.subpath
+ else:
+ path_tuple = traversal_path_info(request.environ['PATH_INFO'])
+ path = _secure_path(path_tuple)
+
+ if path is None:
+ raise HTTPNotFound('Out of bounds: %s' % request.url)
+
+ if self.package_name: # package resource
+ resource_path = '%s/%s' % (self.docroot.rstrip('/'), path)
+ if resource_isdir(self.package_name, resource_path):
+ if not request.path_url.endswith('/'):
+ self.add_slash_redirect(request)
+ resource_path = '%s/%s' % (
+ resource_path.rstrip('/'), self.index
+ )
+
+ if not resource_exists(self.package_name, resource_path):
+ raise HTTPNotFound(request.url)
+ filepath = resource_filename(self.package_name, resource_path)
+
+ else: # filesystem file
+
+ # os.path.normpath converts / to \ on windows
+ filepath = normcase(normpath(join(self.norm_docroot, path)))
+ if isdir(filepath):
+ if not request.path_url.endswith('/'):
+ self.add_slash_redirect(request)
+ filepath = join(filepath, self.index)
+ if not exists(filepath):
+ raise HTTPNotFound(request.url)
+
+ content_type, content_encoding = _guess_type(filepath)
+ return FileResponse(
+ filepath, request, self.cache_max_age,
+ content_type, content_encoding=None)
+
+ def add_slash_redirect(self, request):
+ url = request.path_url + '/'
+ qs = request.query_string
+ if qs:
+ url = url + '?' + qs
+ raise HTTPMovedPermanently(url)
+
+_seps = set(['/', os.sep])
+def _contains_slash(item):
+ for sep in _seps:
+ if sep in item:
+ return True
+
+_has_insecure_pathelement = set(['..', '.', '']).intersection
+
+@lru_cache(1000)
+def _secure_path(path_tuple):
+ if _has_insecure_pathelement(path_tuple):
+ # belt-and-suspenders security; this should never be true
+ # unless someone screws up the traversal_path code
+ # (request.subpath is computed via traversal_path too)
+ return None
+ if any([_contains_slash(item) for item in path_tuple]):
+ return None
+ encoded = slash.join(path_tuple) # will be unicode
+ return encoded
+
+class QueryStringCacheBuster(object):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
+ a token for cache busting in the query string of an asset URL.
+
+ The optional ``param`` argument determines the name of the parameter added
+ to the query string and defaults to ``'x'``.
+
+ To use this class, subclass it and provide a ``tokenize`` method which
+ accepts ``request, pathspec, kw`` and returns a token.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self, param='x'):
+ self.param = param
+
+ def __call__(self, request, subpath, kw):
+ token = self.tokenize(request, subpath, kw)
+ query = kw.setdefault('_query', {})
+ if isinstance(query, dict):
+ query[self.param] = token
+ else:
+ kw['_query'] = tuple(query) + ((self.param, token),)
+ return subpath, kw
+
+class QueryStringConstantCacheBuster(QueryStringCacheBuster):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
+ an arbitrary token for cache busting in the query string of an asset URL.
+
+ The ``token`` parameter is the token string to use for cache busting and
+ will be the same for every request.
+
+ The optional ``param`` argument determines the name of the parameter added
+ to the query string and defaults to ``'x'``.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self, token, param='x'):
+ super(QueryStringConstantCacheBuster, self).__init__(param=param)
+ self._token = token
+
+ def tokenize(self, request, subpath, kw):
+ return self._token
+
+class ManifestCacheBuster(object):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
+ uses a supplied manifest file to map an asset path to a cache-busted
+ version of the path.
+
+ The ``manifest_spec`` can be an absolute path or a :term:`asset
+ specification` pointing to a package-relative file.
+
+ The manifest file is expected to conform to the following simple JSON
+ format:
+
+ .. code-block:: json
+
+ {
+ "css/main.css": "css/main-678b7c80.css",
+ "images/background.png": "images/background-a8169106.png",
+ }
+
+ By default, it is a JSON-serialized dictionary where the keys are the
+ source asset paths used in calls to
+ :meth:`~pyramid.request.Request.static_url`. For example:
+
+ .. code-block:: pycon
+
+ >>> request.static_url('myapp:static/css/main.css')
+ "http://www.example.com/static/css/main-678b7c80.css"
+
+ The file format and location can be changed by subclassing and overriding
+ :meth:`.parse_manifest`.
+
+ If a path is not found in the manifest it will pass through unchanged.
+
+ If ``reload`` is ``True`` then the manifest file will be reloaded when
+ changed. It is not recommended to leave this enabled in production.
+
+ If the manifest file cannot be found on disk it will be treated as
+ an empty mapping unless ``reload`` is ``False``.
+
+ .. versionadded:: 1.6
+ """
+ exists = staticmethod(exists) # testing
+ getmtime = staticmethod(getmtime) # testing
+
+ def __init__(self, manifest_spec, reload=False):
+ package_name = caller_package().__name__
+ self.manifest_path = abspath_from_asset_spec(
+ manifest_spec, package_name)
+ self.reload = reload
+
+ self._mtime = None
+ if not reload:
+ self._manifest = self.get_manifest()
+
+ def get_manifest(self):
+ with open(self.manifest_path, 'rb') as fp:
+ return self.parse_manifest(fp.read())
+
+ def parse_manifest(self, content):
+ """
+ Parse the ``content`` read from the ``manifest_path`` into a
+ dictionary mapping.
+
+ Subclasses may override this method to use something other than
+ ``json.loads`` to load any type of file format and return a conforming
+ dictionary.
+
+ """
+ return json.loads(content.decode('utf-8'))
+
+ @property
+ def manifest(self):
+ """ The current manifest dictionary."""
+ if self.reload:
+ if not self.exists(self.manifest_path):
+ return {}
+ mtime = self.getmtime(self.manifest_path)
+ if self._mtime is None or mtime > self._mtime:
+ self._manifest = self.get_manifest()
+ self._mtime = mtime
+ return self._manifest
+
+ def __call__(self, request, subpath, kw):
+ subpath = self.manifest.get(subpath, subpath)
+ return (subpath, kw)
diff --git a/src/pyramid/testing.py b/src/pyramid/testing.py
new file mode 100644
index 000000000..4986c0e27
--- /dev/null
+++ b/src/pyramid/testing.py
@@ -0,0 +1,641 @@
+import copy
+import os
+from contextlib import contextmanager
+
+from zope.interface import (
+ implementer,
+ alsoProvides,
+ )
+
+from pyramid.interfaces import (
+ IRequest,
+ ISession,
+ )
+
+from pyramid.compat import (
+ PY3,
+ PYPY,
+ class_types,
+ text_,
+ )
+
+from pyramid.config import Configurator
+from pyramid.decorator import reify
+from pyramid.path import caller_package
+from pyramid.response import _get_response_factory
+from pyramid.registry import Registry
+
+from pyramid.security import (
+ Authenticated,
+ Everyone,
+ AuthenticationAPIMixin,
+ AuthorizationAPIMixin,
+ )
+
+from pyramid.threadlocal import (
+ get_current_registry,
+ manager,
+ )
+
+from pyramid.i18n import LocalizerRequestMixin
+from pyramid.request import CallbackMethodsMixin
+from pyramid.url import URLMethodsMixin
+from pyramid.util import InstancePropertyMixin
+from pyramid.view import ViewMethodsMixin
+
+
+_marker = object()
+
+class DummyRootFactory(object):
+ __parent__ = None
+ __name__ = None
+ def __init__(self, request):
+ if 'bfg.routes.matchdict' in request:
+ self.__dict__.update(request['bfg.routes.matchdict'])
+
+class DummySecurityPolicy(object):
+ """ A standin for both an IAuthentication and IAuthorization policy """
+ def __init__(self, userid=None, groupids=(), permissive=True,
+ remember_result=None, forget_result=None):
+ self.userid = userid
+ self.groupids = groupids
+ self.permissive = permissive
+ if remember_result is None:
+ remember_result = []
+ if forget_result is None:
+ forget_result = []
+ self.remember_result = remember_result
+ self.forget_result = forget_result
+
+ def authenticated_userid(self, request):
+ return self.userid
+
+ def unauthenticated_userid(self, request):
+ return self.userid
+
+ def effective_principals(self, request):
+ effective_principals = [Everyone]
+ if self.userid:
+ effective_principals.append(Authenticated)
+ effective_principals.append(self.userid)
+ effective_principals.extend(self.groupids)
+ return effective_principals
+
+ def remember(self, request, userid, **kw):
+ self.remembered = userid
+ return self.remember_result
+
+ def forget(self, request):
+ self.forgotten = True
+ return self.forget_result
+
+ def permits(self, context, principals, permission):
+ return self.permissive
+
+ def principals_allowed_by_permission(self, context, permission):
+ return self.effective_principals(None)
+
+class DummyTemplateRenderer(object):
+ """
+ An instance of this class is returned from
+ :meth:`pyramid.config.Configurator.testing_add_renderer`. It has a
+ helper function (``assert_``) that makes it possible to make an
+ assertion which compares data passed to the renderer by the view
+ function against expected key/value pairs.
+ """
+ def __init__(self, string_response=''):
+ self._received = {}
+ self._string_response = string_response
+ self._implementation = MockTemplate(string_response)
+
+ # For in-the-wild test code that doesn't create its own renderer,
+ # but mutates our internals instead. When all you read is the
+ # source code, *everything* is an API!
+ def _get_string_response(self):
+ return self._string_response
+ def _set_string_response(self, response):
+ self._string_response = response
+ self._implementation.response = response
+ string_response = property(_get_string_response, _set_string_response)
+
+ def implementation(self):
+ return self._implementation
+
+ def __call__(self, kw, system=None):
+ if system:
+ self._received.update(system)
+ self._received.update(kw)
+ return self.string_response
+
+ def __getattr__(self, k):
+ """ Backwards compatibility """
+ val = self._received.get(k, _marker)
+ if val is _marker:
+ val = self._implementation._received.get(k, _marker)
+ if val is _marker:
+ raise AttributeError(k)
+ return val
+
+ def assert_(self, **kw):
+ """ Accept an arbitrary set of assertion key/value pairs. For
+ each assertion key/value pair assert that the renderer
+ (eg. :func:`pyramid.renderers.render_to_response`)
+ received the key with a value that equals the asserted
+ value. If the renderer did not receive the key at all, or the
+ value received by the renderer doesn't match the assertion
+ value, raise an :exc:`AssertionError`."""
+ for k, v in kw.items():
+ myval = self._received.get(k, _marker)
+ if myval is _marker:
+ myval = self._implementation._received.get(k, _marker)
+ if myval is _marker:
+ raise AssertionError(
+ 'A value for key "%s" was not passed to the renderer'
+ % k)
+
+ if myval != v:
+ raise AssertionError(
+ '\nasserted value for %s: %r\nactual value: %r' % (
+ k, v, myval))
+ return True
+
+
+class DummyResource:
+ """ A dummy :app:`Pyramid` :term:`resource` object."""
+ def __init__(self, __name__=None, __parent__=None, __provides__=None,
+ **kw):
+ """ The resource's ``__name__`` attribute will be set to the
+ value of the ``__name__`` argument, and the resource's
+ ``__parent__`` attribute will be set to the value of the
+ ``__parent__`` argument. If ``__provides__`` is specified, it
+ should be an interface object or tuple of interface objects
+ that will be attached to the resulting resource via
+ :func:`zope.interface.alsoProvides`. Any extra keywords passed
+ in the ``kw`` argumnent will be set as direct attributes of
+ the resource object.
+
+ .. note:: For backwards compatibility purposes, this class can also
+ be imported as :class:`pyramid.testing.DummyModel`.
+
+ """
+ self.__name__ = __name__
+ self.__parent__ = __parent__
+ if __provides__ is not None:
+ alsoProvides(self, __provides__)
+ self.kw = kw
+ self.__dict__.update(**kw)
+ self.subs = {}
+
+ def __setitem__(self, name, val):
+ """ When the ``__setitem__`` method is called, the object
+ passed in as ``val`` will be decorated with a ``__parent__``
+ attribute pointing at the dummy resource and a ``__name__``
+ attribute that is the value of ``name``. The value will then
+ be returned when dummy resource's ``__getitem__`` is called with
+ the name ``name```."""
+ val.__name__ = name
+ val.__parent__ = self
+ self.subs[name] = val
+
+ def __getitem__(self, name):
+ """ Return a named subobject (see ``__setitem__``)"""
+ ob = self.subs[name]
+ return ob
+
+ def __delitem__(self, name):
+ del self.subs[name]
+
+ def get(self, name, default=None):
+ return self.subs.get(name, default)
+
+ def values(self):
+ """ Return the values set by __setitem__ """
+ return self.subs.values()
+
+ def items(self):
+ """ Return the items set by __setitem__ """
+ return self.subs.items()
+
+ def keys(self):
+ """ Return the keys set by __setitem__ """
+ return self.subs.keys()
+
+ __iter__ = keys
+
+ def __nonzero__(self):
+ return True
+
+ __bool__ = __nonzero__
+
+ def __len__(self):
+ return len(self.subs)
+
+ def __contains__(self, name):
+ return name in self.subs
+
+ def clone(self, __name__=_marker, __parent__=_marker, **kw):
+ """ Create a clone of the resource object. If ``__name__`` or
+ ``__parent__`` arguments are passed, use these values to
+ override the existing ``__name__`` or ``__parent__`` of the
+ resource. If any extra keyword args are passed in via the ``kw``
+ argument, use these keywords to add to or override existing
+ resource keywords (attributes)."""
+ oldkw = self.kw.copy()
+ oldkw.update(kw)
+ inst = self.__class__(self.__name__, self.__parent__, **oldkw)
+ inst.subs = copy.deepcopy(self.subs)
+ if __name__ is not _marker:
+ inst.__name__ = __name__
+ if __parent__ is not _marker:
+ inst.__parent__ = __parent__
+ return inst
+
+DummyModel = DummyResource # b/w compat (forever)
+
+@implementer(ISession)
+class DummySession(dict):
+ created = None
+ new = True
+ def changed(self):
+ pass
+
+ def invalidate(self):
+ self.clear()
+
+ def flash(self, msg, queue='', allow_duplicate=True):
+ storage = self.setdefault('_f_' + queue, [])
+ if allow_duplicate or (msg not in storage):
+ storage.append(msg)
+
+ def pop_flash(self, queue=''):
+ storage = self.pop('_f_' + queue, [])
+ return storage
+
+ def peek_flash(self, queue=''):
+ storage = self.get('_f_' + queue, [])
+ return storage
+
+ def new_csrf_token(self):
+ token = text_('0123456789012345678901234567890123456789')
+ self['_csrft_'] = token
+ return token
+
+ def get_csrf_token(self):
+ token = self.get('_csrft_', None)
+ if token is None:
+ token = self.new_csrf_token()
+ return token
+
+@implementer(IRequest)
+class DummyRequest(
+ URLMethodsMixin,
+ CallbackMethodsMixin,
+ InstancePropertyMixin,
+ LocalizerRequestMixin,
+ AuthenticationAPIMixin,
+ AuthorizationAPIMixin,
+ ViewMethodsMixin,
+ ):
+ """ A DummyRequest object (incompletely) imitates a :term:`request` object.
+
+ The ``params``, ``environ``, ``headers``, ``path``, and
+ ``cookies`` arguments correspond to their :term:`WebOb`
+ equivalents.
+
+ The ``post`` argument, if passed, populates the request's
+ ``POST`` attribute, but *not* ``params``, in order to allow testing
+ that the app accepts data for a given view only from POST requests.
+ This argument also sets ``self.method`` to "POST".
+
+ Extra keyword arguments are assigned as attributes of the request
+ itself.
+
+ Note that DummyRequest does not have complete fidelity with a "real"
+ request. For example, by default, the DummyRequest ``GET`` and ``POST``
+ attributes are of type ``dict``, unlike a normal Request's GET and POST,
+ which are of type ``MultiDict``. If your code uses the features of
+ MultiDict, you should either use a real :class:`pyramid.request.Request`
+ or adapt your DummyRequest by replacing the attributes with ``MultiDict``
+ instances.
+
+ Other similar incompatibilities exist. If you need all the features of
+ a Request, use the :class:`pyramid.request.Request` class itself rather
+ than this class while writing tests.
+ """
+ method = 'GET'
+ application_url = 'http://example.com'
+ host = 'example.com:80'
+ domain = 'example.com'
+ content_length = 0
+ query_string = ''
+ charset = 'UTF-8'
+ script_name = ''
+ _registry = None
+ request_iface = IRequest
+
+ def __init__(self, params=None, environ=None, headers=None, path='/',
+ cookies=None, post=None, **kw):
+ if environ is None:
+ environ = {}
+ if params is None:
+ params = {}
+ if headers is None:
+ headers = {}
+ if cookies is None:
+ cookies = {}
+ self.environ = environ
+ self.headers = headers
+ self.params = params
+ self.cookies = cookies
+ self.matchdict = {}
+ self.GET = params
+ if post is not None:
+ self.method = 'POST'
+ self.POST = post
+ else:
+ self.POST = params
+ self.host_url = self.application_url
+ self.path_url = self.application_url
+ self.url = self.application_url
+ self.path = path
+ self.path_info = path
+ self.script_name = ''
+ self.path_qs = ''
+ self.body = ''
+ self.view_name = ''
+ self.subpath = ()
+ self.traversed = ()
+ self.virtual_root_path = ()
+ self.context = None
+ self.root = None
+ self.virtual_root = None
+ self.marshalled = params # repoze.monty
+ self.session = DummySession()
+ self.__dict__.update(kw)
+
+ def _get_registry(self):
+ if self._registry is None:
+ return get_current_registry()
+ return self._registry
+
+ def _set_registry(self, registry):
+ self._registry = registry
+
+ def _del_registry(self):
+ self._registry = None
+
+ registry = property(_get_registry, _set_registry, _del_registry)
+
+ @reify
+ def response(self):
+ f = _get_response_factory(self.registry)
+ return f(self)
+
+have_zca = True
+
+
+def setUp(registry=None, request=None, hook_zca=True, autocommit=True,
+ settings=None, package=None):
+ """
+ Set :app:`Pyramid` registry and request thread locals for the
+ duration of a single unit test.
+
+ Use this function in the ``setUp`` method of a unittest test case
+ which directly or indirectly uses:
+
+ - any method of the :class:`pyramid.config.Configurator`
+ object returned by this function.
+
+ - the :func:`pyramid.threadlocal.get_current_registry` or
+ :func:`pyramid.threadlocal.get_current_request` functions.
+
+ If you use the ``get_current_*`` functions (or call :app:`Pyramid` code
+ that uses these functions) without calling ``setUp``,
+ :func:`pyramid.threadlocal.get_current_registry` will return a *global*
+ :term:`application registry`, which may cause unit tests to not be
+ isolated with respect to registrations they perform.
+
+ If the ``registry`` argument is ``None``, a new empty
+ :term:`application registry` will be created (an instance of the
+ :class:`pyramid.registry.Registry` class). If the ``registry``
+ argument is not ``None``, the value passed in should be an
+ instance of the :class:`pyramid.registry.Registry` class or a
+ suitable testing analogue.
+
+ After ``setUp`` is finished, the registry returned by the
+ :func:`pyramid.threadlocal.get_current_registry` function will
+ be the passed (or constructed) registry until
+ :func:`pyramid.testing.tearDown` is called (or
+ :func:`pyramid.testing.setUp` is called again) .
+
+ If the ``hook_zca`` argument is ``True``, ``setUp`` will attempt
+ to perform the operation ``zope.component.getSiteManager.sethook(
+ pyramid.threadlocal.get_current_registry)``, which will cause
+ the :term:`Zope Component Architecture` global API
+ (e.g. :func:`zope.component.getSiteManager`,
+ :func:`zope.component.getAdapter`, and so on) to use the registry
+ constructed by ``setUp`` as the value it returns from
+ :func:`zope.component.getSiteManager`. If the
+ :mod:`zope.component` package cannot be imported, or if
+ ``hook_zca`` is ``False``, the hook will not be set.
+
+ If ``settings`` is not ``None``, it must be a dictionary representing the
+ values passed to a Configurator as its ``settings=`` argument.
+
+ If ``package`` is ``None`` it will be set to the caller's package. The
+ ``package`` setting in the :class:`pyramid.config.Configurator` will
+ affect any relative imports made via
+ :meth:`pyramid.config.Configurator.include` or
+ :meth:`pyramid.config.Configurator.maybe_dotted`.
+
+ This function returns an instance of the
+ :class:`pyramid.config.Configurator` class, which can be
+ used for further configuration to set up an environment suitable
+ for a unit or integration test. The ``registry`` attribute
+ attached to the Configurator instance represents the 'current'
+ :term:`application registry`; the same registry will be returned
+ by :func:`pyramid.threadlocal.get_current_registry` during the
+ execution of the test.
+ """
+ manager.clear()
+ if registry is None:
+ registry = Registry('testing')
+ if package is None:
+ package = caller_package()
+ config = Configurator(registry=registry, autocommit=autocommit,
+ package=package)
+ if settings is None:
+ settings = {}
+ if getattr(registry, 'settings', None) is None:
+ config._set_settings(settings)
+ if hasattr(registry, 'registerUtility'):
+ # Sometimes nose calls us with a non-registry object because
+ # it thinks this function is module test setup. Likewise,
+ # someone may be passing us an esoteric "dummy" registry, and
+ # the below won't succeed if it doesn't have a registerUtility
+ # method.
+ config.add_default_response_adapters()
+ config.add_default_renderers()
+ config.add_default_accept_view_order()
+ config.add_default_view_predicates()
+ config.add_default_view_derivers()
+ config.add_default_route_predicates()
+ config.add_default_tweens()
+ config.add_default_security()
+ config.commit()
+ global have_zca
+ try:
+ have_zca and hook_zca and config.hook_zca()
+ except ImportError: # pragma: no cover
+ # (dont choke on not being able to import z.component)
+ have_zca = False
+ config.begin(request=request)
+ return config
+
+def tearDown(unhook_zca=True):
+ """Undo the effects of :func:`pyramid.testing.setUp`. Use this
+ function in the ``tearDown`` method of a unit test that uses
+ :func:`pyramid.testing.setUp` in its ``setUp`` method.
+
+ If the ``unhook_zca`` argument is ``True`` (the default), call
+ :func:`zope.component.getSiteManager.reset`. This undoes the
+ action of :func:`pyramid.testing.setUp` when called with the
+ argument ``hook_zca=True``. If :mod:`zope.component` cannot be
+ imported, ``unhook_zca`` is set to ``False``.
+ """
+ global have_zca
+ if unhook_zca and have_zca:
+ try:
+ from zope.component import getSiteManager
+ getSiteManager.reset()
+ except ImportError: # pragma: no cover
+ have_zca = False
+ info = manager.pop()
+ manager.clear()
+ if info is not None:
+ registry = info['registry']
+ if hasattr(registry, '__init__') and hasattr(registry, '__name__'):
+ try:
+ registry.__init__(registry.__name__)
+ except TypeError:
+ # calling __init__ is largely for the benefit of
+ # people who want to use the global ZCA registry;
+ # however maybe somebody's using a registry we don't
+ # understand, let's not blow up
+ pass
+
+def cleanUp(*arg, **kw):
+ """ An alias for :func:`pyramid.testing.setUp`. """
+ package = kw.get('package', None)
+ if package is None:
+ package = caller_package()
+ kw['package'] = package
+ return setUp(*arg, **kw)
+
+class DummyRendererFactory(object):
+ """ Registered by
+ :meth:`pyramid.config.Configurator.testing_add_renderer` as
+ a dummy renderer factory. The indecision about what to use as a
+ key (a spec vs. a relative name) is caused by test suites in the
+ wild believing they can register either. The ``factory`` argument
+ passed to this constructor is usually the *real* template renderer
+ factory, found when ``testing_add_renderer`` is called."""
+ def __init__(self, name, factory):
+ self.name = name
+ self.factory = factory # the "real" renderer factory reg'd previously
+ self.renderers = {}
+
+ def add(self, spec, renderer):
+ self.renderers[spec] = renderer
+ if ':' in spec:
+ package, relative = spec.split(':', 1)
+ self.renderers[relative] = renderer
+
+ def __call__(self, info):
+ spec = info.name
+ renderer = self.renderers.get(spec)
+ if renderer is None:
+ if ':' in spec:
+ package, relative = spec.split(':', 1)
+ renderer = self.renderers.get(relative)
+ if renderer is None:
+ if self.factory:
+ renderer = self.factory(info)
+ else:
+ raise KeyError('No testing renderer registered for %r' %
+ spec)
+ return renderer
+
+
+class MockTemplate(object):
+ def __init__(self, response):
+ self._received = {}
+ self.response = response
+ def __getattr__(self, attrname):
+ return self
+ def __getitem__(self, attrname):
+ return self
+ def __call__(self, *arg, **kw):
+ self._received.update(kw)
+ return self.response
+
+def skip_on(*platforms): # pragma: no cover
+ skip = False
+ for platform in platforms:
+ if skip_on.os_name.startswith(platform):
+ skip = True
+ if platform == 'pypy' and PYPY:
+ skip = True
+ if platform == 'py3' and PY3:
+ skip = True
+
+ def decorator(func):
+ if isinstance(func, class_types):
+ if skip:
+ return None
+ else:
+ return func
+ else:
+ def wrapper(*args, **kw):
+ if skip:
+ return
+ return func(*args, **kw)
+ wrapper.__name__ = func.__name__
+ wrapper.__doc__ = func.__doc__
+ return wrapper
+ return decorator
+skip_on.os_name = os.name # for testing
+
+@contextmanager
+def testConfig(registry=None,
+ request=None,
+ hook_zca=True,
+ autocommit=True,
+ settings=None):
+ """Returns a context manager for test set up.
+
+ This context manager calls :func:`pyramid.testing.setUp` when
+ entering and :func:`pyramid.testing.tearDown` when exiting.
+
+ All arguments are passed directly to :func:`pyramid.testing.setUp`.
+ If the ZCA is hooked, it will always be un-hooked in tearDown.
+
+ This context manager allows you to write test code like this:
+
+ .. code-block:: python
+ :linenos:
+
+ with testConfig() as config:
+ config.add_route('bar', '/bar/{id}')
+ req = DummyRequest()
+ resp = myview(req)
+ """
+ config = setUp(registry=registry,
+ request=request,
+ hook_zca=hook_zca,
+ autocommit=autocommit,
+ settings=settings)
+ try:
+ yield config
+ finally:
+ tearDown(unhook_zca=hook_zca)
diff --git a/src/pyramid/threadlocal.py b/src/pyramid/threadlocal.py
new file mode 100644
index 000000000..e8f825715
--- /dev/null
+++ b/src/pyramid/threadlocal.py
@@ -0,0 +1,83 @@
+import threading
+
+from pyramid.registry import global_registry
+
+class ThreadLocalManager(threading.local):
+ def __init__(self, default=None):
+ # http://code.google.com/p/google-app-engine-django/issues/detail?id=119
+ # we *must* use a keyword argument for ``default`` here instead
+ # of a positional argument to work around a bug in the
+ # implementation of _threading_local.local in Python, which is
+ # used by GAE instead of _thread.local
+ self.stack = []
+ self.default = default
+
+ def push(self, info):
+ self.stack.append(info)
+
+ set = push # b/c
+
+ def pop(self):
+ if self.stack:
+ return self.stack.pop()
+
+ def get(self):
+ try:
+ return self.stack[-1]
+ except IndexError:
+ return self.default()
+
+ def clear(self):
+ self.stack[:] = []
+
+def defaults():
+ return {'request': None, 'registry': global_registry}
+
+manager = ThreadLocalManager(default=defaults)
+
+def get_current_request():
+ """
+ Return the currently active request or ``None`` if no request
+ is currently active.
+
+ This function should be used *extremely sparingly*, usually only
+ in unit testing code. It's almost always usually a mistake to use
+ ``get_current_request`` outside a testing context because its
+ usage makes it possible to write code that can be neither easily
+ tested nor scripted.
+
+ """
+ return manager.get()['request']
+
+def get_current_registry(context=None): # context required by getSiteManager API
+ """
+ Return the currently active :term:`application registry` or the
+ global application registry if no request is currently active.
+
+ This function should be used *extremely sparingly*, usually only
+ in unit testing code. It's almost always usually a mistake to use
+ ``get_current_registry`` outside a testing context because its
+ usage makes it possible to write code that can be neither easily
+ tested nor scripted.
+
+ """
+ return manager.get()['registry']
+
+class RequestContext(object):
+ def __init__(self, request):
+ self.request = request
+
+ def begin(self):
+ request = self.request
+ registry = request.registry
+ manager.push({'registry': registry, 'request': request})
+ return request
+
+ def end(self):
+ manager.pop()
+
+ def __enter__(self):
+ return self.begin()
+
+ def __exit__(self, *args):
+ self.end()
diff --git a/src/pyramid/traversal.py b/src/pyramid/traversal.py
new file mode 100644
index 000000000..d8f4690fd
--- /dev/null
+++ b/src/pyramid/traversal.py
@@ -0,0 +1,760 @@
+from zope.interface import implementer
+from zope.interface.interfaces import IInterface
+
+from pyramid.interfaces import (
+ IResourceURL,
+ IRequestFactory,
+ ITraverser,
+ VH_ROOT_KEY,
+ )
+
+from pyramid.compat import (
+ PY2,
+ native_,
+ text_,
+ ascii_native_,
+ text_type,
+ binary_type,
+ is_nonstr_iter,
+ decode_path_info,
+ unquote_bytes_to_wsgi,
+ lru_cache,
+ )
+
+from pyramid.encode import url_quote
+from pyramid.exceptions import URLDecodeError
+from pyramid.location import lineage
+from pyramid.threadlocal import get_current_registry
+
+PATH_SEGMENT_SAFE = "~!$&'()*+,;=:@" # from webob
+PATH_SAFE = PATH_SEGMENT_SAFE + "/"
+
+empty = text_('')
+
+def find_root(resource):
+ """ Find the root node in the resource tree to which ``resource``
+ belongs. Note that ``resource`` should be :term:`location`-aware.
+ Note that the root resource is available in the request object by
+ accessing the ``request.root`` attribute.
+ """
+ for location in lineage(resource):
+ if location.__parent__ is None:
+ resource = location
+ break
+ return resource
+
+def find_resource(resource, path):
+ """ Given a resource object and a string or tuple representing a path
+ (such as the return value of :func:`pyramid.traversal.resource_path` or
+ :func:`pyramid.traversal.resource_path_tuple`), return a resource in this
+ application's resource tree at the specified path. The resource passed
+ in *must* be :term:`location`-aware. If the path cannot be resolved (if
+ the respective node in the resource tree does not exist), a
+ :exc:`KeyError` will be raised.
+
+ This function is the logical inverse of
+ :func:`pyramid.traversal.resource_path` and
+ :func:`pyramid.traversal.resource_path_tuple`; it can resolve any
+ path string or tuple generated by either of those functions.
+
+ Rules for passing a *string* as the ``path`` argument: if the
+ first character in the path string is the ``/``
+ character, the path is considered absolute and the resource tree
+ traversal will start at the root resource. If the first character
+ of the path string is *not* the ``/`` character, the path is
+ considered relative and resource tree traversal will begin at the resource
+ object supplied to the function as the ``resource`` argument. If an
+ empty string is passed as ``path``, the ``resource`` passed in will
+ be returned. Resource path strings must be escaped in the following
+ manner: each Unicode path segment must be encoded as UTF-8 and as
+ each path segment must escaped via Python's :mod:`urllib.quote`.
+ For example, ``/path/to%20the/La%20Pe%C3%B1a`` (absolute) or
+ ``to%20the/La%20Pe%C3%B1a`` (relative). The
+ :func:`pyramid.traversal.resource_path` function generates strings
+ which follow these rules (albeit only absolute ones).
+
+ Rules for passing *text* (Unicode) as the ``path`` argument are the same
+ as those for a string. In particular, the text may not have any nonascii
+ characters in it.
+
+ Rules for passing a *tuple* as the ``path`` argument: if the first
+ element in the path tuple is the empty string (for example ``('',
+ 'a', 'b', 'c')``, the path is considered absolute and the resource tree
+ traversal will start at the resource tree root object. If the first
+ element in the path tuple is not the empty string (for example
+ ``('a', 'b', 'c')``), the path is considered relative and resource tree
+ traversal will begin at the resource object supplied to the function
+ as the ``resource`` argument. If an empty sequence is passed as
+ ``path``, the ``resource`` passed in itself will be returned. No
+ URL-quoting or UTF-8-encoding of individual path segments within
+ the tuple is required (each segment may be any string or unicode
+ object representing a resource name). Resource path tuples generated by
+ :func:`pyramid.traversal.resource_path_tuple` can always be
+ resolved by ``find_resource``.
+ """
+ if isinstance(path, text_type):
+ path = ascii_native_(path)
+ D = traverse(resource, path)
+ view_name = D['view_name']
+ context = D['context']
+ if view_name:
+ raise KeyError('%r has no subelement %s' % (context, view_name))
+ return context
+
+find_model = find_resource # b/w compat (forever)
+
+def find_interface(resource, class_or_interface):
+ """
+ Return the first resource found in the :term:`lineage` of ``resource``
+ which, a) if ``class_or_interface`` is a Python class object, is an
+ instance of the class or any subclass of that class or b) if
+ ``class_or_interface`` is a :term:`interface`, provides the specified
+ interface. Return ``None`` if no resource providing ``interface_or_class``
+ can be found in the lineage. The ``resource`` passed in *must* be
+ :term:`location`-aware.
+ """
+ if IInterface.providedBy(class_or_interface):
+ test = class_or_interface.providedBy
+ else:
+ test = lambda arg: isinstance(arg, class_or_interface)
+ for location in lineage(resource):
+ if test(location):
+ return location
+
+def resource_path(resource, *elements):
+ """ Return a string object representing the absolute physical path of the
+ resource object based on its position in the resource tree, e.g
+ ``/foo/bar``. Any positional arguments passed in as ``elements`` will be
+ appended as path segments to the end of the resource path. For instance,
+ if the resource's path is ``/foo/bar`` and ``elements`` equals ``('a',
+ 'b')``, the returned string will be ``/foo/bar/a/b``. The first
+ character in the string will always be the ``/`` character (a leading
+ ``/`` character in a path string represents that the path is absolute).
+
+ Resource path strings returned will be escaped in the following
+ manner: each unicode path segment will be encoded as UTF-8 and
+ each path segment will be escaped via Python's :mod:`urllib.quote`.
+ For example, ``/path/to%20the/La%20Pe%C3%B1a``.
+
+ This function is a logical inverse of
+ :mod:`pyramid.traversal.find_resource`: it can be used to generate
+ path references that can later be resolved via that function.
+
+ The ``resource`` passed in *must* be :term:`location`-aware.
+
+ .. note::
+
+ Each segment in the path string returned will use the ``__name__``
+ attribute of the resource it represents within the resource tree. Each
+ of these segments *should* be a unicode or string object (as per the
+ contract of :term:`location`-awareness). However, no conversion or
+ safety checking of resource names is performed. For instance, if one of
+ the resources in your tree has a ``__name__`` which (by error) is a
+ dictionary, the :func:`pyramid.traversal.resource_path` function will
+ attempt to append it to a string and it will cause a
+ :exc:`pyramid.exceptions.URLDecodeError`.
+
+ .. note::
+
+ The :term:`root` resource *must* have a ``__name__`` attribute with a
+ value of either ``None`` or the empty string for paths to be generated
+ properly. If the root resource has a non-null ``__name__`` attribute,
+ its name will be prepended to the generated path rather than a single
+ leading '/' character.
+ """
+ # joining strings is a bit expensive so we delegate to a function
+ # which caches the joined result for us
+ return _join_path_tuple(resource_path_tuple(resource, *elements))
+
+model_path = resource_path # b/w compat (forever)
+
+def traverse(resource, path):
+ """Given a resource object as ``resource`` and a string or tuple
+ representing a path as ``path`` (such as the return value of
+ :func:`pyramid.traversal.resource_path` or
+ :func:`pyramid.traversal.resource_path_tuple` or the value of
+ ``request.environ['PATH_INFO']``), return a dictionary with the
+ keys ``context``, ``root``, ``view_name``, ``subpath``,
+ ``traversed``, ``virtual_root``, and ``virtual_root_path``.
+
+ A definition of each value in the returned dictionary:
+
+ - ``context``: The :term:`context` (a :term:`resource` object) found
+ via traversal or url dispatch. If the ``path`` passed in is the
+ empty string, the value of the ``resource`` argument passed to this
+ function is returned.
+
+ - ``root``: The resource object at which :term:`traversal` begins.
+ If the ``resource`` passed in was found via url dispatch or if the
+ ``path`` passed in was relative (non-absolute), the value of the
+ ``resource`` argument passed to this function is returned.
+
+ - ``view_name``: The :term:`view name` found during
+ :term:`traversal` or :term:`url dispatch`; if the ``resource`` was
+ found via traversal, this is usually a representation of the
+ path segment which directly follows the path to the ``context``
+ in the ``path``. The ``view_name`` will be a Unicode object or
+ the empty string. The ``view_name`` will be the empty string if
+ there is no element which follows the ``context`` path. An
+ example: if the path passed is ``/foo/bar``, and a resource
+ object is found at ``/foo`` (but not at ``/foo/bar``), the 'view
+ name' will be ``u'bar'``. If the ``resource`` was found via
+ urldispatch, the view_name will be the name the route found was
+ registered with.
+
+ - ``subpath``: For a ``resource`` found via :term:`traversal`, this
+ is a sequence of path segments found in the ``path`` that follow
+ the ``view_name`` (if any). Each of these items is a Unicode
+ object. If no path segments follow the ``view_name``, the
+ subpath will be the empty sequence. An example: if the path
+ passed is ``/foo/bar/baz/buz``, and a resource object is found at
+ ``/foo`` (but not ``/foo/bar``), the 'view name' will be
+ ``u'bar'`` and the :term:`subpath` will be ``[u'baz', u'buz']``.
+ For a ``resource`` found via url dispatch, the subpath will be a
+ sequence of values discerned from ``*subpath`` in the route
+ pattern matched or the empty sequence.
+
+ - ``traversed``: The sequence of path elements traversed from the
+ root to find the ``context`` object during :term:`traversal`.
+ Each of these items is a Unicode object. If no path segments
+ were traversed to find the ``context`` object (e.g. if the
+ ``path`` provided is the empty string), the ``traversed`` value
+ will be the empty sequence. If the ``resource`` is a resource found
+ via :term:`url dispatch`, traversed will be None.
+
+ - ``virtual_root``: A resource object representing the 'virtual' root
+ of the resource tree being traversed during :term:`traversal`.
+ See :ref:`vhosting_chapter` for a definition of the virtual root
+ object. If no virtual hosting is in effect, and the ``path``
+ passed in was absolute, the ``virtual_root`` will be the
+ *physical* root resource object (the object at which :term:`traversal`
+ begins). If the ``resource`` passed in was found via :term:`URL
+ dispatch` or if the ``path`` passed in was relative, the
+ ``virtual_root`` will always equal the ``root`` object (the
+ resource passed in).
+
+ - ``virtual_root_path`` -- If :term:`traversal` was used to find
+ the ``resource``, this will be the sequence of path elements
+ traversed to find the ``virtual_root`` resource. Each of these
+ items is a Unicode object. If no path segments were traversed
+ to find the ``virtual_root`` resource (e.g. if virtual hosting is
+ not in effect), the ``traversed`` value will be the empty list.
+ If url dispatch was used to find the ``resource``, this will be
+ ``None``.
+
+ If the path cannot be resolved, a :exc:`KeyError` will be raised.
+
+ Rules for passing a *string* as the ``path`` argument: if the
+ first character in the path string is the with the ``/``
+ character, the path will considered absolute and the resource tree
+ traversal will start at the root resource. If the first character
+ of the path string is *not* the ``/`` character, the path is
+ considered relative and resource tree traversal will begin at the resource
+ object supplied to the function as the ``resource`` argument. If an
+ empty string is passed as ``path``, the ``resource`` passed in will
+ be returned. Resource path strings must be escaped in the following
+ manner: each Unicode path segment must be encoded as UTF-8 and
+ each path segment must escaped via Python's :mod:`urllib.quote`.
+ For example, ``/path/to%20the/La%20Pe%C3%B1a`` (absolute) or
+ ``to%20the/La%20Pe%C3%B1a`` (relative). The
+ :func:`pyramid.traversal.resource_path` function generates strings
+ which follow these rules (albeit only absolute ones).
+
+ Rules for passing a *tuple* as the ``path`` argument: if the first
+ element in the path tuple is the empty string (for example ``('',
+ 'a', 'b', 'c')``, the path is considered absolute and the resource tree
+ traversal will start at the resource tree root object. If the first
+ element in the path tuple is not the empty string (for example
+ ``('a', 'b', 'c')``), the path is considered relative and resource tree
+ traversal will begin at the resource object supplied to the function
+ as the ``resource`` argument. If an empty sequence is passed as
+ ``path``, the ``resource`` passed in itself will be returned. No
+ URL-quoting or UTF-8-encoding of individual path segments within
+ the tuple is required (each segment may be any string or unicode
+ object representing a resource name).
+
+ Explanation of the conversion of ``path`` segment values to
+ Unicode during traversal: Each segment is URL-unquoted, and
+ decoded into Unicode. Each segment is assumed to be encoded using
+ the UTF-8 encoding (or a subset, such as ASCII); a
+ :exc:`pyramid.exceptions.URLDecodeError` is raised if a segment
+ cannot be decoded. If a segment name is empty or if it is ``.``,
+ it is ignored. If a segment name is ``..``, the previous segment
+ is deleted, and the ``..`` is ignored. As a result of this
+ process, the return values ``view_name``, each element in the
+ ``subpath``, each element in ``traversed``, and each element in
+ the ``virtual_root_path`` will be Unicode as opposed to a string,
+ and will be URL-decoded.
+ """
+
+ if is_nonstr_iter(path):
+ # the traverser factory expects PATH_INFO to be a string, not
+ # unicode and it expects path segments to be utf-8 and
+ # urlencoded (it's the same traverser which accepts PATH_INFO
+ # from user agents; user agents always send strings).
+ if path:
+ path = _join_path_tuple(tuple(path))
+ else:
+ path = ''
+
+ # The user is supposed to pass us a string object, never Unicode. In
+ # practice, however, users indeed pass Unicode to this API. If they do
+ # pass a Unicode object, its data *must* be entirely encodeable to ASCII,
+ # so we encode it here as a convenience to the user and to prevent
+ # second-order failures from cropping up (all failures will occur at this
+ # step rather than later down the line as the result of calling
+ # ``traversal_path``).
+
+ path = ascii_native_(path)
+
+ if path and path[0] == '/':
+ resource = find_root(resource)
+
+ reg = get_current_registry()
+
+ request_factory = reg.queryUtility(IRequestFactory)
+ if request_factory is None:
+ from pyramid.request import Request # avoid circdep
+ request_factory = Request
+
+ request = request_factory.blank(path)
+ request.registry = reg
+ traverser = reg.queryAdapter(resource, ITraverser)
+ if traverser is None:
+ traverser = ResourceTreeTraverser(resource)
+
+ return traverser(request)
+
+def resource_path_tuple(resource, *elements):
+ """
+ Return a tuple representing the absolute physical path of the
+ ``resource`` object based on its position in a resource tree, e.g
+ ``('', 'foo', 'bar')``. Any positional arguments passed in as
+ ``elements`` will be appended as elements in the tuple
+ representing the resource path. For instance, if the resource's
+ path is ``('', 'foo', 'bar')`` and elements equals ``('a', 'b')``,
+ the returned tuple will be ``('', 'foo', 'bar', 'a', 'b')``. The
+ first element of this tuple will always be the empty string (a
+ leading empty string element in a path tuple represents that the
+ path is absolute).
+
+ This function is a logical inverse of
+ :func:`pyramid.traversal.find_resource`: it can be used to
+ generate path references that can later be resolved by that function.
+
+ The ``resource`` passed in *must* be :term:`location`-aware.
+
+ .. note::
+
+ Each segment in the path tuple returned will equal the ``__name__``
+ attribute of the resource it represents within the resource tree. Each
+ of these segments *should* be a unicode or string object (as per the
+ contract of :term:`location`-awareness). However, no conversion or
+ safety checking of resource names is performed. For instance, if one of
+ the resources in your tree has a ``__name__`` which (by error) is a
+ dictionary, that dictionary will be placed in the path tuple; no warning
+ or error will be given.
+
+ .. note::
+
+ The :term:`root` resource *must* have a ``__name__`` attribute with a
+ value of either ``None`` or the empty string for path tuples to be
+ generated properly. If the root resource has a non-null ``__name__``
+ attribute, its name will be the first element in the generated path tuple
+ rather than the empty string.
+ """
+ return tuple(_resource_path_list(resource, *elements))
+
+model_path_tuple = resource_path_tuple # b/w compat (forever)
+
+def _resource_path_list(resource, *elements):
+ """ Implementation detail shared by resource_path and resource_path_tuple"""
+ path = [loc.__name__ or '' for loc in lineage(resource)]
+ path.reverse()
+ path.extend(elements)
+ return path
+
+_model_path_list = _resource_path_list # b/w compat, not an API
+
+def virtual_root(resource, request):
+ """
+ Provided any :term:`resource` and a :term:`request` object, return
+ the resource object representing the :term:`virtual root` of the
+ current :term:`request`. Using a virtual root in a
+ :term:`traversal` -based :app:`Pyramid` application permits
+ rooting. For example, the resource at the traversal path ``/cms`` will
+ be found at ``http://example.com/`` instead of rooting it at
+ ``http://example.com/cms/``.
+
+ If the ``resource`` passed in is a context obtained via
+ :term:`traversal`, and if the ``HTTP_X_VHM_ROOT`` key is in the
+ WSGI environment, the value of this key will be treated as a
+ 'virtual root path': the :func:`pyramid.traversal.find_resource`
+ API will be used to find the virtual root resource using this path;
+ if the resource is found, it will be returned. If the
+ ``HTTP_X_VHM_ROOT`` key is not present in the WSGI environment,
+ the physical :term:`root` of the resource tree will be returned instead.
+
+ Virtual roots are not useful at all in applications that use
+ :term:`URL dispatch`. Contexts obtained via URL dispatch don't
+ really support being virtually rooted (each URL dispatch context
+ is both its own physical and virtual root). However if this API
+ is called with a ``resource`` argument which is a context obtained
+ via URL dispatch, the resource passed in will be returned
+ unconditionally."""
+ try:
+ reg = request.registry
+ except AttributeError:
+ reg = get_current_registry()
+ url_adapter = reg.queryMultiAdapter((resource, request), IResourceURL)
+ if url_adapter is None:
+ url_adapter = ResourceURL(resource, request)
+
+ vpath, rpath = url_adapter.virtual_path, url_adapter.physical_path
+ if rpath != vpath and rpath.endswith(vpath):
+ vroot_path = rpath[:-len(vpath)]
+ return find_resource(resource, vroot_path)
+
+ try:
+ return request.root
+ except AttributeError:
+ return find_root(resource)
+
+def traversal_path(path):
+ """ Variant of :func:`pyramid.traversal.traversal_path_info` suitable for
+ decoding paths that are URL-encoded.
+
+ If this function is passed a Unicode object instead of a sequence of
+ bytes as ``path``, that Unicode object *must* directly encodeable to
+ ASCII. For example, u'/foo' will work but u'/<unprintable unicode>' (a
+ Unicode object with characters that cannot be encoded to ascii) will
+ not. A :exc:`UnicodeEncodeError` will be raised if the Unicode cannot be
+ encoded directly to ASCII.
+ """
+ if isinstance(path, text_type):
+ # must not possess characters outside ascii
+ path = path.encode('ascii')
+ # we unquote this path exactly like a PEP 3333 server would
+ path = unquote_bytes_to_wsgi(path) # result will be a native string
+ return traversal_path_info(path) # result will be a tuple of unicode
+
+@lru_cache(1000)
+def traversal_path_info(path):
+ """ Given``path``, return a tuple representing that path which can be
+ used to traverse a resource tree. ``path`` is assumed to be an
+ already-URL-decoded ``str`` type as if it had come to us from an upstream
+ WSGI server as the ``PATH_INFO`` environ variable.
+
+ The ``path`` is first decoded to from its WSGI representation to Unicode;
+ it is decoded differently depending on platform:
+
+ - On Python 2, ``path`` is decoded to Unicode from bytes using the UTF-8
+ decoding directly; a :exc:`pyramid.exc.URLDecodeError` is raised if a the
+ URL cannot be decoded.
+
+ - On Python 3, as per the PEP 3333 spec, ``path`` is first encoded to
+ bytes using the Latin-1 encoding; the resulting set of bytes is
+ subsequently decoded to text using the UTF-8 encoding; a
+ :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be
+ decoded.
+
+ The ``path`` is split on slashes, creating a list of segments. If a
+ segment name is empty or if it is ``.``, it is ignored. If a segment
+ name is ``..``, the previous segment is deleted, and the ``..`` is
+ ignored.
+
+ Examples:
+
+ ``/``
+
+ ()
+
+ ``/foo/bar/baz``
+
+ (u'foo', u'bar', u'baz')
+
+ ``foo/bar/baz``
+
+ (u'foo', u'bar', u'baz')
+
+ ``/foo/bar/baz/``
+
+ (u'foo', u'bar', u'baz')
+
+ ``/foo//bar//baz/``
+
+ (u'foo', u'bar', u'baz')
+
+ ``/foo/bar/baz/..``
+
+ (u'foo', u'bar')
+
+ ``/my%20archives/hello``
+
+ (u'my archives', u'hello')
+
+ ``/archives/La%20Pe%C3%B1a``
+
+ (u'archives', u'<unprintable unicode>')
+
+ .. note::
+
+ This function does not generate the same type of tuples that
+ :func:`pyramid.traversal.resource_path_tuple` does. In particular, the
+ leading empty string is not present in the tuple it returns, unlike tuples
+ returned by :func:`pyramid.traversal.resource_path_tuple`. As a result,
+ tuples generated by ``traversal_path`` are not resolveable by the
+ :func:`pyramid.traversal.find_resource` API. ``traversal_path`` is a
+ function mostly used by the internals of :app:`Pyramid` and by people
+ writing their own traversal machinery, as opposed to users writing
+ applications in :app:`Pyramid`.
+ """
+ try:
+ path = decode_path_info(path) # result will be Unicode
+ except UnicodeDecodeError as e:
+ raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason)
+ return split_path_info(path) # result will be tuple of Unicode
+
+@lru_cache(1000)
+def split_path_info(path):
+ # suitable for splitting an already-unquoted-already-decoded (unicode)
+ # path value
+ path = path.strip('/')
+ clean = []
+ for segment in path.split('/'):
+ if not segment or segment == '.':
+ continue
+ elif segment == '..':
+ if clean:
+ del clean[-1]
+ else:
+ clean.append(segment)
+ return tuple(clean)
+
+_segment_cache = {}
+
+quote_path_segment_doc = """ \
+Return a quoted representation of a 'path segment' (such as
+the string ``__name__`` attribute of a resource) as a string. If the
+``segment`` passed in is a unicode object, it is converted to a
+UTF-8 string, then it is URL-quoted using Python's
+``urllib.quote``. If the ``segment`` passed in is a string, it is
+URL-quoted using Python's :mod:`urllib.quote`. If the segment
+passed in is not a string or unicode object, an error will be
+raised. The return value of ``quote_path_segment`` is always a
+string, never Unicode.
+
+You may pass a string of characters that need not be encoded as
+the ``safe`` argument to this function. This corresponds to the
+``safe`` argument to :mod:`urllib.quote`.
+
+.. note::
+
+ The return value for each segment passed to this
+ function is cached in a module-scope dictionary for
+ speed: the cached version is returned when possible
+ rather than recomputing the quoted version. No cache
+ emptying is ever done for the lifetime of an
+ application, however. If you pass arbitrary
+ user-supplied strings to this function (as opposed to
+ some bounded set of values from a 'working set' known to
+ your application), it may become a memory leak.
+"""
+
+
+if PY2:
+ # special-case on Python 2 for speed? unchecked
+ def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE):
+ """ %s """ % quote_path_segment_doc
+ # The bit of this code that deals with ``_segment_cache`` is an
+ # optimization: we cache all the computation of URL path segments
+ # in this module-scope dictionary with the original string (or
+ # unicode value) as the key, so we can look it up later without
+ # needing to reencode or re-url-quote it
+ try:
+ return _segment_cache[(segment, safe)]
+ except KeyError:
+ if segment.__class__ is text_type: #isinstance slighly slower (~15%)
+ result = url_quote(segment.encode('utf-8'), safe)
+ else:
+ result = url_quote(str(segment), safe)
+ # we don't need a lock to mutate _segment_cache, as the below
+ # will generate exactly one Python bytecode (STORE_SUBSCR)
+ _segment_cache[(segment, safe)] = result
+ return result
+else:
+ def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE):
+ """ %s """ % quote_path_segment_doc
+ # The bit of this code that deals with ``_segment_cache`` is an
+ # optimization: we cache all the computation of URL path segments
+ # in this module-scope dictionary with the original string (or
+ # unicode value) as the key, so we can look it up later without
+ # needing to reencode or re-url-quote it
+ try:
+ return _segment_cache[(segment, safe)]
+ except KeyError:
+ if segment.__class__ not in (text_type, binary_type):
+ segment = str(segment)
+ result = url_quote(native_(segment, 'utf-8'), safe)
+ # we don't need a lock to mutate _segment_cache, as the below
+ # will generate exactly one Python bytecode (STORE_SUBSCR)
+ _segment_cache[(segment, safe)] = result
+ return result
+
+slash = text_('/')
+
+@implementer(ITraverser)
+class ResourceTreeTraverser(object):
+ """ A resource tree traverser that should be used (for speed) when
+ every resource in the tree supplies a ``__name__`` and
+ ``__parent__`` attribute (ie. every resource in the tree is
+ :term:`location` aware) ."""
+
+
+ VH_ROOT_KEY = VH_ROOT_KEY
+ VIEW_SELECTOR = '@@'
+
+ def __init__(self, root):
+ self.root = root
+
+ def __call__(self, request):
+ environ = request.environ
+ matchdict = request.matchdict
+
+ if matchdict is not None:
+
+ path = matchdict.get('traverse', slash) or slash
+ if is_nonstr_iter(path):
+ # this is a *traverse stararg (not a {traverse})
+ # routing has already decoded these elements, so we just
+ # need to join them
+ path = '/' + slash.join(path) or slash
+
+ subpath = matchdict.get('subpath', ())
+ if not is_nonstr_iter(subpath):
+ # this is not a *subpath stararg (just a {subpath})
+ # routing has already decoded this string, so we just need
+ # to split it
+ subpath = split_path_info(subpath)
+
+ else:
+ # this request did not match a route
+ subpath = ()
+ try:
+ # empty if mounted under a path in mod_wsgi, for example
+ path = request.path_info or slash
+ except KeyError:
+ # if environ['PATH_INFO'] is just not there
+ path = slash
+ except UnicodeDecodeError as e:
+ raise URLDecodeError(e.encoding, e.object, e.start, e.end,
+ e.reason)
+
+ if self.VH_ROOT_KEY in environ:
+ # HTTP_X_VHM_ROOT
+ vroot_path = decode_path_info(environ[self.VH_ROOT_KEY])
+ vroot_tuple = split_path_info(vroot_path)
+ vpath = vroot_path + path # both will (must) be unicode or asciistr
+ vroot_idx = len(vroot_tuple) - 1
+ else:
+ vroot_tuple = ()
+ vpath = path
+ vroot_idx = -1
+
+ root = self.root
+ ob = vroot = root
+
+ if vpath == slash: # invariant: vpath must not be empty
+ # prevent a call to traversal_path if we know it's going
+ # to return the empty tuple
+ vpath_tuple = ()
+ else:
+ # we do dead reckoning here via tuple slicing instead of
+ # pushing and popping temporary lists for speed purposes
+ # and this hurts readability; apologies
+ i = 0
+ view_selector = self.VIEW_SELECTOR
+ vpath_tuple = split_path_info(vpath)
+ for segment in vpath_tuple:
+ if segment[:2] == view_selector:
+ return {'context': ob,
+ 'view_name': segment[2:],
+ 'subpath': vpath_tuple[i + 1:],
+ 'traversed': vpath_tuple[:vroot_idx + i + 1],
+ 'virtual_root': vroot,
+ 'virtual_root_path': vroot_tuple,
+ 'root': root}
+ try:
+ getitem = ob.__getitem__
+ except AttributeError:
+ return {'context': ob,
+ 'view_name': segment,
+ 'subpath': vpath_tuple[i + 1:],
+ 'traversed': vpath_tuple[:vroot_idx + i + 1],
+ 'virtual_root': vroot,
+ 'virtual_root_path': vroot_tuple,
+ 'root': root}
+
+ try:
+ next = getitem(segment)
+ except KeyError:
+ return {'context': ob,
+ 'view_name': segment,
+ 'subpath': vpath_tuple[i + 1:],
+ 'traversed': vpath_tuple[:vroot_idx + i + 1],
+ 'virtual_root': vroot,
+ 'virtual_root_path': vroot_tuple,
+ 'root': root}
+ if i == vroot_idx:
+ vroot = next
+ ob = next
+ i += 1
+
+ return {'context':ob, 'view_name':empty, 'subpath':subpath,
+ 'traversed':vpath_tuple, 'virtual_root':vroot,
+ 'virtual_root_path':vroot_tuple, 'root':root}
+
+ModelGraphTraverser = ResourceTreeTraverser # b/w compat, not API, used in wild
+
+@implementer(IResourceURL)
+class ResourceURL(object):
+ VH_ROOT_KEY = VH_ROOT_KEY
+
+ def __init__(self, resource, request):
+ physical_path_tuple = resource_path_tuple(resource)
+ physical_path = _join_path_tuple(physical_path_tuple)
+
+ if physical_path_tuple != ('',):
+ physical_path_tuple = physical_path_tuple + ('',)
+ physical_path = physical_path + '/'
+
+ virtual_path = physical_path
+ virtual_path_tuple = physical_path_tuple
+
+ environ = request.environ
+ vroot_path = environ.get(self.VH_ROOT_KEY)
+
+ # if the physical path starts with the virtual root path, trim it out
+ # of the virtual path
+ if vroot_path is not None:
+ vroot_path = vroot_path.rstrip('/')
+ if vroot_path and physical_path.startswith(vroot_path):
+ vroot_path_tuple = tuple(vroot_path.split('/'))
+ numels = len(vroot_path_tuple)
+ virtual_path_tuple = ('',) + physical_path_tuple[numels:]
+ virtual_path = physical_path[len(vroot_path):]
+
+ self.virtual_path = virtual_path # IResourceURL attr
+ self.physical_path = physical_path # IResourceURL attr
+ self.virtual_path_tuple = virtual_path_tuple # IResourceURL attr (1.5)
+ self.physical_path_tuple = physical_path_tuple # IResourceURL attr (1.5)
+
+@lru_cache(1000)
+def _join_path_tuple(tuple):
+ return tuple and '/'.join([quote_path_segment(x) for x in tuple]) or '/'
+
+class DefaultRootFactory:
+ __parent__ = None
+ __name__ = None
+ def __init__(self, request):
+ pass
diff --git a/src/pyramid/tweens.py b/src/pyramid/tweens.py
new file mode 100644
index 000000000..740b6961c
--- /dev/null
+++ b/src/pyramid/tweens.py
@@ -0,0 +1,48 @@
+import sys
+
+from pyramid.compat import reraise
+from pyramid.httpexceptions import HTTPNotFound
+
+def _error_handler(request, exc):
+ # NOTE: we do not need to delete exc_info because this function
+ # should never be in the call stack of the exception
+ exc_info = sys.exc_info()
+
+ try:
+ response = request.invoke_exception_view(exc_info)
+ except HTTPNotFound:
+ # re-raise the original exception as no exception views were
+ # able to handle the error
+ reraise(*exc_info)
+
+ return response
+
+def excview_tween_factory(handler, registry):
+ """ A :term:`tween` factory which produces a tween that catches an
+ exception raised by downstream tweens (or the main Pyramid request
+ handler) and, if possible, converts it into a Response using an
+ :term:`exception view`.
+
+ .. versionchanged:: 1.9
+ The ``request.response`` will be remain unchanged even if the tween
+ handles an exception. Previously it was deleted after handling an
+ exception.
+
+ Also, ``request.exception`` and ``request.exc_info`` are only set if
+ the tween handles an exception and returns a response otherwise they
+ are left at their original values.
+
+ """
+
+ def excview_tween(request):
+ try:
+ response = handler(request)
+ except Exception as exc:
+ response = _error_handler(request, exc)
+ return response
+
+ return excview_tween
+
+MAIN = 'MAIN'
+INGRESS = 'INGRESS'
+EXCVIEW = 'pyramid.tweens.excview_tween_factory'
diff --git a/src/pyramid/url.py b/src/pyramid/url.py
new file mode 100644
index 000000000..852aa5e55
--- /dev/null
+++ b/src/pyramid/url.py
@@ -0,0 +1,894 @@
+""" Utility functions for dealing with URLs in pyramid """
+
+import os
+
+from pyramid.interfaces import (
+ IResourceURL,
+ IRoutesMapper,
+ IStaticURLInfo,
+ )
+
+from pyramid.compat import (
+ bytes_,
+ lru_cache,
+ string_types,
+ )
+from pyramid.encode import (
+ url_quote,
+ urlencode,
+)
+from pyramid.path import caller_package
+from pyramid.threadlocal import get_current_registry
+
+from pyramid.traversal import (
+ ResourceURL,
+ quote_path_segment,
+ PATH_SAFE,
+ PATH_SEGMENT_SAFE,
+ )
+
+QUERY_SAFE = "/?:@!$&'()*+,;=" # RFC 3986
+ANCHOR_SAFE = QUERY_SAFE
+
+def parse_url_overrides(request, kw):
+ """
+ Parse special arguments passed when generating urls.
+
+ The supplied dictionary is mutated when we pop arguments.
+ Returns a 3-tuple of the format:
+
+ ``(app_url, qs, anchor)``.
+
+ """
+ app_url = kw.pop('_app_url', None)
+ scheme = kw.pop('_scheme', None)
+ host = kw.pop('_host', None)
+ port = kw.pop('_port', None)
+ query = kw.pop('_query', '')
+ anchor = kw.pop('_anchor', '')
+
+ if app_url is None:
+ if (scheme is not None or host is not None or port is not None):
+ app_url = request._partial_application_url(scheme, host, port)
+ else:
+ app_url = request.application_url
+
+ qs = ''
+ if query:
+ if isinstance(query, string_types):
+ qs = '?' + url_quote(query, QUERY_SAFE)
+ else:
+ qs = '?' + urlencode(query, doseq=True)
+
+ frag = ''
+ if anchor:
+ frag = '#' + url_quote(anchor, ANCHOR_SAFE)
+
+ return app_url, qs, frag
+
+class URLMethodsMixin(object):
+ """ Request methods mixin for BaseRequest having to do with URL
+ generation """
+
+ def _partial_application_url(self, scheme=None, host=None, port=None):
+ """
+ Construct the URL defined by request.application_url, replacing any
+ of the default scheme, host, or port portions with user-supplied
+ variants.
+
+ If ``scheme`` is passed as ``https``, and the ``port`` is *not*
+ passed, the ``port`` value is assumed to ``443``. Likewise, if
+ ``scheme`` is passed as ``http`` and ``port`` is not passed, the
+ ``port`` value is assumed to be ``80``.
+
+ """
+ e = self.environ
+ if scheme is None:
+ scheme = e['wsgi.url_scheme']
+ else:
+ if scheme == 'https':
+ if port is None:
+ port = '443'
+ if scheme == 'http':
+ if port is None:
+ port = '80'
+ if host is None:
+ host = e.get('HTTP_HOST')
+ if host is None:
+ host = e['SERVER_NAME']
+ if port is None:
+ if ':' in host:
+ host, port = host.split(':', 1)
+ else:
+ port = e['SERVER_PORT']
+ else:
+ port = str(port)
+ if ':' in host:
+ host, _ = host.split(':', 1)
+ if scheme == 'https':
+ if port == '443':
+ port = None
+ elif scheme == 'http':
+ if port == '80':
+ port = None
+ url = scheme + '://' + host
+ if port:
+ url += ':%s' % port
+
+ url_encoding = getattr(self, 'url_encoding', 'utf-8') # webob 1.2b3+
+ bscript_name = bytes_(self.script_name, url_encoding)
+ return url + url_quote(bscript_name, PATH_SAFE)
+
+ def route_url(self, route_name, *elements, **kw):
+ """Generates a fully qualified URL for a named :app:`Pyramid`
+ :term:`route configuration`.
+
+ Use the route's ``name`` as the first positional argument.
+ Additional positional arguments (``*elements``) are appended to the
+ URL as path segments after it is generated.
+
+ Use keyword arguments to supply values which match any dynamic
+ path elements in the route definition. Raises a :exc:`KeyError`
+ exception if the URL cannot be generated for any reason (not
+ enough arguments, for example).
+
+ For example, if you've defined a route named "foobar" with the path
+ ``{foo}/{bar}/*traverse``::
+
+ request.route_url('foobar',
+ foo='1') => <KeyError exception>
+ request.route_url('foobar',
+ foo='1',
+ bar='2') => <KeyError exception>
+ request.route_url('foobar',
+ foo='1',
+ bar='2',
+ traverse=('a','b')) => http://e.com/1/2/a/b
+ request.route_url('foobar',
+ foo='1',
+ bar='2',
+ traverse='/a/b') => http://e.com/1/2/a/b
+
+ Values replacing ``:segment`` arguments can be passed as strings
+ or Unicode objects. They will be encoded to UTF-8 and URL-quoted
+ before being placed into the generated URL.
+
+ Values replacing ``*remainder`` arguments can be passed as strings
+ *or* tuples of Unicode/string values. If a tuple is passed as a
+ ``*remainder`` replacement value, its values are URL-quoted and
+ encoded to UTF-8. The resulting strings are joined with slashes
+ and rendered into the URL. If a string is passed as a
+ ``*remainder`` replacement value, it is tacked on to the URL
+ after being URL-quoted-except-for-embedded-slashes.
+
+ If ``_query`` is provided, it will be used to compose a query string
+ that will be tacked on to the end of the URL. The value of ``_query``
+ may be a sequence of two-tuples *or* a data structure with an
+ ``.items()`` method that returns a sequence of two-tuples
+ (presumably a dictionary). This data structure will be turned into
+ a query string per the documentation of the
+ :func:`pyramid.url.urlencode` function. This will produce a query
+ string in the ``x-www-form-urlencoded`` format. A
+ non-``x-www-form-urlencoded`` query string may be used by passing a
+ *string* value as ``_query`` in which case it will be URL-quoted
+ (e.g. query="foo bar" will become "foo%20bar"). However, the result
+ will not need to be in ``k=v`` form as required by
+ ``x-www-form-urlencoded``. After the query data is turned into a query
+ string, a leading ``?`` is prepended, and the resulting string is
+ appended to the generated URL.
+
+ .. note::
+
+ Python data structures that are passed as ``_query`` which are
+ sequences or dictionaries are turned into a string under the same
+ rules as when run through :func:`urllib.urlencode` with the ``doseq``
+ argument equal to ``True``. This means that sequences can be passed
+ as values, and a k=v pair will be placed into the query string for
+ each value.
+
+ If a keyword argument ``_anchor`` is present, its string
+ representation will be quoted per :rfc:`3986#section-3.5` and used as
+ a named anchor in the generated URL
+ (e.g. if ``_anchor`` is passed as ``foo`` and the route URL is
+ ``http://example.com/route/url``, the resulting generated URL will
+ be ``http://example.com/route/url#foo``).
+
+ .. note::
+
+ If ``_anchor`` is passed as a string, it should be UTF-8 encoded. If
+ ``_anchor`` is passed as a Unicode object, it will be converted to
+ UTF-8 before being appended to the URL.
+
+ If both ``_anchor`` and ``_query`` are specified, the anchor
+ element will always follow the query element,
+ e.g. ``http://example.com?foo=1#bar``.
+
+ If any of the keyword arguments ``_scheme``, ``_host``, or ``_port``
+ is passed and is non-``None``, the provided value will replace the
+ named portion in the generated URL. For example, if you pass
+ ``_host='foo.com'``, and the URL that would have been generated
+ without the host replacement is ``http://example.com/a``, the result
+ will be ``http://foo.com/a``.
+
+ Note that if ``_scheme`` is passed as ``https``, and ``_port`` is not
+ passed, the ``_port`` value is assumed to have been passed as
+ ``443``. Likewise, if ``_scheme`` is passed as ``http`` and
+ ``_port`` is not passed, the ``_port`` value is assumed to have been
+ passed as ``80``. To avoid this behavior, always explicitly pass
+ ``_port`` whenever you pass ``_scheme``.
+
+ If a keyword ``_app_url`` is present, it will be used as the
+ protocol/hostname/port/leading path prefix of the generated URL.
+ For example, using an ``_app_url`` of
+ ``http://example.com:8080/foo`` would cause the URL
+ ``http://example.com:8080/foo/fleeb/flub`` to be returned from
+ this function if the expansion of the route pattern associated
+ with the ``route_name`` expanded to ``/fleeb/flub``. If
+ ``_app_url`` is not specified, the result of
+ ``request.application_url`` will be used as the prefix (the
+ default).
+
+ If both ``_app_url`` and any of ``_scheme``, ``_host``, or ``_port``
+ are passed, ``_app_url`` takes precedence and any values passed for
+ ``_scheme``, ``_host``, and ``_port`` will be ignored.
+
+ This function raises a :exc:`KeyError` if the URL cannot be
+ generated due to missing replacement names. Extra replacement
+ names are ignored.
+
+ If the route object which matches the ``route_name`` argument has
+ a :term:`pregenerator`, the ``*elements`` and ``**kw``
+ arguments passed to this function might be augmented or changed.
+
+ .. versionchanged:: 1.5
+ Allow the ``_query`` option to be a string to enable alternative
+ encodings.
+
+ The ``_anchor`` option will be escaped instead of using
+ its raw string representation.
+
+ .. versionchanged:: 1.9
+ If ``_query`` or ``_anchor`` are falsey (such as ``None`` or an
+ empty string) they will not be included in the generated url.
+
+ """
+ try:
+ reg = self.registry
+ except AttributeError:
+ reg = get_current_registry() # b/c
+ mapper = reg.getUtility(IRoutesMapper)
+ route = mapper.get_route(route_name)
+
+ if route is None:
+ raise KeyError('No such route named %s' % route_name)
+
+ if route.pregenerator is not None:
+ elements, kw = route.pregenerator(self, elements, kw)
+
+ app_url, qs, anchor = parse_url_overrides(self, kw)
+
+ path = route.generate(kw) # raises KeyError if generate fails
+
+ if elements:
+ suffix = _join_elements(elements)
+ if not path.endswith('/'):
+ suffix = '/' + suffix
+ else:
+ suffix = ''
+
+ return app_url + path + suffix + qs + anchor
+
+ def route_path(self, route_name, *elements, **kw):
+ """
+ Generates a path (aka a 'relative URL', a URL minus the host, scheme,
+ and port) for a named :app:`Pyramid` :term:`route configuration`.
+
+ This function accepts the same argument as
+ :meth:`pyramid.request.Request.route_url` and performs the same duty.
+ It just omits the host, port, and scheme information in the return
+ value; only the script_name, path, query parameters, and anchor data
+ are present in the returned string.
+
+ For example, if you've defined a route named 'foobar' with the path
+ ``/{foo}/{bar}``, this call to ``route_path``::
+
+ request.route_path('foobar', foo='1', bar='2')
+
+ Will return the string ``/1/2``.
+
+ .. note::
+
+ Calling ``request.route_path('route')`` is the same as calling
+ ``request.route_url('route', _app_url=request.script_name)``.
+ :meth:`pyramid.request.Request.route_path` is, in fact,
+ implemented in terms of :meth:`pyramid.request.Request.route_url`
+ in just this way. As a result, any ``_app_url`` passed within the
+ ``**kw`` values to ``route_path`` will be ignored.
+
+ """
+ kw['_app_url'] = self.script_name
+ return self.route_url(route_name, *elements, **kw)
+
+ def resource_url(self, resource, *elements, **kw):
+ """
+ Generate a string representing the absolute URL of the
+ :term:`resource` object based on the ``wsgi.url_scheme``,
+ ``HTTP_HOST`` or ``SERVER_NAME`` in the request, plus any
+ ``SCRIPT_NAME``. The overall result of this method is always a
+ UTF-8 encoded string.
+
+ Examples::
+
+ request.resource_url(resource) =>
+
+ http://example.com/
+
+ request.resource_url(resource, 'a.html') =>
+
+ http://example.com/a.html
+
+ request.resource_url(resource, 'a.html', query={'q':'1'}) =>
+
+ http://example.com/a.html?q=1
+
+ request.resource_url(resource, 'a.html', anchor='abc') =>
+
+ http://example.com/a.html#abc
+
+ request.resource_url(resource, app_url='') =>
+
+ /
+
+ Any positional arguments passed in as ``elements`` must be strings
+ Unicode objects, or integer objects. These will be joined by slashes
+ and appended to the generated resource URL. Each of the elements
+ passed in is URL-quoted before being appended; if any element is
+ Unicode, it will converted to a UTF-8 bytestring before being
+ URL-quoted. If any element is an integer, it will be converted to its
+ string representation before being URL-quoted.
+
+ .. warning:: if no ``elements`` arguments are specified, the resource
+ URL will end with a trailing slash. If any
+ ``elements`` are used, the generated URL will *not*
+ end in a trailing slash.
+
+ If ``query`` is provided, it will be used to compose a query string
+ that will be tacked on to the end of the URL. The value of ``query``
+ may be a sequence of two-tuples *or* a data structure with an
+ ``.items()`` method that returns a sequence of two-tuples
+ (presumably a dictionary). This data structure will be turned into
+ a query string per the documentation of the
+ :func:`pyramid.url.urlencode` function. This will produce a query
+ string in the ``x-www-form-urlencoded`` format. A
+ non-``x-www-form-urlencoded`` query string may be used by passing a
+ *string* value as ``query`` in which case it will be URL-quoted
+ (e.g. query="foo bar" will become "foo%20bar"). However, the result
+ will not need to be in ``k=v`` form as required by
+ ``x-www-form-urlencoded``. After the query data is turned into a query
+ string, a leading ``?`` is prepended, and the resulting string is
+ appended to the generated URL.
+
+ .. note::
+
+ Python data structures that are passed as ``query`` which are
+ sequences or dictionaries are turned into a string under the same
+ rules as when run through :func:`urllib.urlencode` with the ``doseq``
+ argument equal to ``True``. This means that sequences can be passed
+ as values, and a k=v pair will be placed into the query string for
+ each value.
+
+ If a keyword argument ``anchor`` is present, its string
+ representation will be used as a named anchor in the generated URL
+ (e.g. if ``anchor`` is passed as ``foo`` and the resource URL is
+ ``http://example.com/resource/url``, the resulting generated URL will
+ be ``http://example.com/resource/url#foo``).
+
+ .. note::
+
+ If ``anchor`` is passed as a string, it should be UTF-8 encoded. If
+ ``anchor`` is passed as a Unicode object, it will be converted to
+ UTF-8 before being appended to the URL.
+
+ If both ``anchor`` and ``query`` are specified, the anchor element
+ will always follow the query element,
+ e.g. ``http://example.com?foo=1#bar``.
+
+ If any of the keyword arguments ``scheme``, ``host``, or ``port`` is
+ passed and is non-``None``, the provided value will replace the named
+ portion in the generated URL. For example, if you pass
+ ``host='foo.com'``, and the URL that would have been generated
+ without the host replacement is ``http://example.com/a``, the result
+ will be ``http://foo.com/a``.
+
+ If ``scheme`` is passed as ``https``, and an explicit ``port`` is not
+ passed, the ``port`` value is assumed to have been passed as ``443``.
+ Likewise, if ``scheme`` is passed as ``http`` and ``port`` is not
+ passed, the ``port`` value is assumed to have been passed as
+ ``80``. To avoid this behavior, always explicitly pass ``port``
+ whenever you pass ``scheme``.
+
+ If a keyword argument ``app_url`` is passed and is not ``None``, it
+ should be a string that will be used as the port/hostname/initial
+ path portion of the generated URL instead of the default request
+ application URL. For example, if ``app_url='http://foo'``, then the
+ resulting url of a resource that has a path of ``/baz/bar`` will be
+ ``http://foo/baz/bar``. If you want to generate completely relative
+ URLs with no leading scheme, host, port, or initial path, you can
+ pass ``app_url=''``. Passing ``app_url=''`` when the resource path is
+ ``/baz/bar`` will return ``/baz/bar``.
+
+ If ``app_url`` is passed and any of ``scheme``, ``port``, or ``host``
+ are also passed, ``app_url`` will take precedence and the values
+ passed for ``scheme``, ``host``, and/or ``port`` will be ignored.
+
+ If the ``resource`` passed in has a ``__resource_url__`` method, it
+ will be used to generate the URL (scheme, host, port, path) for the
+ base resource which is operated upon by this function.
+
+ .. seealso::
+
+ See also :ref:`overriding_resource_url_generation`.
+
+ If ``route_name`` is passed, this function will delegate its URL
+ production to the ``route_url`` function. Calling
+ ``resource_url(someresource, 'element1', 'element2', query={'a':1},
+ route_name='blogentry')`` is roughly equivalent to doing::
+
+ traversal_path = request.resource_path(someobject)
+ url = request.route_url(
+ 'blogentry',
+ 'element1',
+ 'element2',
+ _query={'a':'1'},
+ traverse=traversal_path,
+ )
+
+ It is only sensible to pass ``route_name`` if the route being named has
+ a ``*remainder`` stararg value such as ``*traverse``. The remainder
+ value will be ignored in the output otherwise.
+
+ By default, the resource path value will be passed as the name
+ ``traverse`` when ``route_url`` is called. You can influence this by
+ passing a different ``route_remainder_name`` value if the route has a
+ different ``*stararg`` value at its end. For example if the route
+ pattern you want to replace has a ``*subpath`` stararg ala
+ ``/foo*subpath``::
+
+ request.resource_url(
+ resource,
+ route_name='myroute',
+ route_remainder_name='subpath'
+ )
+
+ If ``route_name`` is passed, it is also permissible to pass
+ ``route_kw``, which will passed as additional keyword arguments to
+ ``route_url``. Saying ``resource_url(someresource, 'element1',
+ 'element2', route_name='blogentry', route_kw={'id':'4'},
+ _query={'a':'1'})`` is roughly equivalent to::
+
+ traversal_path = request.resource_path_tuple(someobject)
+ kw = {'id':'4', '_query':{'a':'1'}, 'traverse':traversal_path}
+ url = request.route_url(
+ 'blogentry',
+ 'element1',
+ 'element2',
+ **kw,
+ )
+
+ If ``route_kw`` or ``route_remainder_name`` is passed, but
+ ``route_name`` is not passed, both ``route_kw`` and
+ ``route_remainder_name`` will be ignored. If ``route_name``
+ is passed, the ``__resource_url__`` method of the resource passed is
+ ignored unconditionally. This feature is incompatible with
+ resources which generate their own URLs.
+
+ .. note::
+
+ If the :term:`resource` used is the result of a :term:`traversal`, it
+ must be :term:`location`-aware. The resource can also be the context
+ of a :term:`URL dispatch`; contexts found this way do not need to be
+ location-aware.
+
+ .. note::
+
+ If a 'virtual root path' is present in the request environment (the
+ value of the WSGI environ key ``HTTP_X_VHM_ROOT``), and the resource
+ was obtained via :term:`traversal`, the URL path will not include the
+ virtual root prefix (it will be stripped off the left hand side of
+ the generated URL).
+
+ .. note::
+
+ For backwards compatibility purposes, this method is also
+ aliased as the ``model_url`` method of request.
+
+ .. versionchanged:: 1.3
+ Added the ``app_url`` keyword argument.
+
+ .. versionchanged:: 1.5
+ Allow the ``query`` option to be a string to enable alternative
+ encodings.
+
+ The ``anchor`` option will be escaped instead of using
+ its raw string representation.
+
+ Added the ``route_name``, ``route_kw``, and
+ ``route_remainder_name`` keyword arguments.
+
+ .. versionchanged:: 1.9
+ If ``query`` or ``anchor`` are falsey (such as ``None`` or an
+ empty string) they will not be included in the generated url.
+ """
+ try:
+ reg = self.registry
+ except AttributeError:
+ reg = get_current_registry() # b/c
+
+ url_adapter = reg.queryMultiAdapter((resource, self), IResourceURL)
+ if url_adapter is None:
+ url_adapter = ResourceURL(resource, self)
+
+ virtual_path = getattr(url_adapter, 'virtual_path', None)
+
+ urlkw = {}
+ for name in (
+ 'app_url', 'scheme', 'host', 'port', 'query', 'anchor'
+ ):
+ val = kw.get(name, None)
+ if val is not None:
+ urlkw['_' + name] = val
+
+ if 'route_name' in kw:
+ route_name = kw['route_name']
+ remainder = getattr(url_adapter, 'virtual_path_tuple', None)
+ if remainder is None:
+ # older user-supplied IResourceURL adapter without 1.5
+ # virtual_path_tuple
+ remainder = tuple(url_adapter.virtual_path.split('/'))
+ remainder_name = kw.get('route_remainder_name', 'traverse')
+ urlkw[remainder_name] = remainder
+
+ if 'route_kw' in kw:
+ route_kw = kw.get('route_kw')
+ if route_kw is not None:
+ urlkw.update(route_kw)
+
+ return self.route_url(route_name, *elements, **urlkw)
+
+ app_url, qs, anchor = parse_url_overrides(self, urlkw)
+
+ resource_url = None
+ local_url = getattr(resource, '__resource_url__', None)
+
+ if local_url is not None:
+ # the resource handles its own url generation
+ d = dict(
+ virtual_path=virtual_path,
+ physical_path=url_adapter.physical_path,
+ app_url=app_url,
+ )
+
+ # allow __resource_url__ to punt by returning None
+ resource_url = local_url(self, d)
+
+ if resource_url is None:
+ # the resource did not handle its own url generation or the
+ # __resource_url__ function returned None
+ resource_url = app_url + virtual_path
+
+ if elements:
+ suffix = _join_elements(elements)
+ else:
+ suffix = ''
+
+ return resource_url + suffix + qs + anchor
+
+ model_url = resource_url # b/w compat forever
+
+ def resource_path(self, resource, *elements, **kw):
+ """
+ Generates a path (aka a 'relative URL', a URL minus the host, scheme,
+ and port) for a :term:`resource`.
+
+ This function accepts the same argument as
+ :meth:`pyramid.request.Request.resource_url` and performs the same
+ duty. It just omits the host, port, and scheme information in the
+ return value; only the script_name, path, query parameters, and
+ anchor data are present in the returned string.
+
+ .. note::
+
+ Calling ``request.resource_path(resource)`` is the same as calling
+ ``request.resource_path(resource, app_url=request.script_name)``.
+ :meth:`pyramid.request.Request.resource_path` is, in fact,
+ implemented in terms of
+ :meth:`pyramid.request.Request.resource_url` in just this way. As
+ a result, any ``app_url`` passed within the ``**kw`` values to
+ ``route_path`` will be ignored. ``scheme``, ``host``, and
+ ``port`` are also ignored.
+ """
+ kw['app_url'] = self.script_name
+ return self.resource_url(resource, *elements, **kw)
+
+ def static_url(self, path, **kw):
+ """
+ Generates a fully qualified URL for a static :term:`asset`.
+ The asset must live within a location defined via the
+ :meth:`pyramid.config.Configurator.add_static_view`
+ :term:`configuration declaration` (see :ref:`static_assets_section`).
+
+ Example::
+
+ request.static_url('mypackage:static/foo.css') =>
+
+ http://example.com/static/foo.css
+
+
+ The ``path`` argument points at a file or directory on disk which
+ a URL should be generated for. The ``path`` may be either a
+ relative path (e.g. ``static/foo.css``) or an absolute path (e.g.
+ ``/abspath/to/static/foo.css``) or a :term:`asset specification`
+ (e.g. ``mypackage:static/foo.css``).
+
+ The purpose of the ``**kw`` argument is the same as the purpose of
+ the :meth:`pyramid.request.Request.route_url` ``**kw`` argument. See
+ the documentation for that function to understand the arguments which
+ you can provide to it. However, typically, you don't need to pass
+ anything as ``*kw`` when generating a static asset URL.
+
+ This function raises a :exc:`ValueError` if a static view
+ definition cannot be found which matches the path specification.
+
+ """
+ if not os.path.isabs(path):
+ if ':' not in path:
+ # if it's not a package:relative/name and it's not an
+ # /absolute/path it's a relative/path; this means its relative
+ # to the package in which the caller's module is defined.
+ package = caller_package()
+ path = '%s:%s' % (package.__name__, path)
+
+ try:
+ reg = self.registry
+ except AttributeError:
+ reg = get_current_registry() # b/c
+
+ info = reg.queryUtility(IStaticURLInfo)
+ if info is None:
+ raise ValueError('No static URL definition matching %s' % path)
+
+ return info.generate(path, self, **kw)
+
+ def static_path(self, path, **kw):
+ """
+ Generates a path (aka a 'relative URL', a URL minus the host, scheme,
+ and port) for a static resource.
+
+ This function accepts the same argument as
+ :meth:`pyramid.request.Request.static_url` and performs the
+ same duty. It just omits the host, port, and scheme information in
+ the return value; only the script_name, path, query parameters, and
+ anchor data are present in the returned string.
+
+ Example::
+
+ request.static_path('mypackage:static/foo.css') =>
+
+ /static/foo.css
+
+ .. note::
+
+ Calling ``request.static_path(apath)`` is the same as calling
+ ``request.static_url(apath, _app_url=request.script_name)``.
+ :meth:`pyramid.request.Request.static_path` is, in fact, implemented
+ in terms of :meth:`pyramid.request.Request.static_url` in just this
+ way. As a result, any ``_app_url`` passed within the ``**kw`` values
+ to ``static_path`` will be ignored.
+ """
+ if not os.path.isabs(path):
+ if ':' not in path:
+ # if it's not a package:relative/name and it's not an
+ # /absolute/path it's a relative/path; this means its relative
+ # to the package in which the caller's module is defined.
+ package = caller_package()
+ path = '%s:%s' % (package.__name__, path)
+
+ kw['_app_url'] = self.script_name
+ return self.static_url(path, **kw)
+
+ def current_route_url(self, *elements, **kw):
+ """
+ Generates a fully qualified URL for a named :app:`Pyramid`
+ :term:`route configuration` based on the 'current route'.
+
+ This function supplements
+ :meth:`pyramid.request.Request.route_url`. It presents an easy way to
+ generate a URL for the 'current route' (defined as the route which
+ matched when the request was generated).
+
+ The arguments to this method have the same meaning as those with the
+ same names passed to :meth:`pyramid.request.Request.route_url`. It
+ also understands an extra argument which ``route_url`` does not named
+ ``_route_name``.
+
+ The route name used to generate a URL is taken from either the
+ ``_route_name`` keyword argument or the name of the route which is
+ currently associated with the request if ``_route_name`` was not
+ passed. Keys and values from the current request :term:`matchdict`
+ are combined with the ``kw`` arguments to form a set of defaults
+ named ``newkw``. Then ``request.route_url(route_name, *elements,
+ **newkw)`` is called, returning a URL.
+
+ Examples follow.
+
+ If the 'current route' has the route pattern ``/foo/{page}`` and the
+ current url path is ``/foo/1`` , the matchdict will be
+ ``{'page':'1'}``. The result of ``request.current_route_url()`` in
+ this situation will be ``/foo/1``.
+
+ If the 'current route' has the route pattern ``/foo/{page}`` and the
+ current url path is ``/foo/1``, the matchdict will be
+ ``{'page':'1'}``. The result of
+ ``request.current_route_url(page='2')`` in this situation will be
+ ``/foo/2``.
+
+ Usage of the ``_route_name`` keyword argument: if our routing table
+ defines routes ``/foo/{action}`` named 'foo' and
+ ``/foo/{action}/{page}`` named ``fooaction``, and the current url
+ pattern is ``/foo/view`` (which has matched the ``/foo/{action}``
+ route), we may want to use the matchdict args to generate a URL to
+ the ``fooaction`` route. In this scenario,
+ ``request.current_route_url(_route_name='fooaction', page='5')``
+ Will return string like: ``/foo/view/5``.
+
+ """
+ if '_route_name' in kw:
+ route_name = kw.pop('_route_name')
+ else:
+ route = getattr(self, 'matched_route', None)
+ route_name = getattr(route, 'name', None)
+ if route_name is None:
+ raise ValueError('Current request matches no route')
+
+ if '_query' not in kw:
+ kw['_query'] = self.GET
+
+ newkw = {}
+ newkw.update(self.matchdict)
+ newkw.update(kw)
+ return self.route_url(route_name, *elements, **newkw)
+
+ def current_route_path(self, *elements, **kw):
+ """
+ Generates a path (aka a 'relative URL', a URL minus the host, scheme,
+ and port) for the :app:`Pyramid` :term:`route configuration` matched
+ by the current request.
+
+ This function accepts the same argument as
+ :meth:`pyramid.request.Request.current_route_url` and performs the
+ same duty. It just omits the host, port, and scheme information in
+ the return value; only the script_name, path, query parameters, and
+ anchor data are present in the returned string.
+
+ For example, if the route matched by the current request has the
+ pattern ``/{foo}/{bar}``, this call to ``current_route_path``::
+
+ request.current_route_path(foo='1', bar='2')
+
+ Will return the string ``/1/2``.
+
+ .. note::
+
+ Calling ``request.current_route_path('route')`` is the same
+ as calling ``request.current_route_url('route',
+ _app_url=request.script_name)``.
+ :meth:`pyramid.request.Request.current_route_path` is, in fact,
+ implemented in terms of
+ :meth:`pyramid.request.Request.current_route_url` in just this
+ way. As a result, any ``_app_url`` passed within the ``**kw``
+ values to ``current_route_path`` will be ignored.
+ """
+ kw['_app_url'] = self.script_name
+ return self.current_route_url(*elements, **kw)
+
+
+def route_url(route_name, request, *elements, **kw):
+ """
+ This is a backwards compatibility function. Its result is the same as
+ calling::
+
+ request.route_url(route_name, *elements, **kw)
+
+ See :meth:`pyramid.request.Request.route_url` for more information.
+ """
+ return request.route_url(route_name, *elements, **kw)
+
+def route_path(route_name, request, *elements, **kw):
+ """
+ This is a backwards compatibility function. Its result is the same as
+ calling::
+
+ request.route_path(route_name, *elements, **kw)
+
+ See :meth:`pyramid.request.Request.route_path` for more information.
+ """
+ return request.route_path(route_name, *elements, **kw)
+
+def resource_url(resource, request, *elements, **kw):
+ """
+ This is a backwards compatibility function. Its result is the same as
+ calling::
+
+ request.resource_url(resource, *elements, **kw)
+
+ See :meth:`pyramid.request.Request.resource_url` for more information.
+ """
+ return request.resource_url(resource, *elements, **kw)
+
+model_url = resource_url # b/w compat (forever)
+
+
+def static_url(path, request, **kw):
+ """
+ This is a backwards compatibility function. Its result is the same as
+ calling::
+
+ request.static_url(path, **kw)
+
+ See :meth:`pyramid.request.Request.static_url` for more information.
+ """
+ if not os.path.isabs(path):
+ if ':' not in path:
+ # if it's not a package:relative/name and it's not an
+ # /absolute/path it's a relative/path; this means its relative
+ # to the package in which the caller's module is defined.
+ package = caller_package()
+ path = '%s:%s' % (package.__name__, path)
+ return request.static_url(path, **kw)
+
+
+def static_path(path, request, **kw):
+ """
+ This is a backwards compatibility function. Its result is the same as
+ calling::
+
+ request.static_path(path, **kw)
+
+ See :meth:`pyramid.request.Request.static_path` for more information.
+ """
+ if not os.path.isabs(path):
+ if ':' not in path:
+ # if it's not a package:relative/name and it's not an
+ # /absolute/path it's a relative/path; this means its relative
+ # to the package in which the caller's module is defined.
+ package = caller_package()
+ path = '%s:%s' % (package.__name__, path)
+ return request.static_path(path, **kw)
+
+def current_route_url(request, *elements, **kw):
+ """
+ This is a backwards compatibility function. Its result is the same as
+ calling::
+
+ request.current_route_url(*elements, **kw)
+
+ See :meth:`pyramid.request.Request.current_route_url` for more
+ information.
+ """
+ return request.current_route_url(*elements, **kw)
+
+def current_route_path(request, *elements, **kw):
+ """
+ This is a backwards compatibility function. Its result is the same as
+ calling::
+
+ request.current_route_path(*elements, **kw)
+
+ See :meth:`pyramid.request.Request.current_route_path` for more
+ information.
+ """
+ return request.current_route_path(*elements, **kw)
+
+@lru_cache(1000)
+def _join_elements(elements):
+ return '/'.join([quote_path_segment(s, safe=PATH_SEGMENT_SAFE) for s in elements])
diff --git a/src/pyramid/urldispatch.py b/src/pyramid/urldispatch.py
new file mode 100644
index 000000000..a61071845
--- /dev/null
+++ b/src/pyramid/urldispatch.py
@@ -0,0 +1,249 @@
+import re
+from zope.interface import implementer
+
+from pyramid.interfaces import (
+ IRoutesMapper,
+ IRoute,
+ )
+
+from pyramid.compat import (
+ PY2,
+ native_,
+ text_,
+ text_type,
+ string_types,
+ binary_type,
+ is_nonstr_iter,
+ decode_path_info,
+ )
+
+from pyramid.exceptions import URLDecodeError
+
+from pyramid.traversal import (
+ quote_path_segment,
+ split_path_info,
+ PATH_SAFE,
+ )
+
+_marker = object()
+
+@implementer(IRoute)
+class Route(object):
+ def __init__(self, name, pattern, factory=None, predicates=(),
+ pregenerator=None):
+ self.pattern = pattern
+ self.path = pattern # indefinite b/w compat, not in interface
+ self.match, self.generate = _compile_route(pattern)
+ self.name = name
+ self.factory = factory
+ self.predicates = predicates
+ self.pregenerator = pregenerator
+
+@implementer(IRoutesMapper)
+class RoutesMapper(object):
+ def __init__(self):
+ self.routelist = []
+ self.static_routes = []
+
+ self.routes = {}
+
+ def has_routes(self):
+ return bool(self.routelist)
+
+ def get_routes(self, include_static=False):
+ if include_static is True:
+ return self.routelist + self.static_routes
+
+ return self.routelist
+
+ def get_route(self, name):
+ return self.routes.get(name)
+
+ def connect(self, name, pattern, factory=None, predicates=(),
+ pregenerator=None, static=False):
+ if name in self.routes:
+ oldroute = self.routes[name]
+ if oldroute in self.routelist:
+ self.routelist.remove(oldroute)
+
+ route = Route(name, pattern, factory, predicates, pregenerator)
+ if not static:
+ self.routelist.append(route)
+ else:
+ self.static_routes.append(route)
+
+ self.routes[name] = route
+ return route
+
+ def generate(self, name, kw):
+ return self.routes[name].generate(kw)
+
+ def __call__(self, request):
+ environ = request.environ
+ try:
+ # empty if mounted under a path in mod_wsgi, for example
+ path = decode_path_info(environ['PATH_INFO'] or '/')
+ except KeyError:
+ path = '/'
+ except UnicodeDecodeError as e:
+ raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason)
+
+ for route in self.routelist:
+ match = route.match(path)
+ if match is not None:
+ preds = route.predicates
+ info = {'match':match, 'route':route}
+ if preds and not all((p(info, request) for p in preds)):
+ continue
+ return info
+
+ return {'route':None, 'match':None}
+
+# stolen from bobo and modified
+old_route_re = re.compile(r'(\:[_a-zA-Z]\w*)')
+star_at_end = re.compile(r'\*(\w*)$')
+
+# The tortuous nature of the regex named ``route_re`` below is due to the
+# fact that we need to support at least one level of "inner" squigglies
+# inside the expr of a {name:expr} pattern. This regex used to be just
+# (\{[a-zA-Z][^\}]*\}) but that choked when supplied with e.g. {foo:\d{4}}.
+route_re = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})')
+
+def update_pattern(matchobj):
+ name = matchobj.group(0)
+ return '{%s}' % name[1:]
+
+def _compile_route(route):
+ # This function really wants to consume Unicode patterns natively, but if
+ # someone passes us a bytestring, we allow it by converting it to Unicode
+ # using the ASCII decoding. We decode it using ASCII because we don't
+ # want to accept bytestrings with high-order characters in them here as
+ # we have no idea what the encoding represents.
+ if route.__class__ is not text_type:
+ try:
+ route = text_(route, 'ascii')
+ except UnicodeDecodeError:
+ raise ValueError(
+ 'The pattern value passed to add_route must be '
+ 'either a Unicode string or a plain string without '
+ 'any non-ASCII characters (you provided %r).' % route)
+
+ if old_route_re.search(route) and not route_re.search(route):
+ route = old_route_re.sub(update_pattern, route)
+
+ if not route.startswith('/'):
+ route = '/' + route
+
+ remainder = None
+ if star_at_end.search(route):
+ route, remainder = route.rsplit('*', 1)
+
+ pat = route_re.split(route)
+
+ # every element in "pat" will be Unicode (regardless of whether the
+ # route_re regex pattern is itself Unicode or str)
+ pat.reverse()
+ rpat = []
+ gen = []
+ prefix = pat.pop() # invar: always at least one element (route='/'+route)
+
+ # We want to generate URL-encoded URLs, so we url-quote the prefix, being
+ # careful not to quote any embedded slashes. We have to replace '%' with
+ # '%%' afterwards, as the strings that go into "gen" are used as string
+ # replacement targets.
+ gen.append(quote_path_segment(prefix, safe='/').replace('%', '%%')) # native
+ rpat.append(re.escape(prefix)) # unicode
+
+ while pat:
+ name = pat.pop() # unicode
+ name = name[1:-1]
+ if ':' in name:
+ # reg may contain colons as well,
+ # so we must strictly split name into two parts
+ name, reg = name.split(':', 1)
+ else:
+ reg = '[^/]+'
+ gen.append('%%(%s)s' % native_(name)) # native
+ name = '(?P<%s>%s)' % (name, reg) # unicode
+ rpat.append(name)
+ s = pat.pop() # unicode
+ if s:
+ rpat.append(re.escape(s)) # unicode
+ # We want to generate URL-encoded URLs, so we url-quote this
+ # literal in the pattern, being careful not to quote the embedded
+ # slashes. We have to replace '%' with '%%' afterwards, as the
+ # strings that go into "gen" are used as string replacement
+ # targets. What is appended to gen is a native string.
+ gen.append(quote_path_segment(s, safe='/').replace('%', '%%'))
+
+ if remainder:
+ rpat.append('(?P<%s>.*?)' % remainder) # unicode
+ gen.append('%%(%s)s' % native_(remainder)) # native
+
+ pattern = ''.join(rpat) + '$' # unicode
+
+ match = re.compile(pattern).match
+ def matcher(path):
+ # This function really wants to consume Unicode patterns natively,
+ # but if someone passes us a bytestring, we allow it by converting it
+ # to Unicode using the ASCII decoding. We decode it using ASCII
+ # because we don't want to accept bytestrings with high-order
+ # characters in them here as we have no idea what the encoding
+ # represents.
+ if path.__class__ is not text_type:
+ path = text_(path, 'ascii')
+ m = match(path)
+ if m is None:
+ return None
+ d = {}
+ for k, v in m.groupdict().items():
+ # k and v will be Unicode 2.6.4 and lower doesnt accept unicode
+ # kwargs as **kw, so we explicitly cast the keys to native
+ # strings in case someone wants to pass the result as **kw
+ nk = native_(k, 'ascii')
+ if k == remainder:
+ d[nk] = split_path_info(v)
+ else:
+ d[nk] = v
+ return d
+
+ gen = ''.join(gen)
+
+ def q(v):
+ return quote_path_segment(v, safe=PATH_SAFE)
+
+ def generator(dict):
+ newdict = {}
+ for k, v in dict.items():
+ if PY2:
+ if v.__class__ is text_type:
+ # url_quote below needs bytes, not unicode on Py2
+ v = v.encode('utf-8')
+ else:
+ if v.__class__ is binary_type:
+ # url_quote below needs a native string, not bytes on Py3
+ v = v.decode('utf-8')
+
+ if k == remainder:
+ # a stararg argument
+ if is_nonstr_iter(v):
+ v = '/'.join(
+ [q(x) for x in v]
+ ) # native
+ else:
+ if v.__class__ not in string_types:
+ v = str(v)
+ v = q(v)
+ else:
+ if v.__class__ not in string_types:
+ v = str(v)
+ # v may be bytes (py2) or native string (py3)
+ v = q(v)
+
+ # at this point, the value will be a native string
+ newdict[k] = v
+
+ result = gen % newdict # native string result
+ return result
+
+ return matcher, generator
diff --git a/src/pyramid/util.py b/src/pyramid/util.py
new file mode 100644
index 000000000..6655455bf
--- /dev/null
+++ b/src/pyramid/util.py
@@ -0,0 +1,651 @@
+from contextlib import contextmanager
+import functools
+try:
+ # py2.7.7+ and py3.3+ have native comparison support
+ from hmac import compare_digest
+except ImportError: # pragma: no cover
+ compare_digest = None
+import inspect
+import weakref
+
+from pyramid.exceptions import (
+ ConfigurationError,
+ CyclicDependencyError,
+ )
+
+from pyramid.compat import (
+ getargspec,
+ im_func,
+ is_nonstr_iter,
+ integer_types,
+ string_types,
+ bytes_,
+ text_,
+ PY2,
+ native_
+ )
+
+from pyramid.path import DottedNameResolver as _DottedNameResolver
+
+_marker = object()
+
+
+class DottedNameResolver(_DottedNameResolver):
+ def __init__(self, package=None): # default to package = None for bw compat
+ _DottedNameResolver.__init__(self, package)
+
+def is_string_or_iterable(v):
+ if isinstance(v, string_types):
+ return True
+ if hasattr(v, '__iter__'):
+ return True
+
+def as_sorted_tuple(val):
+ if not is_nonstr_iter(val):
+ val = (val,)
+ val = tuple(sorted(val))
+ return val
+
+class InstancePropertyHelper(object):
+ """A helper object for assigning properties and descriptors to instances.
+ It is not normally possible to do this because descriptors must be
+ defined on the class itself.
+
+ This class is optimized for adding multiple properties at once to an
+ instance. This is done by calling :meth:`.add_property` once
+ per-property and then invoking :meth:`.apply` on target objects.
+
+ """
+ def __init__(self):
+ self.properties = {}
+
+ @classmethod
+ def make_property(cls, callable, name=None, reify=False):
+ """ Convert a callable into one suitable for adding to the
+ instance. This will return a 2-tuple containing the computed
+ (name, property) pair.
+ """
+
+ is_property = isinstance(callable, property)
+ if is_property:
+ fn = callable
+ if name is None:
+ raise ValueError('must specify "name" for a property')
+ if reify:
+ raise ValueError('cannot reify a property')
+ elif name is not None:
+ fn = lambda this: callable(this)
+ fn.__name__ = get_callable_name(name)
+ fn.__doc__ = callable.__doc__
+ else:
+ name = callable.__name__
+ fn = callable
+ if reify:
+ import pyramid.decorator # avoid circular import
+ fn = pyramid.decorator.reify(fn)
+ elif not is_property:
+ fn = property(fn)
+
+ return name, fn
+
+ @classmethod
+ def apply_properties(cls, target, properties):
+ """Accept a list or dict of ``properties`` generated from
+ :meth:`.make_property` and apply them to a ``target`` object.
+ """
+ attrs = dict(properties)
+ if attrs:
+ parent = target.__class__
+ # fix the module name so it appears to still be the parent
+ # e.g. pyramid.request instead of pyramid.util
+ attrs.setdefault('__module__', parent.__module__)
+ newcls = type(parent.__name__, (parent, object), attrs)
+ # We assign __provides__ and __implemented__ below to prevent a
+ # memory leak that results from from the usage of this instance's
+ # eventual use in an adapter lookup. Adapter lookup results in
+ # ``zope.interface.implementedBy`` being called with the
+ # newly-created class as an argument. Because the newly-created
+ # class has no interface specification data of its own, lookup
+ # causes new ClassProvides and Implements instances related to our
+ # just-generated class to be created and set into the newly-created
+ # class' __dict__. We don't want these instances to be created; we
+ # want this new class to behave exactly like it is the parent class
+ # instead. See GitHub issues #1212, #1529 and #1568 for more
+ # information.
+ for name in ('__implemented__', '__provides__'):
+ # we assign these attributes conditionally to make it possible
+ # to test this class in isolation without having any interfaces
+ # attached to it
+ val = getattr(parent, name, _marker)
+ if val is not _marker:
+ setattr(newcls, name, val)
+ target.__class__ = newcls
+
+ @classmethod
+ def set_property(cls, target, callable, name=None, reify=False):
+ """A helper method to apply a single property to an instance."""
+ prop = cls.make_property(callable, name=name, reify=reify)
+ cls.apply_properties(target, [prop])
+
+ def add_property(self, callable, name=None, reify=False):
+ """Add a new property configuration.
+
+ This should be used in combination with :meth:`.apply` as a
+ more efficient version of :meth:`.set_property`.
+ """
+ name, fn = self.make_property(callable, name=name, reify=reify)
+ self.properties[name] = fn
+
+ def apply(self, target):
+ """ Apply all configured properties to the ``target`` instance."""
+ if self.properties:
+ self.apply_properties(target, self.properties)
+
+class InstancePropertyMixin(object):
+ """ Mixin that will allow an instance to add properties at
+ run-time as if they had been defined via @property or @reify
+ on the class itself.
+ """
+
+ def set_property(self, callable, name=None, reify=False):
+ """ Add a callable or a property descriptor to the instance.
+
+ Properties, unlike attributes, are lazily evaluated by executing
+ an underlying callable when accessed. They can be useful for
+ adding features to an object without any cost if those features
+ go unused.
+
+ A property may also be reified via the
+ :class:`pyramid.decorator.reify` decorator by setting
+ ``reify=True``, allowing the result of the evaluation to be
+ cached. Using this method, the value of the property is only
+ computed once for the lifetime of the object.
+
+ ``callable`` can either be a callable that accepts the instance
+ as its single positional parameter, or it can be a property
+ descriptor.
+
+ If the ``callable`` is a property descriptor, the ``name``
+ parameter must be supplied or a ``ValueError`` will be raised.
+ Also note that a property descriptor cannot be reified, so
+ ``reify`` must be ``False``.
+
+ If ``name`` is None, the name of the property will be computed
+ from the name of the ``callable``.
+
+ .. code-block:: python
+ :linenos:
+
+ class Foo(InstancePropertyMixin):
+ _x = 1
+
+ def _get_x(self):
+ return _x
+
+ def _set_x(self, value):
+ self._x = value
+
+ foo = Foo()
+ foo.set_property(property(_get_x, _set_x), name='x')
+ foo.set_property(_get_x, name='y', reify=True)
+
+ >>> foo.x
+ 1
+ >>> foo.y
+ 1
+ >>> foo.x = 5
+ >>> foo.x
+ 5
+ >>> foo.y # notice y keeps the original value
+ 1
+ """
+ InstancePropertyHelper.set_property(
+ self, callable, name=name, reify=reify)
+
+class WeakOrderedSet(object):
+ """ Maintain a set of items.
+
+ Each item is stored as a weakref to avoid extending their lifetime.
+
+ The values may be iterated over or the last item added may be
+ accessed via the ``last`` property.
+
+ If items are added more than once, the most recent addition will
+ be remembered in the order:
+
+ order = WeakOrderedSet()
+ order.add('1')
+ order.add('2')
+ order.add('1')
+
+ list(order) == ['2', '1']
+ order.last == '1'
+ """
+
+ def __init__(self):
+ self._items = {}
+ self._order = []
+
+ def add(self, item):
+ """ Add an item to the set."""
+ oid = id(item)
+ if oid in self._items:
+ self._order.remove(oid)
+ self._order.append(oid)
+ return
+ ref = weakref.ref(item, lambda x: self._remove_by_id(oid))
+ self._items[oid] = ref
+ self._order.append(oid)
+
+ def _remove_by_id(self, oid):
+ """ Remove an item from the set."""
+ if oid in self._items:
+ del self._items[oid]
+ self._order.remove(oid)
+
+ def remove(self, item):
+ """ Remove an item from the set."""
+ self._remove_by_id(id(item))
+
+ def empty(self):
+ """ Clear all objects from the set."""
+ self._items = {}
+ self._order = []
+
+ def __len__(self):
+ return len(self._order)
+
+ def __contains__(self, item):
+ oid = id(item)
+ return oid in self._items
+
+ def __iter__(self):
+ return (self._items[oid]() for oid in self._order)
+
+ @property
+ def last(self):
+ if self._order:
+ oid = self._order[-1]
+ return self._items[oid]()
+
+def strings_differ(string1, string2, compare_digest=compare_digest):
+ """Check whether two strings differ while avoiding timing attacks.
+
+ This function returns True if the given strings differ and False
+ if they are equal. It's careful not to leak information about *where*
+ they differ as a result of its running time, which can be very important
+ to avoid certain timing-related crypto attacks:
+
+ http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf
+
+ .. versionchanged:: 1.6
+ Support :func:`hmac.compare_digest` if it is available (Python 2.7.7+
+ and Python 3.3+).
+
+ """
+ len_eq = len(string1) == len(string2)
+ if len_eq:
+ invalid_bits = 0
+ left = string1
+ else:
+ invalid_bits = 1
+ left = string2
+ right = string2
+
+ if compare_digest is not None:
+ invalid_bits += not compare_digest(left, right)
+ else:
+ for a, b in zip(left, right):
+ invalid_bits += a != b
+ return invalid_bits != 0
+
+def object_description(object):
+ """ Produce a human-consumable text description of ``object``,
+ usually involving a Python dotted name. For example:
+
+ >>> object_description(None)
+ u'None'
+ >>> from xml.dom import minidom
+ >>> object_description(minidom)
+ u'module xml.dom.minidom'
+ >>> object_description(minidom.Attr)
+ u'class xml.dom.minidom.Attr'
+ >>> object_description(minidom.Attr.appendChild)
+ u'method appendChild of class xml.dom.minidom.Attr'
+
+ If this method cannot identify the type of the object, a generic
+ description ala ``object <object.__name__>`` will be returned.
+
+ If the object passed is already a string, it is simply returned. If it
+ is a boolean, an integer, a list, a tuple, a set, or ``None``, a
+ (possibly shortened) string representation is returned.
+ """
+ if isinstance(object, string_types):
+ return text_(object)
+ if isinstance(object, integer_types):
+ return text_(str(object))
+ if isinstance(object, (bool, float, type(None))):
+ return text_(str(object))
+ if isinstance(object, set):
+ if PY2:
+ return shortrepr(object, ')')
+ else:
+ return shortrepr(object, '}')
+ if isinstance(object, tuple):
+ return shortrepr(object, ')')
+ if isinstance(object, list):
+ return shortrepr(object, ']')
+ if isinstance(object, dict):
+ return shortrepr(object, '}')
+ module = inspect.getmodule(object)
+ if module is None:
+ return text_('object %s' % str(object))
+ modulename = module.__name__
+ if inspect.ismodule(object):
+ return text_('module %s' % modulename)
+ if inspect.ismethod(object):
+ oself = getattr(object, '__self__', None)
+ if oself is None: # pragma: no cover
+ oself = getattr(object, 'im_self', None)
+ return text_('method %s of class %s.%s' %
+ (object.__name__, modulename,
+ oself.__class__.__name__))
+
+ if inspect.isclass(object):
+ dottedname = '%s.%s' % (modulename, object.__name__)
+ return text_('class %s' % dottedname)
+ if inspect.isfunction(object):
+ dottedname = '%s.%s' % (modulename, object.__name__)
+ return text_('function %s' % dottedname)
+ return text_('object %s' % str(object))
+
+def shortrepr(object, closer):
+ r = str(object)
+ if len(r) > 100:
+ r = r[:100] + ' ... %s' % closer
+ return r
+
+class Sentinel(object):
+ def __init__(self, repr):
+ self.repr = repr
+
+ def __repr__(self):
+ return self.repr
+
+FIRST = Sentinel('FIRST')
+LAST = Sentinel('LAST')
+
+class TopologicalSorter(object):
+ """ A utility class which can be used to perform topological sorts against
+ tuple-like data."""
+ def __init__(
+ self,
+ default_before=LAST,
+ default_after=None,
+ first=FIRST,
+ last=LAST,
+ ):
+ self.names = []
+ self.req_before = set()
+ self.req_after = set()
+ self.name2before = {}
+ self.name2after = {}
+ self.name2val = {}
+ self.order = []
+ self.default_before = default_before
+ self.default_after = default_after
+ self.first = first
+ self.last = last
+
+ def values(self):
+ return self.name2val.values()
+
+ def remove(self, name):
+ """ Remove a node from the sort input """
+ self.names.remove(name)
+ del self.name2val[name]
+ after = self.name2after.pop(name, [])
+ if after:
+ self.req_after.remove(name)
+ for u in after:
+ self.order.remove((u, name))
+ before = self.name2before.pop(name, [])
+ if before:
+ self.req_before.remove(name)
+ for u in before:
+ self.order.remove((name, u))
+
+ def add(self, name, val, after=None, before=None):
+ """ Add a node to the sort input. The ``name`` should be a string or
+ any other hashable object, the ``val`` should be the sortable (doesn't
+ need to be hashable). ``after`` and ``before`` represents the name of
+ one of the other sortables (or a sequence of such named) or one of the
+ special sentinel values :attr:`pyramid.util.FIRST`` or
+ :attr:`pyramid.util.LAST` representing the first or last positions
+ respectively. ``FIRST`` and ``LAST`` can also be part of a sequence
+ passed as ``before`` or ``after``. A sortable should not be added
+ after LAST or before FIRST. An example::
+
+ sorter = TopologicalSorter()
+ sorter.add('a', {'a':1}, before=LAST, after='b')
+ sorter.add('b', {'b':2}, before=LAST, after='c')
+ sorter.add('c', {'c':3})
+
+ sorter.sorted() # will be {'c':3}, {'b':2}, {'a':1}
+
+ """
+ if name in self.names:
+ self.remove(name)
+ self.names.append(name)
+ self.name2val[name] = val
+ if after is None and before is None:
+ before = self.default_before
+ after = self.default_after
+ if after is not None:
+ if not is_nonstr_iter(after):
+ after = (after,)
+ self.name2after[name] = after
+ self.order += [(u, name) for u in after]
+ self.req_after.add(name)
+ if before is not None:
+ if not is_nonstr_iter(before):
+ before = (before,)
+ self.name2before[name] = before
+ self.order += [(name, o) for o in before]
+ self.req_before.add(name)
+
+
+ def sorted(self):
+ """ Returns the sort input values in topologically sorted order"""
+ order = [(self.first, self.last)]
+ roots = []
+ graph = {}
+ names = [self.first, self.last]
+ names.extend(self.names)
+
+ for a, b in self.order:
+ order.append((a, b))
+
+ def add_node(node):
+ if node not in graph:
+ roots.append(node)
+ graph[node] = [0] # 0 = number of arcs coming into this node
+
+ def add_arc(fromnode, tonode):
+ graph[fromnode].append(tonode)
+ graph[tonode][0] += 1
+ if tonode in roots:
+ roots.remove(tonode)
+
+ for name in names:
+ add_node(name)
+
+ has_before, has_after = set(), set()
+ for a, b in order:
+ if a in names and b in names: # deal with missing dependencies
+ add_arc(a, b)
+ has_before.add(a)
+ has_after.add(b)
+
+ if not self.req_before.issubset(has_before):
+ raise ConfigurationError(
+ 'Unsatisfied before dependencies: %s'
+ % (', '.join(sorted(self.req_before - has_before)))
+ )
+ if not self.req_after.issubset(has_after):
+ raise ConfigurationError(
+ 'Unsatisfied after dependencies: %s'
+ % (', '.join(sorted(self.req_after - has_after)))
+ )
+
+ sorted_names = []
+
+ while roots:
+ root = roots.pop(0)
+ sorted_names.append(root)
+ children = graph[root][1:]
+ for child in children:
+ arcs = graph[child][0]
+ arcs -= 1
+ graph[child][0] = arcs
+ if arcs == 0:
+ roots.insert(0, child)
+ del graph[root]
+
+ if graph:
+ # loop in input
+ cycledeps = {}
+ for k, v in graph.items():
+ cycledeps[k] = v[1:]
+ raise CyclicDependencyError(cycledeps)
+
+ result = []
+
+ for name in sorted_names:
+ if name in self.names:
+ result.append((name, self.name2val[name]))
+
+ return result
+
+
+def get_callable_name(name):
+ """
+ Verifies that the ``name`` is ascii and will raise a ``ConfigurationError``
+ if it is not.
+ """
+ try:
+ return native_(name, 'ascii')
+ except (UnicodeEncodeError, UnicodeDecodeError):
+ msg = (
+ '`name="%s"` is invalid. `name` must be ascii because it is '
+ 'used on __name__ of the method'
+ )
+ raise ConfigurationError(msg % name)
+
+@contextmanager
+def hide_attrs(obj, *attrs):
+ """
+ Temporarily delete object attrs and restore afterward.
+ """
+ obj_vals = obj.__dict__ if obj is not None else {}
+ saved_vals = {}
+ for name in attrs:
+ saved_vals[name] = obj_vals.pop(name, _marker)
+ try:
+ yield
+ finally:
+ for name in attrs:
+ saved_val = saved_vals[name]
+ if saved_val is not _marker:
+ obj_vals[name] = saved_val
+ elif name in obj_vals:
+ del obj_vals[name]
+
+
+def is_same_domain(host, pattern):
+ """
+ Return ``True`` if the host is either an exact match or a match
+ to the wildcard pattern.
+ Any pattern beginning with a period matches a domain and all of its
+ subdomains. (e.g. ``.example.com`` matches ``example.com`` and
+ ``foo.example.com``). Anything else is an exact string match.
+ """
+ if not pattern:
+ return False
+
+ pattern = pattern.lower()
+ return (pattern[0] == "." and
+ (host.endswith(pattern) or host == pattern[1:]) or
+ pattern == host)
+
+
+def make_contextmanager(fn):
+ if inspect.isgeneratorfunction(fn):
+ return contextmanager(fn)
+
+ if fn is None:
+ fn = lambda *a, **kw: None
+
+ @contextmanager
+ @functools.wraps(fn)
+ def wrapper(*a, **kw):
+ yield fn(*a, **kw)
+ return wrapper
+
+
+def takes_one_arg(callee, attr=None, argname=None):
+ ismethod = False
+ if attr is None:
+ attr = '__call__'
+ if inspect.isroutine(callee):
+ fn = callee
+ elif inspect.isclass(callee):
+ try:
+ fn = callee.__init__
+ except AttributeError:
+ return False
+ ismethod = hasattr(fn, '__call__')
+ else:
+ try:
+ fn = getattr(callee, attr)
+ except AttributeError:
+ return False
+
+ try:
+ argspec = getargspec(fn)
+ except TypeError:
+ return False
+
+ args = argspec[0]
+
+ if hasattr(fn, im_func) or ismethod:
+ # it's an instance method (or unbound method on py2)
+ if not args:
+ return False
+ args = args[1:]
+
+ if not args:
+ return False
+
+ if len(args) == 1:
+ return True
+
+ if argname:
+
+ defaults = argspec[3]
+ if defaults is None:
+ defaults = ()
+
+ if args[0] == argname:
+ if len(args) - len(defaults) == 1:
+ return True
+
+ return False
+
+
+class SimpleSerializer(object):
+ def loads(self, bstruct):
+ return native_(bstruct)
+
+ def dumps(self, appstruct):
+ return bytes_(appstruct)
diff --git a/src/pyramid/view.py b/src/pyramid/view.py
new file mode 100644
index 000000000..769328344
--- /dev/null
+++ b/src/pyramid/view.py
@@ -0,0 +1,761 @@
+import itertools
+import sys
+
+import venusian
+
+from zope.interface import providedBy
+
+from pyramid.interfaces import (
+ IRoutesMapper,
+ IMultiView,
+ ISecuredView,
+ IView,
+ IViewClassifier,
+ IRequest,
+ IExceptionViewClassifier,
+ )
+
+from pyramid.compat import decode_path_info
+from pyramid.compat import reraise as reraise_
+
+from pyramid.exceptions import (
+ ConfigurationError,
+ PredicateMismatch,
+)
+
+from pyramid.httpexceptions import (
+ HTTPNotFound,
+ HTTPTemporaryRedirect,
+ default_exceptionresponse_view,
+ )
+
+from pyramid.threadlocal import (
+ get_current_registry,
+ manager,
+ )
+
+from pyramid.util import hide_attrs
+
+_marker = object()
+
+def render_view_to_response(context, request, name='', secure=True):
+ """ Call the :term:`view callable` configured with a :term:`view
+ configuration` that matches the :term:`view name` ``name``
+ registered against the specified ``context`` and ``request`` and
+ return a :term:`response` object. 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``).
+
+ If `secure`` is ``True``, and the :term:`view callable` found is
+ protected by a permission, the permission will be checked before calling
+ the view function. If the permission check disallows view execution
+ (based on the current :term:`authorization policy`), a
+ :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised.
+ The exception's ``args`` attribute explains why the view access was
+ disallowed.
+
+ If ``secure`` is ``False``, no permission checking is done."""
+
+ registry = getattr(request, 'registry', None)
+ if registry is None:
+ registry = get_current_registry()
+
+ context_iface = providedBy(context)
+ # We explicitly pass in the interfaces provided by the request as
+ # request_iface to _call_view; we don't want _call_view to use
+ # request.request_iface, because render_view_to_response and friends are
+ # pretty much limited to finding views that are not views associated with
+ # routes, and the only thing request.request_iface is used for is to find
+ # route-based views. The render_view_to_response API is (and always has
+ # been) a stepchild API reserved for use of those who actually use
+ # traversal. Doing this fixes an infinite recursion bug introduced in
+ # Pyramid 1.6a1, and causes the render_view* APIs to behave as they did in
+ # 1.5 and previous. We should probably provide some sort of different API
+ # that would allow people to find views for routes. See
+ # https://github.com/Pylons/pyramid/issues/1643 for more info.
+ request_iface = providedBy(request)
+
+ response = _call_view(
+ registry,
+ request,
+ context,
+ context_iface,
+ name,
+ secure=secure,
+ request_iface=request_iface,
+ )
+
+ return response # NB: might be None
+
+
+def render_view_to_iterable(context, request, name='', secure=True):
+ """ Call the :term:`view callable` configured with a :term:`view
+ configuration` that matches the :term:`view name` ``name``
+ registered against the specified ``context`` and ``request`` and
+ return an iterable object which represents the body of a response.
+ 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 function will raise a
+ :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 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
+ permission will be checked before the view function is invoked. If the
+ permission check disallows view execution (based on the current
+ :term:`authentication policy`), a
+ :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised; its
+ ``args`` attribute explains why the view access was disallowed.
+
+ If ``secure`` is ``False``, no permission checking is
+ done."""
+ response = render_view_to_response(context, request, name, secure)
+ if response is None:
+ return None
+ return response.app_iter
+
+def render_view(context, request, name='', secure=True):
+ """ Call the :term:`view callable` configured with a :term:`view
+ 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 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
+ function will raise a :exc:`ValueError` if a view function is
+ found and called but the view function's result does not have an
+ ``app_iter`` attribute. This function will return ``None`` if a
+ corresponding view cannot be found.
+
+ If ``secure`` is ``True``, and the view is protected by a permission, the
+ permission will be checked before the view is invoked. If the permission
+ check disallows view execution (based on the current :term:`authorization
+ policy`), a :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be
+ raised; its ``args`` attribute explains why the view access was
+ disallowed.
+
+ If ``secure`` is ``False``, no permission checking is done."""
+ iterable = render_view_to_iterable(context, request, name, secure)
+ if iterable is None:
+ return None
+ return b''.join(iterable)
+
+class view_config(object):
+ """ A function, class or method :term:`decorator` which allows a
+ developer to create view registrations nearer to a :term:`view
+ callable` definition than use :term:`imperative
+ configuration` to do the same.
+
+ For example, this code in a module ``views.py``::
+
+ from resources import MyResource
+
+ @view_config(name='my_view', context=MyResource, permission='read',
+ route_name='site1')
+ def my_view(context, request):
+ return 'OK'
+
+ Might replace the following call to the
+ :meth:`pyramid.config.Configurator.add_view` method::
+
+ import views
+ from resources import MyResource
+ config.add_view(views.my_view, context=MyResource, name='my_view',
+ permission='read', route_name='site1')
+
+ .. note: :class:`pyramid.view.view_config` is also importable, for
+ backwards compatibility purposes, as the name
+ :class:`pyramid.view.bfg_view`.
+
+ :class:`pyramid.view.view_config` supports the following keyword
+ arguments: ``context``, ``exception``, ``permission``, ``name``,
+ ``request_type``, ``route_name``, ``request_method``, ``request_param``,
+ ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``,
+ ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``,
+ ``require_csrf``, ``match_param``, ``check_csrf``, ``physical_path``, and
+ ``view_options``.
+
+ The meanings of these arguments are the same as the arguments passed to
+ :meth:`pyramid.config.Configurator.add_view`. If any argument is left
+ out, its default will be the equivalent ``add_view`` default.
+
+ Two additional keyword arguments which will be passed to the
+ :term:`venusian` ``attach`` function are ``_depth`` and ``_category``.
+
+ ``_depth`` is provided for people who wish to reuse this class from another
+ decorator. The default value is ``0`` and should be specified relative to
+ the ``view_config`` invocation. 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.
+
+ ``_category`` sets the decorator category name. It can be useful in
+ combination with the ``category`` argument of ``scan`` to control which
+ views should be processed.
+
+ See the :py:func:`venusian.attach` function in Venusian for more
+ information about the ``_depth`` and ``_category`` arguments.
+
+ .. seealso::
+
+ See also :ref:`mapping_views_using_a_decorator_section` for
+ details about using :class:`pyramid.view.view_config`.
+
+ .. warning::
+
+ ``view_config`` will work ONLY on module top level members
+ because of the limitation of ``venusian.Scanner.scan``.
+
+ """
+ venusian = venusian # for testing injection
+ def __init__(self, **settings):
+ if 'for_' in settings:
+ if settings.get('context') is None:
+ settings['context'] = settings['for_']
+ self.__dict__.update(settings)
+
+ def __call__(self, wrapped):
+ settings = self.__dict__.copy()
+ depth = settings.pop('_depth', 0)
+ category = settings.pop('_category', 'pyramid')
+
+ 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=category,
+ depth=depth + 1)
+
+ if info.scope == 'class':
+ # if the decorator was attached to a method in a class, or
+ # otherwise executed at class scope, we need to set an
+ # 'attr' into the settings if one isn't already in there
+ if settings.get('attr') is None:
+ settings['attr'] = wrapped.__name__
+
+ settings['_info'] = info.codeinfo # fbo "action_method"
+ return wrapped
+
+bfg_view = view_config # bw compat (forever)
+
+class view_defaults(view_config):
+ """ A class :term:`decorator` which, when applied to a class, will
+ provide defaults for all view configurations that use the class. This
+ decorator accepts all the arguments accepted by
+ :meth:`pyramid.view.view_config`, and each has the same meaning.
+
+ See :ref:`view_defaults` for more information.
+ """
+
+ def __call__(self, wrapped):
+ wrapped.__view_defaults__ = self.__dict__.copy()
+ return wrapped
+
+class AppendSlashNotFoundViewFactory(object):
+ """ There can only be one :term:`Not Found view` in any
+ :app:`Pyramid` application. Even if you use
+ :func:`pyramid.view.append_slash_notfound_view` as the Not
+ Found view, :app:`Pyramid` still must generate a ``404 Not
+ Found`` response when it cannot redirect to a slash-appended URL;
+ this not found response will be visible to site users.
+
+ If you don't care what this 404 response looks like, and you only
+ need redirections to slash-appended route URLs, you may use the
+ :func:`pyramid.view.append_slash_notfound_view` object as the
+ Not Found view. However, if you wish to use a *custom* notfound
+ view callable when a URL cannot be redirected to a slash-appended
+ URL, you may wish to use an instance of this class as the Not
+ Found view, supplying a :term:`view callable` to be used as the
+ custom notfound view as the first argument to its constructor.
+ For instance:
+
+ .. code-block:: python
+
+ from pyramid.httpexceptions import HTTPNotFound
+ from pyramid.view import AppendSlashNotFoundViewFactory
+
+ def notfound_view(context, request): return HTTPNotFound('nope')
+
+ custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view)
+ config.add_view(custom_append_slash, context=HTTPNotFound)
+
+ The ``notfound_view`` supplied must adhere to the two-argument
+ view callable calling convention of ``(context, request)``
+ (``context`` will be the exception object).
+
+ .. deprecated:: 1.3
+
+ """
+ def __init__(self, notfound_view=None, redirect_class=HTTPTemporaryRedirect):
+ if notfound_view is None:
+ notfound_view = default_exceptionresponse_view
+ self.notfound_view = notfound_view
+ self.redirect_class = redirect_class
+
+ def __call__(self, context, request):
+ path = decode_path_info(request.environ['PATH_INFO'] or '/')
+ registry = request.registry
+ mapper = registry.queryUtility(IRoutesMapper)
+ if mapper is not None and not path.endswith('/'):
+ slashpath = path + '/'
+ for route in mapper.get_routes():
+ if route.match(slashpath) is not None:
+ qs = request.query_string
+ if qs:
+ qs = '?' + qs
+ return self.redirect_class(location=request.path + '/' + qs)
+ return self.notfound_view(context, request)
+
+append_slash_notfound_view = AppendSlashNotFoundViewFactory()
+append_slash_notfound_view.__doc__ = """\
+For behavior like Django's ``APPEND_SLASH=True``, use this view as the
+:term:`Not Found view` in your application.
+
+When this view is the Not Found view (indicating that no view was found), and
+any routes have been defined in the configuration of your application, if the
+value of the ``PATH_INFO`` WSGI environment variable does not already end in
+a slash, and if the value of ``PATH_INFO`` *plus* a slash matches any route's
+path, do an HTTP redirect to the slash-appended PATH_INFO. Note that this
+will *lose* ``POST`` data information (turning it into a GET), so you
+shouldn't rely on this to redirect POST requests. Note also that static
+routes are not considered when attempting to find a matching route.
+
+Use the :meth:`pyramid.config.Configurator.add_view` method to configure this
+view as the Not Found view::
+
+ from pyramid.httpexceptions import HTTPNotFound
+ from pyramid.view import append_slash_notfound_view
+ config.add_view(append_slash_notfound_view, context=HTTPNotFound)
+
+.. deprecated:: 1.3
+
+"""
+
+class notfound_view_config(object):
+ """
+ .. versionadded:: 1.3
+
+ An analogue of :class:`pyramid.view.view_config` which registers a
+ :term:`Not Found View` using
+ :meth:`pyramid.config.Configurator.add_notfound_view`.
+
+ The ``notfound_view_config`` constructor accepts most of the same arguments
+ as the constructor of :class:`pyramid.view.view_config`. It can be used
+ in the same places, and behaves in largely the same way, except it always
+ registers a not found exception view instead of a 'normal' view.
+
+ Example:
+
+ .. code-block:: python
+
+ from pyramid.view import notfound_view_config
+ from pyramid.response import Response
+
+ @notfound_view_config()
+ def notfound(request):
+ return Response('Not found!', status='404 Not Found')
+
+ All arguments except ``append_slash`` have the same meaning as
+ :meth:`pyramid.view.view_config` and each predicate
+ argument restricts the set of circumstances under which this notfound
+ view will be invoked.
+
+ If ``append_slash`` is ``True``, when the Not Found View is invoked, and
+ the current path info does not end in a slash, the notfound logic will
+ attempt to find a :term:`route` that matches the request's path info
+ suffixed with a slash. If such a route exists, Pyramid will issue a
+ redirect to the URL implied by the route; if it does not, Pyramid will
+ return the result of the view callable provided as ``view``, as normal.
+
+ If the argument provided as ``append_slash`` is not a boolean but
+ instead implements :class:`~pyramid.interfaces.IResponse`, the
+ append_slash logic will behave as if ``append_slash=True`` was passed,
+ but the provided class will be used as the response class instead of
+ the default :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect`
+ response class when a redirect is performed. For example:
+
+ .. code-block:: python
+
+ from pyramid.httpexceptions import (
+ HTTPMovedPermanently,
+ HTTPNotFound
+ )
+
+ @notfound_view_config(append_slash=HTTPMovedPermanently)
+ def aview(request):
+ return HTTPNotFound('not found')
+
+ The above means that a redirect to a slash-appended route will be
+ attempted, but instead of :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect`
+ being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will
+ be used` for the redirect response if a slash-appended route is found.
+
+ See :ref:`changing_the_notfound_view` for detailed usage information.
+
+ .. versionchanged:: 1.9.1
+ Added the ``_depth`` and ``_category`` arguments.
+
+ """
+
+ venusian = venusian
+
+ def __init__(self, **settings):
+ self.__dict__.update(settings)
+
+ def __call__(self, wrapped):
+ settings = self.__dict__.copy()
+ depth = settings.pop('_depth', 0)
+ category = settings.pop('_category', 'pyramid')
+
+ def callback(context, name, ob):
+ config = context.config.with_package(info.module)
+ config.add_notfound_view(view=ob, **settings)
+
+ info = self.venusian.attach(wrapped, callback, category=category,
+ depth=depth + 1)
+
+ if info.scope == 'class':
+ # if the decorator was attached to a method in a class, or
+ # otherwise executed at class scope, we need to set an
+ # 'attr' into the settings if one isn't already in there
+ if settings.get('attr') is None:
+ settings['attr'] = wrapped.__name__
+
+ settings['_info'] = info.codeinfo # fbo "action_method"
+ return wrapped
+
+class forbidden_view_config(object):
+ """
+ .. versionadded:: 1.3
+
+ An analogue of :class:`pyramid.view.view_config` which registers a
+ :term:`forbidden view` using
+ :meth:`pyramid.config.Configurator.add_forbidden_view`.
+
+ The forbidden_view_config constructor accepts most of the same arguments
+ as the constructor of :class:`pyramid.view.view_config`. It can be used
+ in the same places, and behaves in largely the same way, except it always
+ registers a forbidden exception view instead of a 'normal' view.
+
+ Example:
+
+ .. code-block:: python
+
+ from pyramid.view import forbidden_view_config
+ from pyramid.response import Response
+
+ @forbidden_view_config()
+ def forbidden(request):
+ return Response('You are not allowed', status='403 Forbidden')
+
+ All arguments passed to this function have the same meaning as
+ :meth:`pyramid.view.view_config` and each predicate argument restricts
+ the set of circumstances under which this notfound view will be invoked.
+
+ See :ref:`changing_the_forbidden_view` for detailed usage information.
+
+ .. versionchanged:: 1.9.1
+ Added the ``_depth`` and ``_category`` arguments.
+
+ """
+
+ venusian = venusian
+
+ def __init__(self, **settings):
+ self.__dict__.update(settings)
+
+ def __call__(self, wrapped):
+ settings = self.__dict__.copy()
+ depth = settings.pop('_depth', 0)
+ category = settings.pop('_category', 'pyramid')
+
+ def callback(context, name, ob):
+ config = context.config.with_package(info.module)
+ config.add_forbidden_view(view=ob, **settings)
+
+ info = self.venusian.attach(wrapped, callback, category=category,
+ depth=depth + 1)
+
+ if info.scope == 'class':
+ # if the decorator was attached to a method in a class, or
+ # otherwise executed at class scope, we need to set an
+ # 'attr' into the settings if one isn't already in there
+ if settings.get('attr') is None:
+ settings['attr'] = wrapped.__name__
+
+ settings['_info'] = info.codeinfo # fbo "action_method"
+ return wrapped
+
+class exception_view_config(object):
+ """
+ .. versionadded:: 1.8
+
+ An analogue of :class:`pyramid.view.view_config` which registers an
+ :term:`exception view` using
+ :meth:`pyramid.config.Configurator.add_exception_view`.
+
+ The ``exception_view_config`` constructor requires an exception context,
+ and additionally accepts most of the same arguments as the constructor of
+ :class:`pyramid.view.view_config`. It can be used in the same places,
+ and behaves in largely the same way, except it always registers an
+ exception view instead of a "normal" view that dispatches on the request
+ :term:`context`.
+
+ Example:
+
+ .. code-block:: python
+
+ from pyramid.view import exception_view_config
+ from pyramid.response import Response
+
+ @exception_view_config(ValueError, renderer='json')
+ def error_view(request):
+ return {'error': str(request.exception)}
+
+ All arguments passed to this function have the same meaning as
+ :meth:`pyramid.view.view_config`, and each predicate argument restricts
+ the set of circumstances under which this exception view will be invoked.
+
+ .. versionchanged:: 1.9.1
+ Added the ``_depth`` and ``_category`` arguments.
+
+ """
+ venusian = venusian
+
+ def __init__(self, *args, **settings):
+ if 'context' not in settings and len(args) > 0:
+ exception, args = args[0], args[1:]
+ settings['context'] = exception
+ if len(args) > 0:
+ raise ConfigurationError('unknown positional arguments')
+ self.__dict__.update(settings)
+
+ def __call__(self, wrapped):
+ settings = self.__dict__.copy()
+ depth = settings.pop('_depth', 0)
+ category = settings.pop('_category', 'pyramid')
+
+ def callback(context, name, ob):
+ config = context.config.with_package(info.module)
+ config.add_exception_view(view=ob, **settings)
+
+ info = self.venusian.attach(wrapped, callback, category=category,
+ depth=depth + 1)
+
+ if info.scope == 'class':
+ # if the decorator was attached to a method in a class, or
+ # otherwise executed at class scope, we need to set an
+ # 'attr' in the settings if one isn't already in there
+ if settings.get('attr') is None:
+ settings['attr'] = wrapped.__name__
+
+ settings['_info'] = info.codeinfo # fbo "action_method"
+ return wrapped
+
+def _find_views(
+ registry,
+ request_iface,
+ context_iface,
+ view_name,
+ view_types=None,
+ view_classifier=None,
+ ):
+ if view_types is None:
+ view_types = (IView, ISecuredView, IMultiView)
+ if view_classifier is None:
+ view_classifier = IViewClassifier
+ registered = registry.adapters.registered
+ cache = registry._view_lookup_cache
+ views = cache.get((request_iface, context_iface, view_name))
+ if views is None:
+ views = []
+ for req_type, ctx_type in itertools.product(
+ request_iface.__sro__, context_iface.__sro__
+ ):
+ source_ifaces = (view_classifier, req_type, ctx_type)
+ for view_type in view_types:
+ view_callable = registered(
+ source_ifaces,
+ view_type,
+ name=view_name,
+ )
+ if view_callable is not None:
+ views.append(view_callable)
+ if views:
+ # do not cache view lookup misses. rationale: dont allow cache to
+ # grow without bound if somebody tries to hit the site with many
+ # missing URLs. we could use an LRU cache instead, but then
+ # purposeful misses by an attacker would just blow out the cache
+ # anyway. downside: misses will almost always consume more CPU than
+ # hits in steady state.
+ with registry._lock:
+ cache[(request_iface, context_iface, view_name)] = views
+
+ return views
+
+def _call_view(
+ registry,
+ request,
+ context,
+ context_iface,
+ view_name,
+ view_types=None,
+ view_classifier=None,
+ secure=True,
+ request_iface=None,
+ ):
+ if request_iface is None:
+ request_iface = getattr(request, 'request_iface', IRequest)
+ view_callables = _find_views(
+ registry,
+ request_iface,
+ context_iface,
+ view_name,
+ view_types=view_types,
+ view_classifier=view_classifier,
+ )
+
+ pme = None
+ response = None
+
+ for view_callable in view_callables:
+ # look for views that meet the predicate criteria
+ try:
+ if not secure:
+ # the view will have a __call_permissive__ attribute if it's
+ # secured; otherwise it won't.
+ view_callable = getattr(
+ view_callable,
+ '__call_permissive__',
+ view_callable
+ )
+
+ # if this view is secured, it will raise a Forbidden
+ # appropriately if the executing user does not have the proper
+ # permission
+ response = view_callable(context, request)
+ return response
+ except PredicateMismatch as _pme:
+ pme = _pme
+
+ if pme is not None:
+ raise pme
+
+ return response
+
+class ViewMethodsMixin(object):
+ """ Request methods mixin for BaseRequest having to do with executing
+ views """
+ def invoke_exception_view(
+ self,
+ exc_info=None,
+ request=None,
+ secure=True,
+ reraise=False,
+ ):
+ """ Executes an exception view related to the request it's called upon.
+ The arguments it takes are these:
+
+ ``exc_info``
+
+ If provided, should be a 3-tuple in the form provided by
+ ``sys.exc_info()``. If not provided,
+ ``sys.exc_info()`` will be called to obtain the current
+ interpreter exception information. Default: ``None``.
+
+ ``request``
+
+ If the request to be used is not the same one as the instance that
+ this method is called upon, it may be passed here. Default:
+ ``None``.
+
+ ``secure``
+
+ If the exception view should not be rendered if the current user
+ does not have the appropriate permission, this should be ``True``.
+ Default: ``True``.
+
+ ``reraise``
+
+ A boolean indicating whether the original error should be reraised
+ if a :term:`response` object could not be created. If ``False``
+ then an :class:`pyramid.httpexceptions.HTTPNotFound`` exception
+ will be raised. Default: ``False``.
+
+ If a response is generated then ``request.exception`` and
+ ``request.exc_info`` will be left at the values used to render the
+ response. Otherwise the previous values for ``request.exception`` and
+ ``request.exc_info`` will be restored.
+
+ .. versionadded:: 1.7
+
+ .. versionchanged:: 1.9
+ The ``request.exception`` and ``request.exc_info`` properties will
+ reflect the exception used to render the response where previously
+ they were reset to the values prior to invoking the method.
+
+ Also added the ``reraise`` argument.
+
+ """
+ if request is None:
+ request = self
+ registry = getattr(request, 'registry', None)
+ if registry is None:
+ registry = get_current_registry()
+
+ if registry is None:
+ raise RuntimeError("Unable to retrieve registry")
+
+ if exc_info is None:
+ exc_info = sys.exc_info()
+
+ exc = exc_info[1]
+ attrs = request.__dict__
+ context_iface = providedBy(exc)
+
+ # clear old generated request.response, if any; it may
+ # have been mutated by the view, and its state is not
+ # sane (e.g. caching headers)
+ with hide_attrs(request, 'response', 'exc_info', 'exception'):
+ attrs['exception'] = exc
+ attrs['exc_info'] = exc_info
+ # we use .get instead of .__getitem__ below due to
+ # https://github.com/Pylons/pyramid/issues/700
+ request_iface = attrs.get('request_iface', IRequest)
+
+ manager.push({'request': request, 'registry': registry})
+
+ try:
+ response = _call_view(
+ registry,
+ request,
+ exc,
+ context_iface,
+ '',
+ view_types=None,
+ view_classifier=IExceptionViewClassifier,
+ secure=secure,
+ request_iface=request_iface.combined,
+ )
+ except Exception:
+ if reraise:
+ reraise_(*exc_info)
+ raise
+ finally:
+ manager.pop()
+
+ if response is None:
+ if reraise:
+ reraise_(*exc_info)
+ raise HTTPNotFound
+
+ # successful response, overwrite exception/exc_info
+ attrs['exception'] = exc
+ attrs['exc_info'] = exc_info
+ return response
diff --git a/src/pyramid/viewderivers.py b/src/pyramid/viewderivers.py
new file mode 100644
index 000000000..d914a4752
--- /dev/null
+++ b/src/pyramid/viewderivers.py
@@ -0,0 +1,472 @@
+import inspect
+
+from zope.interface import (
+ implementer,
+ provider,
+ )
+
+from pyramid.security import NO_PERMISSION_REQUIRED
+from pyramid.csrf import (
+ check_csrf_origin,
+ check_csrf_token,
+)
+from pyramid.response import Response
+
+from pyramid.interfaces import (
+ IAuthenticationPolicy,
+ IAuthorizationPolicy,
+ IDefaultCSRFOptions,
+ IDefaultPermission,
+ IDebugLogger,
+ IResponse,
+ IViewMapper,
+ IViewMapperFactory,
+ )
+
+from pyramid.compat import (
+ is_bound_method,
+ is_unbound_method,
+ )
+
+from pyramid.exceptions import (
+ ConfigurationError,
+ )
+from pyramid.httpexceptions import HTTPForbidden
+from pyramid.util import (
+ object_description,
+ takes_one_arg,
+)
+from pyramid.view import render_view_to_response
+from pyramid import renderers
+
+
+def view_description(view):
+ try:
+ return view.__text__
+ except AttributeError:
+ # custom view mappers might not add __text__
+ return object_description(view)
+
+def requestonly(view, attr=None):
+ return takes_one_arg(view, attr=attr, argname='request')
+
+@implementer(IViewMapper)
+@provider(IViewMapperFactory)
+class DefaultViewMapper(object):
+ def __init__(self, **kw):
+ self.attr = kw.get('attr')
+
+ def __call__(self, view):
+ if is_unbound_method(view) and self.attr is None:
+ raise ConfigurationError((
+ 'Unbound method calls are not supported, please set the class '
+ 'as your `view` and the method as your `attr`'
+ ))
+
+ if inspect.isclass(view):
+ view = self.map_class(view)
+ else:
+ view = self.map_nonclass(view)
+ return view
+
+ def map_class(self, view):
+ ronly = requestonly(view, self.attr)
+ if ronly:
+ mapped_view = self.map_class_requestonly(view)
+ else:
+ mapped_view = self.map_class_native(view)
+ mapped_view.__text__ = 'method %s of %s' % (
+ self.attr or '__call__', object_description(view))
+ return mapped_view
+
+ def map_nonclass(self, view):
+ # We do more work here than appears necessary to avoid wrapping the
+ # view unless it actually requires wrapping (to avoid function call
+ # overhead).
+ mapped_view = view
+ ronly = requestonly(view, self.attr)
+ if ronly:
+ mapped_view = self.map_nonclass_requestonly(view)
+ elif self.attr:
+ mapped_view = self.map_nonclass_attr(view)
+ if inspect.isroutine(mapped_view):
+ # This branch will be true if the view is a function or a method.
+ # We potentially mutate an unwrapped object here if it's a
+ # function. We do this to avoid function call overhead of
+ # injecting another wrapper. However, we must wrap if the
+ # function is a bound method because we can't set attributes on a
+ # bound method.
+ if is_bound_method(view):
+ _mapped_view = mapped_view
+ def mapped_view(context, request):
+ return _mapped_view(context, request)
+ if self.attr is not None:
+ mapped_view.__text__ = 'attr %s of %s' % (
+ self.attr, object_description(view))
+ else:
+ mapped_view.__text__ = object_description(view)
+ return mapped_view
+
+ def map_class_requestonly(self, view):
+ # its a class that has an __init__ which only accepts request
+ attr = self.attr
+ def _class_requestonly_view(context, request):
+ inst = view(request)
+ request.__view__ = inst
+ if attr is None:
+ response = inst()
+ else:
+ response = getattr(inst, attr)()
+ return response
+ return _class_requestonly_view
+
+ def map_class_native(self, view):
+ # its a class that has an __init__ which accepts both context and
+ # request
+ attr = self.attr
+ def _class_view(context, request):
+ inst = view(context, request)
+ request.__view__ = inst
+ if attr is None:
+ response = inst()
+ else:
+ response = getattr(inst, attr)()
+ return response
+ return _class_view
+
+ def map_nonclass_requestonly(self, view):
+ # its a function that has a __call__ which accepts only a single
+ # request argument
+ attr = self.attr
+ def _requestonly_view(context, request):
+ if attr is None:
+ response = view(request)
+ else:
+ response = getattr(view, attr)(request)
+ return response
+ return _requestonly_view
+
+ def map_nonclass_attr(self, view):
+ # its a function that has a __call__ which accepts both context and
+ # request, but still has an attr
+ def _attr_view(context, request):
+ response = getattr(view, self.attr)(context, request)
+ return response
+ return _attr_view
+
+
+def wraps_view(wrapper):
+ def inner(view, info):
+ wrapper_view = wrapper(view, info)
+ return preserve_view_attrs(view, wrapper_view)
+ return inner
+
+def preserve_view_attrs(view, wrapper):
+ if view is None:
+ return wrapper
+
+ if wrapper is view:
+ return view
+
+ original_view = getattr(view, '__original_view__', None)
+
+ if original_view is None:
+ original_view = view
+
+ wrapper.__wraps__ = view
+ wrapper.__original_view__ = original_view
+ wrapper.__module__ = view.__module__
+ wrapper.__doc__ = view.__doc__
+
+ try:
+ wrapper.__name__ = view.__name__
+ except AttributeError:
+ wrapper.__name__ = repr(view)
+
+ # attrs that may not exist on "view", but, if so, must be attached to
+ # "wrapped view"
+ for attr in ('__permitted__', '__call_permissive__', '__permission__',
+ '__predicated__', '__predicates__', '__accept__',
+ '__order__', '__text__'):
+ try:
+ setattr(wrapper, attr, getattr(view, attr))
+ except AttributeError:
+ pass
+
+ return wrapper
+
+def mapped_view(view, info):
+ mapper = info.options.get('mapper')
+ if mapper is None:
+ mapper = getattr(view, '__view_mapper__', None)
+ if mapper is None:
+ mapper = info.registry.queryUtility(IViewMapperFactory)
+ if mapper is None:
+ mapper = DefaultViewMapper
+
+ mapped_view = mapper(**info.options)(view)
+ return mapped_view
+
+mapped_view.options = ('mapper', 'attr')
+
+def owrapped_view(view, info):
+ wrapper_viewname = info.options.get('wrapper')
+ viewname = info.options.get('name')
+ if not wrapper_viewname:
+ return view
+ def _owrapped_view(context, request):
+ response = view(context, request)
+ request.wrapped_response = response
+ request.wrapped_body = response.body
+ request.wrapped_view = view
+ wrapped_response = render_view_to_response(context, request,
+ wrapper_viewname)
+ if wrapped_response is None:
+ raise ValueError(
+ 'No wrapper view named %r found when executing view '
+ 'named %r' % (wrapper_viewname, viewname))
+ return wrapped_response
+ return _owrapped_view
+
+owrapped_view.options = ('name', 'wrapper')
+
+def http_cached_view(view, info):
+ if info.settings.get('prevent_http_cache', False):
+ return view
+
+ seconds = info.options.get('http_cache')
+
+ if seconds is None:
+ return view
+
+ options = {}
+
+ if isinstance(seconds, (tuple, list)):
+ try:
+ seconds, options = seconds
+ except ValueError:
+ raise ConfigurationError(
+ 'If http_cache parameter is a tuple or list, it must be '
+ 'in the form (seconds, options); not %s' % (seconds,))
+
+ def wrapper(context, request):
+ response = view(context, request)
+ prevent_caching = getattr(response.cache_control, 'prevent_auto',
+ False)
+ if not prevent_caching:
+ response.cache_expires(seconds, **options)
+ return response
+
+ return wrapper
+
+http_cached_view.options = ('http_cache',)
+
+def secured_view(view, info):
+ for wrapper in (_secured_view, _authdebug_view):
+ view = wraps_view(wrapper)(view, info)
+ return view
+
+secured_view.options = ('permission',)
+
+def _secured_view(view, info):
+ permission = explicit_val = info.options.get('permission')
+ if permission is None:
+ permission = info.registry.queryUtility(IDefaultPermission)
+ if permission == NO_PERMISSION_REQUIRED:
+ # allow views registered within configurations that have a
+ # default permission to explicitly override the default
+ # permission, replacing it with no permission at all
+ permission = None
+
+ wrapped_view = view
+ authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
+ authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+
+ # no-op on exception-only views without an explicit permission
+ if explicit_val is None and info.exception_only:
+ return view
+
+ if authn_policy and authz_policy and (permission is not None):
+ def permitted(context, request):
+ principals = authn_policy.effective_principals(request)
+ return authz_policy.permits(context, principals, permission)
+ def secured_view(context, request):
+ result = permitted(context, request)
+ if result:
+ return view(context, request)
+ view_name = getattr(view, '__name__', view)
+ msg = getattr(
+ request, 'authdebug_message',
+ 'Unauthorized: %s failed permission check' % view_name)
+ raise HTTPForbidden(msg, result=result)
+ wrapped_view = secured_view
+ wrapped_view.__call_permissive__ = view
+ wrapped_view.__permitted__ = permitted
+ wrapped_view.__permission__ = permission
+
+ return wrapped_view
+
+def _authdebug_view(view, info):
+ wrapped_view = view
+ settings = info.settings
+ permission = explicit_val = info.options.get('permission')
+ if permission is None:
+ permission = info.registry.queryUtility(IDefaultPermission)
+ authn_policy = info.registry.queryUtility(IAuthenticationPolicy)
+ authz_policy = info.registry.queryUtility(IAuthorizationPolicy)
+ logger = info.registry.queryUtility(IDebugLogger)
+
+ # no-op on exception-only views without an explicit permission
+ if explicit_val is None and info.exception_only:
+ return view
+
+ if settings and settings.get('debug_authorization', False):
+ def authdebug_view(context, request):
+ view_name = getattr(request, 'view_name', None)
+
+ if authn_policy and authz_policy:
+ if permission is NO_PERMISSION_REQUIRED:
+ msg = 'Allowed (NO_PERMISSION_REQUIRED)'
+ elif permission is None:
+ msg = 'Allowed (no permission registered)'
+ else:
+ principals = authn_policy.effective_principals(request)
+ msg = str(authz_policy.permits(
+ context, principals, permission))
+ else:
+ msg = 'Allowed (no authorization policy in use)'
+
+ view_name = getattr(request, 'view_name', None)
+ url = getattr(request, 'url', None)
+ msg = ('debug_authorization of url %s (view name %r against '
+ 'context %r): %s' % (url, view_name, context, msg))
+ if logger:
+ logger.debug(msg)
+ if request is not None:
+ request.authdebug_message = msg
+ return view(context, request)
+ wrapped_view = authdebug_view
+
+ return wrapped_view
+
+def rendered_view(view, info):
+ # one way or another this wrapper must produce a Response (unless
+ # the renderer is a NullRendererHelper)
+ renderer = info.options.get('renderer')
+ if renderer is None:
+ # register a default renderer if you want super-dynamic
+ # rendering. registering a default renderer will also allow
+ # override_renderer to work if a renderer is left unspecified for
+ # a view registration.
+ def viewresult_to_response(context, request):
+ result = view(context, request)
+ if result.__class__ is Response: # common case
+ response = result
+ else:
+ response = info.registry.queryAdapterOrSelf(result, IResponse)
+ if response is None:
+ if result is None:
+ append = (' You may have forgotten to return a value '
+ 'from the view callable.')
+ elif isinstance(result, dict):
+ append = (' You may have forgotten to define a '
+ 'renderer in the view configuration.')
+ else:
+ append = ''
+
+ msg = ('Could not convert return value of the view '
+ 'callable %s into a response object. '
+ 'The value returned was %r.' + append)
+
+ raise ValueError(msg % (view_description(view), result))
+
+ return response
+
+ return viewresult_to_response
+
+ if renderer is renderers.null_renderer:
+ return view
+
+ def rendered_view(context, request):
+ result = view(context, request)
+ if result.__class__ is Response: # potential common case
+ response = result
+ else:
+ # this must adapt, it can't do a simple interface check
+ # (avoid trying to render webob responses)
+ response = info.registry.queryAdapterOrSelf(result, IResponse)
+ if response is None:
+ attrs = getattr(request, '__dict__', {})
+ if 'override_renderer' in attrs:
+ # renderer overridden by newrequest event or other
+ renderer_name = attrs.pop('override_renderer')
+ view_renderer = renderers.RendererHelper(
+ name=renderer_name,
+ package=info.package,
+ registry=info.registry)
+ else:
+ view_renderer = renderer.clone()
+ if '__view__' in attrs:
+ view_inst = attrs.pop('__view__')
+ else:
+ view_inst = getattr(view, '__original_view__', view)
+ response = view_renderer.render_view(
+ request, result, view_inst, context)
+ return response
+
+ return rendered_view
+
+rendered_view.options = ('renderer',)
+
+def decorated_view(view, info):
+ decorator = info.options.get('decorator')
+ if decorator is None:
+ return view
+ return decorator(view)
+
+decorated_view.options = ('decorator',)
+
+def csrf_view(view, info):
+ explicit_val = info.options.get('require_csrf')
+ defaults = info.registry.queryUtility(IDefaultCSRFOptions)
+ if defaults is None:
+ default_val = False
+ token = 'csrf_token'
+ header = 'X-CSRF-Token'
+ safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"])
+ callback = None
+ else:
+ default_val = defaults.require_csrf
+ token = defaults.token
+ header = defaults.header
+ safe_methods = defaults.safe_methods
+ callback = defaults.callback
+
+ enabled = (
+ explicit_val is True or
+ # fallback to the default val if not explicitly enabled
+ # but only if the view is not an exception view
+ (
+ explicit_val is not False and default_val and
+ not info.exception_only
+ )
+ )
+ # disable if both header and token are disabled
+ enabled = enabled and (token or header)
+ wrapped_view = view
+ if enabled:
+ def csrf_view(context, request):
+ if (
+ request.method not in safe_methods and
+ (callback is None or callback(request))
+ ):
+ check_csrf_origin(request, raises=True)
+ check_csrf_token(request, token, header, raises=True)
+ return view(context, request)
+ wrapped_view = csrf_view
+ return wrapped_view
+
+csrf_view.options = ('require_csrf',)
+
+VIEW = 'VIEW'
+INGRESS = 'INGRESS'
diff --git a/src/pyramid/wsgi.py b/src/pyramid/wsgi.py
new file mode 100644
index 000000000..1c1bded32
--- /dev/null
+++ b/src/pyramid/wsgi.py
@@ -0,0 +1,85 @@
+from functools import wraps
+from pyramid.request import call_app_with_subpath_as_path_info
+
+def wsgiapp(wrapped):
+ """ Decorator to turn a WSGI application into a :app:`Pyramid`
+ :term:`view callable`. This decorator differs from the
+ :func:`pyramid.wsgi.wsgiapp2` decorator inasmuch as fixups of
+ ``PATH_INFO`` and ``SCRIPT_NAME`` within the WSGI environment *are
+ not* performed before the application is invoked.
+
+ E.g., the following in a ``views.py`` module::
+
+ @wsgiapp
+ def hello_world(environ, start_response):
+ body = 'Hello world'
+ start_response('200 OK', [ ('Content-Type', 'text/plain'),
+ ('Content-Length', len(body)) ] )
+ return [body]
+
+ Allows the following call to
+ :meth:`pyramid.config.Configurator.add_view`::
+
+ from views import hello_world
+ config.add_view(hello_world, name='hello_world.txt')
+
+ The ``wsgiapp`` decorator will convert the result of the WSGI
+ application to a :term:`Response` and return it to
+ :app:`Pyramid` as if the WSGI app were a :app:`Pyramid`
+ view.
+
+ """
+
+ if wrapped is None:
+ raise ValueError('wrapped can not be None')
+
+ def decorator(context, request):
+ return request.get_response(wrapped)
+
+ # Support case where wrapped is a callable object instance
+ if getattr(wrapped, '__name__', None):
+ return wraps(wrapped)(decorator)
+ return wraps(wrapped, ('__module__', '__doc__'))(decorator)
+
+def wsgiapp2(wrapped):
+ """ Decorator to turn a WSGI application into a :app:`Pyramid`
+ view callable. This decorator differs from the
+ :func:`pyramid.wsgi.wsgiapp` decorator inasmuch as fixups of
+ ``PATH_INFO`` and ``SCRIPT_NAME`` within the WSGI environment
+ *are* performed before the application is invoked.
+
+ E.g. the following in a ``views.py`` module::
+
+ @wsgiapp2
+ def hello_world(environ, start_response):
+ body = 'Hello world'
+ start_response('200 OK', [ ('Content-Type', 'text/plain'),
+ ('Content-Length', len(body)) ] )
+ return [body]
+
+ Allows the following call to
+ :meth:`pyramid.config.Configurator.add_view`::
+
+ from views import hello_world
+ config.add_view(hello_world, name='hello_world.txt')
+
+ The ``wsgiapp2`` decorator will convert the result of the WSGI
+ application to a Response and return it to :app:`Pyramid` as if the WSGI
+ app were a :app:`Pyramid` view. The ``SCRIPT_NAME`` and ``PATH_INFO``
+ values present in the WSGI environment are fixed up before the
+ application is invoked. In particular, a new WSGI environment is
+ generated, and the :term:`subpath` of the request passed to ``wsgiapp2``
+ is used as the new request's ``PATH_INFO`` and everything preceding the
+ subpath is used as the ``SCRIPT_NAME``. The new environment is passed to
+ the downstream WSGI application."""
+
+ if wrapped is None:
+ raise ValueError('wrapped can not be None')
+
+ def decorator(context, request):
+ return call_app_with_subpath_as_path_info(request, wrapped)
+
+ # Support case where wrapped is a callable object instance
+ if getattr(wrapped, '__name__', None):
+ return wraps(wrapped)(decorator)
+ return wraps(wrapped, ('__module__', '__doc__'))(decorator)