summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2015-02-17 19:37:52 -0500
committerChris McDonough <chrism@plope.com>2015-02-17 19:37:52 -0500
commitd4333972ad328f06262ba55ef2a3d24963da95b0 (patch)
treec208aafc07a03934307019120910c3a576982978
parentb63f0c5f6e2f322bd9183d194b4aa774f9f4e93f (diff)
parent4dacb8c24efe98cb14b3ef89f6c9a8b2fd196790 (diff)
downloadpyramid-d4333972ad328f06262ba55ef2a3d24963da95b0.tar.gz
pyramid-d4333972ad328f06262ba55ef2a3d24963da95b0.tar.bz2
pyramid-d4333972ad328f06262ba55ef2a3d24963da95b0.zip
fix merge conflicts
-rw-r--r--CHANGES.txt26
-rw-r--r--CONTRIBUTORS.txt4
-rw-r--r--HACKING.txt2
-rw-r--r--docs/api/interfaces.rst3
-rw-r--r--docs/api/request.rst1
-rw-r--r--docs/api/static.rst6
-rw-r--r--docs/glossary.rst3
-rw-r--r--docs/narr/assets.rst15
-rw-r--r--docs/narr/hooks.rst9
-rw-r--r--pyramid/compat.py104
-rw-r--r--pyramid/config/factories.py22
-rw-r--r--pyramid/config/views.py13
-rw-r--r--pyramid/interfaces.py27
-rw-r--r--pyramid/path.py10
-rw-r--r--pyramid/request.py26
-rw-r--r--pyramid/router.py3
-rw-r--r--pyramid/scripting.py8
-rw-r--r--pyramid/scripts/pcreate.py12
-rw-r--r--pyramid/scripts/pserve.py18
-rw-r--r--pyramid/session.py4
-rw-r--r--pyramid/static.py63
-rw-r--r--pyramid/tests/test_compat.py26
-rw-r--r--pyramid/tests/test_config/test_factories.py19
-rw-r--r--pyramid/tests/test_config/test_views.py26
-rw-r--r--pyramid/tests/test_request.py45
-rw-r--r--pyramid/tests/test_router.py8
-rw-r--r--pyramid/tests/test_scripting.py16
-rw-r--r--pyramid/tests/test_scripts/test_pcreate.py11
-rw-r--r--pyramid/tests/test_static.py16
-rw-r--r--pyramid/tests/test_util.py250
-rw-r--r--pyramid/util.py100
-rw-r--r--tox.ini1
32 files changed, 687 insertions, 210 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 6c21e7298..1c82e5f27 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -10,12 +10,26 @@ Features
or group them for improved conflict detection.
See https://github.com/Pylons/pyramid/pull/1513
+- Add ``pyramid.request.apply_request_extensions`` function which can be
+ used in testing to apply any request extensions configured via
+ ``config.add_request_method``. Previously it was only possible to test
+ the extensions by going through Pyramid's router.
+ See https://github.com/Pylons/pyramid/pull/1581
+
+- pcreate when run without a scaffold argument will now print information on
+ the missing flag, as well as a list of available scaffolds.
+ See https://github.com/Pylons/pyramid/pull/1566 and
+ https://github.com/Pylons/pyramid/issues/1297
+
- Added support / testing for 'pypy3' under Tox and Travis.
See https://github.com/Pylons/pyramid/pull/1469
- Cache busting for static resources has been added and is available via a new
argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``.
- See https://github.com/Pylons/pyramid/pull/1380
+ Core APIs are shipped for both cache busting via query strings and
+ path segments and may be extended to fit into custom asset pipelines.
+ See https://github.com/Pylons/pyramid/pull/1380 and
+ https://github.com/Pylons/pyramid/pull/1583
- Add ``pyramid.config.Configurator.root_package`` attribute and init
parameter to assist with includeable packages that wish to resolve
@@ -89,6 +103,10 @@ Features
Bug Fixes
---------
+- Work around an issue where ``pserve --reload`` would leave terminal echo
+ disabled if it reloaded during a pdb session.
+ See https://github.com/Pylons/pyramid/pull/1577
+
- ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise
``ValueError`` when accidentally passed ``None``.
See https://github.com/Pylons/pyramid/pull/1320
@@ -131,6 +149,12 @@ Bug Fixes
callback and thus behave just like the ``pyramid.renderers.JSON` renderer.
See https://github.com/Pylons/pyramid/pull/1561
+- Prevent "parameters to load are deprecated" ``DeprecationWarning``
+ from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541
+
+- Avoiding timing attacks against CSRF tokens.
+ See https://github.com/Pylons/pyramid/pull/1574
+
Deprecations
------------
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index adf2224a5..4f9bd6e41 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -240,3 +240,7 @@ Contributors
- Adrian Teng, 2014/12/17
- Ilja Everila, 2015/02/05
+
+- Geoffrey T. Dairiki, 2015/02/06
+
+- David Glick, 2015/02/12
diff --git a/HACKING.txt b/HACKING.txt
index 16c17699c..e104869ec 100644
--- a/HACKING.txt
+++ b/HACKING.txt
@@ -195,7 +195,7 @@ Test Coverage
-------------
- The codebase *must* have 100% test statement coverage after each commit.
- You can test coverage via ``tox -e coverage``, or alternately by installing
+ You can test coverage via ``tox -e cover``, or alternately by installing
``nose`` and ``coverage`` into your virtualenv (easiest via ``setup.py
dev``) , and running ``setup.py nosetests --with-coverage``.
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index a62976d8a..de2a664a4 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -56,6 +56,9 @@ Other Interfaces
.. autointerface:: IRenderer
:members:
+ .. autointerface:: IResponseFactory
+ :members:
+
.. autointerface:: IViewMapperFactory
:members:
diff --git a/docs/api/request.rst b/docs/api/request.rst
index dd68fa09c..b325ad076 100644
--- a/docs/api/request.rst
+++ b/docs/api/request.rst
@@ -369,3 +369,4 @@
that used as ``request.GET``, ``request.POST``, and ``request.params``),
see :class:`pyramid.interfaces.IMultiDict`.
+.. autofunction:: apply_request_extensions(request)
diff --git a/docs/api/static.rst b/docs/api/static.rst
index 543e526ad..b6b279139 100644
--- a/docs/api/static.rst
+++ b/docs/api/static.rst
@@ -9,6 +9,12 @@
:members:
:inherited-members:
+ .. autoclass:: PathSegmentCacheBuster
+ :members:
+
+ .. autoclass:: QueryStringCacheBuster
+ :members:
+
.. autoclass:: PathSegmentMd5CacheBuster
:members:
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 911c22075..9c0ea8598 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -18,7 +18,8 @@ Glossary
response factory
An object which, provided a :term:`request` as a single positional
- argument, returns a Pyramid-compatible response.
+ argument, returns a Pyramid-compatible response. See
+ :class:`pyramid.interfaces.IResponseFactory`.
response
An object returned by a :term:`view callable` that represents response
diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst
index fc908c2b4..d6bc8cbb8 100644
--- a/docs/narr/assets.rst
+++ b/docs/narr/assets.rst
@@ -446,19 +446,20 @@ In order to implement your own cache buster, you can write your own class from
scratch which implements the :class:`~pyramid.interfaces.ICacheBuster`
interface. Alternatively you may choose to subclass one of the existing
implementations. One of the most likely scenarios is you'd want to change the
-way the asset token is generated. To do this just subclass an existing
-implementation and replace the :meth:`~pyramid.interfaces.ICacheBuster.token`
-method. Here is an example which just uses Git to get the hash of the
-currently checked out code:
+way the asset token is generated. To do this just subclass either
+:class:`~pyramid.static.PathSegmentCacheBuster` or
+:class:`~pyramid.static.QueryStringCacheBuster` and define a
+``tokenize(pathspec)`` method. Here is an example which just uses Git to get
+the hash of the currently checked out code:
.. code-block:: python
:linenos:
import os
import subprocess
- from pyramid.static import PathSegmentMd5CacheBuster
+ from pyramid.static import PathSegmentCacheBuster
- class GitCacheBuster(PathSegmentMd5CacheBuster):
+ class GitCacheBuster(PathSegmentCacheBuster):
"""
Assuming your code is installed as a Git checkout, as opposed to as an
egg from an egg repository like PYPI, you can use this cachebuster to
@@ -470,7 +471,7 @@ currently checked out code:
['git', 'rev-parse', 'HEAD'],
cwd=here).strip()
- def token(self, pathspec):
+ def tokenize(self, pathspec):
return self.sha1
Choosing a Cache Buster
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 17cae2c67..4fd7670b9 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -364,9 +364,12 @@ Whenever :app:`Pyramid` returns a response from a view it creates a
object.
The factory that :app:`Pyramid` uses to create a response object instance can be
-changed by passing a ``response_factory`` argument to the constructor of the
-:term:`configurator`. This argument can be either a callable or a
-:term:`dotted Python name` representing a callable.
+changed by passing a :class:`pyramid.interfaces.IResponseFactory` argument to
+the constructor of the :term:`configurator`. This argument can be either a
+callable or a :term:`dotted Python name` representing a callable.
+
+The factory takes a single positional argument, which is a :term:`Request`
+object. The argument may be ``None``.
.. code-block:: python
:linenos:
diff --git a/pyramid/compat.py b/pyramid/compat.py
index 0b0d1a584..a12790d82 100644
--- a/pyramid/compat.py
+++ b/pyramid/compat.py
@@ -3,27 +3,27 @@ import platform
import sys
import types
-if platform.system() == 'Windows': # pragma: no cover
+if platform.system() == 'Windows': # pragma: no cover
WIN = True
-else: # pragma: no cover
+else: # pragma: no cover
WIN = False
-try: # pragma: no cover
+try: # pragma: no cover
import __pypy__
PYPY = True
-except: # pragma: no cover
+except: # pragma: no cover
__pypy__ = None
PYPY = False
try:
import cPickle as pickle
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
import pickle
# True if we are running on Python 3.
PY3 = sys.version_info[0] == 3
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
string_types = str,
integer_types = int,
class_types = type,
@@ -38,21 +38,23 @@ else:
binary_type = str
long = long
+
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 # pragma: no cover
+ return s # pragma: no cover
+
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): # pragma: no cover
+ if isinstance(s, text_type): # pragma: no cover
return s.encode(encoding, errors)
return s
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def ascii_native_(s):
if isinstance(s, text_type):
s = s.encode('ascii')
@@ -72,7 +74,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return
"""
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def native_(s, encoding='latin-1', errors='strict'):
""" If ``s`` is an instance of ``text_type``, return
``s``, otherwise return ``str(s, encoding, errors)``"""
@@ -95,7 +97,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return
``s.encode(encoding, errors)``, otherwise return ``str(s)``
"""
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
from urllib import parse
urlparse = parse
from urllib.parse import quote as url_quote
@@ -112,18 +114,19 @@ else:
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))
-
-if PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
import builtins
exec_ = getattr(builtins, "exec")
-
def reraise(tp, value, tb=None):
if value is None:
value = tp
@@ -131,10 +134,9 @@ if PY3: # pragma: no cover
raise value.with_traceback(tb)
raise value
-
del builtins
-else: # pragma: no cover
+else: # pragma: no cover
def exec_(code, globs=None, locs=None):
"""Execute code in a namespace."""
if globs is None:
@@ -147,35 +149,38 @@ else: # pragma: no cover
locs = globs
exec("""exec code in globs, locs""")
-
exec_("""def reraise(tp, value, tb=None):
raise tp, value, tb
""")
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def iteritems_(d):
return d.items()
+
def itervalues_(d):
return d.values()
+
def iterkeys_(d):
return d.keys()
-else: # pragma: no cover
+else: # pragma: no cover
def iteritems_(d):
return d.iteritems()
+
def itervalues_(d):
return d.itervalues()
+
def iterkeys_(d):
return d.iterkeys()
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
def map_(*arg):
return list(map(*arg))
else:
map_ = map
-
-if PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
def is_nonstr_iter(v):
if isinstance(v, str):
return False
@@ -183,46 +188,52 @@ if PY3: # pragma: no cover
else:
def is_nonstr_iter(v):
return hasattr(v, '__iter__')
-
-if PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
im_func = '__func__'
im_self = '__self__'
else:
im_func = 'im_func'
im_self = 'im_self'
-try: # pragma: no cover
+try: # pragma: no cover
import configparser
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
import ConfigParser as configparser
try:
from Cookie import SimpleCookie
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
from http.cookies import SimpleCookie
-if PY3: # pragma: no cover
+if PY3: # pragma: no cover
from html import escape
else:
from cgi import escape
-try: # pragma: no cover
+try: # pragma: no cover
input_ = raw_input
-except NameError: # pragma: no cover
+except NameError: # pragma: no cover
input_ = input
-try:
+# support annotations and keyword-only arguments in PY3
+if PY3: # pragma: no cover
+ from inspect import getfullargspec as getargspec
+else:
+ from inspect import getargspec
+
+try:
from StringIO import StringIO as NativeIO
-except ImportError: # pragma: no cover
+except ImportError: # pragma: no cover
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 PY3: # pragma: no cover
+
+if PY3: # pragma: no cover
# see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before
# decoding it to utf-8
def decode_path_info(path):
@@ -231,16 +242,19 @@ else:
def decode_path_info(path):
return path.decode('utf-8')
-if PY3: # pragma: no cover
- # see PEP 3333 for why we decode the path to latin-1
+if PY3: # pragma: no cover
+ # 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')
else:
from urlparse import unquote as unquote_to_bytes
+
def unquote_bytes_to_wsgi(bytestring):
return unquote_to_bytes(bytestring)
+
def is_bound_method(ob):
return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None
@@ -254,3 +268,21 @@ if PY3: # pragma: no cover
from itertools import zip_longest
else:
from itertools import izip_longest as 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 = inspect.getargspec(fn)
+ has_self = len(spec.args) > 0 and spec.args[0] == 'self'
+
+ if PY3 and inspect.isfunction(fn) and has_self: # pragma: no cover
+ return True
+ elif inspect.ismethod(fn):
+ return True
+
+ return False
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index d7a48ba93..f0b6252ae 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -14,9 +14,11 @@ from pyramid.traversal import DefaultRootFactory
from pyramid.util import (
action_method,
- InstancePropertyMixin,
+ get_callable_name,
+ InstancePropertyHelper,
)
+
class FactoriesConfiguratorMixin(object):
@action_method
def set_root_factory(self, factory):
@@ -33,9 +35,10 @@ class FactoriesConfiguratorMixin(object):
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
+ self.registry.registerUtility(factory, IDefaultRootFactory) # b/c
intr = self.introspectable('root factories',
None,
@@ -44,7 +47,7 @@ class FactoriesConfiguratorMixin(object):
intr['factory'] = factory
self.action(IRootFactory, register, introspectables=(intr,))
- _set_root_factory = set_root_factory # bw compat
+ _set_root_factory = set_root_factory # bw compat
@action_method
def set_session_factory(self, factory):
@@ -60,6 +63,7 @@ class FactoriesConfiguratorMixin(object):
achieve the same purpose.
"""
factory = self.maybe_dotted(factory)
+
def register():
self.registry.registerUtility(factory, ISessionFactory)
intr = self.introspectable('session factory', None,
@@ -89,6 +93,7 @@ class FactoriesConfiguratorMixin(object):
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,
@@ -102,9 +107,8 @@ class FactoriesConfiguratorMixin(object):
""" 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. This factory object must have the same
- methods and attributes as the
- :class:`pyramid.request.Response` class.
+ objects. The factory should conform to the
+ :class:`pyramid.interfaces.IResponseFactory` interface.
.. note::
@@ -170,10 +174,12 @@ class FactoriesConfiguratorMixin(object):
property = property or reify
if property:
- name, callable = InstancePropertyMixin._make_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)
@@ -225,9 +231,9 @@ class FactoriesConfiguratorMixin(object):
'set_request_propery() is deprecated as of Pyramid 1.5; use '
'add_request_method() with the property=True argument instead')
+
@implementer(IRequestExtensions)
class _RequestExtensions(object):
def __init__(self):
self.descriptors = {}
self.methods = {}
-
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 1f69d7e0b..24c592f7a 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -42,7 +42,8 @@ from pyramid.compat import (
url_quote,
WIN,
is_bound_method,
- is_nonstr_iter
+ is_unbound_method,
+ is_nonstr_iter,
)
from pyramid.exceptions import (
@@ -418,6 +419,12 @@ class DefaultViewMapper(object):
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:
@@ -1973,9 +1980,9 @@ class StaticURLInfo(object):
cb = self._default_cachebust()
if cb:
def cachebust(subpath, kw):
- token = cb.token(spec + subpath)
subpath_tuple = tuple(subpath.split('/'))
- subpath_tuple, kw = cb.pregenerate(token, subpath_tuple, kw)
+ subpath_tuple, kw = cb.pregenerate(
+ spec + subpath, subpath_tuple, kw)
return '/'.join(subpath_tuple), kw
else:
cachebust = None
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index b21c6b9cc..1508f282e 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -582,18 +582,16 @@ class IStaticURLInfo(Interface):
""" Generate a URL for the given path """
class IResponseFactory(Interface):
- """ A utility which generates a response factory """
- def __call__():
- """ Return a response factory (e.g. a callable that returns an object
- implementing IResponse, e.g. :class:`pyramid.response.Response`). It
- should accept all the arguments that the Pyramid Response class
- accepts."""
+ """ 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 object implementing IRequest, e.g. an instance
- of ``pyramid.request.Request``"""
+ """ Return an instance of ``pyramid.request.Request``"""
def blank(path):
""" Return an empty request object (see
@@ -1194,18 +1192,11 @@ class ICacheBuster(Interface):
.. versionadded:: 1.6
"""
- def token(pathspec):
- """
- Computes and returns a token string used for cache busting.
- ``pathspec`` is the path specification for the resource to be cache
- busted. """
-
- def pregenerate(token, subpath, kw):
+ def pregenerate(pathspec, subpath, kw):
"""
Modifies a subpath and/or keyword arguments from which a static asset
- URL will be computed during URL generation. The ``token`` argument is
- a token string computed by
- :meth:`~pyramid.interfaces.ICacheBuster.token` for a particular asset.
+ URL will be computed during URL generation. The ``pathspec`` argument
+ is the path specification for the resource to be cache busted.
The ``subpath`` argument is a tuple of path elements 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
diff --git a/pyramid/path.py b/pyramid/path.py
index 470e766f8..f2d8fff55 100644
--- a/pyramid/path.py
+++ b/pyramid/path.py
@@ -337,8 +337,14 @@ class DottedNameResolver(Resolver):
value = package.__name__
else:
value = package.__name__ + value
- return pkg_resources.EntryPoint.parse(
- 'x=%s' % value).load(False)
+ # 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 """
diff --git a/pyramid/request.py b/pyramid/request.py
index b2e2efe05..3cbe5d9e3 100644
--- a/pyramid/request.py
+++ b/pyramid/request.py
@@ -8,6 +8,7 @@ from webob import BaseRequest
from pyramid.interfaces import (
IRequest,
+ IRequestExtensions,
IResponse,
ISessionFactory,
)
@@ -16,6 +17,7 @@ from pyramid.compat import (
text_,
bytes_,
native_,
+ iteritems_,
)
from pyramid.decorator import reify
@@ -26,7 +28,10 @@ from pyramid.security import (
AuthorizationAPIMixin,
)
from pyramid.url import URLMethodsMixin
-from pyramid.util import InstancePropertyMixin
+from pyramid.util import (
+ InstancePropertyHelper,
+ InstancePropertyMixin,
+)
class TemplateContext(object):
pass
@@ -307,3 +312,22 @@ def call_app_with_subpath_as_path_info(request, app):
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/pyramid/router.py b/pyramid/router.py
index ba4f85b18..0b1ecade7 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -27,6 +27,7 @@ from pyramid.events import (
from pyramid.exceptions import PredicateMismatch
from pyramid.httpexceptions import HTTPNotFound
from pyramid.request import Request
+from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager
from pyramid.traversal import (
@@ -213,7 +214,7 @@ class Router(object):
try:
extensions = self.request_extensions
if extensions is not None:
- request._set_extensions(extensions)
+ apply_request_extensions(request, extensions=extensions)
response = handle_request(request)
if request.response_callbacks:
diff --git a/pyramid/scripting.py b/pyramid/scripting.py
index fdb4aa430..d9587338f 100644
--- a/pyramid/scripting.py
+++ b/pyramid/scripting.py
@@ -1,12 +1,12 @@
from pyramid.config import global_registries
from pyramid.exceptions import ConfigurationError
-from pyramid.request import Request
from pyramid.interfaces import (
- IRequestExtensions,
IRequestFactory,
IRootFactory,
)
+from pyramid.request import Request
+from pyramid.request import apply_request_extensions
from pyramid.threadlocal import manager as threadlocal_manager
from pyramid.traversal import DefaultRootFactory
@@ -77,9 +77,7 @@ def prepare(request=None, registry=None):
request.registry = registry
threadlocals = {'registry':registry, 'request':request}
threadlocal_manager.push(threadlocals)
- extensions = registry.queryUtility(IRequestExtensions)
- if extensions is not None:
- request._set_extensions(extensions)
+ apply_request_extensions(request)
def closer():
threadlocal_manager.pop()
root_factory = registry.queryUtility(IRootFactory,
diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py
index edf2c39f7..d2c5f8c27 100644
--- a/pyramid/scripts/pcreate.py
+++ b/pyramid/scripts/pcreate.py
@@ -18,7 +18,7 @@ def main(argv=sys.argv, quiet=False):
class PCreateCommand(object):
verbosity = 1 # required
description = "Render Pyramid scaffolding to an output directory"
- usage = "usage: %prog [options] output_directory"
+ usage = "usage: %prog [options] -s <scaffold> output_directory"
parser = optparse.OptionParser(usage, description=description)
parser.add_option('-s', '--scaffold',
dest='scaffold_name',
@@ -63,8 +63,16 @@ class PCreateCommand(object):
def run(self):
if self.options.list:
return self.show_scaffolds()
+ if not self.options.scaffold_name and not self.args:
+ if not self.quiet: # pragma: no cover
+ self.parser.print_help()
+ self.out('')
+ self.show_scaffolds()
+ return 2
if not self.options.scaffold_name:
- self.out('You must provide at least one scaffold name')
+ self.out('You must provide at least one scaffold name: -s <scaffold name>')
+ self.out('')
+ self.show_scaffolds()
return 2
if not self.args:
self.out('You must provide a project name')
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index 314efd839..3b79aabd7 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -36,6 +36,11 @@ from pyramid.scripts.common import parse_vars
MAXFD = 1024
+try:
+ import termios
+except ImportError: # pragma: no cover
+ termios = None
+
if WIN and not hasattr(os, 'kill'): # pragma: no cover
# py 2.6 on windows
def kill(pid, sig=None):
@@ -709,15 +714,22 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover
raise SystemExit
signal.signal(signal.SIGTERM, handle_term)
+def ensure_echo_on(): # pragma: no cover
+ if termios:
+ fd = sys.stdin.fileno()
+ attr_list = termios.tcgetattr(fd)
+ if not attr_list[3] & termios.ECHO:
+ attr_list[3] |= termios.ECHO
+ termios.tcsetattr(fd, termios.TCSANOW, attr_list)
+
def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover
"""
Install the reloading monitor.
On some platforms server threads may not terminate when the main
- thread does, causing ports to remain open/locked. The
- ``raise_keyboard_interrupt`` option creates a unignorable signal
- which causes the whole application to shut-down (rudely).
+ thread does, causing ports to remain open/locked.
"""
+ ensure_echo_on()
mon = Monitor(poll_interval=poll_interval)
if extra_files is None:
extra_files = []
diff --git a/pyramid/session.py b/pyramid/session.py
index a95c3f258..c4cfc1949 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -125,8 +125,8 @@ def check_csrf_token(request,
.. versionadded:: 1.4a2
"""
- supplied_token = request.params.get(token, request.headers.get(header))
- if supplied_token != request.session.get_csrf_token():
+ supplied_token = request.params.get(token, request.headers.get(header, ""))
+ if strings_differ(request.session.get_csrf_token(), supplied_token):
if raises:
raise BadCSRFToken('check_csrf_token(): Invalid token')
return False
diff --git a/pyramid/static.py b/pyramid/static.py
index c4a9e3cc4..4ff02f798 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -174,7 +174,7 @@ class Md5AssetTokenGenerator(object):
def __init__(self):
self.token_cache = {}
- def token(self, pathspec):
+ def tokenize(self, pathspec):
# An astute observer will notice that this use of token_cache doesn't
# look particularly thread safe. Basic read/write operations on Python
# dicts, however, are atomic, so simply accessing and writing values
@@ -192,38 +192,54 @@ class Md5AssetTokenGenerator(object):
self.token_cache[pathspec] = token = _generate_md5(pathspec)
return token
-class PathSegmentMd5CacheBuster(Md5AssetTokenGenerator):
+class PathSegmentCacheBuster(object):
"""
An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
- inserts an md5 checksum token for cache busting in the path portion of an
- asset URL. Generated md5 checksums are cached in order to speed up
- subsequent calls.
+ inserts a token for cache busting in the path portion of an asset URL.
+
+ To use this class, subclass it and provide a ``tokenize`` method which
+ accepts a ``pathspec`` and returns a token.
.. versionadded:: 1.6
"""
- def pregenerate(self, token, subpath, kw):
+ def pregenerate(self, pathspec, subpath, kw):
+ token = self.tokenize(pathspec)
return (token,) + subpath, kw
def match(self, subpath):
return subpath[1:]
-class QueryStringMd5CacheBuster(Md5AssetTokenGenerator):
+class PathSegmentMd5CacheBuster(PathSegmentCacheBuster,
+ Md5AssetTokenGenerator):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which
+ inserts an md5 checksum token for cache busting in the path portion of an
+ asset URL. Generated md5 checksums are cached in order to speed up
+ subsequent calls.
+
+ .. versionadded:: 1.6
+ """
+ def __init__(self):
+ super(PathSegmentMd5CacheBuster, self).__init__()
+
+class QueryStringCacheBuster(object):
"""
An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
- an md5 checksum token for cache busting in the query string of an asset
- URL. Generated md5 checksums are cached in order to speed up subsequent
- calls.
+ 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 a ``pathspec`` and returns a token.
+
.. versionadded:: 1.6
"""
def __init__(self, param='x'):
- super(QueryStringMd5CacheBuster, self).__init__()
self.param = param
- def pregenerate(self, token, subpath, kw):
+ def pregenerate(self, pathspec, subpath, kw):
+ token = self.tokenize(pathspec)
query = kw.setdefault('_query', {})
if isinstance(query, dict):
query[self.param] = token
@@ -231,7 +247,23 @@ class QueryStringMd5CacheBuster(Md5AssetTokenGenerator):
kw['_query'] = tuple(query) + ((self.param, token),)
return subpath, kw
-class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster):
+class QueryStringMd5CacheBuster(QueryStringCacheBuster,
+ Md5AssetTokenGenerator):
+ """
+ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds
+ an md5 checksum token for cache busting in the query string of an asset
+ URL. Generated md5 checksums are cached in order to speed up subsequent
+ calls.
+
+ 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, param='x'):
+ super(QueryStringMd5CacheBuster, self).__init__(param=param)
+
+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.
@@ -245,9 +277,8 @@ class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster):
.. versionadded:: 1.6
"""
def __init__(self, token, param='x'):
+ super(QueryStringConstantCacheBuster, self).__init__(param=param)
self._token = token
- self.param = param
- def token(self, pathspec):
+ def tokenize(self, pathspec):
return self._token
-
diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py
new file mode 100644
index 000000000..23ccce82e
--- /dev/null
+++ b/pyramid/tests/test_compat.py
@@ -0,0 +1,26 @@
+import unittest
+from pyramid.compat import is_unbound_method
+
+class TestUnboundMethods(unittest.TestCase):
+ def test_old_style_bound(self):
+ self.assertFalse(is_unbound_method(OldStyle().run))
+
+ def test_new_style_bound(self):
+ self.assertFalse(is_unbound_method(NewStyle().run))
+
+ def test_old_style_unbound(self):
+ self.assertTrue(is_unbound_method(OldStyle.run))
+
+ def test_new_style_unbound(self):
+ self.assertTrue(is_unbound_method(NewStyle.run))
+
+ def test_normal_func_unbound(self):
+ def func(): return 'OK'
+
+ self.assertFalse(is_unbound_method(func))
+
+class OldStyle:
+ def run(self): return 'OK'
+
+class NewStyle(object):
+ def run(self): return 'OK'
diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py
index 0bd5336ff..42bb5accc 100644
--- a/pyramid/tests/test_config/test_factories.py
+++ b/pyramid/tests/test_config/test_factories.py
@@ -126,6 +126,23 @@ class TestFactoriesMixin(unittest.TestCase):
config = self._makeOne(autocommit=True)
self.assertRaises(AttributeError, config.add_request_method)
+ def test_add_request_method_with_text_type_name(self):
+ from pyramid.interfaces import IRequestExtensions
+ from pyramid.compat import text_, PY3
+ from pyramid.exceptions import ConfigurationError
+
+ config = self._makeOne(autocommit=True)
+ def boomshaka(r): pass
+
+ def get_bad_name():
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ config.add_request_method(boomshaka, name=name)
+
+ self.assertRaises(ConfigurationError, get_bad_name)
class TestDeprecatedFactoriesMixinMethods(unittest.TestCase):
def setUp(self):
@@ -135,7 +152,7 @@ class TestDeprecatedFactoriesMixinMethods(unittest.TestCase):
def tearDown(self):
from zope.deprecation import __show__
__show__.on()
-
+
def _makeOne(self, *arg, **kw):
from pyramid.config import Configurator
config = Configurator(*arg, **kw)
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index b0d03fb72..36c86f78c 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -1666,6 +1666,20 @@ class TestViewsConfigurationMixin(unittest.TestCase):
renderer=null_renderer)
self.assertRaises(ConfigurationConflictError, config.commit)
+ def test_add_view_class_method_no_attr(self):
+ from pyramid.renderers import null_renderer
+ from zope.interface import directlyProvides
+ from pyramid.exceptions import ConfigurationError
+
+ config = self._makeOne(autocommit=True)
+ class DummyViewClass(object):
+ def run(self): pass
+
+ def configure_view():
+ config.add_view(view=DummyViewClass.run, renderer=null_renderer)
+
+ self.assertRaises(ConfigurationError, configure_view)
+
def test_derive_view_function(self):
from pyramid.renderers import null_renderer
def view(request):
@@ -3981,7 +3995,7 @@ class TestStaticURLInfo(unittest.TestCase):
def test_add_cachebust_default(self):
config = self._makeConfig()
inst = self._makeOne()
- inst._default_cachebust = DummyCacheBuster
+ inst._default_cachebust = lambda: DummyCacheBuster('foo')
inst.add(config, 'view', 'mypackage:path', cachebust=True)
cachebust = config.registry._static_url_registrations[0][3]
subpath, kw = cachebust('some/path', {})
@@ -4000,7 +4014,7 @@ class TestStaticURLInfo(unittest.TestCase):
config = self._makeConfig()
inst = self._makeOne()
inst.add(config, 'view', 'mypackage:path',
- cachebust=DummyCacheBuster())
+ cachebust=DummyCacheBuster('foo'))
cachebust = config.registry._static_url_registrations[0][3]
subpath, kw = cachebust('some/path', {})
self.assertEqual(subpath, 'some/path')
@@ -4113,10 +4127,10 @@ class DummyMultiView:
""" """
class DummyCacheBuster(object):
- def token(self, pathspec):
- return 'foo'
- def pregenerate(self, token, subpath, kw):
- kw['x'] = token
+ def __init__(self, token):
+ self.token = token
+ def pregenerate(self, pathspec, subpath, kw):
+ kw['x'] = self.token
return subpath, kw
def parse_httpdate(s):
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
index 48af98f59..f142e4536 100644
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -435,7 +435,50 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase):
self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded)
self.assertEqual(request.environ['PATH_INFO'], '/' + encoded)
-class DummyRequest:
+class Test_apply_request_extensions(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, request, extensions=None):
+ from pyramid.request import apply_request_extensions
+ return apply_request_extensions(request, extensions=extensions)
+
+ def test_it_with_registry(self):
+ from pyramid.interfaces import IRequestExtensions
+ extensions = Dummy()
+ extensions.methods = {'foo': lambda x, y: y}
+ extensions.descriptors = {'bar': property(lambda x: 'bar')}
+ self.config.registry.registerUtility(extensions, IRequestExtensions)
+ request = DummyRequest()
+ request.registry = self.config.registry
+ self._callFUT(request)
+ self.assertEqual(request.bar, 'bar')
+ self.assertEqual(request.foo('abc'), 'abc')
+
+ def test_it_override_extensions(self):
+ from pyramid.interfaces import IRequestExtensions
+ ignore = Dummy()
+ ignore.methods = {'x': lambda x, y, z: 'asdf'}
+ ignore.descriptors = {'bar': property(lambda x: 'asdf')}
+ self.config.registry.registerUtility(ignore, IRequestExtensions)
+ request = DummyRequest()
+ request.registry = self.config.registry
+
+ extensions = Dummy()
+ extensions.methods = {'foo': lambda x, y: y}
+ extensions.descriptors = {'bar': property(lambda x: 'bar')}
+ self._callFUT(request, extensions=extensions)
+ self.assertRaises(AttributeError, lambda: request.x)
+ self.assertEqual(request.bar, 'bar')
+ self.assertEqual(request.foo('abc'), 'abc')
+
+class Dummy(object):
+ pass
+
+class DummyRequest(object):
def __init__(self, environ=None):
if environ is None:
environ = {}
diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py
index 30ebd5918..b57c248d5 100644
--- a/pyramid/tests/test_router.py
+++ b/pyramid/tests/test_router.py
@@ -317,6 +317,7 @@ class TestRouter(unittest.TestCase):
from pyramid.interfaces import IRequestExtensions
from pyramid.interfaces import IRequest
from pyramid.request import Request
+ from pyramid.util import InstancePropertyHelper
context = DummyContext()
self._registerTraverserFactory(context)
class Extensions(object):
@@ -324,11 +325,12 @@ class TestRouter(unittest.TestCase):
self.methods = {}
self.descriptors = {}
extensions = Extensions()
- L = []
+ ext_method = lambda r: 'bar'
+ name, fn = InstancePropertyHelper.make_property(ext_method, name='foo')
+ extensions.descriptors[name] = fn
request = Request.blank('/')
request.request_iface = IRequest
request.registry = self.registry
- request._set_extensions = lambda *x: L.extend(x)
def request_factory(environ):
return request
self.registry.registerUtility(extensions, IRequestExtensions)
@@ -342,7 +344,7 @@ class TestRouter(unittest.TestCase):
router.request_factory = request_factory
start_response = DummyStartResponse()
router(environ, start_response)
- self.assertEqual(L, [extensions])
+ self.assertEqual(view.request.foo, 'bar')
def test_call_view_registered_nonspecific_default_path(self):
from pyramid.interfaces import IViewClassifier
diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py
index a36d1ed71..1e952062b 100644
--- a/pyramid/tests/test_scripting.py
+++ b/pyramid/tests/test_scripting.py
@@ -122,11 +122,15 @@ class Test_prepare(unittest.TestCase):
self.assertEqual(request.context, context)
def test_it_with_extensions(self):
- exts = Dummy()
+ from pyramid.util import InstancePropertyHelper
+ exts = DummyExtensions()
+ ext_method = lambda r: 'bar'
+ name, fn = InstancePropertyHelper.make_property(ext_method, 'foo')
+ exts.descriptors[name] = fn
request = DummyRequest({})
registry = request.registry = self._makeRegistry([exts, DummyFactory])
info = self._callFUT(request=request, registry=registry)
- self.assertEqual(request.extensions, exts)
+ self.assertEqual(request.foo, 'bar')
root, closer = info['root'], info['closer']
closer()
@@ -199,11 +203,13 @@ class DummyThreadLocalManager:
def pop(self):
self.popped.append(True)
-class DummyRequest:
+class DummyRequest(object):
matchdict = None
matched_route = None
def __init__(self, environ):
self.environ = environ
- def _set_extensions(self, exts):
- self.extensions = exts
+class DummyExtensions:
+ def __init__(self):
+ self.descriptors = {}
+ self.methods = {}
diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py
index 020721ca7..63e5e6368 100644
--- a/pyramid/tests/test_scripts/test_pcreate.py
+++ b/pyramid/tests/test_scripts/test_pcreate.py
@@ -12,10 +12,10 @@ class TestPCreateCommand(unittest.TestCase):
from pyramid.scripts.pcreate import PCreateCommand
return PCreateCommand
- def _makeOne(self, *args):
+ def _makeOne(self, *args, **kw):
effargs = ['pcreate']
effargs.extend(args)
- cmd = self._getTargetClass()(effargs)
+ cmd = self._getTargetClass()(effargs, **kw)
cmd.out = self.out
return cmd
@@ -34,8 +34,13 @@ class TestPCreateCommand(unittest.TestCase):
out = self.out_.getvalue()
self.assertTrue(out.startswith('No scaffolds available'))
+ def test_run_no_scaffold_no_args(self):
+ cmd = self._makeOne(quiet=True)
+ result = cmd.run()
+ self.assertEqual(result, 2)
+
def test_run_no_scaffold_name(self):
- cmd = self._makeOne()
+ cmd = self._makeOne('dummy')
result = cmd.run()
self.assertEqual(result, 2)
out = self.out_.getvalue()
diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py
index 2f4de249e..a3df74b44 100644
--- a/pyramid/tests/test_static.py
+++ b/pyramid/tests/test_static.py
@@ -393,13 +393,13 @@ class TestMd5AssetTokenGenerator(unittest.TestCase):
return cls()
def test_package_resource(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
expected = '76d653a3a044e2f4b38bb001d283e3d9'
token = fut('pyramid.tests:fixtures/static/index.html')
self.assertEqual(token, expected)
def test_filesystem_resource(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
expected = 'd5155f250bef0e9923e894dbc713c5dd'
with open(self.fspath, 'w') as f:
f.write("Are we rich yet?")
@@ -407,7 +407,7 @@ class TestMd5AssetTokenGenerator(unittest.TestCase):
self.assertEqual(token, expected)
def test_cache(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
expected = 'd5155f250bef0e9923e894dbc713c5dd'
with open(self.fspath, 'w') as f:
f.write("Are we rich yet?")
@@ -425,11 +425,11 @@ class TestPathSegmentMd5CacheBuster(unittest.TestCase):
def _makeOne(self):
from pyramid.static import PathSegmentMd5CacheBuster as cls
inst = cls()
- inst.token = lambda pathspec: 'foo'
+ inst.tokenize = lambda pathspec: 'foo'
return inst
def test_token(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
self.assertEqual(fut('whatever'), 'foo')
def test_pregenerate(self):
@@ -448,11 +448,11 @@ class TestQueryStringMd5CacheBuster(unittest.TestCase):
inst = cls(param)
else:
inst = cls()
- inst.token = lambda pathspec: 'foo'
+ inst.tokenize = lambda pathspec: 'foo'
return inst
def test_token(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
self.assertEqual(fut('whatever'), 'foo')
def test_pregenerate(self):
@@ -490,7 +490,7 @@ class TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster):
return inst
def test_token(self):
- fut = self._makeOne().token
+ fut = self._makeOne().tokenize
self.assertEqual(fut('whatever'), 'foo')
def test_pregenerate(self):
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index ac5ea0683..459c729a0 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -1,9 +1,193 @@
import unittest
from pyramid.compat import PY3
+
+class Test_InstancePropertyHelper(unittest.TestCase):
+ def _makeOne(self):
+ cls = self._getTargetClass()
+ return cls()
+
+ def _getTargetClass(self):
+ from pyramid.util import InstancePropertyHelper
+ return InstancePropertyHelper
+
+ def test_callable(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker)
+ foo.bar = 1
+ self.assertEqual(1, foo.worker)
+ foo.bar = 2
+ self.assertEqual(2, foo.worker)
+
+ def test_callable_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ foo.bar = 1
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+
+ def test_callable_with_reify(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, reify=True)
+ foo.bar = 1
+ self.assertEqual(1, foo.worker)
+ foo.bar = 2
+ self.assertEqual(1, foo.worker)
+
+ def test_callable_with_name_reify(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ helper.set_property(foo, worker, name='y', reify=True)
+ foo.bar = 1
+ self.assertEqual(1, foo.y)
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+ self.assertEqual(1, foo.y)
+
+ def test_property_without_name(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ self.assertRaises(ValueError, helper.set_property, foo, property(worker))
+
+ def test_property_with_name(self):
+ def worker(obj):
+ return obj.bar
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, property(worker), name='x')
+ foo.bar = 1
+ self.assertEqual(1, foo.x)
+ foo.bar = 2
+ self.assertEqual(2, foo.x)
+
+ def test_property_with_reify(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ self.assertRaises(ValueError, helper.set_property,
+ foo, property(worker), name='x', reify=True)
+
+ def test_override_property(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x')
+ def doit():
+ foo.x = 1
+ self.assertRaises(AttributeError, doit)
+
+ def test_override_reify(self):
+ def worker(obj): pass
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, worker, name='x', reify=True)
+ foo.x = 1
+ self.assertEqual(1, foo.x)
+ foo.x = 2
+ self.assertEqual(2, foo.x)
+
+ def test_reset_property(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, lambda _: 1, name='x')
+ self.assertEqual(1, foo.x)
+ helper.set_property(foo, lambda _: 2, name='x')
+ self.assertEqual(2, foo.x)
+
+ def test_reset_reify(self):
+ """ This is questionable behavior, but may as well get notified
+ if it changes."""
+ foo = Dummy()
+ helper = self._getTargetClass()
+ helper.set_property(foo, lambda _: 1, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+ helper.set_property(foo, lambda _: 2, name='x', reify=True)
+ self.assertEqual(1, foo.x)
+
+ def test_make_property(self):
+ from pyramid.decorator import reify
+ helper = self._getTargetClass()
+ name, fn = helper.make_property(lambda x: 1, name='x', reify=True)
+ self.assertEqual(name, 'x')
+ self.assertTrue(isinstance(fn, reify))
+
+ def test_apply_properties_with_iterable(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ x = helper.make_property(lambda _: 1, name='x', reify=True)
+ y = helper.make_property(lambda _: 2, name='y')
+ helper.apply_properties(foo, [x, y])
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test_apply_properties_with_dict(self):
+ foo = Dummy()
+ helper = self._getTargetClass()
+ x_name, x_fn = helper.make_property(lambda _: 1, name='x', reify=True)
+ y_name, y_fn = helper.make_property(lambda _: 2, name='y')
+ helper.apply_properties(foo, {x_name: x_fn, y_name: y_fn})
+ self.assertEqual(1, foo.x)
+ self.assertEqual(2, foo.y)
+
+ def test_make_property_unicode(self):
+ from pyramid.compat import text_
+ from pyramid.exceptions import ConfigurationError
+
+ cls = self._getTargetClass()
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ def make_bad_name():
+ cls.make_property(lambda x: 1, name=name, reify=True)
+
+ self.assertRaises(ConfigurationError, make_bad_name)
+
+ def test_add_property(self):
+ helper = self._makeOne()
+ helper.add_property(lambda obj: obj.bar, name='x', reify=True)
+ helper.add_property(lambda obj: obj.bar, name='y')
+ self.assertEqual(len(helper.properties), 2)
+ foo = Dummy()
+ helper.apply(foo)
+ foo.bar = 1
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(foo.y, 1)
+ foo.bar = 2
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(foo.y, 2)
+
+ def test_apply_multiple_times(self):
+ helper = self._makeOne()
+ helper.add_property(lambda obj: 1, name='x')
+ foo, bar = Dummy(), Dummy()
+ helper.apply(foo)
+ self.assertEqual(foo.x, 1)
+ helper.add_property(lambda obj: 2, name='x')
+ helper.apply(bar)
+ self.assertEqual(foo.x, 1)
+ self.assertEqual(bar.x, 2)
+
class Test_InstancePropertyMixin(unittest.TestCase):
def _makeOne(self):
cls = self._getTargetClass()
+
class Foo(cls):
pass
return Foo()
@@ -109,43 +293,6 @@ class Test_InstancePropertyMixin(unittest.TestCase):
foo.set_property(lambda _: 2, name='x', reify=True)
self.assertEqual(1, foo.x)
- def test__make_property(self):
- from pyramid.decorator import reify
- cls = self._getTargetClass()
- name, fn = cls._make_property(lambda x: 1, name='x', reify=True)
- self.assertEqual(name, 'x')
- self.assertTrue(isinstance(fn, reify))
-
- def test__set_properties_with_iterable(self):
- foo = self._makeOne()
- x = foo._make_property(lambda _: 1, name='x', reify=True)
- y = foo._make_property(lambda _: 2, name='y')
- foo._set_properties([x, y])
- self.assertEqual(1, foo.x)
- self.assertEqual(2, foo.y)
-
- def test__set_properties_with_dict(self):
- foo = self._makeOne()
- x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True)
- y_name, y_fn = foo._make_property(lambda _: 2, name='y')
- foo._set_properties({x_name: x_fn, y_name: y_fn})
- self.assertEqual(1, foo.x)
- self.assertEqual(2, foo.y)
-
- def test__set_extensions(self):
- inst = self._makeOne()
- def foo(self, result):
- return result
- n, bar = inst._make_property(lambda _: 'bar', name='bar')
- class Extensions(object):
- def __init__(self):
- self.methods = {'foo':foo}
- self.descriptors = {'bar':bar}
- extensions = Extensions()
- inst._set_extensions(extensions)
- self.assertEqual(inst.bar, 'bar')
- self.assertEqual(inst.foo('abc'), 'abc')
-
class Test_WeakOrderedSet(unittest.TestCase):
def _makeOne(self):
from pyramid.config import WeakOrderedSet
@@ -619,7 +766,36 @@ class TestActionInfo(unittest.TestCase):
"Line 0 of file filename:\n linerepr ")
+class TestCallableName(unittest.TestCase):
+ def test_valid_ascii(self):
+ from pyramid.util import get_callable_name
+ from pyramid.compat import text_, PY3
+
+ if PY3: # pragma: nocover
+ name = b'hello world'
+ else: # pragma: nocover
+ name = text_(b'hello world', 'utf-8')
+
+ self.assertEqual(get_callable_name(name), 'hello world')
+
+ def test_invalid_ascii(self):
+ from pyramid.util import get_callable_name
+ from pyramid.compat import text_, PY3
+ from pyramid.exceptions import ConfigurationError
+
+ def get_bad_name():
+ if PY3: # pragma: nocover
+ name = b'La Pe\xc3\xb1a'
+ else: # pragma: nocover
+ name = text_(b'La Pe\xc3\xb1a', 'utf-8')
+
+ get_callable_name(name)
+
+ self.assertRaises(ConfigurationError, get_bad_name)
+
+
def dummyfunc(): pass
+
class Dummy(object):
pass
diff --git a/pyramid/util.py b/pyramid/util.py
index 4ca2937a1..5721a93fc 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -15,10 +15,6 @@ from pyramid.exceptions import (
CyclicDependencyError,
)
-from pyramid.interfaces import (
- IResponseFactory,
- )
-
from pyramid.compat import (
iteritems_,
is_nonstr_iter,
@@ -26,10 +22,10 @@ from pyramid.compat import (
string_types,
text_,
PY3,
+ native_
)
from pyramid.interfaces import IActionInfo
-from pyramid.response import Response
from pyramid.path import DottedNameResolver as _DottedNameResolver
class DottedNameResolver(_DottedNameResolver):
@@ -38,14 +34,21 @@ class DottedNameResolver(_DottedNameResolver):
_marker = object()
-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.
+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):
+ 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.
@@ -60,7 +63,7 @@ class InstancePropertyMixin(object):
raise ValueError('cannot reify a property')
elif name is not None:
fn = lambda this: callable(this)
- fn.__name__ = name
+ fn.__name__ = get_callable_name(name)
fn.__doc__ = callable.__doc__
else:
name = callable.__name__
@@ -73,25 +76,15 @@ class InstancePropertyMixin(object):
return name, fn
- def _set_properties(self, properties):
- """ Create several properties on the instance at once.
-
- This is a more efficient version of
- :meth:`pyramid.util.InstancePropertyMixin.set_property` which
- can accept multiple ``(name, property)`` pairs generated via
- :meth:`pyramid.util.InstancePropertyMixin._make_property`.
-
- ``properties`` is a sequence of two-tuples *or* a data structure
- with an ``.items()`` method which returns a sequence of two-tuples
- (presumably a dictionary). It will be used to add several
- properties to the instance in a manner that is more efficient
- than simply calling ``set_property`` repeatedly.
+ @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 = self.__class__
- cls = type(parent.__name__, (parent, object), attrs)
+ parent = target.__class__
+ newcls = type(parent.__name__, (parent, object), attrs)
# We assign __provides__, __implemented__ and __providedBy__ below
# to prevent a memory leak that results from from the usage of this
# instance's eventual use in an adapter lookup. Adapter lookup
@@ -110,14 +103,34 @@ class InstancePropertyMixin(object):
# attached to it
val = getattr(parent, name, _marker)
if val is not _marker:
- setattr(cls, name, val)
- self.__class__ = cls
+ 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 _set_extensions(self, extensions):
- for name, fn in iteritems_(extensions.methods):
- method = fn.__get__(self, self.__class__)
- setattr(self, name, method)
- self._set_properties(extensions.descriptors)
+ 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.
@@ -171,8 +184,8 @@ class InstancePropertyMixin(object):
>>> foo.y # notice y keeps the original value
1
"""
- prop = self._make_property(callable, name=name, reify=reify)
- self._set_properties([prop])
+ InstancePropertyHelper.set_property(
+ self, callable, name=name, reify=reify)
class WeakOrderedSet(object):
""" Maintain a set of items.
@@ -555,3 +568,18 @@ def action_method(wrapped):
functools.update_wrapper(wrapper, wrapped)
wrapper.__docobj__ = wrapped
return wrapper
+
+
+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)
diff --git a/tox.ini b/tox.ini
index 29bd48639..202e29e30 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,4 +1,5 @@
[tox]
+skipsdist = True
envlist =
py26,py27,py32,py33,py34,pypy,pypy3,cover