summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml4
-rw-r--r--CHANGES.txt53
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--HACKING.txt2
-rw-r--r--docs/api/config.rst5
-rw-r--r--docs/api/request.rst1
-rw-r--r--docs/api/static.rst6
-rw-r--r--docs/narr/assets.rst15
-rw-r--r--docs/narr/extconfig.rst104
-rw-r--r--docs/narr/security.rst60
-rw-r--r--docs/narr/urldispatch.rst58
-rw-r--r--pyramid/compat.py65
-rw-r--r--pyramid/config/__init__.py108
-rw-r--r--pyramid/config/assets.py26
-rw-r--r--pyramid/config/factories.py4
-rw-r--r--pyramid/config/views.py14
-rw-r--r--pyramid/httpexceptions.py8
-rw-r--r--pyramid/i18n.py8
-rw-r--r--pyramid/interfaces.py22
-rw-r--r--pyramid/renderers.py64
-rw-r--r--pyramid/request.py26
-rw-r--r--pyramid/router.py3
-rw-r--r--pyramid/scaffolds/copydir.py8
-rw-r--r--pyramid/scaffolds/tests.py8
-rw-r--r--pyramid/scripting.py8
-rw-r--r--pyramid/scripts/pserve.py41
-rw-r--r--pyramid/session.py4
-rw-r--r--pyramid/static.py63
-rw-r--r--pyramid/tests/test_config/pkgs/asset/models.py8
-rw-r--r--pyramid/tests/test_config/pkgs/asset/views.py22
-rw-r--r--pyramid/tests/test_config/test_adapters.py2
-rw-r--r--pyramid/tests/test_config/test_assets.py48
-rw-r--r--pyramid/tests/test_config/test_init.py67
-rw-r--r--pyramid/tests/test_config/test_views.py28
-rw-r--r--pyramid/tests/test_path.py2
-rw-r--r--pyramid/tests/test_renderers.py51
-rw-r--r--pyramid/tests/test_request.py47
-rw-r--r--pyramid/tests/test_router.py8
-rw-r--r--pyramid/tests/test_scripting.py16
-rw-r--r--pyramid/tests/test_scripts/pystartup.py1
-rw-r--r--pyramid/tests/test_scripts/pystartup.txt3
-rw-r--r--pyramid/tests/test_scripts/test_pserve.py2
-rw-r--r--pyramid/tests/test_scripts/test_pshell.py2
-rw-r--r--pyramid/tests/test_static.py16
-rw-r--r--pyramid/tests/test_traversal.py2
-rw-r--r--pyramid/tests/test_urldispatch.py2
-rw-r--r--pyramid/tests/test_util.py240
-rw-r--r--pyramid/traversal.py2
-rw-r--r--pyramid/urldispatch.py2
-rw-r--r--pyramid/util.py81
-rw-r--r--setup.cfg3
-rw-r--r--tox.ini71
53 files changed, 1181 insertions, 339 deletions
diff --git a/.gitignore b/.gitignore
index 8dca2069c..fe132412a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
*.egg
*.egg-info
+.eggs/
*.pyc
*$py.class
*.pt.py
@@ -7,9 +8,12 @@
*~
.*.swp
.coverage
+.coverage.*
.tox/
nosetests.xml
coverage.xml
+nosetests-*.xml
+coverage-*.xml
tutorial.db
build/
dist/
diff --git a/.travis.yml b/.travis.yml
index cb98fddbe..42b3073c7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,7 +10,9 @@ env:
- TOXENV=py34
- TOXENV=pypy
- TOXENV=pypy3
- - TOXENV=cover
+ - TOXENV=py2-docs
+ - TOXENV=py3-docs
+ - TOXENV=py2-cover,py3-cover,coverage
install:
- travis_retry pip install tox
diff --git a/CHANGES.txt b/CHANGES.txt
index 27052cf0f..19d77eb68 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,6 +4,20 @@ Next release
Features
--------
+- The ``pyramid.config.Configurator`` has grown the ability to allow
+ actions to call other actions during a commit-cycle. This enables much more
+ logic to be placed into actions, such as the ability to invoke other actions
+ or group them for improved conflict detection. We have also exposed and
+ documented the config phases that Pyramid uses in order to further assist
+ in building conforming addons.
+ 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
@@ -12,9 +26,15 @@ Features
- Added support / testing for 'pypy3' under Tox and Travis.
See https://github.com/Pylons/pyramid/pull/1469
+- Automate code coverage metrics across py2 and py3 instead of just py2.
+ See https://github.com/Pylons/pyramid/pull/1471
+
- 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
@@ -85,9 +105,28 @@ Features
- Support keyword-only arguments and function annotations in views in
Python 3. See https://github.com/Pylons/pyramid/pull/1556
+- ``request.response`` will no longer be mutated when using the
+ ``pyramid.renderers.render_to_response()`` API. It is now necessary to
+ pass in a ``response=`` argument to ``render_to_response`` if you wish to
+ supply the renderer with a custom response object for it to use. If you
+ do not pass one then a response object will be created using the
+ application's ``IResponseFactory``. Almost all renderers
+ mutate the ``request.response`` response object (for example, the JSON
+ renderer sets ``request.response.content_type`` to ``application/json``).
+ However, when invoking ``render_to_response`` it is not expected that the
+ response object being returned would be the same one used later in the
+ request. The response object returned from ``render_to_response`` is now
+ explicitly different from ``request.response``. This does not change the
+ API of a renderer. See https://github.com/Pylons/pyramid/pull/1563
+
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,
+ https://github.com/Pylons/pyramid/pull/1592
+
- ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise
``ValueError`` when accidentally passed ``None``.
See https://github.com/Pylons/pyramid/pull/1320
@@ -127,12 +166,22 @@ Bug Fixes
- Allow the ``pyramid.renderers.JSONP`` renderer to work even if there is no
valid request object. In this case it will not wrap the object in a
- callback and thus behave just like the ``pyramid.renderers.JSON` renderer.
+ 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 sharing the ``IRenderer`` objects across threads when attached to
+ a view using the `renderer=` argument. These renderers were instantiated
+ at time of first render and shared between requests, causing potentially
+ subtle effects like `pyramid.reload_templates = true` failing to work
+ in `pyramid_mako`. See https://github.com/Pylons/pyramid/pull/1575
+ and https://github.com/Pylons/pyramid/issues/1268
+
+- Avoiding timing attacks against CSRF tokens.
+ See https://github.com/Pylons/pyramid/pull/1574
+
Deprecations
------------
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 319d41434..4f9bd6e41 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -242,3 +242,5 @@ Contributors
- 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/config.rst b/docs/api/config.rst
index 48dd2f0b9..ae913d32c 100644
--- a/docs/api/config.rst
+++ b/docs/api/config.rst
@@ -132,3 +132,8 @@
are being used.
.. autoclass:: not_
+
+.. attribute:: PHASE0_CONFIG
+.. attribute:: PHASE1_CONFIG
+.. attribute:: PHASE2_CONFIG
+.. attribute:: PHASE3_CONFIG
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/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/extconfig.rst b/docs/narr/extconfig.rst
index 6587aef92..a61eca7b7 100644
--- a/docs/narr/extconfig.rst
+++ b/docs/narr/extconfig.rst
@@ -215,13 +215,115 @@ registers an action with a higher order than the
passed to it, that a route by this name was already registered by
``add_route``, and if such a route has not already been registered, it's a
configuration error (a view that names a nonexistent route via its
-``route_name`` parameter will never be called).
+``route_name`` parameter will never be called). As of Pyramid 1.6 it is
+possible for one action to invoke another. See :ref:`ordering_actions` for
+more information.
``introspectables`` is a sequence of :term:`introspectable` objects. You can
pass a sequence of introspectables to the
:meth:`~pyramid.config.Configurator.action` method, which allows you to
augment Pyramid's configuration introspection system.
+.. _ordering_actions:
+
+Ordering Actions
+----------------
+
+In Pyramid every :term:`action` has an inherent ordering relative to other
+actions. The logic within actions is deferred until a call to
+:meth:`pyramid.config.Configurator.commit` (which is automatically invoked by
+:meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call
+``config.add_view(route_name='foo')`` **before**
+``config.add_route('foo', '/foo')`` because nothing actually happens until
+commit-time. During a commit cycle conflicts are resolved, actions are ordered
+and executed.
+
+By default, almost every action in Pyramid has an ``order`` of
+:const:`pyramid.config.PHASE3_CONFIG`. Every action within the same order-level
+will be executed in the order it was called.
+This means that if an action must be reliably executed before or after another
+action, the ``order`` must be defined explicitly to make this work. For
+example, views are dependent on routes being defined. Thus the action created
+by :meth:`pyramid.config.Configurator.add_route` has an ``order`` of
+:const:`pyramid.config.PHASE2_CONFIG`.
+
+Pre-defined Phases
+~~~~~~~~~~~~~~~~~~
+
+:const:`pyramid.config.PHASE0_CONFIG`
+
+- This phase is reserved for developers who want to execute actions prior
+ to Pyramid's core directives.
+
+:const:`pyramid.config.PHASE1_CONFIG`
+
+- :meth:`pyramid.config.Configurator.add_renderer`
+- :meth:`pyramid.config.Configurator.add_route_predicate`
+- :meth:`pyramid.config.Configurator.add_subscriber_predicate`
+- :meth:`pyramid.config.Configurator.add_view_predicate`
+- :meth:`pyramid.config.Configurator.set_authorization_policy`
+- :meth:`pyramid.config.Configurator.set_default_permission`
+- :meth:`pyramid.config.Configurator.set_view_mapper`
+
+:const:`pyramid.config.PHASE2_CONFIG`
+
+- :meth:`pyramid.config.Configurator.add_route`
+- :meth:`pyramid.config.Configurator.set_authentication_policy`
+
+:const:`pyramid.config.PHASE3_CONFIG`
+
+- The default for all builtin or custom directives unless otherwise specified.
+
+Calling Actions From Actions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 1.6
+
+Pyramid's configurator allows actions to be added during a commit-cycle as
+long as they are added to the current or a later ``order`` phase. This means
+that your custom action can defer decisions until commit-time and then do
+things like invoke :meth:`pyramid.config.Configurator.add_route`. It can also
+provide better conflict detection if your addon needs to call more than one
+other action.
+
+For example, let's make an addon that invokes ``add_route`` and ``add_view``,
+but we want it to conflict with any other call to our addon:
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.config import PHASE0_CONFIG
+
+ def includeme(config):
+ config.add_directive('add_auto_route', add_auto_route)
+
+ def add_auto_route(config, name, view):
+ def register():
+ config.add_view(route_name=name, view=view)
+ config.add_route(name, '/' + name)
+ config.action(('auto route', name), register, order=PHASE0_CONFIG)
+
+Now someone else can use your addon and be informed if there is a conflict
+between this route and another, or two calls to ``add_auto_route``.
+Notice how we had to invoke our action **before** ``add_view`` or
+``add_route``. If we tried to invoke this afterward, the subsequent calls to
+``add_view`` and ``add_route`` would cause conflicts because that phase had
+already been executed, and the configurator cannot go back in time to add more
+views during that commit-cycle.
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.config import Configurator
+
+ def main(global_config, **settings):
+ config = Configurator()
+ config.include('auto_route_addon')
+ config.add_auto_route('foo', my_view)
+
+ def my_view(request):
+ return request.response
+
.. _introspection:
Adding Configuration Introspection
diff --git a/docs/narr/security.rst b/docs/narr/security.rst
index 2dc0c76af..75f4dc7c5 100644
--- a/docs/narr/security.rst
+++ b/docs/narr/security.rst
@@ -341,9 +341,7 @@ third argument is a permission or sequence of permission names.
A principal is usually a user id, however it also may be a group id if your
authentication system provides group information and the effective
:term:`authentication policy` policy is written to respect group information.
-For example, the
-:class:`pyramid.authentication.RepozeWho1AuthenticationPolicy` respects group
-information if you configure it with a ``callback``.
+See :ref:`extending_default_authentication_policies`.
Each ACE in an ACL is processed by an authorization policy *in the
order dictated by the ACL*. So if you have an ACL like this:
@@ -583,6 +581,60 @@ via print statements when a call to
:meth:`~pyramid.request.Request.has_permission` fails is often useful.
.. index::
+ single: authentication policy (extending)
+
+.. _extending_default_authentication_policies:
+
+Extending Default Authentication Policies
+-----------------------------------------
+
+Pyramid ships with some builtin authentication policies for use in your
+applications. See :mod:`pyramid.authentication` for the available
+policies. They differ on their mechanisms for tracking authentication
+credentials between requests, however they all interface with your
+application in mostly the same way.
+
+Above you learned about :ref:`assigning_acls`. Each :term:`principal` used
+in the :term:`ACL` is matched against the list returned from
+:meth:`pyramid.interfaces.IAuthenticationPolicy.effective_principals`.
+Similarly, :meth:`pyramid.request.Request.authenticated_userid` maps to
+:meth:`pyramid.interfaces.IAuthenticationPolicy.authenticated_userid`.
+
+You may control these values by subclassing the default authentication
+policies. For example, below we subclass the
+:class:`pyramid.authentication.AuthTktAuthenticationPolicy` and define
+extra functionality to query our database before confirming that the
+:term:`userid` is valid in order to avoid blindly trusting the value in the
+cookie (what if the cookie is still valid but the user has deleted their
+account?). We then use that :term:`userid` to augment the
+``effective_principals`` with information about groups and other state for
+that user.
+
+.. code-block:: python
+ :linenos:
+
+ from pyramid.authentication import AuthTktAuthenticationPolicy
+
+ class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
+ def authenticated_userid(self, request):
+ userid = self.unauthenticated_userid(request)
+ if userid:
+ if request.verify_userid_is_still_valid(userid):
+ return userid
+
+ def effective_principals(self, request):
+ principals = [Everyone]
+ userid = self.authenticated_userid(request)
+ if userid:
+ principals += [Authenticated, str(userid)]
+ return principals
+
+In most instances ``authenticated_userid`` and ``effective_principals`` are
+application-specific whereas ``unauthenticated_userid``, ``remember`` and
+``forget`` are generic and focused on transport/serialization of data
+between consecutive requests.
+
+.. index::
single: authentication policy (creating)
.. _creating_an_authentication_policy:
@@ -653,7 +705,7 @@ that implements the following interface:
"""
After you do so, you can pass an instance of such a class into the
-:class:`~pyramid.config.Configurator.set_authentication_policy` method
+:class:`~pyramid.config.Configurator.set_authentication_policy` method at
configuration time to use it.
.. index::
diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst
index 87a962a9a..ca6a55164 100644
--- a/docs/narr/urldispatch.rst
+++ b/docs/narr/urldispatch.rst
@@ -495,17 +495,21 @@ result in a particular view callable being invoked:
:linenos:
config.add_route('idea', 'site/{id}')
- config.add_view('mypackage.views.site_view', route_name='idea')
+ config.scan()
When a route configuration with a ``view`` attribute is added to the system,
and an incoming request matches the *pattern* of the route configuration, the
:term:`view callable` named as the ``view`` attribute of the route
configuration will be invoked.
-In the case of the above example, when the URL of a request matches
-``/site/{id}``, the view callable at the Python dotted path name
-``mypackage.views.site_view`` will be called with the request. In other
-words, we've associated a view callable directly with a route pattern.
+Recall that the ``@view_config`` is equivalent to calling ``config.add_view``,
+because the ``config.scan()`` call will import ``mypackage.views``, shown
+below, and execute ``config.add_view`` under the hood. Each view then maps the
+route name to the matching view callable. In the case of the above
+example, when the URL of a request matches ``/site/{id}``, the view callable at
+the Python dotted path name ``mypackage.views.site_view`` will be called with
+the request. In other words, we've associated a view callable directly with a
+route pattern.
When the ``/site/{id}`` route pattern matches during a request, the
``site_view`` view callable is invoked with that request as its sole
@@ -519,8 +523,10 @@ The ``mypackage.views`` module referred to above might look like so:
.. code-block:: python
:linenos:
+ from pyramid.view import view_config
from pyramid.response import Response
+ @view_config(route_name='idea')
def site_view(request):
return Response(request.matchdict['id'])
@@ -542,11 +548,30 @@ add to your application:
config.add_route('idea', 'ideas/{idea}')
config.add_route('user', 'users/{user}')
config.add_route('tag', 'tags/{tag}')
+ config.scan()
+
+Here is an example of a corresponding ``mypackage.views`` module:
- config.add_view('mypackage.views.idea_view', route_name='idea')
- config.add_view('mypackage.views.user_view', route_name='user')
- config.add_view('mypackage.views.tag_view', route_name='tag')
+.. code-block:: python
+ :linenos:
+
+ from pyramid.view import view_config
+ from pyramid.response import Response
+ @view_config(route_name='idea')
+ def idea_view(request):
+ return Response(request.matchdict['id'])
+
+ @view_config(route_name='user')
+ def user_view(request):
+ user = request.matchdict['user']
+ return Response(u'The user is {}.'.format(user))
+
+ @view_config(route_name='tag')
+ def tag_view(request):
+ tag = request.matchdict['tag']
+ return Response(u'The tag is {}.'.format(tag))
+
The above configuration will allow :app:`Pyramid` to service URLs in these
forms:
@@ -596,7 +621,7 @@ An example of using a route with a factory:
:linenos:
config.add_route('idea', 'ideas/{idea}', factory='myproject.resources.Idea')
- config.add_view('myproject.views.idea_view', route_name='idea')
+ config.scan()
The above route will manufacture an ``Idea`` resource as a :term:`context`,
assuming that ``mypackage.resources.Idea`` resolves to a class that accepts a
@@ -610,7 +635,20 @@ request in its ``__init__``. For example:
pass
In a more complicated application, this root factory might be a class
-representing a :term:`SQLAlchemy` model.
+representing a :term:`SQLAlchemy` model. The view ``mypackage.views.idea_view``
+might look like this:
+
+.. code-block:: python
+ :linenos:
+
+ @view_config(route_name='idea')
+ def idea_view(request):
+ idea = request.context
+ return Response(idea)
+
+Here, ``request.context`` is an instance of ``Idea``. If indeed the resource
+object is a SQLAlchemy model, you do not even have to perform a query in the
+view callable, since you have access to the resource via ``request.context``.
See :ref:`route_factories` for more details about how to use route factories.
diff --git a/pyramid/compat.py b/pyramid/compat.py
index 3aa191968..e9edda359 100644
--- a/pyramid/compat.py
+++ b/pyramid/compat.py
@@ -23,7 +23,7 @@ except ImportError: # pragma: no cover
# True if we are running on Python 3.
PY3 = sys.version_info[0] == 3
-if PY3: # pragma: no cover
+if PY3:
string_types = str,
integer_types = int,
class_types = type,
@@ -38,23 +38,21 @@ 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
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):
return s.encode(encoding, errors)
return s
-if PY3: # pragma: no cover
+if PY3:
def ascii_native_(s):
if isinstance(s, text_type):
s = s.encode('ascii')
@@ -74,7 +72,7 @@ Python 2: If ``s`` is an instance of ``text_type``, return
"""
-if PY3: # pragma: no cover
+if PY3:
def native_(s, encoding='latin-1', errors='strict'):
""" If ``s`` is an instance of ``text_type``, return
``s``, otherwise return ``str(s, encoding, errors)``"""
@@ -97,7 +95,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:
from urllib import parse
urlparse = parse
from urllib.parse import quote as url_quote
@@ -174,13 +172,13 @@ else: # pragma: no cover
return d.iterkeys()
-if PY3: # pragma: no cover
+if PY3:
def map_(*arg):
return list(map(*arg))
else:
map_ = map
-if PY3: # pragma: no cover
+if PY3:
def is_nonstr_iter(v):
if isinstance(v, str):
return False
@@ -189,51 +187,48 @@ else:
def is_nonstr_iter(v):
return hasattr(v, '__iter__')
-if PY3: # pragma: no cover
+if PY3:
im_func = '__func__'
im_self = '__self__'
else:
im_func = 'im_func'
im_self = 'im_self'
-try: # pragma: no cover
+try:
import configparser
-except ImportError: # pragma: no cover
+except ImportError:
import ConfigParser as configparser
try:
- from Cookie import SimpleCookie
-except ImportError: # pragma: no cover
from http.cookies import SimpleCookie
+except ImportError:
+ from Cookie import SimpleCookie
-if PY3: # pragma: no cover
+if PY3:
from html import escape
else:
from cgi import escape
-try: # pragma: no cover
- input_ = raw_input
-except NameError: # pragma: no cover
+if PY3:
input_ = input
+else:
+ input_ = raw_input
-
-# support annotations and keyword-only arguments in PY3
-if PY3: # pragma: no cover
+if PY3:
from inspect import getfullargspec as getargspec
else:
from inspect import getargspec
-try:
- from StringIO import StringIO as NativeIO
-except ImportError: # pragma: no cover
+if PY3:
from io import StringIO as NativeIO
+else:
+ from io import BytesIO 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:
# see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before
# decoding it to utf-8
def decode_path_info(path):
@@ -242,8 +237,8 @@ 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:
+ # 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):
@@ -258,6 +253,16 @@ else:
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 PY3: # pragma: no cover
+ from inspect import getfullargspec as getargspec
+else:
+ from inspect import getargspec
+
+if PY3: # pragma: no cover
+ from itertools import zip_longest
+else:
+ from itertools import izip_longest as zip_longest
def is_unbound_method(fn):
"""
@@ -267,7 +272,7 @@ def is_unbound_method(fn):
is_bound = is_bound_method(fn)
if not is_bound and inspect.isroutine(fn):
- spec = inspect.getargspec(fn)
+ spec = 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
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index c89c94538..7e8eb0326 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -12,7 +12,10 @@ from pyramid.interfaces import (
IDebugLogger,
IExceptionResponse,
IPredicateList,
+ PHASE0_CONFIG,
PHASE1_CONFIG,
+ PHASE2_CONFIG,
+ PHASE3_CONFIG,
)
from pyramid.asset import resolve_asset_spec
@@ -23,6 +26,7 @@ from pyramid.compat import (
text_,
reraise,
string_types,
+ zip_longest,
)
from pyramid.events import ApplicationCreated
@@ -54,7 +58,9 @@ from pyramid.settings import aslist
from pyramid.threadlocal import manager
from pyramid.util import (
+ ActionInfo,
WeakOrderedSet,
+ action_method,
object_description,
)
@@ -74,11 +80,6 @@ from pyramid.config.zca import ZCAConfiguratorMixin
from pyramid.path import DottedNameResolver
-from pyramid.util import (
- action_method,
- ActionInfo,
- )
-
empty = text_('')
_marker = object()
@@ -86,6 +87,10 @@ ConfigurationError = ConfigurationError # pyflakes
not_ = not_ # pyflakes, this is an API
+PHASE0_CONFIG = PHASE0_CONFIG # api
+PHASE1_CONFIG = PHASE1_CONFIG # api
+PHASE2_CONFIG = PHASE2_CONFIG # api
+PHASE3_CONFIG = PHASE3_CONFIG # api
class Configurator(
TestingConfiguratorMixin,
@@ -1072,10 +1077,82 @@ class ActionState(object):
>>> 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:
- for action in resolveConflicts(self.actions):
+ all_actions = []
+ executed_actions = []
+ pending_actions = iter([])
+
+ # resolve the new action list against what we have already
+ # executed -- if a new action appears intertwined in the list
+ # of already-executed actions then someone wrote a broken
+ # re-entrant action because it scheduled the action *after* it
+ # should have been executed (as defined by the action order)
+ def resume(actions):
+ for a, b in zip_longest(actions, executed_actions):
+ if b is None and a is not None:
+ # common case is that we are executing every action
+ yield a
+ elif b is not None and a != b:
+ raise ConfigurationError(
+ 'During execution a re-entrant action was added '
+ 'that modified the planned execution order in a '
+ 'way that is incompatible with what has already '
+ 'been executed.')
+ else:
+ # resolved action is in the same location as before,
+ # so we are in good shape, but the action is already
+ # executed so we skip it
+ assert b is not None and a == b
+
+ 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:
+ # Only resolve the new actions against executed_actions
+ # and pending_actions instead of everything to avoid
+ # redundant checks.
+ # Assume ``actions = resolveConflicts([A, B, C])`` which
+ # after conflict checks, resulted in ``actions == [A]``
+ # then we know action A won out or a conflict would have
+ # been raised. Thus, when action D is added later, we only
+ # need to check the new action against A.
+ # ``actions = resolveConflicts([A, D]) should drop the
+ # number of redundant checks down from O(n^2) closer to
+ # O(n lg n).
+ all_actions.extend(self.actions)
+ pending_actions = resume(resolveConflicts(
+ executed_actions +
+ list(pending_actions) +
+ self.actions
+ ))
+ self.actions = []
+
+ action = next(pending_actions, None)
+ if action is None:
+ # we are done!
+ break
+
callable = action['callable']
args = action['args']
kw = action['kw']
@@ -1101,10 +1178,14 @@ class ActionState(object):
if introspector is not None:
for introspectable in introspectables:
introspectable.register(introspector, info)
-
+
+ executed_actions.append(action)
+
finally:
if clear:
del self.actions[:]
+ else:
+ self.actions = all_actions
# this function is licensed under the ZPL (stolen from Zope)
def resolveConflicts(actions):
@@ -1197,18 +1278,20 @@ def resolveConflicts(actions):
for _, _, action in rest:
includepath = action['includepath']
# Test whether path is a prefix of opath
- if (includepath[:len(basepath)] != basepath # not a prefix
- or includepath == basepath):
+ 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 conflict-resolved actions by (order, i) and yield them one by one
+ # sort conflict-resolved actions by (order, i) and yield them one
+ # by one
for a in [x[2] for x in sorted(output, key=operator.itemgetter(0, 1))]:
yield a
-
+
+
def expand_action(discriminator, callable=None, args=(), kw=None,
includepath=(), info=None, order=0, introspectables=()):
if kw is None:
@@ -1225,4 +1308,3 @@ def expand_action(discriminator, callable=None, args=(), kw=None,
)
global_registries = WeakOrderedSet()
-
diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py
index c05a7dbf1..bbdf18ced 100644
--- a/pyramid/config/assets.py
+++ b/pyramid/config/assets.py
@@ -214,6 +214,10 @@ class PackageAssetSource(object):
"""
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):
@@ -221,33 +225,33 @@ class PackageAssetSource(object):
def get_filename(self, resource_name):
path = self.get_path(resource_name)
- if pkg_resources.resource_exists(self.package, path):
- return pkg_resources.resource_filename(self.package, path)
+ 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.package, path):
- return pkg_resources.resource_stream(self.package, path)
+ 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.package, path):
- return pkg_resources.resource_string(self.package, path)
+ 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.package, path):
+ 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.package, path):
- return pkg_resources.resource_isdir(self.package, path)
+ 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.package, path):
- return pkg_resources.resource_listdir(self.package, path)
+ if pkg_resources.resource_exists(self.pkg_name, path):
+ return pkg_resources.resource_listdir(self.pkg_name, path)
class FSAssetSource(object):
diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py
index 10678df55..f0b6252ae 100644
--- a/pyramid/config/factories.py
+++ b/pyramid/config/factories.py
@@ -14,8 +14,8 @@ from pyramid.traversal import DefaultRootFactory
from pyramid.util import (
action_method,
- InstancePropertyMixin,
get_callable_name,
+ InstancePropertyHelper,
)
@@ -174,7 +174,7 @@ 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__
diff --git a/pyramid/config/views.py b/pyramid/config/views.py
index 2b3fa98f6..eff1e2e95 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -356,9 +356,8 @@ class ViewDeriver(object):
def _rendered_view(self, view, view_renderer):
def rendered_view(context, request):
- renderer = view_renderer
result = view(context, request)
- if result.__class__ is Response: # potential common case
+ if result.__class__ is Response: # potential common case
response = result
else:
registry = self.registry
@@ -374,6 +373,9 @@ class ViewDeriver(object):
name=renderer_name,
package=self.kw.get('package'),
registry=registry)
+ else:
+ renderer = view_renderer.clone()
+
if '__view__' in attrs:
view_inst = attrs.pop('__view__')
else:
@@ -386,9 +388,10 @@ class ViewDeriver(object):
def _response_resolved_view(self, view):
registry = self.registry
+
def viewresult_to_response(context, request):
result = view(context, request)
- if result.__class__ is Response: # common case
+ if result.__class__ is Response: # common case
response = result
else:
response = registry.queryAdapterOrSelf(result, IResponse)
@@ -419,6 +422,7 @@ class ViewDeriver(object):
return view
return decorator(view)
+
@implementer(IViewMapper)
@provider(IViewMapperFactory)
class DefaultViewMapper(object):
@@ -1992,9 +1996,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/httpexceptions.py b/pyramid/httpexceptions.py
index c7a9e9da9..465769834 100644
--- a/pyramid/httpexceptions.py
+++ b/pyramid/httpexceptions.py
@@ -1103,9 +1103,11 @@ def default_exceptionresponse_view(context, request):
status_map = {}
code = None
for name, value in list(globals().items()):
- if (isinstance(value, class_types) and
- issubclass(value, HTTPException)
- and not name.startswith('_')):
+ if (
+ isinstance(value, class_types) and
+ issubclass(value, HTTPException) and
+ not name.startswith('_')
+ ):
code = getattr(value, 'code', None)
if code:
status_map[code] = value
diff --git a/pyramid/i18n.py b/pyramid/i18n.py
index dac3a609a..458f6168d 100644
--- a/pyramid/i18n.py
+++ b/pyramid/i18n.py
@@ -332,9 +332,9 @@ class Translations(gettext.GNUTranslations, object):
"""Like ``ugettext()``, but look the message up in the specified
domain.
"""
- if PY3: # pragma: no cover
+ if PY3:
return self._domains.get(domain, self).gettext(message)
- else: # pragma: no cover
+ else:
return self._domains.get(domain, self).ugettext(message)
def dngettext(self, domain, singular, plural, num):
@@ -353,10 +353,10 @@ class Translations(gettext.GNUTranslations, object):
"""Like ``ungettext()`` but look the message up in the specified
domain.
"""
- if PY3: # pragma: no cover
+ if PY3:
return self._domains.get(domain, self).ngettext(
singular, plural, num)
- else: # pragma: no cover
+ else:
return self._domains.get(domain, self).ungettext(
singular, plural, num)
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index f00f106bb..7b9a850d1 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -382,6 +382,9 @@ class IRendererInfo(Interface):
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
@@ -591,8 +594,7 @@ class IResponseFactory(Interface):
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
@@ -1193,18 +1195,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
@@ -1236,6 +1231,7 @@ class ICacheBuster(Interface):
# 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/pyramid/renderers.py b/pyramid/renderers.py
index 3c35551ea..088d451bb 100644
--- a/pyramid/renderers.py
+++ b/pyramid/renderers.py
@@ -1,3 +1,4 @@
+import contextlib
import json
import os
@@ -73,24 +74,16 @@ def render(renderer_name, value, request=None, package=None):
helper = RendererHelper(name=renderer_name, package=package,
registry=registry)
- saved_response = None
- # save the current response, preventing the renderer from affecting it
- attrs = request.__dict__ if request is not None else {}
- if 'response' in attrs:
- saved_response = attrs['response']
- del attrs['response']
-
- result = helper.render(value, None, request=request)
-
- # restore the original response, overwriting any changes
- if saved_response is not None:
- attrs['response'] = saved_response
- elif 'response' in attrs:
- del attrs['response']
+ with temporary_response(request):
+ result = helper.render(value, None, request=request)
return result
-def render_to_response(renderer_name, value, request=None, package=None):
+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
@@ -121,9 +114,16 @@ def render_to_response(renderer_name, value, request=None, package=None):
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 if the ``request`` parameter is
- not passed in, any changes to ``request.response`` attributes made
- before calling this function will be ignored.
+ 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:
@@ -134,7 +134,33 @@ def render_to_response(renderer_name, value, request=None, package=None):
package = caller_package()
helper = RendererHelper(name=renderer_name, package=package,
registry=registry)
- return helper.render_to_response(value, None, request=request)
+
+ with temporary_response(request):
+ if response is not None:
+ request.response = response
+ result = helper.render_to_response(value, None, request=request)
+
+ return result
+
+@contextlib.contextmanager
+def temporary_response(request):
+ """
+ Temporarily delete request.response and restore it afterward.
+ """
+ saved_response = None
+ # save the current response, preventing the renderer from affecting it
+ attrs = request.__dict__ if request is not None else {}
+ if 'response' in attrs:
+ saved_response = attrs['response']
+ del attrs['response']
+
+ yield
+
+ # restore the original response, overwriting any changes
+ if saved_response is not None:
+ attrs['response'] = saved_response
+ elif 'response' in attrs:
+ del attrs['response']
def get_renderer(renderer_name, package=None):
""" Return the renderer object for the renderer ``renderer_name``.
diff --git a/pyramid/request.py b/pyramid/request.py
index be9ce0db3..d9fcd6d8b 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
@@ -308,3 +313,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/scaffolds/copydir.py b/pyramid/scaffolds/copydir.py
index 5eef2c40a..4471777f2 100644
--- a/pyramid/scaffolds/copydir.py
+++ b/pyramid/scaffolds/copydir.py
@@ -186,10 +186,10 @@ def query_interactive(src_fn, dest_fn, src_content, dest_content,
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('---')])
+ 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:
diff --git a/pyramid/scaffolds/tests.py b/pyramid/scaffolds/tests.py
index e26162f2d..49358c1cf 100644
--- a/pyramid/scaffolds/tests.py
+++ b/pyramid/scaffolds/tests.py
@@ -6,13 +6,13 @@ import tempfile
import time
try:
+ import http.client as httplib
+except ImportError:
import httplib
-except ImportError: # pragma: no cover
- import http.client as httplib # py3
class TemplateTest(object):
- def make_venv(self, directory): # pragma: no cover
+ def make_venv(self, directory): # pragma: no cover
import virtualenv
from virtualenv import Logger
logger = Logger([(Logger.level_for_integer(2), sys.stdout)])
@@ -22,7 +22,7 @@ class TemplateTest(object):
clear=False,
unzip_setuptools=True)
- def install(self, tmpl_name): # pragma: no cover
+ def install(self, tmpl_name): # pragma: no cover
try:
self.old_cwd = os.getcwd()
self.directory = tempfile.mkdtemp()
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/pserve.py b/pyramid/scripts/pserve.py
index 2c508170d..e97bdcd48 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):
@@ -183,15 +188,17 @@ class PServeCommand(object):
print(msg)
def get_options(self):
- if (len(self.args) > 1
- and self.args[1] in self.possible_subcommands):
+ if (
+ len(self.args) > 1 and
+ self.args[1] in self.possible_subcommands
+ ):
restvars = self.args[2:]
else:
restvars = self.args[1:]
return parse_vars(restvars)
- def run(self): # pragma: no cover
+ def run(self): # pragma: no cover
if self.options.stop_daemon:
return self.stop_daemon()
@@ -208,8 +215,10 @@ class PServeCommand(object):
return 2
app_spec = self.args[0]
- if (len(self.args) > 1
- and self.args[1] in self.possible_subcommands):
+ if (
+ len(self.args) > 1 and
+ self.args[1] in self.possible_subcommands
+ ):
cmd = self.args[1]
else:
cmd = None
@@ -294,8 +303,10 @@ class PServeCommand(object):
self.out(str(ex))
return 2
- if (self.options.monitor_restart
- and not os.environ.get(self._monitor_environ_key)):
+ if (
+ self.options.monitor_restart and
+ not os.environ.get(self._monitor_environ_key)
+ ):
return self.restart_with_monitor()
if self.options.pid_file:
@@ -345,7 +356,7 @@ class PServeCommand(object):
def open_browser():
context = loadcontext(SERVER, app_spec, name=app_name, relative_to=base,
global_conf=vars)
- url = 'http://{host}:{port}/'.format(**context.config())
+ url = 'http://127.0.0.1:{port}/'.format(**context.config())
time.sleep(1)
webbrowser.open(url)
t = threading.Thread(target=open_browser)
@@ -712,15 +723,23 @@ 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
+ if fd.isatty():
+ 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 095c59a52..cb78feb9b 100644
--- a/pyramid/static.py
+++ b/pyramid/static.py
@@ -176,7 +176,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
@@ -194,38 +194,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
@@ -233,7 +249,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.
@@ -247,9 +279,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_config/pkgs/asset/models.py b/pyramid/tests/test_config/pkgs/asset/models.py
deleted file mode 100644
index d80d14bb3..000000000
--- a/pyramid/tests/test_config/pkgs/asset/models.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from zope.interface import Interface
-
-class IFixture(Interface):
- pass
-
-def fixture():
- """ """
-
diff --git a/pyramid/tests/test_config/pkgs/asset/views.py b/pyramid/tests/test_config/pkgs/asset/views.py
deleted file mode 100644
index cbfc5a574..000000000
--- a/pyramid/tests/test_config/pkgs/asset/views.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from zope.interface import Interface
-from webob import Response
-from pyramid.httpexceptions import HTTPForbidden
-
-def fixture_view(context, request):
- """ """
- return Response('fixture')
-
-def erroneous_view(context, request):
- """ """
- raise RuntimeError()
-
-def exception_view(context, request):
- """ """
- return Response('supressed')
-
-def protected_view(context, request):
- """ """
- raise HTTPForbidden()
-
-class IDummy(Interface):
- pass
diff --git a/pyramid/tests/test_config/test_adapters.py b/pyramid/tests/test_config/test_adapters.py
index 4cbb1bf80..b3b7576a3 100644
--- a/pyramid/tests/test_config/test_adapters.py
+++ b/pyramid/tests/test_config/test_adapters.py
@@ -219,7 +219,7 @@ class AdaptersConfiguratorMixinTests(unittest.TestCase):
def test_add_response_adapter_dottednames(self):
from pyramid.interfaces import IResponse
config = self._makeOne(autocommit=True)
- if PY3: # pragma: no cover
+ if PY3:
str_name = 'builtins.str'
else:
str_name = '__builtin__.str'
diff --git a/pyramid/tests/test_config/test_assets.py b/pyramid/tests/test_config/test_assets.py
index b605a602d..842c73da6 100644
--- a/pyramid/tests/test_config/test_assets.py
+++ b/pyramid/tests/test_config/test_assets.py
@@ -54,6 +54,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
self.assertEqual(source.package, subpackage)
self.assertEqual(source.prefix, 'templates/bar.pt')
+ resource_name = ''
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
def test_override_asset_package_with_package(self):
from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
@@ -71,6 +77,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
self.assertEqual(source.package, subpackage)
self.assertEqual(source.prefix, '')
+ resource_name = 'templates/bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
def test_override_asset_directory_with_directory(self):
from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
@@ -88,6 +100,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
self.assertEqual(source.package, subpackage)
self.assertEqual(source.prefix, 'templates/')
+ resource_name = 'bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
def test_override_asset_directory_with_package(self):
from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
@@ -105,6 +123,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
self.assertEqual(source.package, subpackage)
self.assertEqual(source.prefix, '')
+ resource_name = 'templates/bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
def test_override_asset_package_with_directory(self):
from pyramid.config.assets import PackageAssetSource
config = self._makeOne(autocommit=True)
@@ -122,6 +146,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
self.assertEqual(source.package, subpackage)
self.assertEqual(source.prefix, 'templates/')
+ resource_name = 'bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
def test_override_asset_directory_with_absfile(self):
from pyramid.exceptions import ConfigurationError
config = self._makeOne()
@@ -161,6 +191,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
self.assertTrue(isinstance(source, FSAssetSource))
self.assertEqual(source.prefix, abspath)
+ resource_name = ''
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
def test_override_asset_directory_with_absdirectory(self):
from pyramid.config.assets import FSAssetSource
config = self._makeOne(autocommit=True)
@@ -177,6 +213,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
self.assertTrue(isinstance(source, FSAssetSource))
self.assertEqual(source.prefix, abspath)
+ resource_name = 'bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
def test_override_asset_package_with_absdirectory(self):
from pyramid.config.assets import FSAssetSource
config = self._makeOne(autocommit=True)
@@ -193,6 +235,12 @@ class TestAssetsConfiguratorMixin(unittest.TestCase):
self.assertTrue(isinstance(source, FSAssetSource))
self.assertEqual(source.prefix, abspath)
+ resource_name = 'bar.pt'
+ expected = os.path.join(here, 'pkgs', 'asset',
+ 'subpackage', 'templates', 'bar.pt')
+ self.assertEqual(override.source.get_filename(resource_name),
+ expected)
+
def test__override_not_yet_registered(self):
from pyramid.interfaces import IPackageOverrides
package = DummyPackage('package')
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index aeebe3c91..0ed04eb06 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -1515,6 +1515,73 @@ class TestActionState(unittest.TestCase):
self.assertRaises(ConfigurationExecutionError, c.execute_actions)
self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})])
+ def test_reentrant_action(self):
+ output = []
+ c = self._makeOne()
+ def f(*a, **k):
+ output.append(('f', a, k))
+ c.actions.append((3, g, (8,), {}))
+ def g(*a, **k):
+ output.append(('g', a, k))
+ c.actions = [
+ (1, f, (1,)),
+ ]
+ c.execute_actions()
+ self.assertEqual(output, [('f', (1,), {}), ('g', (8,), {})])
+
+ def test_reentrant_action_error(self):
+ from pyramid.exceptions import ConfigurationError
+ c = self._makeOne()
+ def f(*a, **k):
+ c.actions.append((3, g, (8,), {}, (), None, -1))
+ def g(*a, **k): pass
+ c.actions = [
+ (1, f, (1,)),
+ ]
+ self.assertRaises(ConfigurationError, c.execute_actions)
+
+ def test_reentrant_action_without_clear(self):
+ c = self._makeOne()
+ def f(*a, **k):
+ c.actions.append((3, g, (8,)))
+ def g(*a, **k): pass
+ c.actions = [
+ (1, f, (1,)),
+ ]
+ c.execute_actions(clear=False)
+ self.assertEqual(c.actions, [
+ (1, f, (1,)),
+ (3, g, (8,)),
+ ])
+
+class Test_reentrant_action_functional(unittest.TestCase):
+ def _makeConfigurator(self, *arg, **kw):
+ from pyramid.config import Configurator
+ config = Configurator(*arg, **kw)
+ return config
+
+ def test_functional(self):
+ def add_auto_route(config, name, view):
+ def register():
+ config.add_view(route_name=name, view=view)
+ config.add_route(name, '/' + name)
+ config.action(
+ ('auto route', name), register, order=-30
+ )
+ config = self._makeConfigurator()
+ config.add_directive('add_auto_route', add_auto_route)
+ def my_view(request): return request.response
+ config.add_auto_route('foo', my_view)
+ config.commit()
+ from pyramid.interfaces import IRoutesMapper
+ mapper = config.registry.getUtility(IRoutesMapper)
+ routes = mapper.get_routes()
+ route = routes[0]
+ self.assertEqual(len(routes), 1)
+ self.assertEqual(route.name, 'foo')
+ self.assertEqual(route.path, '/foo')
+
+
class Test_resolveConflicts(unittest.TestCase):
def _callFUT(self, actions):
from pyramid.config import resolveConflicts
diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py
index d1eb1ed3c..180050941 100644
--- a/pyramid/tests/test_config/test_views.py
+++ b/pyramid/tests/test_config/test_views.py
@@ -2548,6 +2548,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst, view)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
def view(request):
return 'OK'
deriver = self._makeOne(renderer=moo())
@@ -2585,6 +2587,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst, 'view')
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
def view(request):
return 'OK'
deriver = self._makeOne(renderer=moo())
@@ -3179,6 +3183,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst.__class__, View)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View(object):
def __init__(self, context, request):
pass
@@ -3203,6 +3209,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst.__class__, View)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View(object):
def __init__(self, request):
pass
@@ -3227,6 +3235,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst.__class__, View)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View:
def __init__(self, context, request):
pass
@@ -3251,6 +3261,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst.__class__, View)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View:
def __init__(self, request):
pass
@@ -3275,6 +3287,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst, view)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View:
def index(self, context, request):
return {'a':'1'}
@@ -3297,6 +3311,8 @@ class TestViewDeriver(unittest.TestCase):
self.assertEqual(view_inst, view)
self.assertEqual(ctx, context)
return response
+ def clone(self):
+ return self
class View:
def index(self, request):
return {'a':'1'}
@@ -3995,7 +4011,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', {})
@@ -4014,7 +4030,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')
@@ -4127,10 +4143,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_path.py b/pyramid/tests/test_path.py
index fd927996a..f85373fd9 100644
--- a/pyramid/tests/test_path.py
+++ b/pyramid/tests/test_path.py
@@ -376,7 +376,7 @@ class TestDottedNameResolver(unittest.TestCase):
def test_zope_dottedname_style_resolve_builtin(self):
typ = self._makeOne()
- if PY3: # pragma: no cover
+ if PY3:
result = typ._zope_dottedname_style('builtins.str', None)
else:
result = typ._zope_dottedname_style('__builtin__.str', None)
diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py
index 6d79cc291..ed6344a40 100644
--- a/pyramid/tests/test_renderers.py
+++ b/pyramid/tests/test_renderers.py
@@ -517,10 +517,11 @@ class Test_render_to_response(unittest.TestCase):
def tearDown(self):
testing.tearDown()
- def _callFUT(self, renderer_name, value, request=None, package=None):
+ def _callFUT(self, renderer_name, value, request=None, package=None,
+ response=None):
from pyramid.renderers import render_to_response
return render_to_response(renderer_name, value, request=request,
- package=package)
+ package=package, response=response)
def test_it_no_request(self):
renderer = self.config.testing_add_renderer(
@@ -554,6 +555,43 @@ class Test_render_to_response(unittest.TestCase):
renderer.assert_(a=1)
renderer.assert_(request=request)
+ def test_response_preserved(self):
+ request = testing.DummyRequest()
+ response = object() # should error if mutated
+ request.response = response
+ # use a json renderer, which will mutate the response
+ result = self._callFUT('json', dict(a=1), request=request)
+ self.assertEqual(result.body, b'{"a": 1}')
+ self.assertNotEqual(request.response, result)
+ self.assertEqual(request.response, response)
+
+ def test_no_response_to_preserve(self):
+ from pyramid.decorator import reify
+ class DummyRequestWithClassResponse(object):
+ _response = DummyResponse()
+ _response.content_type = None
+ _response.default_content_type = None
+ @reify
+ def response(self):
+ return self._response
+ request = DummyRequestWithClassResponse()
+ # use a json renderer, which will mutate the response
+ result = self._callFUT('json', dict(a=1), request=request)
+ self.assertEqual(result.body, b'{"a": 1}')
+ self.assertFalse('response' in request.__dict__)
+
+ def test_custom_response_object(self):
+ class DummyRequestWithClassResponse(object):
+ pass
+ request = DummyRequestWithClassResponse()
+ response = DummyResponse()
+ # use a json renderer, which will mutate the response
+ result = self._callFUT('json', dict(a=1), request=request,
+ response=response)
+ self.assertTrue(result is response)
+ self.assertEqual(result.body, b'{"a": 1}')
+ self.assertFalse('response' in request.__dict__)
+
class Test_get_renderer(unittest.TestCase):
def setUp(self):
self.config = testing.setUp()
@@ -614,7 +652,14 @@ class Dummy:
class DummyResponse:
status = '200 OK'
+ default_content_type = 'text/html'
+ content_type = default_content_type
headerlist = ()
app_iter = ()
- body = ''
+ body = b''
+
+ # compat for renderer that will set unicode on py3
+ def _set_text(self, val): # pragma: no cover
+ self.body = val.encode('utf8')
+ text = property(fset=_set_text)
diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py
index 48af98f59..79cf1abb8 100644
--- a/pyramid/tests/test_request.py
+++ b/pyramid/tests/test_request.py
@@ -310,7 +310,7 @@ class TestRequest(unittest.TestCase):
b'/\xe6\xb5\x81\xe8\xa1\x8c\xe8\xb6\x8b\xe5\x8a\xbf',
'utf-8'
)
- if PY3: # pragma: no cover
+ if PY3:
body = bytes(json.dumps({'a':inp}), 'utf-16')
else:
body = json.dumps({'a':inp}).decode('utf-8').encode('utf-16')
@@ -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/pystartup.py b/pyramid/tests/test_scripts/pystartup.py
deleted file mode 100644
index c4e5bcc80..000000000
--- a/pyramid/tests/test_scripts/pystartup.py
+++ /dev/null
@@ -1 +0,0 @@
-foo = 1
diff --git a/pyramid/tests/test_scripts/pystartup.txt b/pyramid/tests/test_scripts/pystartup.txt
new file mode 100644
index 000000000..c62c4ca74
--- /dev/null
+++ b/pyramid/tests/test_scripts/pystartup.txt
@@ -0,0 +1,3 @@
+# this file has a .txt extension to avoid coverage reports
+# since it is not imported but rather the contents are read and exec'd
+foo = 1
diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py
index 107ff4c0a..75d4f5bef 100644
--- a/pyramid/tests/test_scripts/test_pserve.py
+++ b/pyramid/tests/test_scripts/test_pserve.py
@@ -4,7 +4,7 @@ import tempfile
import unittest
from pyramid.compat import PY3
-if PY3: # pragma: no cover
+if PY3:
import builtins as __builtin__
else:
import __builtin__
diff --git a/pyramid/tests/test_scripts/test_pshell.py b/pyramid/tests/test_scripts/test_pshell.py
index a6ba2eaea..dab32fecd 100644
--- a/pyramid/tests/test_scripts/test_pshell.py
+++ b/pyramid/tests/test_scripts/test_pshell.py
@@ -379,7 +379,7 @@ class TestPShellCommand(unittest.TestCase):
os.path.abspath(
os.path.join(
os.path.dirname(__file__),
- 'pystartup.py')))
+ 'pystartup.txt')))
shell = dummy.DummyShell()
command.run(shell)
self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp')
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_traversal.py b/pyramid/tests/test_traversal.py
index 0dcc4a027..aa3f1ad16 100644
--- a/pyramid/tests/test_traversal.py
+++ b/pyramid/tests/test_traversal.py
@@ -335,7 +335,7 @@ class ResourceTreeTraverserTests(unittest.TestCase):
foo = DummyContext(bar, path)
root = DummyContext(foo, 'root')
policy = self._makeOne(root)
- if PY3: # pragma: no cover
+ if PY3:
vhm_root = b'/Qu\xc3\xa9bec'.decode('latin-1')
else:
vhm_root = b'/Qu\xc3\xa9bec'
diff --git a/pyramid/tests/test_urldispatch.py b/pyramid/tests/test_urldispatch.py
index 1755d9f47..20a3a4fc8 100644
--- a/pyramid/tests/test_urldispatch.py
+++ b/pyramid/tests/test_urldispatch.py
@@ -120,7 +120,7 @@ class RoutesMapperTests(unittest.TestCase):
def test___call__pathinfo_cant_be_decoded(self):
from pyramid.exceptions import URLDecodeError
mapper = self._makeOne()
- if PY3: # pragma: no cover
+ if PY3:
path_info = b'\xff\xfe\xe6\x00'.decode('latin-1')
else:
path_info = b'\xff\xfe\xe6\x00'
diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py
index 371cd8703..2bf6a710f 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -2,6 +2,188 @@ 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()
@@ -111,58 +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__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__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
@@ -301,9 +431,9 @@ class Test_object_description(unittest.TestCase):
self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')")
def test_set(self):
- if PY3: # pragma: no cover
+ if PY3:
self.assertEqual(self._callFUT(set(['a'])), "{'a'}")
- else: # pragma: no cover
+ else:
self.assertEqual(self._callFUT(set(['a'])), "set(['a'])")
def test_list(self):
@@ -646,7 +776,7 @@ class TestCallableName(unittest.TestCase):
else: # pragma: nocover
name = text_(b'hello world', 'utf-8')
- self.assertEquals(get_callable_name(name), 'hello world')
+ self.assertEqual(get_callable_name(name), 'hello world')
def test_invalid_ascii(self):
from pyramid.util import get_callable_name
diff --git a/pyramid/traversal.py b/pyramid/traversal.py
index ae9530258..db73d13fc 100644
--- a/pyramid/traversal.py
+++ b/pyramid/traversal.py
@@ -575,7 +575,7 @@ the ``safe`` argument to this function. This corresponds to the
"""
-if PY3: # pragma: no cover
+if PY3:
# special-case on Python 2 for speed? unchecked
def quote_path_segment(segment, safe=''):
""" %s """ % quote_path_segment_doc
diff --git a/pyramid/urldispatch.py b/pyramid/urldispatch.py
index 349742c4a..4a8828810 100644
--- a/pyramid/urldispatch.py
+++ b/pyramid/urldispatch.py
@@ -210,7 +210,7 @@ def _compile_route(route):
def generator(dict):
newdict = {}
for k, v in dict.items():
- if PY3: # pragma: no cover
+ if PY3:
if v.__class__ is binary_type:
# url_quote below needs a native string, not bytes on Py3
v = v.decode('utf-8')
diff --git a/pyramid/util.py b/pyramid/util.py
index bbae56f02..de8ca34cf 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -16,7 +16,6 @@ from pyramid.exceptions import (
)
from pyramid.compat import (
- iteritems_,
is_nonstr_iter,
integer_types,
string_types,
@@ -28,20 +27,29 @@ from pyramid.compat import (
from pyramid.interfaces import IActionInfo
from pyramid.path import DottedNameResolver as _DottedNameResolver
+
class DottedNameResolver(_DottedNameResolver):
def __init__(self, package=None): # default to package = None for bw compat
return _DottedNameResolver.__init__(self, package)
_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.
@@ -69,25 +77,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
@@ -106,15 +104,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)
+ def apply(self, target):
+ """ Apply all configured properties to the ``target`` instance."""
+ if self.properties:
+ self.apply_properties(target, self.properties)
- self._set_properties(extensions.descriptors)
+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.
@@ -168,8 +185,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.
@@ -293,7 +310,7 @@ def object_description(object):
if isinstance(object, (bool, float, type(None))):
return text_(str(object))
if isinstance(object, set):
- if PY3: # pragma: no cover
+ if PY3:
return shortrepr(object, '}')
else:
return shortrepr(object, ')')
diff --git a/setup.cfg b/setup.cfg
index 7d905e370..3ef28451f 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,9 +5,6 @@ zip_ok = false
match=^test
where=pyramid
nocapture=1
-cover-package=pyramid
-cover-erase=1
-cover-min-percentage=100
[aliases]
dev = develop easy_install pyramid[testing]
diff --git a/tox.ini b/tox.ini
index 756988c91..b52eac67e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,22 +1,29 @@
[tox]
-envlist =
- py26,py27,py32,py33,py34,pypy,pypy3,cover
+envlist =
+ py26,py27,py32,py33,py34,pypy,pypy3,pep8,
+ {py2,py3}-docs,
+ {py2,py3}-cover,coverage
[testenv]
-commands =
- python setup.py -q dev
- python setup.py -q test -q
-
-[testenv:cover]
+# Most of these are defaults but if you specify any you can't fall back
+# to defaults for others.
basepython =
- python2.6
-commands =
- python setup.py -q dev
- nosetests --with-xunit --with-xcoverage
-deps =
- nosexcover
+ py26: python2.6
+ py27: python2.7
+ py32: python3.2
+ py33: python3.3
+ py34: python3.4
+ pypy: pypy
+ pypy3: pypy3
+ py2: python2.7
+ py3: python3.4
+
+commands =
+ pip install pyramid[testing]
+ nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:}
[testenv:pep8]
+basepython = python3.4
commands =
flake8 pyramid/
deps =
@@ -25,4 +32,42 @@ deps =
# we separate coverage into its own testenv because a) "last run wins" wrt
# cobertura jenkins reporting and b) pypy and jython can't handle any
# combination of versions of coverage and nosexcover that i can find.
+[testenv:py2-cover]
+commands =
+ pip install pyramid[testing]
+ coverage run --source=pyramid {envbindir}/nosetests
+ coverage xml -o coverage-py2.xml
+setenv =
+ COVERAGE_FILE=.coverage.py2
+
+[testenv:py3-cover]
+commands =
+ pip install pyramid[testing]
+ coverage run --source=pyramid {envbindir}/nosetests
+ coverage xml -o coverage-py3.xml
+setenv =
+ COVERAGE_FILE=.coverage.py3
+[testenv:py2-docs]
+whitelist_externals = make
+commands =
+ pip install pyramid[docs]
+ make -C docs html
+
+[testenv:py3-docs]
+whitelist_externals = make
+commands =
+ pip install pyramid[docs]
+ make -C docs html
+
+[testenv:coverage]
+basepython = python3.4
+commands =
+ coverage erase
+ coverage combine
+ coverage xml
+ coverage report --show-missing --fail-under=100
+deps =
+ coverage
+setenv =
+ COVERAGE_FILE=.coverage