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 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 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 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