summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2015-03-08 14:44:18 -0400
committerChris McDonough <chrism@plope.com>2015-03-08 14:44:18 -0400
commit8ebde50fdc7322e82b96ad103ab168b92ca2b74a (patch)
tree3be6e2cae14d7fd3b9951111a9c98c6bb7bfb4de
parent5fff455548067480c820da2f64bc0b9c16a916a0 (diff)
parent6c1a1c60123d150a41fef3062df9a64b995305c5 (diff)
downloadpyramid-8ebde50fdc7322e82b96ad103ab168b92ca2b74a.tar.gz
pyramid-8ebde50fdc7322e82b96ad103ab168b92ca2b74a.tar.bz2
pyramid-8ebde50fdc7322e82b96ad103ab168b92ca2b74a.zip
Merge branch 'master' of github.com:Pylons/pyramid
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml4
-rw-r--r--CHANGES.txt29
-rw-r--r--docs/narr/security.rst60
-rw-r--r--docs/narr/urldispatch.rst58
-rw-r--r--pyramid/compat.py55
-rw-r--r--pyramid/config/assets.py26
-rw-r--r--pyramid/config/views.py3
-rw-r--r--pyramid/i18n.py8
-rw-r--r--pyramid/interfaces.py3
-rw-r--r--pyramid/renderers.py64
-rw-r--r--pyramid/scaffolds/tests.py4
-rw-r--r--pyramid/scripts/pserve.py13
-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_views.py16
-rw-r--r--pyramid/tests/test_path.py2
-rw-r--r--pyramid/tests/test_renderers.py51
-rw-r--r--pyramid/tests/test_request.py2
-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_traversal.py2
-rw-r--r--pyramid/tests/test_urldispatch.py2
-rw-r--r--pyramid/tests/test_util.py4
-rw-r--r--pyramid/traversal.py2
-rw-r--r--pyramid/urldispatch.py2
-rw-r--r--pyramid/util.py2
-rw-r--r--setup.cfg3
-rw-r--r--tox.ini74
33 files changed, 425 insertions, 156 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 f2bedbcc9..19d77eb68 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -26,6 +26,9 @@ 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``.
Core APIs are shipped for both cache busting via query strings and
@@ -102,12 +105,27 @@ 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
+ 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``.
@@ -148,12 +166,19 @@ 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
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 a12790d82..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):
@@ -277,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/assets.py b/pyramid/config/assets.py
index 9da092f08..6dabea358 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/views.py b/pyramid/config/views.py
index 24c592f7a..aba28467d 100644
--- a/pyramid/config/views.py
+++ b/pyramid/config/views.py
@@ -349,7 +349,6 @@ 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
response = result
@@ -367,6 +366,8 @@ 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:
diff --git a/pyramid/i18n.py b/pyramid/i18n.py
index 4c8f4b55d..c30351f7a 100644
--- a/pyramid/i18n.py
+++ b/pyramid/i18n.py
@@ -331,9 +331,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):
@@ -352,10 +352,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 4c171f9cc..bab91b0ee 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
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/scaffolds/tests.py b/pyramid/scaffolds/tests.py
index dfbf9b6cf..db828759e 100644
--- a/pyramid/scaffolds/tests.py
+++ b/pyramid/scaffolds/tests.py
@@ -6,9 +6,9 @@ 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
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index 3b79aabd7..57e4ab012 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -350,7 +350,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)
@@ -716,11 +716,12 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover
def ensure_echo_on(): # pragma: no cover
if termios:
- fd = sys.stdin.fileno()
- attr_list = termios.tcgetattr(fd)
- if not attr_list[3] & termios.ECHO:
- attr_list[3] |= termios.ECHO
- termios.tcsetattr(fd, termios.TCSANOW, attr_list)
+ 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
"""
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_views.py b/pyramid/tests/test_config/test_views.py
index 36c86f78c..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'}
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 f142e4536..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')
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_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 459c729a0..2bf6a710f 100644
--- a/pyramid/tests/test_util.py
+++ b/pyramid/tests/test_util.py
@@ -431,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):
diff --git a/pyramid/traversal.py b/pyramid/traversal.py
index 4c275c4c1..a38cf271e 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 5721a93fc..7a8af4899 100644
--- a/pyramid/util.py
+++ b/pyramid/util.py
@@ -309,7 +309,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 9633b6980..875480594 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 202e29e30..e0f99e7f6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,23 +1,63 @@
[tox]
-skipsdist = True
-envlist =
- py26,py27,py32,py33,py34,pypy,pypy3,cover
+envlist =
+ py26,py27,py32,py33,py34,pypy,pypy3,
+ {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:}
-# 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