diff options
| author | Michael Merickel <github@m.merickel.org> | 2018-10-15 09:03:53 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-10-15 09:03:53 -0500 |
| commit | 81576ee51564c49d5ff3c1c07f214f22a8438231 (patch) | |
| tree | 5b3fe0b39a0fc33d545733d821738845909f638c /src | |
| parent | 433efe06191a7007ca8c5bf8fafee5c7c1439ebb (diff) | |
| parent | 17e3abf320f6d9cd90f7e5a0352280c2fef584af (diff) | |
| download | pyramid-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')
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 Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.png diff --git a/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png 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 © 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 application generated 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 Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.png diff --git a/src/pyramid/scaffolds/starter/+package+/static/pyramid.png b/src/pyramid/scaffolds/starter/+package+/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/src/pyramid/scaffolds/starter/+package+/static/pyramid.png 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 © 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 application generated 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 Binary files differnew file mode 100644 index 000000000..979203112 --- /dev/null +++ b/src/pyramid/scaffolds/zodb/+package+/static/pyramid-16x16.png diff --git a/src/pyramid/scaffolds/zodb/+package+/static/pyramid.png b/src/pyramid/scaffolds/zodb/+package+/static/pyramid.png Binary files differnew file mode 100644 index 000000000..4ab837be9 --- /dev/null +++ b/src/pyramid/scaffolds/zodb/+package+/static/pyramid.png 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 application generated 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 © 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) |
