From cf4ad5eaea4af3da3bbac4c421fe5f959ed1d256 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 18:54:20 -0600 Subject: update tox/travis to check code coverage on py3 coverage is combined between py2 and py3 for an aggregate coverage metric. This means we can stop putting "no cover" gates around py3 code and ignoring whether it's ever tested. --- .travis.yml | 3 +- pyramid/compat.py | 45 +++++++++++++++--------------- pyramid/i18n.py | 8 +++--- pyramid/scaffolds/tests.py | 4 +-- pyramid/tests/test_config/test_adapters.py | 2 +- pyramid/tests/test_path.py | 2 +- pyramid/tests/test_request.py | 2 +- pyramid/tests/test_scripts/test_pserve.py | 2 +- pyramid/tests/test_traversal.py | 2 +- pyramid/tests/test_urldispatch.py | 2 +- pyramid/tests/test_util.py | 4 +-- pyramid/traversal.py | 2 +- pyramid/urldispatch.py | 2 +- pyramid/util.py | 2 +- setup.cfg | 1 - tox.ini | 12 ++++---- 16 files changed, 48 insertions(+), 47 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5a205b268..482d2a910 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: python env: + - TOXENV=clean - TOXENV=py26 - TOXENV=py27 - TOXENV=py32 @@ -9,7 +10,7 @@ env: - TOXENV=py34 - TOXENV=pypy - TOXENV=pypy3 - - TOXENV=cover + - TOXENV=report install: - travis_retry pip install tox diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..919a6d244 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, @@ -43,16 +43,16 @@ def text_(s, encoding='latin-1', errors='strict'): ``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') @@ -72,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)``""" @@ -95,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 @@ -169,13 +169,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 @@ -184,45 +184,44 @@ 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 - -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): @@ -231,7 +230,7 @@ else: def decode_path_info(path): return path.decode('utf-8') -if PY3: # pragma: no cover +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): 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/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/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_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_request.py b/pyramid/tests/test_request.py index 48af98f59..5ae0b80b7 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/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_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 a18fa8d16..292dfa024 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -284,9 +284,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 fe4d433c3..c40bec526 100644 --- a/pyramid/urldispatch.py +++ b/pyramid/urldispatch.py @@ -201,7 +201,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 6de53d559..abc7b2c88 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -291,7 +291,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 a877ffb7f..bc092a6ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,6 @@ match=^test where=pyramid nocapture=1 cover-package=pyramid -cover-erase=1 [aliases] dev = develop easy_install pyramid[testing] diff --git a/tox.ini b/tox.ini index 3f32dbc3f..ba0007d7a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,17 @@ [tox] envlist = - py26,py27,py32,py33,py34,pypy,pypy3,cover + clean,py26,py27,py32,py33,py34,pypy,pypy3,report + +[testenv:clean] +commands = coverage erase +deps = coverage [testenv] commands = python setup.py dev - python setup.py test -q + python setup.py nosetests --with-coverage -[testenv:cover] -basepython = - python2.6 +[testenv:report] commands = python setup.py dev python setup.py nosetests --with-xunit --with-xcoverage --cover-min-percentage=100 -- cgit v1.2.3 From 149d36342e9970295ee49b0dfca7f1c896bce2a9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 19:02:36 -0600 Subject: update changelog --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ea3323aa0..d6264b892 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,6 +6,9 @@ Features - Added support / testing for 'pypy3' under Tox and Travis. +- 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 -- cgit v1.2.3 From 11ba5a50ef1c3c8eba4d58d0af0333f6bfe5ae61 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 21:34:17 -0600 Subject: test to see if files are shared between builds --- .travis.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 482d2a910..e2f379802 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,16 @@ # Wire up travis language: python - -env: - - TOXENV=clean - - TOXENV=py26 - - TOXENV=py27 - - TOXENV=py32 - - TOXENV=py33 - - TOXENV=py34 - - TOXENV=pypy - - TOXENV=pypy3 - - TOXENV=report +# +#env: +# - TOXENV=clean +# - TOXENV=py26 +# - TOXENV=py27 +# - TOXENV=py32 +# - TOXENV=py33 +# - TOXENV=py34 +# - TOXENV=pypy +# - TOXENV=pypy3 +# - TOXENV=report install: - travis_retry pip install tox -- cgit v1.2.3 From bb60b86feeea7cfbb531460b22ad40f211562708 Mon Sep 17 00:00:00 2001 From: Zack Marvel Date: Wed, 10 Dec 2014 01:25:11 -0500 Subject: Revise URL Dispatch documentation to use config.scan() in Examples 1, 2, and 3 In response to #600. --- docs/narr/urldispatch.rst | 57 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 87a962a9a..2fd971917 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -495,17 +495,20 @@ 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 ``config.scan`` is equivalent to calling ``config.add_view``, +because the ``@view_config`` decorator in ``mypackage.views``, shown below, +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 +522,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 +547,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 +620,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 +634,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. -- cgit v1.2.3 From dd22319b1b8df9b1772451035fca582bd666e218 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 01:56:27 -0600 Subject: update render_to_response to prevent renderers from mutating request.response fixes #1536 --- pyramid/renderers.py | 43 +++++++++++++++++++++++++++-------------- pyramid/tests/test_renderers.py | 25 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 3c35551ea..c2be8c2eb 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -1,3 +1,4 @@ +import contextlib import json import os @@ -73,20 +74,8 @@ 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 @@ -134,7 +123,31 @@ 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): + 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/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 6d79cc291..31e9d14f8 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -554,6 +554,31 @@ 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__) + class Test_get_renderer(unittest.TestCase): def setUp(self): self.config = testing.setUp() -- cgit v1.2.3 From d23e6986b1122e8d7344f2c882ddd3e3f423e30f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:06:07 -0600 Subject: update changelog and docs --- CHANGES.txt | 10 ++++++++++ pyramid/renderers.py | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b334f5258..30f30cec7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -80,6 +80,16 @@ 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. 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 renderers. See https://github.com/Pylons/pyramid/pull/1563 + Bug Fixes --------- diff --git a/pyramid/renderers.py b/pyramid/renderers.py index c2be8c2eb..42a4c98dc 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -110,9 +110,9 @@ 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. """ try: -- cgit v1.2.3 From e382164aa71731390f97db9734ce0b0bb014c78a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:09:20 -0600 Subject: moar docs --- pyramid/renderers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 42a4c98dc..c4ea22429 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -114,6 +114,11 @@ def render_to_response(renderer_name, value, request=None, package=None): prior to calling this function will not be reflected in the resulting response object. A new response object will be created for each call. + .. 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. + """ try: registry = request.registry -- cgit v1.2.3 From 72bf6bb1b942a56a39d5ae33634e7aa8fac7080a Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 13:00:59 -0600 Subject: add a respones arg to render_to_response --- pyramid/renderers.py | 14 +++++++++++--- pyramid/tests/test_renderers.py | 26 +++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index c4ea22429..088d451bb 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -79,7 +79,11 @@ def render(renderer_name, value, request=None, package=None): 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 @@ -112,12 +116,14 @@ def render_to_response(renderer_name, value, request=None, package=None): with the most correct 'system' values (``request`` and ``context`` in particular). Keep in mind that any changes made to ``request.response`` prior to calling this function will not be reflected in the resulting - response object. A new response object will be created for each call. + 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. + longer the case. If you wish to send in a pre-initialized response + then you may pass one in the ``response`` argument. """ try: @@ -130,6 +136,8 @@ def render_to_response(renderer_name, value, request=None, package=None): registry=registry) with temporary_response(request): + if response is not None: + request.response = response result = helper.render_to_response(value, None, request=request) return result diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 31e9d14f8..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( @@ -579,6 +580,18 @@ class Test_render_to_response(unittest.TestCase): 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() @@ -639,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) -- cgit v1.2.3 From 04206845591cb5af1038a29f6e943bfb169c2d5c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 13:03:20 -0600 Subject: update changelog --- CHANGES.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 30f30cec7..a02ae504b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -81,14 +81,18 @@ Features 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. Almost all renderers + ``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 renderers. See https://github.com/Pylons/pyramid/pull/1563 + API of a renderer. See https://github.com/Pylons/pyramid/pull/1563 Bug Fixes --------- -- cgit v1.2.3 From b6e148dc678cad3bc63d64f41385114134e017be Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 11 Feb 2015 00:23:42 -0600 Subject: clone a new RendererHelper per request --- pyramid/config/views.py | 3 ++- pyramid/tests/test_config/test_views.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 1f69d7e0b..338021c24 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -348,7 +348,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 @@ -366,6 +365,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/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index b0d03fb72..ed5426b12 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -2534,6 +2534,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()) @@ -2571,6 +2573,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()) @@ -3165,6 +3169,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 @@ -3189,6 +3195,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 @@ -3213,6 +3221,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 @@ -3237,6 +3247,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 @@ -3261,6 +3273,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'} @@ -3283,6 +3297,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'} -- cgit v1.2.3 From 06bb4a4db67321e7629f4f2438516ba358f226e9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 11 Feb 2015 00:25:53 -0600 Subject: update changelog --- CHANGES.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 2dee64a84..95566a54b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -128,6 +128,13 @@ Bug Fixes - 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 + Deprecations ------------ -- cgit v1.2.3 From 14126cae5cf308ed466ed3eea576094e9c2193b4 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Wed, 11 Feb 2015 12:25:02 -0600 Subject: fix up tests to generate coverage reports always and combine at the end --- .gitignore | 3 +++ .travis.yml | 21 ++++++++++---------- pyramid/tests/test_config/pkgs/asset/models.py | 8 -------- pyramid/tests/test_config/pkgs/asset/views.py | 22 --------------------- pyramid/tests/test_scripts/pystartup.py | 1 - pyramid/tests/test_scripts/pystartup.txt | 3 +++ pyramid/tests/test_scripts/test_pshell.py | 2 +- setup.cfg | 1 - tox.ini | 27 ++++++++++++++------------ 9 files changed, 32 insertions(+), 56 deletions(-) delete mode 100644 pyramid/tests/test_config/pkgs/asset/models.py delete mode 100644 pyramid/tests/test_config/pkgs/asset/views.py delete mode 100644 pyramid/tests/test_scripts/pystartup.py create mode 100644 pyramid/tests/test_scripts/pystartup.txt diff --git a/.gitignore b/.gitignore index 8dca2069c..b60cd530a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,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 e2f379802..7cf2b6a16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,15 @@ # Wire up travis language: python -# -#env: -# - TOXENV=clean -# - TOXENV=py26 -# - TOXENV=py27 -# - TOXENV=py32 -# - TOXENV=py33 -# - TOXENV=py34 -# - TOXENV=pypy -# - TOXENV=pypy3 -# - TOXENV=report + +env: + - TOXENV=py26 + - TOXENV=py27 + - TOXENV=py32 + - TOXENV=py33 + - TOXENV=py34 + - TOXENV=pypy + - TOXENV=pypy3 + - TOXENV=py26,py32,cover install: - travis_retry pip install tox 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_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_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/setup.cfg b/setup.cfg index bc092a6ca..875480594 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,6 @@ zip_ok = false match=^test where=pyramid nocapture=1 -cover-package=pyramid [aliases] dev = develop easy_install pyramid[testing] diff --git a/tox.ini b/tox.ini index ba0007d7a..dae31ddd6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,24 +1,27 @@ [tox] +skipsdist = True envlist = - clean,py26,py27,py32,py33,py34,pypy,pypy3,report - -[testenv:clean] -commands = coverage erase -deps = coverage + py26,py27,py32,py33,py34,pypy,pypy3,cover [testenv] commands = - python setup.py dev - python setup.py nosetests --with-coverage + {envbindir}/python setup.py dev + {envbindir}/coverage run --source={toxinidir}/pyramid {envbindir}/nosetests --xunit-file=nosetests-{envname}.xml + {envbindir}/coverage xml -o coverage-{envname}.xml +setenv = + COVERAGE_FILE=.coverage.{envname} -[testenv:report] +[testenv:cover] commands = - python setup.py dev - python setup.py nosetests --with-xunit --with-xcoverage --cover-min-percentage=100 + {envbindir}/coverage erase + {envbindir}/coverage combine + {envbindir}/coverage xml + {envbindir}/coverage report --show-missing --fail-under=100 deps = - nosexcover + coverage +setenv = + COVERAGE_FILE=.coverage # 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. - -- cgit v1.2.3 From 750b783e9726684b2860bac4c1ab9d385f4cfb78 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 20:50:33 -0600 Subject: fix typo on changes.rst --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index f2bedbcc9..ca2020cdb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -148,7 +148,7 @@ 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`` -- cgit v1.2.3 From 3c163b212a6848c1d45916073d6a60a9020ea5c1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 21:08:58 -0600 Subject: reword a small part to clarify what's happening with view_config --- docs/narr/urldispatch.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/narr/urldispatch.rst b/docs/narr/urldispatch.rst index 2fd971917..ca6a55164 100644 --- a/docs/narr/urldispatch.rst +++ b/docs/narr/urldispatch.rst @@ -502,9 +502,10 @@ 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. -Recall that ``config.scan`` is equivalent to calling ``config.add_view``, -because the ``@view_config`` decorator in ``mypackage.views``, shown below, -maps the route name to the matching view callable. In the case of the above +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 -- cgit v1.2.3 From 459493929a92b14a986ba387bdabd3c551ddee72 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 21:14:49 -0600 Subject: grammar --- docs/narr/security.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index 2dc0c76af..a02f65660 100644 --- a/docs/narr/security.rst +++ b/docs/narr/security.rst @@ -653,7 +653,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:: -- cgit v1.2.3 From df966ac2f5c6fc230db920d945be4a6567521e40 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 21:45:56 -0600 Subject: enhance security docs with an example of subclassing a builtin policy --- docs/narr/security.rst | 58 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/docs/narr/security.rst b/docs/narr/security.rst index a02f65660..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: @@ -582,6 +580,60 @@ denied or allowed. Introspecting this information in the debugger or 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) -- cgit v1.2.3 From 99bf8b84fbadf5c50232fc90ee2cdc5708b6f6bf Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 22:11:24 -0600 Subject: pserve -b will always open 127.0.0.1 --- pyramid/scripts/pserve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 3b79aabd7..d68075e01 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) -- cgit v1.2.3 From 5ace6591cfb49199befc258ccb256a69c455477e Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Feb 2015 15:37:43 -0800 Subject: Enhance test_assets to expose #1580 This enhances existing tests so that they detect the issue in #1580. Then I'm going to fix the issue in PR #1587. See #1580 --- pyramid/tests/test_config/test_assets.py | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) 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') -- cgit v1.2.3 From e51295bee250a144adee0d31b4c6d0a62ad27770 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Thu, 19 Feb 2015 13:57:37 -0800 Subject: Fix asset override with package `AssetsConfiguratorMixin.override_asset` does: ```python __import__(override_package) to_package = sys.modules[override_package] override_source = PackageAssetSource(to_package, override_prefix) ``` so it's assuming that the `package` argument to `PackageAssetSource.__init__` takes a module object. But then `PackageAssetSource` had a bunch of methods that did stuff like: - `pkg_resources.resource_exists(self.package, path)` - `pkg_resources.resource_filename(self.package, path)` - `pkg_resources.resource_stream(self.package, path)` and all these `pkg_resources` functions need their `package_or_requirement` argument to be a **string**; not a module - see https://pythonhosted.org/setuptools/pkg_resources.html#basic-resource-access, which says: > the `package_or_requirement argument` may be either a Python package/module > name (e.g. `foo.bar`) or a `Requirement` instance. This causes errors when overriding assets -- e.g.: I am using Kotti and Kotti has this code (https://github.com/Kotti/Kotti/blob/master/kotti/__init__.py#L251): ```python for override in [a.strip() for a in settings['kotti.asset_overrides'].split() if a.strip()]: config.override_asset(to_override='kotti', override_with=override) ``` A Kotti add-on called kotti_navigation does this (https://github.com/Kotti/kotti_navigation/blob/master/kotti_navigation/__init__.py#L12): ```python settings['kotti.asset_overrides'] += ' kotti_navigation:kotti-overrides/' ``` The above code is all legit as far as I can tell and it works fine in pyramid 1.5.2, but it fails with pyramid master with the following: ```pytb File "/Users/marca/python/virtualenvs/kotti_inventorysvc/lib/python2.7/site-packages/pkg_resources.py", line 959, in resource_filename self, resource_name File "/Users/marca/dev/git-repos/pyramid/pyramid/config/assets.py", line 31, in get_resource_filename filename = overrides.get_filename(resource_name) File "/Users/marca/dev/git-repos/pyramid/pyramid/config/assets.py", line 125, in get_filename result = source.get_filename(path) File "/Users/marca/dev/git-repos/pyramid/pyramid/config/assets.py", line 224, in get_filename if pkg_resources.resource_exists(self.package, path): File "/Users/marca/python/virtualenvs/kotti_inventorysvc/lib/python2.7/site-packages/pkg_resources.py", line 948, in resource_exists return get_provider(package_or_requirement).has_resource(resource_name) File "/Users/marca/python/virtualenvs/kotti_inventorysvc/lib/python2.7/site-packages/pkg_resources.py", line 225, in get_provider __import__(moduleOrReq) TypeError: __import__() argument 1 must be string, not module ``` This was a little tricky to resolve because the `override_asset` function wants to pass a module object to `PackageAssetSource.__init__`, but there are a number of tests in `pyramid/tests/test_config/test_assets.py` that assume that it takes a string. So I ended up making it legal to pass either one, so that I don't have to change as much calling code. See https://github.com/Kotti/kotti_navigation/issues/13 --- pyramid/config/assets.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) 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): -- cgit v1.2.3 From a9282da880bfc1da654fc1c547a20fdfb3539fd1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 20 Feb 2015 17:52:41 -0600 Subject: split out targets for py2-cover and py3-cover and add docs building --- .travis.yml | 4 +++- tox.ini | 70 ++++++++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/.travis.yml b/.travis.yml index 74b64d343..42b3073c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,9 @@ env: - TOXENV=py34 - TOXENV=pypy - TOXENV=pypy3 - - TOXENV=py26,py32,cover + - TOXENV=py2-docs + - TOXENV=py3-docs + - TOXENV=py2-cover,py3-cover,coverage install: - travis_retry pip install tox diff --git a/tox.ini b/tox.ini index dae31ddd6..22f21a6a3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +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 = - {envbindir}/python setup.py dev - {envbindir}/coverage run --source={toxinidir}/pyramid {envbindir}/nosetests --xunit-file=nosetests-{envname}.xml - {envbindir}/coverage xml -o coverage-{envname}.xml +# Most of these are defaults but if you specify any you can't fall back +# to defaults for others. +basepython = + 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:py2-cover] +commands = + pip install pyramid[testing] + coverage run --source=pyramid {envbindir}/nosetests --with-xunit --xunit-file=nosetests-py2.xml {posargs:} + 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 --with-xunit --xunit-file=nosetests-py3.xml {posargs:} + coverage xml -o coverage-py3.xml setenv = - COVERAGE_FILE=.coverage.{envname} + COVERAGE_FILE=.coverage.py3 + +[testenv:py2-docs] +whitelist_externals = make +commands = + pip install pyramid[docs] + make -C docs html -[testenv:cover] +[testenv:py3-docs] +whitelist_externals = make +commands = + pip install pyramid[docs] + make -C docs html + +[testenv:coverage] +basepython = python3.4 commands = - {envbindir}/coverage erase - {envbindir}/coverage combine - {envbindir}/coverage xml - {envbindir}/coverage report --show-missing --fail-under=100 + coverage erase + coverage combine + coverage xml + coverage report --show-missing --fail-under=100 deps = coverage setenv = COVERAGE_FILE=.coverage - -# 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. -- cgit v1.2.3 From d476e6932f67adee5116620b546590ba0c2083de Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 20 Feb 2015 17:57:25 -0600 Subject: dump xunit for py2/py3 targets --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 22f21a6a3..e0f99e7f6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = py26,py27,py32,py33,py34,pypy,pypy3, - {py2,py3}-docs + {py2,py3}-docs, {py2,py3}-cover,coverage [testenv] @@ -25,7 +25,7 @@ commands = [testenv:py2-cover] commands = pip install pyramid[testing] - coverage run --source=pyramid {envbindir}/nosetests --with-xunit --xunit-file=nosetests-py2.xml {posargs:} + coverage run --source=pyramid {envbindir}/nosetests coverage xml -o coverage-py2.xml setenv = COVERAGE_FILE=.coverage.py2 @@ -33,7 +33,7 @@ setenv = [testenv:py3-cover] commands = pip install pyramid[testing] - coverage run --source=pyramid {envbindir}/nosetests --with-xunit --xunit-file=nosetests-py3.xml {posargs:} + coverage run --source=pyramid {envbindir}/nosetests coverage xml -o coverage-py3.xml setenv = COVERAGE_FILE=.coverage.py3 -- cgit v1.2.3 From 5080a3ffcf39ed1ea6ca9b8076eea445f23b80d5 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 22 Feb 2015 12:59:31 -0600 Subject: ignore new .eggs folder created by "setup.py test" deps --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8dca2069c..1e3f68f26 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.egg *.egg-info +.eggs/ *.pyc *$py.class *.pt.py -- cgit v1.2.3 From 5b8d1e69fa6035a32e3167f48b328b29a1568991 Mon Sep 17 00:00:00 2001 From: David Glick Date: Tue, 24 Feb 2015 15:33:16 -0800 Subject: only reset terminal echo flag if stdin is a tty --- pyramid/scripts/pserve.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index d68075e01..57e4ab012 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -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 """ -- cgit v1.2.3 From 1bcc347b21e6ce317b1fb31610b570b7b9930419 Mon Sep 17 00:00:00 2001 From: David Glick Date: Tue, 24 Feb 2015 15:35:05 -0800 Subject: update changelog --- CHANGES.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 3084bcfe6..9b10c0831 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -121,7 +121,8 @@ 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``. -- cgit v1.2.3 From ec46918c86d4b1e82a1555ed488d453c65663549 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Mar 2015 10:11:56 -0600 Subject: add clone to the IRendererInfo interface --- pyramid/interfaces.py | 3 +++ 1 file changed, 3 insertions(+) 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 -- cgit v1.2.3