diff options
| author | Chris McDonough <chrism@plope.com> | 2015-03-08 14:44:18 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2015-03-08 14:44:18 -0400 |
| commit | 8ebde50fdc7322e82b96ad103ab168b92ca2b74a (patch) | |
| tree | 3be6e2cae14d7fd3b9951111a9c98c6bb7bfb4de | |
| parent | 5fff455548067480c820da2f64bc0b9c16a916a0 (diff) | |
| parent | 6c1a1c60123d150a41fef3062df9a64b995305c5 (diff) | |
| download | pyramid-8ebde50fdc7322e82b96ad103ab168b92ca2b74a.tar.gz pyramid-8ebde50fdc7322e82b96ad103ab168b92ca2b74a.tar.bz2 pyramid-8ebde50fdc7322e82b96ad103ab168b92ca2b74a.zip | |
Merge branch 'master' of github.com:Pylons/pyramid
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, ')') @@ -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] @@ -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 |
