summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt21
-rw-r--r--docs/narr/commandline.rst31
-rw-r--r--pyramid/config/__init__.py206
-rw-r--r--pyramid/config/tweens.py15
-rw-r--r--pyramid/paster.py14
-rw-r--r--pyramid/router.py9
-rw-r--r--pyramid/scripting.py35
-rw-r--r--pyramid/testing.py1
-rw-r--r--pyramid/tests/test_config/test_init.py123
-rw-r--r--pyramid/tests/test_scripting.py21
10 files changed, 333 insertions, 143 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 3eb23b9ec..49613b242 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -25,6 +25,16 @@ Features
credentials from a ``request`` object, and returns them as a named tuple.
See https://github.com/Pylons/pyramid/pull/2662
+- Pyramid 1.4 silently dropped a feature of the configurator that has been
+ restored. It's again possible for action discriminators to conflict across
+ different action orders.
+ See https://github.com/Pylons/pyramid/pull/2757
+
+- ``pyramid.paster.bootstrap`` and its sibling ``pyramid.scripting.prepare``
+ can now be used as context managers to automatically invoke the ``closer``
+ and pop threadlocals off of the stack to prevent memory leaks.
+ See https://github.com/Pylons/pyramid/pull/2760
+
Bug Fixes
---------
@@ -44,6 +54,17 @@ Bug Fixes
use a primitive type instead.
See https://github.com/Pylons/pyramid/pull/2715
+- Pyramid 1.6 introduced the ability for an action to invoke another action.
+ There was a bug in the way that ``config.add_view`` would interact with
+ custom view derivers introduced in Pyramid 1.7 because the view's
+ discriminator cannot be computed until view derivers and view predicates
+ have been created in earlier orders. Invoking an action from another action
+ would trigger an unrolling of the pipeline and would compute discriminators
+ before they were ready. The new behavior respects the ``order`` of the action
+ and ensures the discriminators are not computed until dependent actions
+ from previous orders have executed.
+ See https://github.com/Pylons/pyramid/pull/2757
+
Deprecations
------------
diff --git a/docs/narr/commandline.rst b/docs/narr/commandline.rst
index 6cd90d42f..242bc7ec7 100644
--- a/docs/narr/commandline.rst
+++ b/docs/narr/commandline.rst
@@ -649,6 +649,10 @@ using the :func:`pyramid.paster.bootstrap` command in the body of your script.
.. versionadded:: 1.1
:func:`pyramid.paster.bootstrap`
+.. versionchanged:: 1.8
+ Added the ability for ``bootstrap`` to cleanup automatically via the
+ ``with`` statement.
+
In the simplest case, :func:`pyramid.paster.bootstrap` can be used with a
single argument, which accepts the :term:`PasteDeploy` ``.ini`` file
representing your Pyramid application's configuration as a single argument:
@@ -656,8 +660,9 @@ representing your Pyramid application's configuration as a single argument:
.. code-block:: python
from pyramid.paster import bootstrap
- env = bootstrap('/path/to/my/development.ini')
- print(env['request'].route_url('home'))
+
+ with bootstrap('/path/to/my/development.ini') as env:
+ print(env['request'].route_url('home'))
:func:`pyramid.paster.bootstrap` returns a dictionary containing
framework-related information. This dictionary will always contain a
@@ -723,8 +728,9 @@ load instead of ``main``:
.. code-block:: python
from pyramid.paster import bootstrap
- env = bootstrap('/path/to/my/development.ini#another')
- print(env['request'].route_url('home'))
+
+ with bootstrap('/path/to/my/development.ini#another') as env:
+ print(env['request'].route_url('home'))
The above example specifies the ``another`` ``app``, ``pipeline``, or
``composite`` section of your PasteDeploy configuration file. The ``app``
@@ -761,9 +767,9 @@ desired request and passing it into :func:`~pyramid.paster.bootstrap`:
from pyramid.request import Request
request = Request.blank('/', base_url='https://example.com/prefix')
- env = bootstrap('/path/to/my/development.ini#another', request=request)
- print(env['request'].application_url)
- # will print 'https://example.com/prefix'
+ with bootstrap('/path/to/my/development.ini#another', request=request) as env:
+ print(env['request'].application_url)
+ # will print 'https://example.com/prefix'
Now you can readily use Pyramid's APIs for generating URLs:
@@ -776,7 +782,9 @@ Now you can readily use Pyramid's APIs for generating URLs:
Cleanup
~~~~~~~
-When your scripting logic finishes, it's good manners to call the ``closer``
+If you're using the ``with``-statement variant then there's nothing to
+worry about. However if you're using the returned environment directly then
+when your scripting logic finishes, it's good manners to call the ``closer``
callback:
.. code-block:: python
@@ -891,15 +899,12 @@ contains the following code:
omit = options.omit
if omit is None:
omit = []
- env = bootstrap(config_uri)
- settings, closer = env['registry'].settings, env['closer']
- try:
+ with bootstrap(config_uri) as env:
+ settings = env['registry'].settings
for k, v in settings.items():
if any([k.startswith(x) for x in omit]):
continue
print('%-40s %-20s' % (k, v))
- finally:
- closer()
This script uses the Python ``optparse`` module to allow us to make sense out
of extra arguments passed to the script. It uses the
diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py
index 553f32c9b..d4064dc78 100644
--- a/pyramid/config/__init__.py
+++ b/pyramid/config/__init__.py
@@ -27,7 +27,6 @@ from pyramid.compat import (
text_,
reraise,
string_types,
- zip_longest,
)
from pyramid.events import ApplicationCreated
@@ -380,6 +379,7 @@ class Configurator(
self.add_default_view_predicates()
self.add_default_view_derivers()
self.add_default_route_predicates()
+ self.add_default_tweens()
if exceptionresponse_view is not None:
exceptionresponse_view = self.maybe_dotted(exceptionresponse_view)
@@ -1110,29 +1110,8 @@ class ActionState(object):
try:
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
- # 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 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
- elif b is not None and a != b:
- raise ConfigurationError(
- '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 executed.')
- 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
+ action_iter = iter([])
+ conflict_state = ConflictResolverState()
while True:
# We clear the actions list prior to execution so if there
@@ -1141,26 +1120,14 @@ class ActionState(object):
# ensures that the previously executed actions have no new
# conflicts.
if self.actions:
- # Only resolve the new actions against executed_actions
- # 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
- # 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).
all_actions.extend(self.actions)
- pending_actions = resume(resolveConflicts(
- executed_actions +
- list(pending_actions) +
- self.actions
- ))
+ action_iter = resolveConflicts(
+ self.actions,
+ state=conflict_state,
+ )
self.actions = []
- action = next(pending_actions, None)
+ action = next(action_iter, None)
if action is None:
# we are done!
break
@@ -1176,9 +1143,7 @@ class ActionState(object):
try:
if callable is not None:
callable(*args, **kw)
- except (KeyboardInterrupt, SystemExit): # pragma: no cover
- raise
- except:
+ except Exception:
t, v, tb = sys.exc_info()
try:
reraise(ConfigurationExecutionError,
@@ -1193,65 +1158,102 @@ class ActionState(object):
executed_actions.append(action)
+ self.actions = all_actions
+ return executed_actions
+
finally:
if clear:
- del self.actions[:]
- else:
- self.actions = all_actions
+ self.actions = []
+
+
+class ConflictResolverState(object):
+ def __init__(self):
+ # keep a set of resolved discriminators to test against to ensure
+ # that a new action does not conflict with something already executed
+ self.resolved_ainfos = {}
+
+ # actions left over from a previous iteration
+ self.remaining_actions = []
+
+ # after executing an action we memoize its order to avoid any new
+ # actions sending us backward
+ self.min_order = None
+
+ # unique tracks the index of the action so we need it to increase
+ # monotonically across invocations to resolveConflicts
+ self.start = 0
+
# this function is licensed under the ZPL (stolen from Zope)
-def resolveConflicts(actions):
+def resolveConflicts(actions, state=None):
"""Resolve conflicting actions
Given an actions list, identify and try to resolve conflicting actions.
Actions conflict if they have the same non-None discriminator.
+
Conflicting actions can be resolved if the include path of one of
the actions is a prefix of the includepaths of the other
conflicting actions and is unequal to the include paths in the
other conflicting actions.
+
+ Actions are resolved on a per-order basis because some discriminators
+ cannot be computed until earlier actions have executed. An action in an
+ earlier order may execute successfully only to find out later that it was
+ overridden by another action with a smaller include path. This will result
+ in a conflict as there is no way to revert the original action.
+
+ ``state`` may be an instance of ``ConflictResolverState`` that
+ can be used to resume execution and resolve the new actions against the
+ list of executed actions from a previous call.
+
"""
+ if state is None:
+ state = ConflictResolverState()
+
+ # pick up where we left off last time, but track the new actions as well
+ state.remaining_actions.extend(normalize_actions(actions))
+ actions = state.remaining_actions
def orderandpos(v):
n, v = v
- if not isinstance(v, dict):
- # old-style tuple action
- v = expand_action(*v)
return (v['order'] or 0, n)
- sactions = sorted(enumerate(actions), key=orderandpos)
-
def orderonly(v):
n, v = v
- if not isinstance(v, dict):
- # old-style tuple action
- v = expand_action(*v)
return v['order'] or 0
+ sactions = sorted(enumerate(actions, start=state.start), key=orderandpos)
for order, actiongroup in itertools.groupby(sactions, orderonly):
# "order" is an integer grouping. Actions in a lower order will be
# executed before actions in a higher order. All of the actions in
# one grouping will be executed (its callable, if any will be called)
# before any of the actions in the next.
-
- unique = {}
output = []
+ unique = {}
+
+ # error out if we went backward in order
+ if state.min_order is not None and order < state.min_order:
+ r = ['Actions were added to order={0} after execution had moved '
+ 'on to order={1}. Conflicting actions: '
+ .format(order, state.min_order)]
+ for i, action in actiongroup:
+ for line in str(action['info']).rstrip().split('\n'):
+ r.append(" " + line)
+ raise ConfigurationError('\n'.join(r))
for i, action in actiongroup:
# Within an order, actions are executed sequentially based on
# original action ordering ("i").
- if not isinstance(action, dict):
- # old-style tuple action
- action = expand_action(*action)
-
- # "ainfo" is a tuple of (order, i, action) where "order" is a
- # user-supplied grouping, "i" is an integer expressing the relative
- # position of this action in the action list being resolved, and
- # "action" is an action dictionary. The purpose of an ainfo is to
- # associate an "order" and an "i" with a particular action; "order"
- # and "i" exist for sorting purposes after conflict resolution.
- ainfo = (order, i, action)
+ # "ainfo" is a tuple of (i, action) where "i" is an integer
+ # expressing the relative position of this action in the action
+ # list being resolved, and "action" is an action dictionary. The
+ # purpose of an ainfo is to associate an "i" with a particular
+ # action; "i" exists for sorting after conflict resolution.
+ ainfo = (i, action)
+ # wait to defer discriminators until we are on their order because
+ # the discriminator may depend on state from a previous order
discriminator = undefer(action['discriminator'])
action['discriminator'] = discriminator
@@ -1266,28 +1268,39 @@ def resolveConflicts(actions):
# Check for conflicts
conflicts = {}
-
for discriminator, ainfos in unique.items():
- # We use (includepath, order, i) as a sort key because we need to
+ # We use (includepath, i) as a sort key because we need to
# sort the actions by the paths so that the shortest path with a
# given prefix comes first. The "first" action is the one with the
- # shortest include path. We break sorting ties using "order", then
- # "i".
+ # shortest include path. We break sorting ties using "i".
def bypath(ainfo):
- path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1]
+ path, i = ainfo[1]['includepath'], ainfo[0]
return path, order, i
ainfos.sort(key=bypath)
ainfo, rest = ainfos[0], ainfos[1:]
- output.append(ainfo)
- _, _, action = ainfo
- basepath, baseinfo, discriminator = (
- action['includepath'],
- action['info'],
- action['discriminator'],
- )
+ _, action = ainfo
+
+ # ensure this new action does not conflict with a previously
+ # resolved action from an earlier order / invocation
+ prev_ainfo = state.resolved_ainfos.get(discriminator)
+ if prev_ainfo is not None:
+ _, paction = prev_ainfo
+ basepath, baseinfo = paction['includepath'], paction['info']
+ includepath = action['includepath']
+ # if the new action conflicts with the resolved action then
+ # note the conflict, otherwise drop the action as it's
+ # effectively overriden by the previous action
+ if (includepath[:len(basepath)] != basepath or
+ includepath == basepath):
+ L = conflicts.setdefault(discriminator, [baseinfo])
+ L.append(action['info'])
+
+ else:
+ output.append(ainfo)
- for _, _, action in rest:
+ basepath, baseinfo = action['includepath'], action['info']
+ for _, action in rest:
includepath = action['includepath']
# Test whether path is a prefix of opath
if (includepath[:len(basepath)] != basepath or # not a prefix
@@ -1298,14 +1311,30 @@ def resolveConflicts(actions):
if conflicts:
raise ConfigurationConflictError(conflicts)
- # sort conflict-resolved actions by (order, i) and yield them one
- # by one
- for a in [x[2] for x in sorted(output, key=operator.itemgetter(0, 1))]:
- yield a
+ # sort resolved actions by "i" and yield them one by one
+ for i, action in sorted(output, key=operator.itemgetter(0)):
+ # do not memoize the order until we resolve an action inside it
+ state.min_order = action['order']
+ state.start = i + 1
+ state.remaining_actions.remove(action)
+ state.resolved_ainfos[action['discriminator']] = (i, action)
+ yield action
-def expand_action(discriminator, callable=None, args=(), kw=None,
- includepath=(), info=None, order=0, introspectables=()):
+def normalize_actions(actions):
+ """Convert old-style tuple actions to new-style dicts."""
+ result = []
+ for v in actions:
+ if not isinstance(v, dict):
+ v = expand_action_tuple(*v)
+ result.append(v)
+ return result
+
+
+def expand_action_tuple(
+ discriminator, callable=None, args=(), kw=None, includepath=(),
+ info=None, order=0, introspectables=(),
+):
if kw is None:
kw = {}
return dict(
@@ -1319,4 +1348,5 @@ def expand_action(discriminator, callable=None, args=(), kw=None,
introspectables=introspectables,
)
+
global_registries = WeakOrderedSet()
diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py
index 8e1800f33..0aeb01fe3 100644
--- a/pyramid/config/tweens.py
+++ b/pyramid/config/tweens.py
@@ -10,7 +10,6 @@ from pyramid.compat import (
from pyramid.exceptions import ConfigurationError
from pyramid.tweens import (
- excview_tween_factory,
MAIN,
INGRESS,
EXCVIEW,
@@ -107,6 +106,9 @@ class TweensConfiguratorMixin(object):
return self._add_tween(tween_factory, under=under, over=over,
explicit=False)
+ def add_default_tweens(self):
+ self.add_tween(EXCVIEW)
+
@action_method
def _add_tween(self, tween_factory, under=None, over=None, explicit=False):
@@ -142,17 +144,6 @@ class TweensConfiguratorMixin(object):
if tweens is None:
tweens = Tweens()
registry.registerUtility(tweens, ITweens)
- ex_intr = self.introspectable('tweens',
- ('tween', EXCVIEW, False),
- EXCVIEW,
- 'implicit tween')
- ex_intr['name'] = EXCVIEW
- ex_intr['factory'] = excview_tween_factory
- ex_intr['type'] = 'implicit'
- ex_intr['under'] = None
- ex_intr['over'] = MAIN
- introspectables.append(ex_intr)
- tweens.add_implicit(EXCVIEW, excview_tween_factory, over=MAIN)
def register():
if explicit:
diff --git a/pyramid/paster.py b/pyramid/paster.py
index 3916be8f0..1b7afb5dc 100644
--- a/pyramid/paster.py
+++ b/pyramid/paster.py
@@ -129,8 +129,22 @@ def bootstrap(config_uri, request=None, options=None):
{'http_port': 8080} and then use %(http_port)s in the
config file.
+ This function may be used as a context manager to call the ``closer``
+ automatically:
+
+ .. code-block:: python
+
+ with bootstrap('development.ini') as env:
+ request = env['request']
+ # ...
+
See :ref:`writing_a_script` for more information about how to use this
function.
+
+ .. versionchanged:: 1.8
+
+ Added the ability to use the return value as a context manager.
+
"""
app = get_app(config_uri, options=options)
env = prepare(request)
diff --git a/pyramid/router.py b/pyramid/router.py
index 19773cf62..fd11925e9 100644
--- a/pyramid/router.py
+++ b/pyramid/router.py
@@ -34,8 +34,6 @@ from pyramid.traversal import (
ResourceTreeTraverser,
)
-from pyramid.tweens import excview_tween_factory
-
@implementer(IRouter)
class Router(object):
@@ -51,11 +49,10 @@ class Router(object):
self.routes_mapper = q(IRoutesMapper)
self.request_factory = q(IRequestFactory, default=Request)
self.request_extensions = q(IRequestExtensions)
- tweens = q(ITweens)
- if tweens is None:
- tweens = excview_tween_factory
self.orig_handle_request = self.handle_request
- self.handle_request = tweens(self.handle_request, registry)
+ tweens = q(ITweens)
+ if tweens is not None:
+ self.handle_request = tweens(self.handle_request, registry)
self.root_policy = self.root_factory # b/w compat
self.registry = registry
settings = registry.settings
diff --git a/pyramid/scripting.py b/pyramid/scripting.py
index d9587338f..7607d3ea3 100644
--- a/pyramid/scripting.py
+++ b/pyramid/scripting.py
@@ -56,12 +56,25 @@ def prepare(request=None, registry=None):
``root`` returned is the application's root resource object. The
``closer`` returned is a callable (accepting no arguments) that
should be called when your scripting application is finished
- using the root. ``registry`` is the registry object passed or
- the last registry loaded into
- :attr:`pyramid.config.global_registries` if no registry is passed.
+ using the root. ``registry`` is the resolved registry object.
``request`` is the request object passed or the constructed request
if no request is passed. ``root_factory`` is the root factory used
to construct the root.
+
+ This function may be used as a context manager to call the ``closer``
+ automatically:
+
+ .. code-block:: python
+
+ registry = config.registry
+ with prepare(registry) as env:
+ request = env['request']
+ # ...
+
+ .. versionchanged:: 1.8
+
+ Added the ability to use the return value as a context manager.
+
"""
if registry is None:
registry = getattr(request, 'registry', global_registries.last)
@@ -85,8 +98,20 @@ def prepare(request=None, registry=None):
root = root_factory(request)
if getattr(request, 'context', None) is None:
request.context = root
- return {'root':root, 'closer':closer, 'registry':registry,
- 'request':request, 'root_factory':root_factory}
+ return AppEnvironment(
+ root=root,
+ closer=closer,
+ registry=registry,
+ request=request,
+ root_factory=root_factory,
+ )
+
+class AppEnvironment(dict):
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self['closer']()
def _make_request(path, registry=None):
""" Return a :meth:`pyramid.request.Request` object anchored at a
diff --git a/pyramid/testing.py b/pyramid/testing.py
index ec06fe379..877b351db 100644
--- a/pyramid/testing.py
+++ b/pyramid/testing.py
@@ -478,6 +478,7 @@ def setUp(registry=None, request=None, hook_zca=True, autocommit=True,
config.add_default_view_predicates()
config.add_default_view_derivers()
config.add_default_route_predicates()
+ config.add_default_tweens()
config.commit()
global have_zca
try:
diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py
index de199d079..7078d7e26 100644
--- a/pyramid/tests/test_config/test_init.py
+++ b/pyramid/tests/test_config/test_init.py
@@ -1545,6 +1545,31 @@ class TestActionState(unittest.TestCase):
c.execute_actions()
self.assertEqual(output, [('f', (1,), {}), ('g', (8,), {})])
+ def test_reentrant_action_with_deferred_discriminator(self):
+ # see https://github.com/Pylons/pyramid/issues/2697
+ from pyramid.registry import Deferred
+ output = []
+ c = self._makeOne()
+ def f(*a, **k):
+ output.append(('f', a, k))
+ c.actions.append((4, g, (4,), {}, (), None, 2))
+ def g(*a, **k):
+ output.append(('g', a, k))
+ def h(*a, **k):
+ output.append(('h', a, k))
+ def discrim():
+ self.assertEqual(output, [('f', (1,), {}), ('g', (2,), {})])
+ return 3
+ d = Deferred(discrim)
+ c.actions = [
+ (d, h, (3,), {}, (), None, 1), # order 1
+ (1, f, (1,)), # order 0
+ (2, g, (2,)), # order 0
+ ]
+ c.execute_actions()
+ self.assertEqual(output, [
+ ('f', (1,), {}), ('g', (2,), {}), ('h', (3,), {}), ('g', (4,), {})])
+
def test_reentrant_action_error(self):
from pyramid.exceptions import ConfigurationError
c = self._makeOne()
@@ -1570,6 +1595,28 @@ class TestActionState(unittest.TestCase):
(3, g, (8,)),
])
+ def test_executing_conflicting_action_across_orders(self):
+ from pyramid.exceptions import ConfigurationConflictError
+ c = self._makeOne()
+ def f(*a, **k): pass
+ def g(*a, **k): pass
+ c.actions = [
+ (1, f, (1,), {}, (), None, -1),
+ (1, g, (2,)),
+ ]
+ self.assertRaises(ConfigurationConflictError, c.execute_actions)
+
+ def test_executing_conflicting_action_across_reentrant_orders(self):
+ from pyramid.exceptions import ConfigurationConflictError
+ c = self._makeOne()
+ def f(*a, **k):
+ c.actions.append((1, g, (8,)))
+ def g(*a, **k): pass
+ c.actions = [
+ (1, f, (1,), {}, (), None, -1),
+ ]
+ self.assertRaises(ConfigurationConflictError, c.execute_actions)
+
class Test_reentrant_action_functional(unittest.TestCase):
def _makeConfigurator(self, *arg, **kw):
from pyramid.config import Configurator
@@ -1597,6 +1644,21 @@ class Test_reentrant_action_functional(unittest.TestCase):
self.assertEqual(route.name, 'foo')
self.assertEqual(route.path, '/foo')
+ def test_deferred_discriminator(self):
+ # see https://github.com/Pylons/pyramid/issues/2697
+ from pyramid.config import PHASE0_CONFIG
+ config = self._makeConfigurator()
+ def deriver(view, info): return view
+ deriver.options = ('foo',)
+ config.add_view_deriver(deriver, 'foo_view')
+ # add_view uses a deferred discriminator and will fail if executed
+ # prior to add_view_deriver executing its action
+ config.add_view(lambda r: r.response, name='', foo=1)
+ def dummy_action():
+ # trigger a re-entrant action
+ config.action(None, lambda: None)
+ config.action(None, dummy_action, order=PHASE0_CONFIG)
+ config.commit()
class Test_resolveConflicts(unittest.TestCase):
def _callFUT(self, actions):
@@ -1666,15 +1728,14 @@ class Test_resolveConflicts(unittest.TestCase):
def test_it_success_dicts(self):
from pyramid.tests.test_config import dummyfactory as f
- from pyramid.config import expand_action
result = self._callFUT([
- expand_action(None, f),
- expand_action(1, f, (1,), {}, (), 'first'),
- expand_action(1, f, (2,), {}, ('x',), 'second'),
- expand_action(1, f, (3,), {}, ('y',), 'third'),
- expand_action(4, f, (4,), {}, ('y',), 'should be last', 99999),
- expand_action(3, f, (3,), {}, ('y',)),
- expand_action(None, f, (5,), {}, ('y',)),
+ (None, f),
+ (1, f, (1,), {}, (), 'first'),
+ (1, f, (2,), {}, ('x',), 'second'),
+ (1, f, (3,), {}, ('y',), 'third'),
+ (4, f, (4,), {}, ('y',), 'should be last', 99999),
+ (3, f, (3,), {}, ('y',)),
+ (None, f, (5,), {}, ('y',)),
])
result = list(result)
self.assertEqual(
@@ -1740,17 +1801,16 @@ class Test_resolveConflicts(unittest.TestCase):
def test_it_with_actions_grouped_by_order(self):
from pyramid.tests.test_config import dummyfactory as f
- from pyramid.config import expand_action
result = self._callFUT([
- expand_action(None, f), # X
- expand_action(1, f, (1,), {}, (), 'third', 10), # X
- expand_action(1, f, (2,), {}, ('x',), 'fourth', 10),
- expand_action(1, f, (3,), {}, ('y',), 'fifth', 10),
- expand_action(2, f, (1,), {}, (), 'sixth', 10), # X
- expand_action(3, f, (1,), {}, (), 'seventh', 10), # X
- expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), # X
- expand_action(4, f, (3,), {}, (), 'first', 5), # X
- expand_action(4, f, (5,), {}, ('y',), 'second', 5),
+ (None, f), # X
+ (1, f, (1,), {}, (), 'third', 10), # X
+ (1, f, (2,), {}, ('x',), 'fourth', 10),
+ (1, f, (3,), {}, ('y',), 'fifth', 10),
+ (2, f, (1,), {}, (), 'sixth', 10), # X
+ (3, f, (1,), {}, (), 'seventh', 10), # X
+ (5, f, (4,), {}, ('y',), 'eighth', 99999), # X
+ (4, f, (3,), {}, (), 'first', 5), # X
+ (4, f, (5,), {}, ('y',), 'second', 5),
])
result = list(result)
self.assertEqual(len(result), 6)
@@ -1812,7 +1872,32 @@ class Test_resolveConflicts(unittest.TestCase):
'order': 99999}
]
)
-
+
+ def test_override_success_across_orders(self):
+ from pyramid.tests.test_config import dummyfactory as f
+ result = self._callFUT([
+ (1, f, (2,), {}, ('x',), 'eek', 0),
+ (1, f, (3,), {}, ('x', 'y'), 'ack', 10),
+ ])
+ result = list(result)
+ self.assertEqual(result, [
+ {'info': 'eek',
+ 'args': (2,),
+ 'callable': f,
+ 'introspectables': (),
+ 'kw': {},
+ 'discriminator': 1,
+ 'includepath': ('x',),
+ 'order': 0},
+ ])
+
+ def test_conflicts_across_orders(self):
+ from pyramid.tests.test_config import dummyfactory as f
+ result = self._callFUT([
+ (1, f, (2,), {}, ('x', 'y'), 'eek', 0),
+ (1, f, (3,), {}, ('x'), 'ack', 10),
+ ])
+ self.assertRaises(ConfigurationConflictError, list, result)
class TestGlobalRegistriesIntegration(unittest.TestCase):
def setUp(self):
diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py
index 1e952062b..00f738e02 100644
--- a/pyramid/tests/test_scripting.py
+++ b/pyramid/tests/test_scripting.py
@@ -134,6 +134,27 @@ class Test_prepare(unittest.TestCase):
root, closer = info['root'], info['closer']
closer()
+ def test_it_is_a_context_manager(self):
+ request = DummyRequest({})
+ registry = request.registry = self._makeRegistry()
+ closer_called = [False]
+ with self._callFUT(request=request) as info:
+ root, request = info['root'], info['request']
+ pushed = self.manager.get()
+ self.assertEqual(pushed['request'], request)
+ self.assertEqual(pushed['registry'], registry)
+ self.assertEqual(pushed['request'].registry, registry)
+ self.assertEqual(root.a, (request,))
+ orig_closer = info['closer']
+ def closer():
+ orig_closer()
+ closer_called[0] = True
+ info['closer'] = closer
+ self.assertTrue(closer_called[0])
+ self.assertEqual(self.default, self.manager.get())
+ self.assertEqual(request.context, root)
+ self.assertEqual(request.registry, registry)
+
class Test__make_request(unittest.TestCase):
def _callFUT(self, path='/', registry=None):
from pyramid.scripting import _make_request