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 bc26debd9ed2a46fca1b0931c78b4054bd37841d Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 25 Dec 2014 23:42:05 -0800 Subject: Add support for passing unbound class methods to `add_view` --- pyramid/config/views.py | 13 ++++++++++++- pyramid/tests/test_config/test_views.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c01b72e12..3e305055f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,7 +42,8 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, - is_nonstr_iter + is_nonstr_iter, + im_self, ) from pyramid.exceptions import ( @@ -418,6 +419,16 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') def __call__(self, view): + # Map the attr directly if the passed in view is method and a + # constructor is defined and must be unbound (for backwards + # compatibility) + if inspect.ismethod(view): + is_bound = getattr(view, im_self, None) is not None + + if not is_bound: + self.attr = view.__name__ + view = view.im_class + if inspect.isclass(view): view = self.map_class(view) else: diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index b0d03fb72..664208fad 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1666,6 +1666,27 @@ class TestViewsConfigurationMixin(unittest.TestCase): renderer=null_renderer) self.assertRaises(ConfigurationConflictError, config.commit) + def test_add_view_class_method_no_attr(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + + class ViewClass(object): + def __init__(self, request): + self.request = request + + def run(self): + return 'OK' + + config = self._makeOne(autocommit=True) + config.add_view(view=ViewClass.run, renderer=null_renderer) + + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + result = wrapper(context, request) + self.assertEqual(result, 'OK') + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): -- cgit v1.2.3 From 2253647075ace9e99171f3e227f5debbcafdd8b8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 10:46:57 -0600 Subject: first cut at a re-entrant configurator where tests still pass --- pyramid/config/__init__.py | 67 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index cfa35ec6c..83683daeb 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -975,7 +975,7 @@ class Configurator( class ActionState(object): def __init__(self): # NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func - self.actions = [] + self.actions = [] self._seen_files = set() def processSpec(self, spec): @@ -1059,10 +1059,54 @@ class ActionState(object): >>> output [('f', (1,), {}), ('f', (2,), {})] - """ + The execution is re-entrant such that actions may be added by other + actions with the one caveat that the order of any added actions must + be equal to or larger than the current action. + + >>> output = [] + >>> def f(*a, **k): + ... output.append(('f', a, k)) + ... context.actions.append((3, g, (8,), {})) + >>> def g(*a, **k): + ... output.append(('g', a, k)) + >>> context.actions = [ + ... (1, f, (1,)), + ... (2, f, (2,)), + ... ] + >>> context.execute_actions() + >>> output + [('f', (1,), {}), ('f', (2,), {}), ('g', (8,), {})] + """ try: - for action in resolveConflicts(self.actions): + all_actions = self.actions + self.actions = [] + executed_actions = [] + + # resolve the new action list against what we have already + # executed -- if a new action appears intertwined in the list + # of already-executed actions then someone wrote a broken + # re-entrant action because it scheduled the action *after* it + # should have been executed (as defined by the action order) + def resume(actions): + for a, b in itertools.izip_longest(actions, executed_actions): + if b is None and a is not None: + # common case is that we are executing every action + yield a + elif b is not None and a != b: + raise RuntimeError('Re-entrant failure - attempted ' + 'to resolve actions in a different ' + 'order from the active execution ' + 'path.') + else: + # resolved action is in the same location as before, + # so we are in good shape, but the action is already + # executed so we skip it + assert b is not None and a == b + + pending_actions = resume(resolveConflicts(all_actions)) + action = next(pending_actions, None) + while action is not None: callable = action['callable'] args = action['args'] kw = action['kw'] @@ -1088,10 +1132,25 @@ class ActionState(object): if introspector is not None: for introspectable in introspectables: introspectable.register(introspector, info) - + + executed_actions.append(action) + + # We cleared the actions list prior to execution so if there + # are some new actions then we add them to the mix and resolve + # conflicts again. This orders the new actions as well as + # ensures that the previously executed actions have no new + # conflicts. + if self.actions: + all_actions.extend(self.actions) + self.actions = [] + pending_actions = resume(resolveConflicts(all_actions)) + action = next(pending_actions, None) + finally: if clear: del self.actions[:] + else: + self.actions = all_actions # this function is licensed under the ZPL (stolen from Zope) def resolveConflicts(actions): -- cgit v1.2.3 From 5d2302a2b8d968245a123e54a8f01cd62c97cf69 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:57:08 -0600 Subject: izip_longest is not valid on py3 --- pyramid/compat.py | 4 ++++ pyramid/config/__init__.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..301984749 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,7 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +if PY3: # pragma: no cover + from itertools import zip_longest +else: + from itertools import izip_longest as zip_longest diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 83683daeb..e907cbb14 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -23,6 +23,7 @@ from pyramid.compat import ( text_, reraise, string_types, + zip_longest, ) from pyramid.events import ApplicationCreated @@ -1089,7 +1090,7 @@ class ActionState(object): # re-entrant action because it scheduled the action *after* it # should have been executed (as defined by the action order) def resume(actions): - for a, b in itertools.izip_longest(actions, executed_actions): + for a, b in zip_longest(actions, executed_actions): if b is None and a is not None: # common case is that we are executing every action yield a -- cgit v1.2.3 From a52326b00b843b94b569d35a8d91a2a4c78b56a0 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:57:26 -0600 Subject: refactor loop to combine conflict resolution paths into one --- pyramid/config/__init__.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index e907cbb14..740c9c47d 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1080,9 +1080,9 @@ class ActionState(object): """ try: - all_actions = self.actions - self.actions = [] + all_actions = [] executed_actions = [] + pending_actions = iter([]) # resolve the new action list against what we have already # executed -- if a new action appears intertwined in the list @@ -1095,19 +1095,32 @@ class ActionState(object): # common case is that we are executing every action yield a elif b is not None and a != b: - raise RuntimeError('Re-entrant failure - attempted ' - 'to resolve actions in a different ' - 'order from the active execution ' - 'path.') + raise ConfigurationError( + 'Re-entrant failure - attempted to resolve ' + 'actions in a different order from the active ' + 'execution path.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already # executed so we skip it assert b is not None and a == b - pending_actions = resume(resolveConflicts(all_actions)) - action = next(pending_actions, None) - while action is not None: + while True: + # We clear the actions list prior to execution so if there + # are some new actions then we add them to the mix and resolve + # conflicts again. This orders the new actions as well as + # ensures that the previously executed actions have no new + # conflicts. + if self.actions: + all_actions.extend(self.actions) + self.actions = [] + pending_actions = resume(resolveConflicts(all_actions)) + + action = next(pending_actions, None) + if action is None: + # we are done! + break + callable = action['callable'] args = action['args'] kw = action['kw'] @@ -1128,7 +1141,7 @@ class ActionState(object): ConfigurationExecutionError(t, v, info), tb) finally: - del t, v, tb + del t, v, tb if introspector is not None: for introspectable in introspectables: @@ -1136,17 +1149,6 @@ class ActionState(object): executed_actions.append(action) - # We cleared the actions list prior to execution so if there - # are some new actions then we add them to the mix and resolve - # conflicts again. This orders the new actions as well as - # ensures that the previously executed actions have no new - # conflicts. - if self.actions: - all_actions.extend(self.actions) - self.actions = [] - pending_actions = resume(resolveConflicts(all_actions)) - action = next(pending_actions, None) - finally: if clear: del self.actions[:] -- cgit v1.2.3 From a1a5306b89bc652ad089551f0976a8b5f68d6b63 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 15:58:21 -0600 Subject: optimize the conflict resolution to occur against only executed actions --- pyramid/config/__init__.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 740c9c47d..1a9cc3f5a 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1112,9 +1112,20 @@ class ActionState(object): # ensures that the previously executed actions have no new # conflicts. if self.actions: + # Only resolve the new actions against executed_actions + # instead of everything to avoid redundant checks. + # Assume ``actions = resolveConflicts([A, B, C])`` which + # after conflict checks, resulted in ``actions == [A]`` + # then we know action A won out or a conflict would have + # been raised. Thus, when action D is added later, we only + # need to check the new action against A. + # ``actions = resolveConflicts([A, D]) should drop the + # number of redundant checks down from O(n^2) closer to + # O(n lg n). + pending_actions = resume(resolveConflicts( + executed_actions + self.actions)) all_actions.extend(self.actions) self.actions = [] - pending_actions = resume(resolveConflicts(all_actions)) action = next(pending_actions, None) if action is None: -- cgit v1.2.3 From d643c10413d49d5ec9c2bc0d6dc2dc4fb08c99c9 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 21 Nov 2014 16:05:20 -0600 Subject: improve error output a bit --- pyramid/config/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 1a9cc3f5a..0bd61bc39 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1096,9 +1096,10 @@ class ActionState(object): yield a elif b is not None and a != b: raise ConfigurationError( - 'Re-entrant failure - attempted to resolve ' - 'actions in a different order from the active ' - 'execution path.') + 'During execution a re-entrant action was added ' + 'that modified the planned execution order in a ' + 'way that is incompatible with what has already ' + 'been done.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already -- cgit v1.2.3 From cf6e03bf042483283c2a7a51fec29a6d73887965 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 26 Dec 2014 22:59:43 -0600 Subject: modify text --- pyramid/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 0bd61bc39..c35338826 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1099,7 +1099,7 @@ class ActionState(object): 'During execution a re-entrant action was added ' 'that modified the planned execution order in a ' 'way that is incompatible with what has already ' - 'been done.') + 'been executed.') else: # resolved action is in the same location as before, # so we are in good shape, but the action is already -- cgit v1.2.3 From 873fa0483a7bfeafa5590b6d992ac52228d1b509 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 00:10:03 -0600 Subject: add reentrant tests --- pyramid/config/__init__.py | 11 ++++++---- pyramid/tests/test_config/test_init.py | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index c35338826..e81ccee3f 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1072,7 +1072,6 @@ class ActionState(object): ... output.append(('g', a, k)) >>> context.actions = [ ... (1, f, (1,)), - ... (2, f, (2,)), ... ] >>> context.execute_actions() >>> output @@ -1114,7 +1113,8 @@ class ActionState(object): # conflicts. if self.actions: # Only resolve the new actions against executed_actions - # instead of everything to avoid redundant checks. + # and pending_actions instead of everything to avoid + # redundant checks. # Assume ``actions = resolveConflicts([A, B, C])`` which # after conflict checks, resulted in ``actions == [A]`` # then we know action A won out or a conflict would have @@ -1123,9 +1123,12 @@ class ActionState(object): # ``actions = resolveConflicts([A, D]) should drop the # number of redundant checks down from O(n^2) closer to # O(n lg n). - pending_actions = resume(resolveConflicts( - executed_actions + self.actions)) all_actions.extend(self.actions) + pending_actions = resume(resolveConflicts( + executed_actions + + list(pending_actions) + + self.actions + )) self.actions = [] action = next(pending_actions, None) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 1e58e4d0f..40cc83885 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1503,6 +1503,45 @@ class TestActionState(unittest.TestCase): self.assertRaises(ConfigurationExecutionError, c.execute_actions) self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})]) + def test_reentrant_action(self): + output = [] + c = self._makeOne() + def f(*a, **k): + output.append(('f', a, k)) + c.actions.append((3, g, (8,), {})) + def g(*a, **k): + output.append(('g', a, k)) + c.actions = [ + (1, f, (1,)), + ] + c.execute_actions() + self.assertEqual(output, [('f', (1,), {}), ('g', (8,), {})]) + + def test_reentrant_action_error(self): + from pyramid.exceptions import ConfigurationError + c = self._makeOne() + def f(*a, **k): + c.actions.append((3, g, (8,), {}, (), None, -1)) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,)), + ] + self.assertRaises(ConfigurationError, c.execute_actions) + + def test_reentrant_action_without_clear(self): + c = self._makeOne() + def f(*a, **k): + c.actions.append((3, g, (8,))) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,)), + ] + c.execute_actions(clear=False) + self.assertEqual(c.actions, [ + (1, f, (1,)), + (3, g, (8,)), + ]) + class Test_resolveConflicts(unittest.TestCase): def _callFUT(self, actions): from pyramid.config import resolveConflicts -- cgit v1.2.3 From 4a7029f6b313b65ba94d0726042ea3adbad38e81 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 22:48:41 -0800 Subject: Raise errors if unbound methods are passed in --- pyramid/compat.py | 17 ++++++++++++ pyramid/config/views.py | 15 +++++------ pyramid/tests/test_compat.py | 46 +++++++++++++++++++++++++++++++++ pyramid/tests/test_config/test_views.py | 21 +++++---------- 4 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 pyramid/tests/test_compat.py diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..749435ebc 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,20 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +def is_unbound_method(fn): + """ + This consistently verifies that the callable is bound to a + class. + """ + is_bound = is_bound_method(fn) + + if not is_bound and inspect.isroutine(fn): + spec = inspect.getargspec(fn) + has_self = len(spec.args) > 0 and spec.args[0] == 'self' + + if PY3 and inspect.isfunction(fn) and has_self: # pragma: no cover + return True + elif inspect.ismethod(fn): + return True + + return False diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3e305055f..d498395e1 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,6 +42,7 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, + is_unbound_method, is_nonstr_iter, im_self, ) @@ -419,15 +420,11 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') def __call__(self, view): - # Map the attr directly if the passed in view is method and a - # constructor is defined and must be unbound (for backwards - # compatibility) - if inspect.ismethod(view): - is_bound = getattr(view, im_self, None) is not None - - if not is_bound: - self.attr = view.__name__ - view = view.im_class + if is_unbound_method(view) and self.attr is None: + raise ConfigurationError(( + 'Unbound method calls are not supported, please set the class ' + 'as your `view` and the method as your `attr`' + )) if inspect.isclass(view): view = self.map_class(view) diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py new file mode 100644 index 000000000..2f80100dd --- /dev/null +++ b/pyramid/tests/test_compat.py @@ -0,0 +1,46 @@ +import unittest + +class TestUnboundMethods(unittest.TestCase): + def test_old_style_bound(self): + from pyramid.compat import is_unbound_method + + class OldStyle: + def run(self): + return 'OK' + + self.assertFalse(is_unbound_method(OldStyle().run)) + + def test_new_style_bound(self): + from pyramid.compat import is_unbound_method + + class NewStyle(object): + def run(self): + return 'OK' + + self.assertFalse(is_unbound_method(NewStyle().run)) + + def test_old_style_unbound(self): + from pyramid.compat import is_unbound_method + + class OldStyle: + def run(self): + return 'OK' + + self.assertTrue(is_unbound_method(OldStyle.run)) + + def test_new_style_unbound(self): + from pyramid.compat import is_unbound_method + + class NewStyle(object): + def run(self): + return 'OK' + + self.assertTrue(is_unbound_method(NewStyle.run)) + + def test_normal_func_unbound(self): + from pyramid.compat import is_unbound_method + + def func(): + return 'OK' + + self.assertFalse(is_unbound_method(func)) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 664208fad..d1eb1ed3c 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1669,23 +1669,16 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_class_method_no_attr(self): from pyramid.renderers import null_renderer from zope.interface import directlyProvides - - class ViewClass(object): - def __init__(self, request): - self.request = request - - def run(self): - return 'OK' + from pyramid.exceptions import ConfigurationError config = self._makeOne(autocommit=True) - config.add_view(view=ViewClass.run, renderer=null_renderer) + class DummyViewClass(object): + def run(self): pass - wrapper = self._getViewCallable(config) - context = DummyContext() - directlyProvides(context, IDummy) - request = self._makeRequest(config) - result = wrapper(context, request) - self.assertEqual(result, 'OK') + def configure_view(): + config.add_view(view=DummyViewClass.run, renderer=null_renderer) + + self.assertRaises(ConfigurationError, configure_view) def test_derive_view_function(self): from pyramid.renderers import null_renderer -- cgit v1.2.3 From 6d4676137885f63f364a2b2ae6205c6931a57220 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 23:04:56 -0800 Subject: Don't need im_self --- pyramid/config/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index d498395e1..afacc1e0b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -44,7 +44,6 @@ from pyramid.compat import ( is_bound_method, is_unbound_method, is_nonstr_iter, - im_self, ) from pyramid.exceptions import ( -- cgit v1.2.3 From 03a0d79306b2846313df1983a721d5cccf4ec3ce Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 23:19:32 -0800 Subject: Clean up compat tests --- pyramid/tests/test_compat.py | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py index 2f80100dd..23ccce82e 100644 --- a/pyramid/tests/test_compat.py +++ b/pyramid/tests/test_compat.py @@ -1,46 +1,26 @@ import unittest +from pyramid.compat import is_unbound_method class TestUnboundMethods(unittest.TestCase): def test_old_style_bound(self): - from pyramid.compat import is_unbound_method - - class OldStyle: - def run(self): - return 'OK' - self.assertFalse(is_unbound_method(OldStyle().run)) def test_new_style_bound(self): - from pyramid.compat import is_unbound_method - - class NewStyle(object): - def run(self): - return 'OK' - self.assertFalse(is_unbound_method(NewStyle().run)) def test_old_style_unbound(self): - from pyramid.compat import is_unbound_method - - class OldStyle: - def run(self): - return 'OK' - self.assertTrue(is_unbound_method(OldStyle.run)) def test_new_style_unbound(self): - from pyramid.compat import is_unbound_method - - class NewStyle(object): - def run(self): - return 'OK' - self.assertTrue(is_unbound_method(NewStyle.run)) def test_normal_func_unbound(self): - from pyramid.compat import is_unbound_method - - def func(): - return 'OK' + def func(): return 'OK' self.assertFalse(is_unbound_method(func)) + +class OldStyle: + def run(self): return 'OK' + +class NewStyle(object): + def run(self): return 'OK' -- cgit v1.2.3 From c569571bdb6e8c001ab0bc11777a2e0cca72d2fb Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 01:55:25 -0600 Subject: add action-order documentation --- docs/narr/extconfig.rst | 99 +++++++++++++++++++++++++++++++++++++++++++++- pyramid/config/__init__.py | 2 +- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 6587aef92..c4d3e0250 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -215,13 +215,110 @@ registers an action with a higher order than the passed to it, that a route by this name was already registered by ``add_route``, and if such a route has not already been registered, it's a configuration error (a view that names a nonexistent route via its -``route_name`` parameter will never be called). +``route_name`` parameter will never be called). As of Pyramid 1.6 it is +possible for one action to invoke another. See :ref:`ordering_actions` for +more information. ``introspectables`` is a sequence of :term:`introspectable` objects. You can pass a sequence of introspectables to the :meth:`~pyramid.config.Configurator.action` method, which allows you to augment Pyramid's configuration introspection system. +.. _ordering_actions: + +Ordering Actions +---------------- + +In Pyramid every :term:`action` has an inherent ordering relative to other +actions. The logic within actions is deferred until a call to +:meth:`pyramid.config.Configurator.commit` (which is automatically invoked by +:meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call +``config.add_view(route_name='foo')`` **before** +``config.add_route('foo', '/foo')`` because nothing actually happens until +commit-time when conflicts are resolved, actions are ordered and executed. + +By default, almost every action in Pyramid has an ``order`` of ``0``. Every +action within the same order-level will be executed in the order it was called. +This means that if an action must be reliably executed before or after another +action, the ``order`` must be defined explicitly to make this work. For +example, views are dependent on routes being defined. Thus the action created +by :meth:`pyramid.config.Configurator.add_route` has an ``order`` of +:const:`pyramid.interfaces.PHASE2_CONFIG`. + +Pre-defined Phases +~~~~~~~~~~~~~~~~~~ + +:const:`pyramid.interfaces.PHASE1_CONFIG` + +- :meth:`pyramid.config.Configurator.add_renderer` +- :meth:`pyramid.config.Configurator.add_route_predicate` +- :meth:`pyramid.config.Configurator.add_subscriber_predicate` +- :meth:`pyramid.config.Configurator.add_view_predicate` +- :meth:`pyramid.config.Configurator.set_authorization_policy` +- :meth:`pyramid.config.Configurator.set_default_permission` +- :meth:`pyramid.config.Configurator.set_view_mapper` + +:const:`pyramid.interfaces.PHASE2_CONFIG` + +- :meth:`pyramid.config.Configurator.add_route` +- :meth:`pyramid.config.Configurator.set_authentication_policy` + +``0`` + +- The default for all builtin or custom directives unless otherwise specified. + +Calling Actions From Actions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.6 + +Pyramid's configurator allows actions to be added during a commit-cycle as +long as they are added to the current or a later ``order`` phase. This means +that your custom action can defer decisions until commit-time and then do +things like invoke :meth:`pyramid.config.Configurator.add_route`. It can also +provide better conflict detection if your addon needs to call more than one +other action. + +For example, let's make an addon that invokes ``add_route`` and ``add_view``, +but we want it to conflict with any other call to our addon: + +.. code-block:: python + :linenos: + + from pyramid.interfaces import PHASE1_CONFIG + + PHASE0_CONFIG = PHASE1_CONFIG - 10 + + def includeme(config): + config.add_directive(add_auto_route, 'add_auto_route') + + def add_auto_route(config, name, view): + def register(): + config.add_view(route_name=name, view=view) + config.add_route(name, '/' + name) + config.action(('auto route', name), register, order=PHASE0_CONFIG) + +Now someone else can use your addon and be informed if there is a conflict +between this route and another, or two calls to ``add_auto_route``. +Notice how we had to invoke our action **before** ``add_view`` or +``add_route``. If we tried to invoke this afterward, the subsequent calls to +``add_view`` and ``add_route`` would cause conflicts because that phase had +already been executed, and the configurator cannot go back in time to add more +views during that commit-cycle. + +.. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def main(global_config, **settings): + config = Configurator() + config.include('auto_route_addon') + config.add_auto_route('foo', my_view) + + def my_view(request): + return request.response + .. _introspection: Adding Configuration Introspection diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index e81ccee3f..a114cf039 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1075,7 +1075,7 @@ class ActionState(object): ... ] >>> context.execute_actions() >>> output - [('f', (1,), {}), ('f', (2,), {}), ('g', (8,), {})] + [('f', (1,), {}), ('g', (8,), {})] """ try: -- cgit v1.2.3 From d35a916095943b020f30acb90e878abe9bfd4fb1 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 27 Dec 2014 01:58:59 -0600 Subject: update changelog --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 46c331268..b60600198 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,12 @@ Next release Features -------- +- The ``pyramid.config.Configurator`` has grown the ability to allow + actions to call other actions during a commit-cycle. This enables much more + logic to be placed into actions, such as the ability to invoke other actions + or group them for improved conflict detection. + See https://github.com/Pylons/pyramid/pull/1513 + - Added support / testing for 'pypy3' under Tox and Travis. See https://github.com/Pylons/pyramid/pull/1469 -- cgit v1.2.3 From 9e7248258800ef2c7072f497901172ab94988708 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 20:06:48 -0800 Subject: Catch bad `name` and raise a `ValueError` --- pyramid/tests/test_config/test_factories.py | 18 +++++++++++++++++- pyramid/util.py | 10 +++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 6e679397f..5ae486c4b 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -111,6 +111,22 @@ class TestFactoriesMixin(unittest.TestCase): config = self._makeOne(autocommit=True) self.assertRaises(AttributeError, config.add_request_method) + def test_add_request_method_with_text_type_name(self): + from pyramid.interfaces import IRequestExtensions + from pyramid.compat import text_ + from pyramid.util import InstancePropertyMixin + + config = self._makeOne(autocommit=True) + def boomshaka(r): pass + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + config.add_request_method(boomshaka, name=name) + exts = config.registry.getUtility(IRequestExtensions) + inst = InstancePropertyMixin() + + def set_extensions(): + inst._set_extensions(exts) + self.assertRaises(ValueError, set_extensions) + self.assertTrue(name in exts.methods) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): @@ -120,7 +136,7 @@ class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def tearDown(self): from zope.deprecation import __show__ __show__.on() - + def _makeOne(self, *arg, **kw): from pyramid.config import Configurator config = Configurator(*arg, **kw) diff --git a/pyramid/util.py b/pyramid/util.py index 6de53d559..e9f5760a6 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -111,7 +111,15 @@ class InstancePropertyMixin(object): def _set_extensions(self, extensions): for name, fn in iteritems_(extensions.methods): method = fn.__get__(self, self.__class__) - setattr(self, name, method) + try: + setattr(self, name, method) + except UnicodeEncodeError: + msg = ( + '`name="%s"` is invalid. `name` must be ascii because it is ' + 'used on __name__ of the method' + ) + raise ValueError(msg % name) + self._set_properties(extensions.descriptors) def set_property(self, callable, name=None, reify=False): -- cgit v1.2.3 From e094ab229eed6f8bf9e7a6a4d4406faefece41e4 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 20:26:22 -0800 Subject: Added py3 support --- pyramid/tests/test_config/test_factories.py | 6 +++++- pyramid/util.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 5ae486c4b..e93ba6908 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -120,13 +120,17 @@ class TestFactoriesMixin(unittest.TestCase): def boomshaka(r): pass name = text_(b'La Pe\xc3\xb1a', 'utf-8') config.add_request_method(boomshaka, name=name) + + name2 = b'La Pe\xc3\xb1a' + config.add_request_method(boomshaka, name=name2) + exts = config.registry.getUtility(IRequestExtensions) inst = InstancePropertyMixin() def set_extensions(): inst._set_extensions(exts) + self.assertRaises(ValueError, set_extensions) - self.assertTrue(name in exts.methods) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/util.py b/pyramid/util.py index e9f5760a6..6ab621fd4 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -113,7 +113,7 @@ class InstancePropertyMixin(object): method = fn.__get__(self, self.__class__) try: setattr(self, name, method) - except UnicodeEncodeError: + except (UnicodeEncodeError, TypeError): msg = ( '`name="%s"` is invalid. `name` must be ascii because it is ' 'used on __name__ of the method' -- cgit v1.2.3 From 4fe3efda811c9d328a1a3da4acda32ecf18dbf03 Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Sat, 24 Jan 2015 19:13:20 -0800 Subject: Tighten test_call_eventsends - Make it check context of events - Rename aftertraversal_events => context_found_events --- pyramid/tests/test_router.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index c6c6eea1c..30ebd5918 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -599,17 +599,19 @@ class TestRouter(unittest.TestCase): environ = self._makeEnviron() self._registerView(view, '', IViewClassifier, None, None) request_events = self._registerEventListener(INewRequest) - aftertraversal_events = self._registerEventListener(IContextFound) + context_found_events = self._registerEventListener(IContextFound) response_events = self._registerEventListener(INewResponse) router = self._makeOne() start_response = DummyStartResponse() result = router(environ, start_response) self.assertEqual(len(request_events), 1) self.assertEqual(request_events[0].request.environ, environ) - self.assertEqual(len(aftertraversal_events), 1) - self.assertEqual(aftertraversal_events[0].request.environ, environ) + self.assertEqual(len(context_found_events), 1) + self.assertEqual(context_found_events[0].request.environ, environ) + self.assertEqual(context_found_events[0].request.context, context) self.assertEqual(len(response_events), 1) self.assertEqual(response_events[0].response, response) + self.assertEqual(response_events[0].request.context, context) self.assertEqual(result, response.app_iter) def test_call_newrequest_evllist_exc_can_be_caught_by_exceptionview(self): -- cgit v1.2.3 From 561591252bd029981952c5c229f4cf27832d34a1 Mon Sep 17 00:00:00 2001 From: saarni Date: Thu, 5 Feb 2015 12:14:21 +0200 Subject: use getfullargspec in PY3, allowing annotations in subscribers --- CONTRIBUTORS.txt | 2 ++ pyramid/config/util.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e4132cda5..adf2224a5 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -238,3 +238,5 @@ Contributors - Hugo Branquinho, 2014/11/25 - Adrian Teng, 2014/12/17 + +- Ilja Everila, 2015/02/05 diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 892592196..b91f3f7ab 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -22,6 +22,12 @@ ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() +# support annotations and keyword-only arguments in PY3 +try: + getargspec = inspect.getfullargspec +except AttributeError: + getargspec = inspect.getargspec + def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) @@ -201,7 +207,7 @@ def takes_one_arg(callee, attr=None, argname=None): return False try: - argspec = inspect.getargspec(fn) + argspec = getargspec(fn) except TypeError: return False -- cgit v1.2.3 From 0ccb82204b8d04f8ffeb8b49a94fb77f981d1122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 5 Feb 2015 19:58:54 +0200 Subject: PY3 only test for function annotations --- pyramid/tests/test_config/test_util.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index bb61714ae..c9b0e9e9b 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -1,5 +1,5 @@ import unittest -from pyramid.compat import text_ +from pyramid.compat import text_, PY3 class TestPredicateList(unittest.TestCase): @@ -568,6 +568,14 @@ class Test_takes_one_arg(unittest.TestCase): foo = Foo() self.assertTrue(self._callFUT(foo.method)) + if PY3: + def test_function_annotations(self): + def foo(bar): + """ """ + # avoid SyntaxErrors in python2 + foo.__annotations__.update({'bar': 'baz'}) + self.assertTrue(self._callFUT(foo)) + class TestNotted(unittest.TestCase): def _makeOne(self, predicate): from pyramid.config.util import Notted -- cgit v1.2.3 From d49949081da1669914ddebb487c87edba3f41000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 5 Feb 2015 20:48:54 +0200 Subject: ugly nop dict update hack for PY2 and coverage --- pyramid/tests/test_config/test_util.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index c9b0e9e9b..0d0de9579 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -568,13 +568,12 @@ class Test_takes_one_arg(unittest.TestCase): foo = Foo() self.assertTrue(self._callFUT(foo.method)) - if PY3: - def test_function_annotations(self): - def foo(bar): - """ """ - # avoid SyntaxErrors in python2 - foo.__annotations__.update({'bar': 'baz'}) - self.assertTrue(self._callFUT(foo)) + def test_function_annotations(self): + def foo(bar): + """ """ + # avoid SyntaxErrors in python2, this if effectively nop + getattr(foo, '__annotations__', {}).update({'bar': 'baz'}) + self.assertTrue(self._callFUT(foo)) class TestNotted(unittest.TestCase): def _makeOne(self, predicate): -- cgit v1.2.3 From d6ff994619d18981dbde6dce7e8a10140f063e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilja=20Everil=C3=A4?= Date: Thu, 5 Feb 2015 21:01:24 +0200 Subject: remove unused import --- pyramid/tests/test_config/test_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 0d0de9579..ccf7fa260 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -1,5 +1,5 @@ import unittest -from pyramid.compat import text_, PY3 +from pyramid.compat import text_ class TestPredicateList(unittest.TestCase): -- cgit v1.2.3 From 31e4924465304f8b01b8c04b22085bf24f40096e Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Feb 2015 22:36:59 -0600 Subject: move getargspec import into pyramid.compat --- pyramid/compat.py | 5 +++++ pyramid/config/util.py | 7 +------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..5909debf2 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,8 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +# support annotations and keyword-only arguments in PY3 +if PY3: + from inspect import getfullargspec as getargspec +else: + from inspect import getargspec diff --git a/pyramid/config/util.py b/pyramid/config/util.py index b91f3f7ab..23cdc6be8 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -3,6 +3,7 @@ import inspect from pyramid.compat import ( bytes_, + getargspec, is_nonstr_iter, ) @@ -22,12 +23,6 @@ ActionInfo = ActionInfo # support bw compat imports MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() -# support annotations and keyword-only arguments in PY3 -try: - getargspec = inspect.getfullargspec -except AttributeError: - getargspec = inspect.getargspec - def as_sorted_tuple(val): if not is_nonstr_iter(val): val = (val,) -- cgit v1.2.3 From 86f4d59b23f91f4a070417bebe1302661d761b2f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Feb 2015 22:38:29 -0600 Subject: update changelog --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index b1bd36904..22c7a20c2 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -77,6 +77,9 @@ Features output by showing the module instead of just ``__repr__``. See https://github.com/Pylons/pyramid/pull/1488 +- Support keyword-only arguments and function annotations in views in + Python 3. See https://github.com/Pylons/pyramid/pull/1556 + Bug Fixes --------- -- cgit v1.2.3 From bbe0ad003c2f0fe5bf66953f12bd15043702838c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Thu, 5 Feb 2015 23:58:40 -0600 Subject: fix coverage --- pyramid/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/compat.py b/pyramid/compat.py index 5909debf2..c49ea1e73 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -245,7 +245,7 @@ def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None # support annotations and keyword-only arguments in PY3 -if PY3: +if PY3: # pragma: no cover from inspect import getfullargspec as getargspec else: from inspect import getargspec -- cgit v1.2.3 From d7c9f0a8190b5041f8efb7f83535d181f6499605 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 14 Dec 2014 18:51:23 -0700 Subject: Move documentation for accept to non-predicate While accept is partially documented as a predicate, it is actually NOT a predicate in that the view machinery has all kinds of special cases for it. This also means that `not_` will not function on it correctly since it is not actually a predicate. --- pyramid/config/views.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c01b72e12..8ee7ca6c9 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -841,6 +841,17 @@ class ViewsConfiguratorMixin(object): very useful for 'civilians' who are just developing stock Pyramid applications. Pay no attention to the man behind the curtain. + accept + + The value of this argument represents a match query for one + or more mimetypes in the ``Accept`` HTTP request header. If + this value is specified, it must be in one of the following + forms: a mimetype match token in the form ``text/plain``, a + wildcard mimetype match token in the form ``text/*`` or a + match-all wildcard mimetype match token in the form ``*/*``. + If any of the forms matches the ``Accept`` header of the + request, this predicate will be true. + Predicate Arguments name @@ -941,17 +952,6 @@ class ViewsConfiguratorMixin(object): This is useful for detecting AJAX requests issued from jQuery, Prototype and other Javascript libraries. - accept - - The value of this argument represents a match query for one - or more mimetypes in the ``Accept`` HTTP request header. If - this value is specified, it must be in one of the following - forms: a mimetype match token in the form ``text/plain``, a - wildcard mimetype match token in the form ``text/*`` or a - match-all wildcard mimetype match token in the form ``*/*``. - If any of the forms matches the ``Accept`` header of the - request, this predicate will be true. - header This value represents an HTTP header name or a header -- cgit v1.2.3 From 63366c3ddf353bbd2a237875f0c42e8f470c31c7 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 14 Dec 2014 18:56:33 -0700 Subject: Update CHANGES --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 22c7a20c2..b2f56e02e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -130,6 +130,12 @@ Deprecations Docs ---- +- Moved the documentation for ``accept`` on ``Configurator.add_view`` to no + longer be part of the decorator list. See + https://github.com/Pylons/pyramid/issues/1391 for a bug report stating + ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the + conclusion that it should not be documented as a predicate. + - Removed logging configuration from Quick Tutorial ini files except for scaffolding- and logging-related chapters to avoid needing to explain it too early. -- cgit v1.2.3 From 958c4c7de27b3353dab6e2a7552a17321236c138 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 14 Dec 2014 19:21:55 -0700 Subject: Update add_route accept to non-predicate --- pyramid/config/routes.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 509955cdd..c6e8fe563 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -138,6 +138,19 @@ class RoutesConfiguratorMixin(object): .. versionadded:: 1.1 + accept + + This value represents a match query for one or more + mimetypes in the ``Accept`` HTTP request header. If this + value is specified, it must be in one of the following + forms: a mimetype match token in the form ``text/plain``, a + wildcard mimetype match token in the form ``text/*`` or a + match-all wildcard mimetype match token in the form ``*/*``. + If any of the forms matches the ``Accept`` header of the + request, or if the ``Accept`` header isn't set at all in the + request, this predicate will be true. If this predicate + returns ``False``, route matching continues. + Predicate Arguments pattern @@ -220,19 +233,6 @@ class RoutesConfiguratorMixin(object): case of the header name is not significant. If this predicate returns ``False``, route matching continues. - accept - - This value represents a match query for one or more - mimetypes in the ``Accept`` HTTP request header. If this - value is specified, it must be in one of the following - forms: a mimetype match token in the form ``text/plain``, a - wildcard mimetype match token in the form ``text/*`` or a - match-all wildcard mimetype match token in the form ``*/*``. - If any of the forms matches the ``Accept`` header of the - request, or if the ``Accept`` header isn't set at all in the - request, this predicate will be true. If this predicate - returns ``False``, route matching continues. - effective_principals If specified, this value should be a :term:`principal` identifier or -- cgit v1.2.3 From c015da76f51304a5186909f01a7850de073eabdc Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sun, 14 Dec 2014 19:56:25 -0700 Subject: Fix typo --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index b2f56e02e..d3788afa5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -131,7 +131,7 @@ Docs ---- - Moved the documentation for ``accept`` on ``Configurator.add_view`` to no - longer be part of the decorator list. See + longer be part of the predicate list. See https://github.com/Pylons/pyramid/issues/1391 for a bug report stating ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the conclusion that it should not be documented as a predicate. -- cgit v1.2.3 From 42075618568881a36d9fb03812a0e651d1c084ce Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 00:06:01 -0600 Subject: fix typo --- pyramid/config/adapters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index f6a652e3d..3d11980da 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -143,7 +143,7 @@ class AdaptersConfiguratorMixin(object): Adds a subscriber predicate factory. The associated subscriber predicate can later be named as a keyword argument to :meth:`pyramid.config.Configurator.add_subscriber` in the - ``**predicates`` anonyous keyword argument dictionary. + ``**predicates`` anonymous keyword argument dictionary. ``name`` should be the name of the predicate. It must be a valid Python identifier (it will be used as a ``**predicates`` keyword -- cgit v1.2.3 From da1f208369607a755be4ad355df07fac0bc7719d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 5 Feb 2015 23:12:36 -0700 Subject: Remove all signs of predicate While this is somewhat a predicate, it really isn't for all intents and purposes because it is treated special. Make sure we document it that way. --- pyramid/config/routes.py | 19 +++++++++---------- pyramid/config/views.py | 17 +++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index c6e8fe563..24f38a4fd 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -140,16 +140,15 @@ class RoutesConfiguratorMixin(object): accept - This value represents a match query for one or more - mimetypes in the ``Accept`` HTTP request header. If this - value is specified, it must be in one of the following - forms: a mimetype match token in the form ``text/plain``, a - wildcard mimetype match token in the form ``text/*`` or a - match-all wildcard mimetype match token in the form ``*/*``. - If any of the forms matches the ``Accept`` header of the - request, or if the ``Accept`` header isn't set at all in the - request, this predicate will be true. If this predicate - returns ``False``, route matching continues. + This value represents a match query for one or more mimetypes in the + ``Accept`` HTTP request header. If this value is specified, it must + be in one of the following forms: a mimetype match token in the form + ``text/plain``, a wildcard mimetype match token in the form + ``text/*`` or a match-all wildcard mimetype match token in the form + ``*/*``. If any of the forms matches the ``Accept`` header of the + request, or if the ``Accept`` header isn't set at all in the request, + this will match the current route. If this does not match the + ``Accept`` header of the request, route matching continues. Predicate Arguments diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 8ee7ca6c9..1f69d7e0b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -843,14 +843,15 @@ class ViewsConfiguratorMixin(object): accept - The value of this argument represents a match query for one - or more mimetypes in the ``Accept`` HTTP request header. If - this value is specified, it must be in one of the following - forms: a mimetype match token in the form ``text/plain``, a - wildcard mimetype match token in the form ``text/*`` or a - match-all wildcard mimetype match token in the form ``*/*``. - If any of the forms matches the ``Accept`` header of the - request, this predicate will be true. + This value represents a match query for one or more mimetypes in the + ``Accept`` HTTP request header. If this value is specified, it must + be in one of the following forms: a mimetype match token in the form + ``text/plain``, a wildcard mimetype match token in the form + ``text/*`` or a match-all wildcard mimetype match token in the form + ``*/*``. If any of the forms matches the ``Accept`` header of the + request, or if the ``Accept`` header isn't set at all in the request, + this will match the current view. If this does not match the + ``Accept`` header of the request, view matching continues. Predicate Arguments -- cgit v1.2.3 From f176630ebd9848173e6cc748f361b4ce9acf76f3 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Thu, 5 Feb 2015 23:15:51 -0700 Subject: Add link to PR --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index d3788afa5..832a2c216 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -135,6 +135,7 @@ Docs https://github.com/Pylons/pyramid/issues/1391 for a bug report stating ``not_`` was failing on ``accept``. Discussion with @mcdonc led to the conclusion that it should not be documented as a predicate. + See https://github.com/Pylons/pyramid/pull/1487 for this PR - Removed logging configuration from Quick Tutorial ini files except for scaffolding- and logging-related chapters to avoid needing to explain it too -- cgit v1.2.3 From fcb6cc082ea537b046df4b958f885f6a50b18d72 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 01:01:32 -0600 Subject: fix #1535 by avoiding the request if it's None --- pyramid/renderers.py | 24 ++++++++++++------------ pyramid/tests/test_renderers.py | 6 ++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index d840cc317..3c35551ea 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -24,7 +24,7 @@ from pyramid.events import BeforeRender from pyramid.path import caller_package -from pyramid.response import Response, _get_response_factory +from pyramid.response import _get_response_factory from pyramid.threadlocal import get_current_registry # API @@ -355,19 +355,19 @@ class JSONP(JSON): ``self.param_name`` is present in request.GET; otherwise returns plain-JSON encoded string with content-type ``application/json``""" def _render(value, system): - request = system['request'] + request = system.get('request') default = self._make_default(request) val = self.serializer(value, default=default, **self.kw) - callback = request.GET.get(self.param_name) - if callback is None: - ct = 'application/json' - body = val - else: - ct = 'application/javascript' - body = '%s(%s);' % (callback, val) - response = request.response - if response.content_type == response.default_content_type: - response.content_type = ct + ct = 'application/json' + body = val + if request is not None: + callback = request.GET.get(self.param_name) + if callback is not None: + ct = 'application/javascript' + body = '%s(%s);' % (callback, val) + response = request.response + if response.content_type == response.default_content_type: + response.content_type = ct return body return _render diff --git a/pyramid/tests/test_renderers.py b/pyramid/tests/test_renderers.py index 30fdef051..6d79cc291 100644 --- a/pyramid/tests/test_renderers.py +++ b/pyramid/tests/test_renderers.py @@ -602,6 +602,12 @@ class TestJSONP(unittest.TestCase): self.assertEqual(request.response.content_type, 'application/json') + def test_render_without_request(self): + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + result = renderer({'a':'1'}, {}) + self.assertEqual(result, '{"a": "1"}') + class Dummy: pass -- cgit v1.2.3 From 1ef35b7194ad744f23cf2881bbf881690d680c83 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 01:04:17 -0600 Subject: update changelog --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 22c7a20c2..9b0ee90e9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -120,6 +120,11 @@ Bug Fixes - Fix route generation for static view asset specifications having no path. See https://github.com/Pylons/pyramid/pull/1377 +- 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. + See https://github.com/Pylons/pyramid/pull/1561 + Deprecations ------------ -- 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 db6280393d6f87f391b3c12a23c65fe803556286 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 3c35551ea..805118647 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -125,6 +125,11 @@ def render_to_response(renderer_name, value, request=None, package=None): not passed in, any changes to ``request.response`` attributes made before calling this function will be ignored. + .. 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 803ea0bf2d2c2d0354cc5d89fe627bc87c326081 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:09:37 -0600 Subject: Revert "moar docs" This reverts commit db6280393d6f87f391b3c12a23c65fe803556286. --- pyramid/renderers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 805118647..3c35551ea 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -125,11 +125,6 @@ def render_to_response(renderer_name, value, request=None, package=None): not passed in, any changes to ``request.response`` attributes made before calling this function will be ignored. - .. 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 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 e30c3b9138605a16386a3e67d233b72cbbcfc5e8 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Thu, 22 Jan 2015 11:01:32 -0800 Subject: Prevent DeprecationWarning from setuptools>=11.3 --- CHANGES.txt | 3 +++ pyramid/path.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b334f5258..a7138db1a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -125,6 +125,9 @@ Bug Fixes callback and thus behave just like the ``pyramid.renderers.JSON` renderer. See https://github.com/Pylons/pyramid/pull/1561 +- Prevent "parameters to load are deprecated" ``DeprecationWarning`` + from setuptools>=11.3. + Deprecations ------------ diff --git a/pyramid/path.py b/pyramid/path.py index 470e766f8..8eecc282b 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -337,8 +337,13 @@ class DottedNameResolver(Resolver): value = package.__name__ else: value = package.__name__ + value - return pkg_resources.EntryPoint.parse( - 'x=%s' % value).load(False) + # Calling EntryPoint.load with an argument is deprecated. + # See https://pythonhosted.org/setuptools/history.html#id8 + ep = pkg_resources.EntryPoint.parse('x=%s' % value) + if hasattr(ep, 'resolve'): + return ep.resolve() # setuptools>=10.2 + else: + return ep.load(False) def _zope_dottedname_style(self, value, package): """ package.module.attr style """ -- cgit v1.2.3 From c04115ab48c57d9a259e3c7f968cf71842449cdb Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Thu, 22 Jan 2015 14:30:25 -0800 Subject: Add NO COVER pragmas --- pyramid/path.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyramid/path.py b/pyramid/path.py index 8eecc282b..f2d8fff55 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -341,9 +341,10 @@ class DottedNameResolver(Resolver): # See https://pythonhosted.org/setuptools/history.html#id8 ep = pkg_resources.EntryPoint.parse('x=%s' % value) if hasattr(ep, 'resolve'): - return ep.resolve() # setuptools>=10.2 + # setuptools>=10.2 + return ep.resolve() # pragma: NO COVER else: - return ep.load(False) + return ep.load(False) # pragma: NO COVER def _zope_dottedname_style(self, value, package): """ package.module.attr style """ -- cgit v1.2.3 From b5c0ea42424abf400683baf5dbfc2c41cf049ad1 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Fri, 6 Feb 2015 07:36:02 -0800 Subject: Sign CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index adf2224a5..319d41434 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -240,3 +240,5 @@ Contributors - Adrian Teng, 2014/12/17 - Ilja Everila, 2015/02/05 + +- Geoffrey T. Dairiki, 2015/02/06 -- cgit v1.2.3 From 3279854b8ffc02593a604f84d0f72b8e7d4f24a8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 10:05:12 -0600 Subject: update changelog for #1541 --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index a7138db1a..2dee64a84 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -126,7 +126,7 @@ Bug Fixes See https://github.com/Pylons/pyramid/pull/1561 - Prevent "parameters to load are deprecated" ``DeprecationWarning`` - from setuptools>=11.3. + from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541 Deprecations ------------ -- 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 20d708e89c321e5da937160beb7225c0b4fce46f Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:33:20 -0700 Subject: When running pcreate without scaffold, list scaffolds This fixes #1297 --- pyramid/scripts/pcreate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index edf2c39f7..a1479a2dd 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -64,7 +64,9 @@ class PCreateCommand(object): if self.options.list: return self.show_scaffolds() if not self.options.scaffold_name: - self.out('You must provide at least one scaffold name') + self.out('You must provide at least one scaffold name: -s ') + self.out('') + self.show_scaffolds() return 2 if not self.args: self.out('You must provide a project name') -- cgit v1.2.3 From c9cb19b9e14e2d1ec9ba17691212ea706f19f61c Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:35:36 -0700 Subject: Add changelog entry --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 832a2c216..09a4bbf88 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,11 @@ Next release Features -------- +- pcreate when run without a scaffold argument will now print information on + the missing flag, as well as a list of available scaffolds. + See https://github.com/Pylons/pyramid/pull/1566 and + https://github.com/Pylons/pyramid/issues/1297 + - Added support / testing for 'pypy3' under Tox and Travis. See https://github.com/Pylons/pyramid/pull/1469 -- cgit v1.2.3 From 665027ba49c9869abe8f0b8fe5d771c358a99e6d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:41:44 -0700 Subject: Update usage line to show required -s --- pyramid/scripts/pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index a1479a2dd..c634119bd 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -18,7 +18,7 @@ def main(argv=sys.argv, quiet=False): class PCreateCommand(object): verbosity = 1 # required description = "Render Pyramid scaffolding to an output directory" - usage = "usage: %prog [options] output_directory" + usage = "usage: %prog [options] -s output_directory" parser = optparse.OptionParser(usage, description=description) parser.add_option('-s', '--scaffold', dest='scaffold_name', -- cgit v1.2.3 From 0786c75a63b8d861183a08c1bf74d8afe8b929e7 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:42:07 -0700 Subject: Show help if missing arguments This will print the full help, followed by the available scaffolds if the user just calls pcreate without any arguments/flags at all. --- pyramid/scripts/pcreate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index c634119bd..2d2189686 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -63,6 +63,12 @@ class PCreateCommand(object): def run(self): if self.options.list: return self.show_scaffolds() + if not self.options.scaffold_name and not self.args: + if not self.quiet: + self.parser.print_help() + self.out('') + self.show_scaffolds() + return 2 if not self.options.scaffold_name: self.out('You must provide at least one scaffold name: -s ') self.out('') -- cgit v1.2.3 From 5de795938f4ec23c53cd4678021e36a72d3188cb Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:06:29 -0700 Subject: Document the factory requires a positional argument --- docs/narr/hooks.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 17cae2c67..8e6cf8343 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -368,6 +368,9 @@ changed by passing a ``response_factory`` argument to the constructor of the :term:`configurator`. This argument can be either a callable or a :term:`dotted Python name` representing a callable. +The factory takes a single positional argument, which is a :term:`Request` +object. The argument may be the value ``None``. + .. code-block:: python :linenos: -- cgit v1.2.3 From 972dfae78a94ac19e97b96b36dfa91f9f7c3fed4 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:22:44 -0700 Subject: Fix failing test --- pyramid/tests/test_scripts/test_pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 020721ca7..89fdea6be 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -35,7 +35,7 @@ class TestPCreateCommand(unittest.TestCase): self.assertTrue(out.startswith('No scaffolds available')) def test_run_no_scaffold_name(self): - cmd = self._makeOne() + cmd = self._makeOne('dummy') result = cmd.run() self.assertEqual(result, 2) out = self.out_.getvalue() -- cgit v1.2.3 From da5f5f9ea02c2c9830c7ae016547d2bedd0e0171 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 7 Feb 2015 02:38:54 -0600 Subject: move the IResponseFactory into the public api --- docs/api/interfaces.rst | 3 +++ docs/glossary.rst | 3 ++- docs/narr/hooks.rst | 8 ++++---- pyramid/config/factories.py | 5 ++--- pyramid/interfaces.py | 11 +++++------ pyramid/util.py | 5 ----- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index a62976d8a..de2a664a4 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -56,6 +56,9 @@ Other Interfaces .. autointerface:: IRenderer :members: + .. autointerface:: IResponseFactory + :members: + .. autointerface:: IViewMapperFactory :members: diff --git a/docs/glossary.rst b/docs/glossary.rst index 911c22075..9c0ea8598 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -18,7 +18,8 @@ Glossary response factory An object which, provided a :term:`request` as a single positional - argument, returns a Pyramid-compatible response. + argument, returns a Pyramid-compatible response. See + :class:`pyramid.interfaces.IResponseFactory`. response An object returned by a :term:`view callable` that represents response diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 8e6cf8343..4fd7670b9 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -364,12 +364,12 @@ Whenever :app:`Pyramid` returns a response from a view it creates a object. The factory that :app:`Pyramid` uses to create a response object instance can be -changed by passing a ``response_factory`` argument to the constructor of the -:term:`configurator`. This argument can be either a callable or a -:term:`dotted Python name` representing a callable. +changed by passing a :class:`pyramid.interfaces.IResponseFactory` argument to +the constructor of the :term:`configurator`. This argument can be either a +callable or a :term:`dotted Python name` representing a callable. The factory takes a single positional argument, which is a :term:`Request` -object. The argument may be the value ``None``. +object. The argument may be ``None``. .. code-block:: python :linenos: diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index d7a48ba93..15cfb796f 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -102,9 +102,8 @@ class FactoriesConfiguratorMixin(object): """ The object passed as ``factory`` should be an object (or a :term:`dotted Python name` which refers to an object) which will be used by the :app:`Pyramid` as the default response - objects. This factory object must have the same - methods and attributes as the - :class:`pyramid.request.Response` class. + objects. The factory should conform to the + :class:`pyramid.interfaces.IResponseFactory` interface. .. note:: diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index b21c6b9cc..0f1b4efc3 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -582,12 +582,11 @@ class IStaticURLInfo(Interface): """ Generate a URL for the given path """ class IResponseFactory(Interface): - """ A utility which generates a response factory """ - def __call__(): - """ Return a response factory (e.g. a callable that returns an object - implementing IResponse, e.g. :class:`pyramid.response.Response`). It - should accept all the arguments that the Pyramid Response class - accepts.""" + """ A utility which generates a response """ + def __call__(request): + """ Return a response object implementing IResponse, + e.g. :class:`pyramid.response.Response`). It should handle the + case when ``request`` is ``None``.""" class IRequestFactory(Interface): """ A utility which generates a request """ diff --git a/pyramid/util.py b/pyramid/util.py index 4ca2937a1..18cef4602 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -15,10 +15,6 @@ from pyramid.exceptions import ( CyclicDependencyError, ) -from pyramid.interfaces import ( - IResponseFactory, - ) - from pyramid.compat import ( iteritems_, is_nonstr_iter, @@ -29,7 +25,6 @@ from pyramid.compat import ( ) from pyramid.interfaces import IActionInfo -from pyramid.response import Response from pyramid.path import DottedNameResolver as _DottedNameResolver class DottedNameResolver(_DottedNameResolver): -- cgit v1.2.3 From 9449be0316aba5b465ffb2c02a1bb0daafccd8e6 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:50:48 -0700 Subject: Use hammer to fix hole --- pyramid/scripts/pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 2d2189686..d2c5f8c27 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -64,7 +64,7 @@ class PCreateCommand(object): if self.options.list: return self.show_scaffolds() if not self.options.scaffold_name and not self.args: - if not self.quiet: + if not self.quiet: # pragma: no cover self.parser.print_help() self.out('') self.show_scaffolds() -- cgit v1.2.3 From 58b8adf4135656efcc063eb822e4d29f6112d329 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:51:04 -0700 Subject: Add test for no scaffold no project name This test at least makes sure that if there is no scaffold and no project name that the command exists with error 2 --- pyramid/tests/test_scripts/test_pcreate.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 89fdea6be..63e5e6368 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -12,10 +12,10 @@ class TestPCreateCommand(unittest.TestCase): from pyramid.scripts.pcreate import PCreateCommand return PCreateCommand - def _makeOne(self, *args): + def _makeOne(self, *args, **kw): effargs = ['pcreate'] effargs.extend(args) - cmd = self._getTargetClass()(effargs) + cmd = self._getTargetClass()(effargs, **kw) cmd.out = self.out return cmd @@ -34,6 +34,11 @@ class TestPCreateCommand(unittest.TestCase): out = self.out_.getvalue() self.assertTrue(out.startswith('No scaffolds available')) + def test_run_no_scaffold_no_args(self): + cmd = self._makeOne(quiet=True) + result = cmd.run() + self.assertEqual(result, 2) + def test_run_no_scaffold_name(self): cmd = self._makeOne('dummy') result = cmd.run() -- cgit v1.2.3 From 1e0d648503fd992323737c7c702be204337e1e36 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 12:33:03 -0800 Subject: Raise error at configuration time --- pyramid/config/factories.py | 15 +++++++++--- pyramid/tests/test_config/test_factories.py | 21 +++++++--------- pyramid/tests/test_util.py | 38 +++++++++++++++++++++++++++++ pyramid/util.py | 27 +++++++++++++------- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 15cfb796f..4b2517ff1 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -15,8 +15,12 @@ from pyramid.traversal import DefaultRootFactory from pyramid.util import ( action_method, InstancePropertyMixin, + get_callable_name, ) +from pyramid.compat import native_ + + class FactoriesConfiguratorMixin(object): @action_method def set_root_factory(self, factory): @@ -33,9 +37,10 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) if factory is None: factory = DefaultRootFactory + def register(): self.registry.registerUtility(factory, IRootFactory) - self.registry.registerUtility(factory, IDefaultRootFactory) # b/c + self.registry.registerUtility(factory, IDefaultRootFactory) # b/c intr = self.introspectable('root factories', None, @@ -44,7 +49,7 @@ class FactoriesConfiguratorMixin(object): intr['factory'] = factory self.action(IRootFactory, register, introspectables=(intr,)) - _set_root_factory = set_root_factory # bw compat + _set_root_factory = set_root_factory # bw compat @action_method def set_session_factory(self, factory): @@ -60,6 +65,7 @@ class FactoriesConfiguratorMixin(object): achieve the same purpose. """ factory = self.maybe_dotted(factory) + def register(): self.registry.registerUtility(factory, ISessionFactory) intr = self.introspectable('session factory', None, @@ -89,6 +95,7 @@ class FactoriesConfiguratorMixin(object): can be used to achieve the same purpose. """ factory = self.maybe_dotted(factory) + def register(): self.registry.registerUtility(factory, IRequestFactory) intr = self.introspectable('request factory', None, @@ -173,6 +180,8 @@ class FactoriesConfiguratorMixin(object): callable, name=name, reify=reify) elif name is None: name = callable.__name__ + else: + name = get_callable_name(name) def register(): exts = self.registry.queryUtility(IRequestExtensions) @@ -224,9 +233,9 @@ class FactoriesConfiguratorMixin(object): 'set_request_propery() is deprecated as of Pyramid 1.5; use ' 'add_request_method() with the property=True argument instead') + @implementer(IRequestExtensions) class _RequestExtensions(object): def __init__(self): self.descriptors = {} self.methods = {} - diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 35677a91b..42bb5accc 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -128,24 +128,21 @@ class TestFactoriesMixin(unittest.TestCase): def test_add_request_method_with_text_type_name(self): from pyramid.interfaces import IRequestExtensions - from pyramid.compat import text_ - from pyramid.util import InstancePropertyMixin + from pyramid.compat import text_, PY3 + from pyramid.exceptions import ConfigurationError config = self._makeOne(autocommit=True) def boomshaka(r): pass - name = text_(b'La Pe\xc3\xb1a', 'utf-8') - config.add_request_method(boomshaka, name=name) - name2 = b'La Pe\xc3\xb1a' - config.add_request_method(boomshaka, name=name2) + def get_bad_name(): + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') - exts = config.registry.getUtility(IRequestExtensions) - inst = InstancePropertyMixin() - - def set_extensions(): - inst._set_extensions(exts) + config.add_request_method(boomshaka, name=name) - self.assertRaises(ValueError, set_extensions) + self.assertRaises(ConfigurationError, get_bad_name) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index ac5ea0683..405fe927a 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -124,6 +124,21 @@ class Test_InstancePropertyMixin(unittest.TestCase): self.assertEqual(1, foo.x) self.assertEqual(2, foo.y) + def test__make_property_unicode(self): + from pyramid.compat import text_ + from pyramid.exceptions import ConfigurationError + + cls = self._getTargetClass() + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + def make_bad_name(): + cls._make_property(lambda x: 1, name=name, reify=True) + + self.assertRaises(ConfigurationError, make_bad_name) + def test__set_properties_with_dict(self): foo = self._makeOne() x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True) @@ -619,7 +634,30 @@ class TestActionInfo(unittest.TestCase): "Line 0 of file filename:\n linerepr ") +class TestCallableName(unittest.TestCase): + def test_valid_ascii(self): + from pyramid.util import get_callable_name + name = u'hello world' + self.assertEquals(get_callable_name(name), name) + + def test_invalid_ascii(self): + from pyramid.util import get_callable_name + from pyramid.compat import text_, PY3 + from pyramid.exceptions import ConfigurationError + + def get_bad_name(): + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + get_callable_name(name) + + self.assertRaises(ConfigurationError, get_bad_name) + + def dummyfunc(): pass + class Dummy(object): pass diff --git a/pyramid/util.py b/pyramid/util.py index c036c1c2e..7e8535aaf 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -22,6 +22,7 @@ from pyramid.compat import ( string_types, text_, PY3, + native_ ) from pyramid.interfaces import IActionInfo @@ -55,7 +56,7 @@ class InstancePropertyMixin(object): raise ValueError('cannot reify a property') elif name is not None: fn = lambda this: callable(this) - fn.__name__ = name + fn.__name__ = get_callable_name(name) fn.__doc__ = callable.__doc__ else: name = callable.__name__ @@ -111,14 +112,7 @@ class InstancePropertyMixin(object): def _set_extensions(self, extensions): for name, fn in iteritems_(extensions.methods): method = fn.__get__(self, self.__class__) - try: - setattr(self, name, method) - except (UnicodeEncodeError, TypeError): - msg = ( - '`name="%s"` is invalid. `name` must be ascii because it is ' - 'used on __name__ of the method' - ) - raise ValueError(msg % name) + setattr(self, name, method) self._set_properties(extensions.descriptors) @@ -558,3 +552,18 @@ def action_method(wrapped): functools.update_wrapper(wrapper, wrapped) wrapper.__docobj__ = wrapped return wrapper + + +def get_callable_name(name): + """ + Verifies that the ``name`` is ascii and will raise a ``ConfigurationError`` + if it is not. + """ + try: + return native_(name, 'ascii') + except (UnicodeEncodeError, UnicodeDecodeError): + msg = ( + '`name="%s"` is invalid. `name` must be ascii because it is ' + 'used on __name__ of the method' + ) + raise ConfigurationError(msg % name) -- cgit v1.2.3 From 4a86b211fe7d294d2c598b42bc80e0c150a08443 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 12:34:31 -0800 Subject: Remove `native_` import, not used anymore --- pyramid/config/factories.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 4b2517ff1..10678df55 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -18,8 +18,6 @@ from pyramid.util import ( get_callable_name, ) -from pyramid.compat import native_ - class FactoriesConfiguratorMixin(object): @action_method -- cgit v1.2.3 From fd840237d4eb374c0d3f4ac2bb394aefaa43d40c Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 17:33:39 -0800 Subject: Fix py32 support --- pyramid/tests/test_util.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 405fe927a..371cd8703 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -1,9 +1,11 @@ import unittest from pyramid.compat import PY3 + class Test_InstancePropertyMixin(unittest.TestCase): def _makeOne(self): cls = self._getTargetClass() + class Foo(cls): pass return Foo() @@ -637,8 +639,14 @@ class TestActionInfo(unittest.TestCase): class TestCallableName(unittest.TestCase): def test_valid_ascii(self): from pyramid.util import get_callable_name - name = u'hello world' - self.assertEquals(get_callable_name(name), name) + from pyramid.compat import text_, PY3 + + if PY3: # pragma: nocover + name = b'hello world' + else: # pragma: nocover + name = text_(b'hello world', 'utf-8') + + self.assertEquals(get_callable_name(name), 'hello world') def test_invalid_ascii(self): from pyramid.util import get_callable_name -- cgit v1.2.3 From b809c72a6fc6d286373dea1fcfe6f674efea24a5 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Tue, 10 Feb 2015 13:36:25 -0500 Subject: Prevent timing attacks when checking CSRF token --- pyramid/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/session.py b/pyramid/session.py index a95c3f258..29ffcfc2a 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -126,7 +126,7 @@ def check_csrf_token(request, .. versionadded:: 1.4a2 """ supplied_token = request.params.get(token, request.headers.get(header)) - if supplied_token != request.session.get_csrf_token(): + if strings_differ(request.session.get_csrf_token(), supplied_token): if raises: raise BadCSRFToken('check_csrf_token(): Invalid token') return False -- cgit v1.2.3 From 9756f6111b06de79306d3769edd83f6735275701 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Tue, 10 Feb 2015 13:46:33 -0500 Subject: Default to an empty string instead of None --- pyramid/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/session.py b/pyramid/session.py index 29ffcfc2a..c4cfc1949 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -125,7 +125,7 @@ def check_csrf_token(request, .. versionadded:: 1.4a2 """ - supplied_token = request.params.get(token, request.headers.get(header)) + supplied_token = request.params.get(token, request.headers.get(header, "")) if strings_differ(request.session.get_csrf_token(), supplied_token): if raises: raise BadCSRFToken('check_csrf_token(): Invalid token') -- cgit v1.2.3 From b4e9902fe8cc28bac8e3e7dae0d8b2a270cf1640 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 10 Feb 2015 12:58:23 -0600 Subject: update changelog for #1574 --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 27052cf0f..1e50a623f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -133,6 +133,9 @@ Bug Fixes - Prevent "parameters to load are deprecated" ``DeprecationWarning`` from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541 +- Avoiding timing attacks against CSRF tokens. + See https://github.com/Pylons/pyramid/pull/1574 + Deprecations ------------ -- 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 c534e00ed06ef17506cf5f74553310ec15653834 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 10 Feb 2015 22:43:39 -0800 Subject: Don't create sdist with tox (latest setuptools doesn't like mixing) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 29bd48639..202e29e30 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +skipsdist = True envlist = py26,py27,py32,py33,py34,pypy,pypy3,cover -- 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 c45d6aea833245fa4fd9bb81352feb37045dfb07 Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:10:30 -0800 Subject: Add workaround to make sure echo is enabled after reload (refs #689) Also add myself to CONTRIBUTORS.txt --- CHANGES.txt | 3 +++ CONTRIBUTORS.txt | 2 ++ pyramid/scripts/pserve.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 1e50a623f..6a174bb1c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -88,6 +88,9 @@ Features Bug Fixes --------- +- Work around an issue where ``pserve --reload`` would leave terminal echo + disabled if it reloaded during a pdb session. + - ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise ``ValueError`` when accidentally passed ``None``. See https://github.com/Pylons/pyramid/pull/1320 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 319d41434..4f9bd6e41 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -242,3 +242,5 @@ Contributors - Ilja Everila, 2015/02/05 - Geoffrey T. Dairiki, 2015/02/06 + +- David Glick, 2015/02/12 diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 314efd839..d2ea1719b 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -36,6 +36,11 @@ from pyramid.scripts.common import parse_vars MAXFD = 1024 +try: + import termios +except ImportError: # pragma: no cover + termios = None + if WIN and not hasattr(os, 'kill'): # pragma: no cover # py 2.6 on windows def kill(pid, sig=None): @@ -709,6 +714,14 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover raise SystemExit signal.signal(signal.SIGTERM, handle_term) +def ensure_echo_on(): # pragma: no cover + if termios: + fd = sys.stdin.fileno() + attr_list = termios.tcgetattr(fd) + if not attr_list[3] & termios.ECHO: + attr_list[3] |= termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attr_list) + def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover """ Install the reloading monitor. @@ -718,6 +731,7 @@ def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover ``raise_keyboard_interrupt`` option creates a unignorable signal which causes the whole application to shut-down (rudely). """ + ensure_echo_on() mon = Monitor(poll_interval=poll_interval) if extra_files is None: extra_files = [] -- cgit v1.2.3 From 9343dbc71b268cf3c4ff4ac7e164af76ce39d5ec Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:16:12 -0800 Subject: remove obsolete note about raise_keyboard_interrupt that's left over from paste --- pyramid/scripts/pserve.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 314efd839..c5e54d670 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -714,9 +714,7 @@ def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover Install the reloading monitor. On some platforms server threads may not terminate when the main - thread does, causing ports to remain open/locked. The - ``raise_keyboard_interrupt`` option creates a unignorable signal - which causes the whole application to shut-down (rudely). + thread does, causing ports to remain open/locked. """ mon = Monitor(poll_interval=poll_interval) if extra_files is None: -- cgit v1.2.3 From c94c39bf9cc6a5c0fd9207046e8feb8b9a917447 Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:16:43 -0800 Subject: fix instructions for running coverage via tox --- HACKING.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING.txt b/HACKING.txt index 16c17699c..e104869ec 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -195,7 +195,7 @@ Test Coverage ------------- - The codebase *must* have 100% test statement coverage after each commit. - You can test coverage via ``tox -e coverage``, or alternately by installing + You can test coverage via ``tox -e cover``, or alternately by installing ``nose`` and ``coverage`` into your virtualenv (easiest via ``setup.py dev``) , and running ``setup.py nosetests --with-coverage``. -- cgit v1.2.3 From 03d964a924e0ef183c3cd78a61c043b1f74f5570 Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 13 Feb 2015 09:20:33 -0800 Subject: add pull request reference --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index 6a174bb1c..37803b3ed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -90,6 +90,7 @@ Bug Fixes - Work around an issue where ``pserve --reload`` would leave terminal echo disabled if it reloaded during a pdb session. + See https://github.com/Pylons/pyramid/pull/1577 - ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise ``ValueError`` when accidentally passed ``None``. -- cgit v1.2.3 From 04cc91a7ac2d203e5acda41aa7c4975f78171274 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 22:09:35 -0600 Subject: add InstancePropertyHelper and apply_request_extensions --- docs/api/request.rst | 1 + pyramid/config/factories.py | 4 +- pyramid/interfaces.py | 3 +- pyramid/request.py | 26 ++++- pyramid/router.py | 3 +- pyramid/scripting.py | 8 +- pyramid/tests/test_request.py | 45 +++++++- pyramid/tests/test_router.py | 8 +- pyramid/tests/test_scripting.py | 16 ++- pyramid/tests/test_util.py | 236 +++++++++++++++++++++++++++++++--------- pyramid/util.py | 75 +++++++------ 11 files changed, 321 insertions(+), 104 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index dd68fa09c..b325ad076 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -369,3 +369,4 @@ that used as ``request.GET``, ``request.POST``, and ``request.params``), see :class:`pyramid.interfaces.IMultiDict`. +.. autofunction:: apply_request_extensions(request) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 10678df55..f0b6252ae 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -14,8 +14,8 @@ from pyramid.traversal import DefaultRootFactory from pyramid.util import ( action_method, - InstancePropertyMixin, get_callable_name, + InstancePropertyHelper, ) @@ -174,7 +174,7 @@ class FactoriesConfiguratorMixin(object): property = property or reify if property: - name, callable = InstancePropertyMixin._make_property( + name, callable = InstancePropertyHelper.make_property( callable, name=name, reify=reify) elif name is None: name = callable.__name__ diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 0f1b4efc3..d7422bdde 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -591,8 +591,7 @@ class IResponseFactory(Interface): class IRequestFactory(Interface): """ A utility which generates a request """ def __call__(environ): - """ Return an object implementing IRequest, e.g. an instance - of ``pyramid.request.Request``""" + """ Return an instance of ``pyramid.request.Request``""" def blank(path): """ Return an empty request object (see diff --git a/pyramid/request.py b/pyramid/request.py index b2e2efe05..3cbe5d9e3 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -8,6 +8,7 @@ from webob import BaseRequest from pyramid.interfaces import ( IRequest, + IRequestExtensions, IResponse, ISessionFactory, ) @@ -16,6 +17,7 @@ from pyramid.compat import ( text_, bytes_, native_, + iteritems_, ) from pyramid.decorator import reify @@ -26,7 +28,10 @@ from pyramid.security import ( AuthorizationAPIMixin, ) from pyramid.url import URLMethodsMixin -from pyramid.util import InstancePropertyMixin +from pyramid.util import ( + InstancePropertyHelper, + InstancePropertyMixin, +) class TemplateContext(object): pass @@ -307,3 +312,22 @@ def call_app_with_subpath_as_path_info(request, app): new_request.environ['PATH_INFO'] = new_path_info return new_request.get_response(app) + +def apply_request_extensions(request, extensions=None): + """Apply request extensions (methods and properties) to an instance of + :class:`pyramid.interfaces.IRequest`. This method is dependent on the + ``request`` containing a properly initialized registry. + + After invoking this method, the ``request`` should have the methods + and properties that were defined using + :meth:`pyramid.config.Configurator.add_request_method`. + """ + if extensions is None: + extensions = request.registry.queryUtility(IRequestExtensions) + if extensions is not None: + for name, fn in iteritems_(extensions.methods): + method = fn.__get__(request, request.__class__) + setattr(request, name, method) + + InstancePropertyHelper.apply_properties( + request, extensions.descriptors) diff --git a/pyramid/router.py b/pyramid/router.py index ba4f85b18..0b1ecade7 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -27,6 +27,7 @@ from pyramid.events import ( from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request +from pyramid.request import apply_request_extensions from pyramid.threadlocal import manager from pyramid.traversal import ( @@ -213,7 +214,7 @@ class Router(object): try: extensions = self.request_extensions if extensions is not None: - request._set_extensions(extensions) + apply_request_extensions(request, extensions=extensions) response = handle_request(request) if request.response_callbacks: diff --git a/pyramid/scripting.py b/pyramid/scripting.py index fdb4aa430..d9587338f 100644 --- a/pyramid/scripting.py +++ b/pyramid/scripting.py @@ -1,12 +1,12 @@ from pyramid.config import global_registries from pyramid.exceptions import ConfigurationError -from pyramid.request import Request from pyramid.interfaces import ( - IRequestExtensions, IRequestFactory, IRootFactory, ) +from pyramid.request import Request +from pyramid.request import apply_request_extensions from pyramid.threadlocal import manager as threadlocal_manager from pyramid.traversal import DefaultRootFactory @@ -77,9 +77,7 @@ def prepare(request=None, registry=None): request.registry = registry threadlocals = {'registry':registry, 'request':request} threadlocal_manager.push(threadlocals) - extensions = registry.queryUtility(IRequestExtensions) - if extensions is not None: - request._set_extensions(extensions) + apply_request_extensions(request) def closer(): threadlocal_manager.pop() root_factory = registry.queryUtility(IRootFactory, diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 48af98f59..f142e4536 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -435,7 +435,50 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase): self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded) self.assertEqual(request.environ['PATH_INFO'], '/' + encoded) -class DummyRequest: +class Test_apply_request_extensions(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request, extensions=None): + from pyramid.request import apply_request_extensions + return apply_request_extensions(request, extensions=extensions) + + def test_it_with_registry(self): + from pyramid.interfaces import IRequestExtensions + extensions = Dummy() + extensions.methods = {'foo': lambda x, y: y} + extensions.descriptors = {'bar': property(lambda x: 'bar')} + self.config.registry.registerUtility(extensions, IRequestExtensions) + request = DummyRequest() + request.registry = self.config.registry + self._callFUT(request) + self.assertEqual(request.bar, 'bar') + self.assertEqual(request.foo('abc'), 'abc') + + def test_it_override_extensions(self): + from pyramid.interfaces import IRequestExtensions + ignore = Dummy() + ignore.methods = {'x': lambda x, y, z: 'asdf'} + ignore.descriptors = {'bar': property(lambda x: 'asdf')} + self.config.registry.registerUtility(ignore, IRequestExtensions) + request = DummyRequest() + request.registry = self.config.registry + + extensions = Dummy() + extensions.methods = {'foo': lambda x, y: y} + extensions.descriptors = {'bar': property(lambda x: 'bar')} + self._callFUT(request, extensions=extensions) + self.assertRaises(AttributeError, lambda: request.x) + self.assertEqual(request.bar, 'bar') + self.assertEqual(request.foo('abc'), 'abc') + +class Dummy(object): + pass + +class DummyRequest(object): def __init__(self, environ=None): if environ is None: environ = {} diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 30ebd5918..b57c248d5 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -317,6 +317,7 @@ class TestRouter(unittest.TestCase): from pyramid.interfaces import IRequestExtensions from pyramid.interfaces import IRequest from pyramid.request import Request + from pyramid.util import InstancePropertyHelper context = DummyContext() self._registerTraverserFactory(context) class Extensions(object): @@ -324,11 +325,12 @@ class TestRouter(unittest.TestCase): self.methods = {} self.descriptors = {} extensions = Extensions() - L = [] + ext_method = lambda r: 'bar' + name, fn = InstancePropertyHelper.make_property(ext_method, name='foo') + extensions.descriptors[name] = fn request = Request.blank('/') request.request_iface = IRequest request.registry = self.registry - request._set_extensions = lambda *x: L.extend(x) def request_factory(environ): return request self.registry.registerUtility(extensions, IRequestExtensions) @@ -342,7 +344,7 @@ class TestRouter(unittest.TestCase): router.request_factory = request_factory start_response = DummyStartResponse() router(environ, start_response) - self.assertEqual(L, [extensions]) + self.assertEqual(view.request.foo, 'bar') def test_call_view_registered_nonspecific_default_path(self): from pyramid.interfaces import IViewClassifier diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py index a36d1ed71..1e952062b 100644 --- a/pyramid/tests/test_scripting.py +++ b/pyramid/tests/test_scripting.py @@ -122,11 +122,15 @@ class Test_prepare(unittest.TestCase): self.assertEqual(request.context, context) def test_it_with_extensions(self): - exts = Dummy() + from pyramid.util import InstancePropertyHelper + exts = DummyExtensions() + ext_method = lambda r: 'bar' + name, fn = InstancePropertyHelper.make_property(ext_method, 'foo') + exts.descriptors[name] = fn request = DummyRequest({}) registry = request.registry = self._makeRegistry([exts, DummyFactory]) info = self._callFUT(request=request, registry=registry) - self.assertEqual(request.extensions, exts) + self.assertEqual(request.foo, 'bar') root, closer = info['root'], info['closer'] closer() @@ -199,11 +203,13 @@ class DummyThreadLocalManager: def pop(self): self.popped.append(True) -class DummyRequest: +class DummyRequest(object): matchdict = None matched_route = None def __init__(self, environ): self.environ = environ - def _set_extensions(self, exts): - self.extensions = exts +class DummyExtensions: + def __init__(self): + self.descriptors = {} + self.methods = {} diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 371cd8703..459c729a0 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -2,6 +2,188 @@ import unittest from pyramid.compat import PY3 +class Test_InstancePropertyHelper(unittest.TestCase): + def _makeOne(self): + cls = self._getTargetClass() + return cls() + + def _getTargetClass(self): + from pyramid.util import InstancePropertyHelper + return InstancePropertyHelper + + def test_callable(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(2, foo.worker) + + def test_callable_with_name(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_callable_with_reify(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, reify=True) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(1, foo.worker) + + def test_callable_with_name_reify(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + helper.set_property(foo, worker, name='y', reify=True) + foo.bar = 1 + self.assertEqual(1, foo.y) + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + self.assertEqual(1, foo.y) + + def test_property_without_name(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + self.assertRaises(ValueError, helper.set_property, foo, property(worker)) + + def test_property_with_name(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, property(worker), name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_property_with_reify(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + self.assertRaises(ValueError, helper.set_property, + foo, property(worker), name='x', reify=True) + + def test_override_property(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + def doit(): + foo.x = 1 + self.assertRaises(AttributeError, doit) + + def test_override_reify(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x', reify=True) + foo.x = 1 + self.assertEqual(1, foo.x) + foo.x = 2 + self.assertEqual(2, foo.x) + + def test_reset_property(self): + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, lambda _: 1, name='x') + self.assertEqual(1, foo.x) + helper.set_property(foo, lambda _: 2, name='x') + self.assertEqual(2, foo.x) + + def test_reset_reify(self): + """ This is questionable behavior, but may as well get notified + if it changes.""" + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, lambda _: 1, name='x', reify=True) + self.assertEqual(1, foo.x) + helper.set_property(foo, lambda _: 2, name='x', reify=True) + self.assertEqual(1, foo.x) + + def test_make_property(self): + from pyramid.decorator import reify + helper = self._getTargetClass() + name, fn = helper.make_property(lambda x: 1, name='x', reify=True) + self.assertEqual(name, 'x') + self.assertTrue(isinstance(fn, reify)) + + def test_apply_properties_with_iterable(self): + foo = Dummy() + helper = self._getTargetClass() + x = helper.make_property(lambda _: 1, name='x', reify=True) + y = helper.make_property(lambda _: 2, name='y') + helper.apply_properties(foo, [x, y]) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test_apply_properties_with_dict(self): + foo = Dummy() + helper = self._getTargetClass() + x_name, x_fn = helper.make_property(lambda _: 1, name='x', reify=True) + y_name, y_fn = helper.make_property(lambda _: 2, name='y') + helper.apply_properties(foo, {x_name: x_fn, y_name: y_fn}) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test_make_property_unicode(self): + from pyramid.compat import text_ + from pyramid.exceptions import ConfigurationError + + cls = self._getTargetClass() + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + def make_bad_name(): + cls.make_property(lambda x: 1, name=name, reify=True) + + self.assertRaises(ConfigurationError, make_bad_name) + + def test_add_property(self): + helper = self._makeOne() + helper.add_property(lambda obj: obj.bar, name='x', reify=True) + helper.add_property(lambda obj: obj.bar, name='y') + self.assertEqual(len(helper.properties), 2) + foo = Dummy() + helper.apply(foo) + foo.bar = 1 + self.assertEqual(foo.x, 1) + self.assertEqual(foo.y, 1) + foo.bar = 2 + self.assertEqual(foo.x, 1) + self.assertEqual(foo.y, 2) + + def test_apply_multiple_times(self): + helper = self._makeOne() + helper.add_property(lambda obj: 1, name='x') + foo, bar = Dummy(), Dummy() + helper.apply(foo) + self.assertEqual(foo.x, 1) + helper.add_property(lambda obj: 2, name='x') + helper.apply(bar) + self.assertEqual(foo.x, 1) + self.assertEqual(bar.x, 2) + class Test_InstancePropertyMixin(unittest.TestCase): def _makeOne(self): cls = self._getTargetClass() @@ -111,58 +293,6 @@ class Test_InstancePropertyMixin(unittest.TestCase): foo.set_property(lambda _: 2, name='x', reify=True) self.assertEqual(1, foo.x) - def test__make_property(self): - from pyramid.decorator import reify - cls = self._getTargetClass() - name, fn = cls._make_property(lambda x: 1, name='x', reify=True) - self.assertEqual(name, 'x') - self.assertTrue(isinstance(fn, reify)) - - def test__set_properties_with_iterable(self): - foo = self._makeOne() - x = foo._make_property(lambda _: 1, name='x', reify=True) - y = foo._make_property(lambda _: 2, name='y') - foo._set_properties([x, y]) - self.assertEqual(1, foo.x) - self.assertEqual(2, foo.y) - - def test__make_property_unicode(self): - from pyramid.compat import text_ - from pyramid.exceptions import ConfigurationError - - cls = self._getTargetClass() - if PY3: # pragma: nocover - name = b'La Pe\xc3\xb1a' - else: # pragma: nocover - name = text_(b'La Pe\xc3\xb1a', 'utf-8') - - def make_bad_name(): - cls._make_property(lambda x: 1, name=name, reify=True) - - self.assertRaises(ConfigurationError, make_bad_name) - - def test__set_properties_with_dict(self): - foo = self._makeOne() - x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True) - y_name, y_fn = foo._make_property(lambda _: 2, name='y') - foo._set_properties({x_name: x_fn, y_name: y_fn}) - self.assertEqual(1, foo.x) - self.assertEqual(2, foo.y) - - def test__set_extensions(self): - inst = self._makeOne() - def foo(self, result): - return result - n, bar = inst._make_property(lambda _: 'bar', name='bar') - class Extensions(object): - def __init__(self): - self.methods = {'foo':foo} - self.descriptors = {'bar':bar} - extensions = Extensions() - inst._set_extensions(extensions) - self.assertEqual(inst.bar, 'bar') - self.assertEqual(inst.foo('abc'), 'abc') - class Test_WeakOrderedSet(unittest.TestCase): def _makeOne(self): from pyramid.config import WeakOrderedSet @@ -646,7 +776,7 @@ class TestCallableName(unittest.TestCase): else: # pragma: nocover name = text_(b'hello world', 'utf-8') - self.assertEquals(get_callable_name(name), 'hello world') + self.assertEqual(get_callable_name(name), 'hello world') def test_invalid_ascii(self): from pyramid.util import get_callable_name diff --git a/pyramid/util.py b/pyramid/util.py index 7e8535aaf..63d113361 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -34,14 +34,21 @@ class DottedNameResolver(_DottedNameResolver): _marker = object() -class InstancePropertyMixin(object): - """ Mixin that will allow an instance to add properties at - run-time as if they had been defined via @property or @reify - on the class itself. +class InstancePropertyHelper(object): + """A helper object for assigning properties and descriptors to instances. + It is not normally possible to do this because descriptors must be + defined on the class itself. + + This class is optimized for adding multiple properties at once to an + instance. This is done by calling :meth:`.add_property` once + per-property and then invoking :meth:`.apply` on target objects. + """ + def __init__(self): + self.properties = {} @classmethod - def _make_property(cls, callable, name=None, reify=False): + def make_property(cls, callable, name=None, reify=False): """ Convert a callable into one suitable for adding to the instance. This will return a 2-tuple containing the computed (name, property) pair. @@ -69,25 +76,12 @@ class InstancePropertyMixin(object): return name, fn - def _set_properties(self, properties): - """ Create several properties on the instance at once. - - This is a more efficient version of - :meth:`pyramid.util.InstancePropertyMixin.set_property` which - can accept multiple ``(name, property)`` pairs generated via - :meth:`pyramid.util.InstancePropertyMixin._make_property`. - - ``properties`` is a sequence of two-tuples *or* a data structure - with an ``.items()`` method which returns a sequence of two-tuples - (presumably a dictionary). It will be used to add several - properties to the instance in a manner that is more efficient - than simply calling ``set_property`` repeatedly. - """ + @classmethod + def apply_properties(cls, target, properties): attrs = dict(properties) - if attrs: - parent = self.__class__ - cls = type(parent.__name__, (parent, object), attrs) + parent = target.__class__ + newcls = type(parent.__name__, (parent, object), attrs) # We assign __provides__, __implemented__ and __providedBy__ below # to prevent a memory leak that results from from the usage of this # instance's eventual use in an adapter lookup. Adapter lookup @@ -106,15 +100,34 @@ class InstancePropertyMixin(object): # attached to it val = getattr(parent, name, _marker) if val is not _marker: - setattr(cls, name, val) - self.__class__ = cls + setattr(newcls, name, val) + target.__class__ = newcls + + @classmethod + def set_property(cls, target, callable, name=None, reify=False): + """A helper method to apply a single property to an instance.""" + prop = cls.make_property(callable, name=name, reify=reify) + cls.apply_properties(target, [prop]) + + def add_property(self, callable, name=None, reify=False): + """Add a new property configuration. + + This should be used in combination with :meth:`.apply` as a + more efficient version of :meth:`.set_property`. + """ + name, fn = self.make_property(callable, name=name, reify=reify) + self.properties[name] = fn - def _set_extensions(self, extensions): - for name, fn in iteritems_(extensions.methods): - method = fn.__get__(self, self.__class__) - setattr(self, name, method) + def apply(self, target): + """ Apply all configured properties to the ``target`` instance.""" + if self.properties: + self.apply_properties(target, self.properties) - self._set_properties(extensions.descriptors) +class InstancePropertyMixin(object): + """ Mixin that will allow an instance to add properties at + run-time as if they had been defined via @property or @reify + on the class itself. + """ def set_property(self, callable, name=None, reify=False): """ Add a callable or a property descriptor to the instance. @@ -168,8 +181,8 @@ class InstancePropertyMixin(object): >>> foo.y # notice y keeps the original value 1 """ - prop = self._make_property(callable, name=name, reify=reify) - self._set_properties([prop]) + InstancePropertyHelper.set_property( + self, callable, name=name, reify=reify) class WeakOrderedSet(object): """ Maintain a set of items. -- cgit v1.2.3 From 46bc7fd9e221a084ca2f4d0cb8b158d2e239c373 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 22:14:24 -0600 Subject: update changelog for #1581 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 37803b3ed..8cee9c09d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,12 @@ Next release Features -------- +- Add ``pyramid.request.apply_request_extensions`` function which can be + used in testing to apply any request extensions configured via + ``config.add_request_method``. Previously it was only possible to test + the extensions by going through Pyramid's router. + See https://github.com/Pylons/pyramid/pull/1581 + - pcreate when run without a scaffold argument will now print information on the missing flag, as well as a list of available scaffolds. See https://github.com/Pylons/pyramid/pull/1566 and -- cgit v1.2.3 From 2f0ba093f1bd50fd43e0a55f244b90d1fe50ff19 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 23:02:43 -0600 Subject: docstring on apply_properties --- pyramid/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyramid/util.py b/pyramid/util.py index 63d113361..5721a93fc 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -78,6 +78,9 @@ class InstancePropertyHelper(object): @classmethod def apply_properties(cls, target, properties): + """Accept a list or dict of ``properties`` generated from + :meth:`.make_property` and apply them to a ``target`` object. + """ attrs = dict(properties) if attrs: parent = target.__class__ -- cgit v1.2.3 From 780889f18d17b86fc12625166a245c7f9947cbe6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 01:05:04 -0600 Subject: remove the token from the ICacheBuster api This exposes the QueryStringCacheBuster and PathSegmentCacheBuster public APIs alongside the md5-variants. These should be more cleanly subclassed by people wishing to extend their implementations. --- docs/api/static.rst | 6 +++ docs/narr/assets.rst | 15 ++++---- pyramid/config/views.py | 4 +- pyramid/interfaces.py | 13 ++----- pyramid/static.py | 65 +++++++++++++++++++++++++-------- pyramid/tests/test_config/test_views.py | 12 +++--- pyramid/tests/test_static.py | 16 ++++---- 7 files changed, 82 insertions(+), 49 deletions(-) diff --git a/docs/api/static.rst b/docs/api/static.rst index 543e526ad..b6b279139 100644 --- a/docs/api/static.rst +++ b/docs/api/static.rst @@ -9,6 +9,12 @@ :members: :inherited-members: + .. autoclass:: PathSegmentCacheBuster + :members: + + .. autoclass:: QueryStringCacheBuster + :members: + .. autoclass:: PathSegmentMd5CacheBuster :members: diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index fc908c2b4..d6bc8cbb8 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -446,19 +446,20 @@ In order to implement your own cache buster, you can write your own class from scratch which implements the :class:`~pyramid.interfaces.ICacheBuster` interface. Alternatively you may choose to subclass one of the existing implementations. One of the most likely scenarios is you'd want to change the -way the asset token is generated. To do this just subclass an existing -implementation and replace the :meth:`~pyramid.interfaces.ICacheBuster.token` -method. Here is an example which just uses Git to get the hash of the -currently checked out code: +way the asset token is generated. To do this just subclass either +:class:`~pyramid.static.PathSegmentCacheBuster` or +:class:`~pyramid.static.QueryStringCacheBuster` and define a +``tokenize(pathspec)`` method. Here is an example which just uses Git to get +the hash of the currently checked out code: .. code-block:: python :linenos: import os import subprocess - from pyramid.static import PathSegmentMd5CacheBuster + from pyramid.static import PathSegmentCacheBuster - class GitCacheBuster(PathSegmentMd5CacheBuster): + class GitCacheBuster(PathSegmentCacheBuster): """ Assuming your code is installed as a Git checkout, as opposed to as an egg from an egg repository like PYPI, you can use this cachebuster to @@ -470,7 +471,7 @@ currently checked out code: ['git', 'rev-parse', 'HEAD'], cwd=here).strip() - def token(self, pathspec): + def tokenize(self, pathspec): return self.sha1 Choosing a Cache Buster diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 85e252f2f..24c592f7a 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1980,9 +1980,9 @@ class StaticURLInfo(object): cb = self._default_cachebust() if cb: def cachebust(subpath, kw): - token = cb.token(spec + subpath) subpath_tuple = tuple(subpath.split('/')) - subpath_tuple, kw = cb.pregenerate(token, subpath_tuple, kw) + subpath_tuple, kw = cb.pregenerate( + spec + subpath, subpath_tuple, kw) return '/'.join(subpath_tuple), kw else: cachebust = None diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index d7422bdde..1508f282e 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1192,18 +1192,11 @@ class ICacheBuster(Interface): .. versionadded:: 1.6 """ - def token(pathspec): - """ - Computes and returns a token string used for cache busting. - ``pathspec`` is the path specification for the resource to be cache - busted. """ - - def pregenerate(token, subpath, kw): + def pregenerate(pathspec, subpath, kw): """ Modifies a subpath and/or keyword arguments from which a static asset - URL will be computed during URL generation. The ``token`` argument is - a token string computed by - :meth:`~pyramid.interfaces.ICacheBuster.token` for a particular asset. + URL will be computed during URL generation. The ``pathspec`` argument + is the path specification for the resource to be cache busted. The ``subpath`` argument is a tuple of path elements that represent the portion of the asset URL which is used to find the asset. The ``kw`` argument is a dict of keywords that are to be passed eventually to diff --git a/pyramid/static.py b/pyramid/static.py index c4a9e3cc4..460639a89 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -174,7 +174,7 @@ class Md5AssetTokenGenerator(object): def __init__(self): self.token_cache = {} - def token(self, pathspec): + def tokenize(self, pathspec): # An astute observer will notice that this use of token_cache doesn't # look particularly thread safe. Basic read/write operations on Python # dicts, however, are atomic, so simply accessing and writing values @@ -192,38 +192,55 @@ class Md5AssetTokenGenerator(object): self.token_cache[pathspec] = token = _generate_md5(pathspec) return token -class PathSegmentMd5CacheBuster(Md5AssetTokenGenerator): +class PathSegmentCacheBuster(object): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which - inserts an md5 checksum token for cache busting in the path portion of an - asset URL. Generated md5 checksums are cached in order to speed up - subsequent calls. + inserts a token for cache busting in the path portion of an asset URL. + + To use this class, subclass it and provide a ``tokenize`` method which + accepts a ``pathspec`` and returns a token. .. versionadded:: 1.6 """ - def pregenerate(self, token, subpath, kw): + def pregenerate(self, pathspec, subpath, kw): + token = self.tokenize(pathspec) return (token,) + subpath, kw def match(self, subpath): return subpath[1:] -class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): +class PathSegmentMd5CacheBuster(PathSegmentCacheBuster, + Md5AssetTokenGenerator): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which + inserts an md5 checksum token for cache busting in the path portion of an + asset URL. Generated md5 checksums are cached in order to speed up + subsequent calls. + + .. versionadded:: 1.6 + """ + def __init__(self): + PathSegmentCacheBuster.__init__(self) + Md5AssetTokenGenerator.__init__(self) + +class QueryStringCacheBuster(object): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds - an md5 checksum token for cache busting in the query string of an asset - URL. Generated md5 checksums are cached in order to speed up subsequent - calls. + a token for cache busting in the query string of an asset URL. The optional ``param`` argument determines the name of the parameter added to the query string and defaults to ``'x'``. + To use this class, subclass it and provide a ``tokenize`` method which + accepts a ``pathspec`` and returns a token. + .. versionadded:: 1.6 """ def __init__(self, param='x'): - super(QueryStringMd5CacheBuster, self).__init__() self.param = param - def pregenerate(self, token, subpath, kw): + def pregenerate(self, pathspec, subpath, kw): + token = self.tokenize(pathspec) query = kw.setdefault('_query', {}) if isinstance(query, dict): query[self.param] = token @@ -231,7 +248,24 @@ class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): kw['_query'] = tuple(query) + ((self.param, token),) return subpath, kw -class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): +class QueryStringMd5CacheBuster(QueryStringCacheBuster, + Md5AssetTokenGenerator): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + an md5 checksum token for cache busting in the query string of an asset + URL. Generated md5 checksums are cached in order to speed up subsequent + calls. + + The optional ``param`` argument determines the name of the parameter added + to the query string and defaults to ``'x'``. + + .. versionadded:: 1.6 + """ + def __init__(self, param='x'): + QueryStringCacheBuster.__init__(self, param=param) + Md5AssetTokenGenerator.__init__(self) + +class QueryStringConstantCacheBuster(QueryStringCacheBuster): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds an arbitrary token for cache busting in the query string of an asset URL. @@ -245,9 +279,8 @@ class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): .. versionadded:: 1.6 """ def __init__(self, token, param='x'): + QueryStringCacheBuster.__init__(self, param=param) self._token = token - self.param = param - def token(self, pathspec): + def tokenize(self, pathspec): return self._token - diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index d1eb1ed3c..36c86f78c 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3995,7 +3995,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_add_cachebust_default(self): config = self._makeConfig() inst = self._makeOne() - inst._default_cachebust = DummyCacheBuster + inst._default_cachebust = lambda: DummyCacheBuster('foo') inst.add(config, 'view', 'mypackage:path', cachebust=True) cachebust = config.registry._static_url_registrations[0][3] subpath, kw = cachebust('some/path', {}) @@ -4014,7 +4014,7 @@ class TestStaticURLInfo(unittest.TestCase): config = self._makeConfig() inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', - cachebust=DummyCacheBuster()) + cachebust=DummyCacheBuster('foo')) cachebust = config.registry._static_url_registrations[0][3] subpath, kw = cachebust('some/path', {}) self.assertEqual(subpath, 'some/path') @@ -4127,10 +4127,10 @@ class DummyMultiView: """ """ class DummyCacheBuster(object): - def token(self, pathspec): - return 'foo' - def pregenerate(self, token, subpath, kw): - kw['x'] = token + def __init__(self, token): + self.token = token + def pregenerate(self, pathspec, subpath, kw): + kw['x'] = self.token return subpath, kw def parse_httpdate(s): diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 2f4de249e..a3df74b44 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -393,13 +393,13 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): return cls() def test_package_resource(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = '76d653a3a044e2f4b38bb001d283e3d9' token = fut('pyramid.tests:fixtures/static/index.html') self.assertEqual(token, expected) def test_filesystem_resource(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = 'd5155f250bef0e9923e894dbc713c5dd' with open(self.fspath, 'w') as f: f.write("Are we rich yet?") @@ -407,7 +407,7 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): self.assertEqual(token, expected) def test_cache(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = 'd5155f250bef0e9923e894dbc713c5dd' with open(self.fspath, 'w') as f: f.write("Are we rich yet?") @@ -425,11 +425,11 @@ class TestPathSegmentMd5CacheBuster(unittest.TestCase): def _makeOne(self): from pyramid.static import PathSegmentMd5CacheBuster as cls inst = cls() - inst.token = lambda pathspec: 'foo' + inst.tokenize = lambda pathspec: 'foo' return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): @@ -448,11 +448,11 @@ class TestQueryStringMd5CacheBuster(unittest.TestCase): inst = cls(param) else: inst = cls() - inst.token = lambda pathspec: 'foo' + inst.tokenize = lambda pathspec: 'foo' return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): @@ -490,7 +490,7 @@ class TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster): return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): -- cgit v1.2.3 From 4a9c13647b93c79ba3414c32c96906bc43e325d3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 02:40:07 -0600 Subject: use super with mixins... for reasons --- pyramid/static.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index 460639a89..4ff02f798 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -220,8 +220,7 @@ class PathSegmentMd5CacheBuster(PathSegmentCacheBuster, .. versionadded:: 1.6 """ def __init__(self): - PathSegmentCacheBuster.__init__(self) - Md5AssetTokenGenerator.__init__(self) + super(PathSegmentMd5CacheBuster, self).__init__() class QueryStringCacheBuster(object): """ @@ -262,8 +261,7 @@ class QueryStringMd5CacheBuster(QueryStringCacheBuster, .. versionadded:: 1.6 """ def __init__(self, param='x'): - QueryStringCacheBuster.__init__(self, param=param) - Md5AssetTokenGenerator.__init__(self) + super(QueryStringMd5CacheBuster, self).__init__(param=param) class QueryStringConstantCacheBuster(QueryStringCacheBuster): """ @@ -279,7 +277,7 @@ class QueryStringConstantCacheBuster(QueryStringCacheBuster): .. versionadded:: 1.6 """ def __init__(self, token, param='x'): - QueryStringCacheBuster.__init__(self, param=param) + super(QueryStringConstantCacheBuster, self).__init__(param=param) self._token = token def tokenize(self, pathspec): -- cgit v1.2.3 From 5fdf9a5f63b7731963de7f49df6c29077155525f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 11:39:10 -0600 Subject: update changelog --- CHANGES.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8cee9c09d..596e5f506 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,7 +20,10 @@ Features - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. - See https://github.com/Pylons/pyramid/pull/1380 + Core APIs are shipped for both cache busting via query strings and + path segments and may be extended to fit into custom asset pipelines. + See https://github.com/Pylons/pyramid/pull/1380 and + https://github.com/Pylons/pyramid/pull/1583 - Add ``pyramid.config.Configurator.root_package`` attribute and init parameter to assist with includeable packages that wish to resolve -- cgit v1.2.3 From 568a025d3156ee1e7bdf92e14c9eba7390c1dd26 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 18:58:53 -0600 Subject: expose public config phases in pyramid.config --- CHANGES.txt | 4 +++- docs/api/config.rst | 5 +++++ docs/narr/extconfig.rst | 17 ++++++++++------- pyramid/config/__init__.py | 23 ++++++++++++++++------- pyramid/interfaces.py | 3 ++- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 1c82e5f27..f2bedbcc9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -7,7 +7,9 @@ Features - The ``pyramid.config.Configurator`` has grown the ability to allow actions to call other actions during a commit-cycle. This enables much more logic to be placed into actions, such as the ability to invoke other actions - or group them for improved conflict detection. + or group them for improved conflict detection. We have also exposed and + documented the config phases that Pyramid uses in order to further assist + in building conforming addons. See https://github.com/Pylons/pyramid/pull/1513 - Add ``pyramid.request.apply_request_extensions`` function which can be diff --git a/docs/api/config.rst b/docs/api/config.rst index 48dd2f0b9..ae913d32c 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -132,3 +132,8 @@ are being used. .. autoclass:: not_ + +.. attribute:: PHASE0_CONFIG +.. attribute:: PHASE1_CONFIG +.. attribute:: PHASE2_CONFIG +.. attribute:: PHASE3_CONFIG diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index c4d3e0250..c805f1572 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -243,12 +243,17 @@ This means that if an action must be reliably executed before or after another action, the ``order`` must be defined explicitly to make this work. For example, views are dependent on routes being defined. Thus the action created by :meth:`pyramid.config.Configurator.add_route` has an ``order`` of -:const:`pyramid.interfaces.PHASE2_CONFIG`. +:const:`pyramid.config.PHASE2_CONFIG`. Pre-defined Phases ~~~~~~~~~~~~~~~~~~ -:const:`pyramid.interfaces.PHASE1_CONFIG` +:const:`pyramid.config.PHASE0_CONFIG` + +- This phase is reserved for developers who want to execute actions prior + to Pyramid's core directives. + +:const:`pyramid.config.PHASE1_CONFIG` - :meth:`pyramid.config.Configurator.add_renderer` - :meth:`pyramid.config.Configurator.add_route_predicate` @@ -258,12 +263,12 @@ Pre-defined Phases - :meth:`pyramid.config.Configurator.set_default_permission` - :meth:`pyramid.config.Configurator.set_view_mapper` -:const:`pyramid.interfaces.PHASE2_CONFIG` +:const:`pyramid.config.PHASE2_CONFIG` - :meth:`pyramid.config.Configurator.add_route` - :meth:`pyramid.config.Configurator.set_authentication_policy` -``0`` +:const:`pyramid.config.PHASE3_CONFIG` - The default for all builtin or custom directives unless otherwise specified. @@ -285,9 +290,7 @@ but we want it to conflict with any other call to our addon: .. code-block:: python :linenos: - from pyramid.interfaces import PHASE1_CONFIG - - PHASE0_CONFIG = PHASE1_CONFIG - 10 + from pyramid.config import PHASE0_CONFIG def includeme(config): config.add_directive(add_auto_route, 'add_auto_route') diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index b5b5e841d..ea84aa1dc 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -12,7 +12,10 @@ from pyramid.interfaces import ( IDebugLogger, IExceptionResponse, IPredicateList, + PHASE0_CONFIG, PHASE1_CONFIG, + PHASE2_CONFIG, + PHASE3_CONFIG, ) from pyramid.asset import resolve_asset_spec @@ -55,7 +58,9 @@ from pyramid.settings import aslist from pyramid.threadlocal import manager from pyramid.util import ( + ActionInfo, WeakOrderedSet, + action_method, object_description, ) @@ -69,17 +74,18 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import PredicateList, not_ +from pyramid.config.util import ( + PredicateList, + not_, + PHASE1_CONFIG, + PHASE2_CONFIG, + PHASE3_CONFIG, +) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin from pyramid.path import DottedNameResolver -from pyramid.util import ( - action_method, - ActionInfo, - ) - empty = text_('') _marker = object() @@ -87,6 +93,10 @@ ConfigurationError = ConfigurationError # pyflakes not_ = not_ # pyflakes, this is an API +PHASE0_CONFIG = PHASE0_CONFIG # api +PHASE1_CONFIG = PHASE1_CONFIG # api +PHASE2_CONFIG = PHASE2_CONFIG # api +PHASE3_CONFIG = PHASE3_CONFIG # api class Configurator( TestingConfiguratorMixin, @@ -1301,4 +1311,3 @@ def expand_action(discriminator, callable=None, args=(), kw=None, ) global_registries = WeakOrderedSet() - diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 1508f282e..4c171f9cc 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1228,6 +1228,7 @@ class ICacheBuster(Interface): # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. +PHASE0_CONFIG = -30 PHASE1_CONFIG = -20 PHASE2_CONFIG = -10 - +PHASE3_CONFIG = 0 -- cgit v1.2.3 From c0063b33e3b570120aab09b7d0a0adcf31c8705c Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:01:15 -0600 Subject: fix odd sentence --- docs/narr/extconfig.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index c805f1572..47f2fcb46 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -235,7 +235,8 @@ actions. The logic within actions is deferred until a call to :meth:`pyramid.config.Configurator.make_wsgi_app`). This means you may call ``config.add_view(route_name='foo')`` **before** ``config.add_route('foo', '/foo')`` because nothing actually happens until -commit-time when conflicts are resolved, actions are ordered and executed. +commit-time. During a commit cycle conflicts are resolved, actions are ordered +and executed. By default, almost every action in Pyramid has an ``order`` of ``0``. Every action within the same order-level will be executed in the order it was called. -- cgit v1.2.3 From bba15920ee77a626c2ea3636d9d3b4f8d571afa6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:02:14 -0600 Subject: avoid saying order=0, instead say PHASE3_CONFIG --- docs/narr/extconfig.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 47f2fcb46..d17842bf2 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -238,8 +238,9 @@ actions. The logic within actions is deferred until a call to commit-time. During a commit cycle conflicts are resolved, actions are ordered and executed. -By default, almost every action in Pyramid has an ``order`` of ``0``. Every -action within the same order-level will be executed in the order it was called. +By default, almost every action in Pyramid has an ``order`` of +:const:`pyramid.config.PHASE3_CONFIG`. Every action within the same order-level +will be executed in the order it was called. This means that if an action must be reliably executed before or after another action, the ``order`` must be defined explicitly to make this work. For example, views are dependent on routes being defined. Thus the action created -- cgit v1.2.3 From 0bf2fded1a5dfa1614120c989f1d051908fa0b56 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:03:13 -0600 Subject: fix syntax --- docs/narr/extconfig.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index d17842bf2..a61eca7b7 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -295,7 +295,7 @@ but we want it to conflict with any other call to our addon: from pyramid.config import PHASE0_CONFIG def includeme(config): - config.add_directive(add_auto_route, 'add_auto_route') + config.add_directive('add_auto_route', add_auto_route) def add_auto_route(config, name, view): def register(): -- cgit v1.2.3 From a8fab3816726affaee2a8b91037372ba77cc1487 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 17 Feb 2015 20:15:11 -0500 Subject: add functest for config reentrancy --- pyramid/tests/test_config/test_init.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 2930734fa..4eb3f3385 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1554,6 +1554,35 @@ class TestActionState(unittest.TestCase): (3, g, (8,)), ]) +class Test_reentrant_action_functional(unittest.TestCase): + def _makeConfigurator(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_functional(self): + def add_auto_route(config, name, view): + def register(): + config.add_view(route_name=name, view=view) + config.add_route(name, '/' + name) + config.action( + ('auto route', name), register, order=-30 + ) + config = self._makeConfigurator() + config.add_directive('add_auto_route', add_auto_route) + def my_view(request): + return request.response + config.add_auto_route('foo', my_view) + config.commit() + from pyramid.interfaces import IRoutesMapper + mapper = config.registry.getUtility(IRoutesMapper) + routes = mapper.get_routes() + route = routes[0] + self.assertEqual(len(routes), 1) + self.assertEqual(route.name, 'foo') + self.assertEqual(route.path, '/foo') + + class Test_resolveConflicts(unittest.TestCase): def _callFUT(self, actions): from pyramid.config import resolveConflicts -- cgit v1.2.3 From bae121df8a31fa4303b68d9fcb71283293ad0c79 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 19:22:07 -0600 Subject: dammit, forgot to revert import --- pyramid/config/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index ea84aa1dc..401def208 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -74,13 +74,7 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import ( - PredicateList, - not_, - PHASE1_CONFIG, - PHASE2_CONFIG, - PHASE3_CONFIG, -) +from pyramid.config.util import PredicateList, not_ from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin -- cgit v1.2.3 From 4f28c2e2bd59c3fdbfc784d2ba8ef569bbe3b484 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 17 Feb 2015 20:28:38 -0500 Subject: appease coverage --- pyramid/tests/test_config/test_init.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 4eb3f3385..0ed04eb06 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1570,8 +1570,7 @@ class Test_reentrant_action_functional(unittest.TestCase): ) config = self._makeConfigurator() config.add_directive('add_auto_route', add_auto_route) - def my_view(request): - return request.response + def my_view(request): return request.response config.add_auto_route('foo', my_view) config.commit() from pyramid.interfaces import IRoutesMapper -- 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