From 8517d44072ee79535097ccfda25764f72ec45f81 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 7 Aug 2011 11:25:53 -0400 Subject: first cut at topological sorting of tweens given hints provided by the framework extender --- pyramid/config.py | 62 ++++----------- pyramid/paster.py | 14 ++-- pyramid/tests/test_config.py | 54 +++---------- pyramid/tests/test_paster.py | 31 +++++--- pyramid/tests/test_router.py | 4 +- pyramid/tests/test_tweens.py | 180 +++++++++++++++++++++++++++++++++++++++++++ pyramid/tweens.py | 147 +++++++++++++++++++++++++++++++++++ 7 files changed, 381 insertions(+), 111 deletions(-) create mode 100644 pyramid/tests/test_tweens.py diff --git a/pyramid/config.py b/pyramid/config.py index a12df8ef7..6ae9a70ce 100644 --- a/pyramid/config.py +++ b/pyramid/config.py @@ -80,6 +80,9 @@ from pyramid.traversal import DefaultRootFactory from pyramid.traversal import find_interface from pyramid.traversal import traversal_path from pyramid.tweens import excview_tween_factory +from pyramid.tweens import Tweens +from pyramid.tweens import tween_factory_name +from pyramid.tweens import MAIN from pyramid.urldispatch import RoutesMapper from pyramid.util import DottedNameResolver from pyramid.util import WeakOrderedSet @@ -733,9 +736,6 @@ class Configurator(object): # cope with WebOb exc objects not decoratored with IExceptionResponse from webob.exc import WSGIHTTPException as WebobWSGIHTTPException registry.registerSelfAdapter((WebobResponse,), IResponse) - # add a handler manager - for factory in tweens: - self._add_tween(factory, explicit=True) if debug_logger is None: debug_logger = logging.getLogger(self.package_name) @@ -784,6 +784,8 @@ class Configurator(object): self.commit() for inc in includes: self.include(inc) + for factory in tweens: + self._add_tween(factory, explicit=True) def hook_zca(self): """ Call :func:`zope.component.getSiteManager.sethook` with @@ -903,7 +905,7 @@ class Configurator(object): return self._derive_view(view, attr=attr, renderer=renderer) @action_method - def add_tween(self, tween_factory): + def add_tween(self, tween_factory, below=None, atop=None): """ Add a 'tween factory'. A :term:`tween` (think: 'between') is a bit of code that sits between the Pyramid router's main request handling @@ -920,9 +922,10 @@ class Configurator(object): .. note:: This feature is new as of Pyramid 1.1.1. """ - return self._add_tween(tween_factory, explicit=False) + return self._add_tween(tween_factory, below=below, atop=atop, + explicit=False) - def _add_tween(self, tween_factory, explicit): + def _add_tween(self, tween_factory, below=None, atop=None, explicit=False): tween_factory = self.maybe_dotted(tween_factory) name = tween_factory_name(tween_factory) def register(): @@ -931,11 +934,12 @@ class Configurator(object): if tweens is None: tweens = Tweens() registry.registerUtility(tweens, ITweens) - tweens.add( - tween_factory_name(excview_tween_factory), - excview_tween_factory, - explicit=False) - tweens.add(name, tween_factory, explicit) + tweens.add_implicit(tween_factory_name(excview_tween_factory), + excview_tween_factory, below=MAIN) + if explicit: + tweens.add_explicit(name, tween_factory) + else: + tweens.add_implicit(name, tween_factory, below=below, atop=atop) self.action(('tween', name, explicit), register) @action_method @@ -3378,39 +3382,3 @@ def isexception(o): global_registries = WeakOrderedSet() -class Tweens(object): - implements(ITweens) - def __init__(self): - self.explicit = [] - self.implicit = [] - - def add(self, name, factory, explicit=False): - if explicit: - self.explicit.append((name, factory)) - else: - self.implicit.append((name, factory)) - - def __call__(self, handler, registry): - factories = self.implicit - if self.explicit: - factories = self.explicit - for name, factory in factories: - handler = factory(handler, registry) - return handler - -def tween_factory_name(factory): - if (hasattr(factory, '__name__') and - hasattr(factory, '__module__')): - # function or class - name = '.'.join([factory.__module__, - factory.__name__]) - elif hasattr(factory, '__module__'): - # instance - name = '.'.join([factory.__module__, - factory.__class__.__name__, - str(id(factory))]) - else: - raise ConfigurationError( - 'A tween factory must be a class, an instance, or a function; ' - '%s is not a suitable tween factory' % factory) - return name diff --git a/pyramid/paster.py b/pyramid/paster.py index 54f5a51a6..e7ec8fb93 100644 --- a/pyramid/paster.py +++ b/pyramid/paster.py @@ -574,27 +574,31 @@ class PTweensCommand(PCommand): registry = env['registry'] tweens = self._get_tweens(registry) if tweens is not None: + implicit = tweens.implicit() + explicit = tweens.explicit ordering = [] - if tweens.explicit: + if explicit: self.out('"pyramid.tweens" config value set ' '(explicitly ordered tweens used)') self.out('') - ordering.append((tweens.explicit, + ordering.append((explicit, 'Explicit Tween Chain (used)')) - ordering.append((tweens.implicit, + ordering.append((implicit, 'Implicit Tween Chain (not used)')) else: self.out('"pyramid.tweens" config value NOT set ' '(implicitly ordered tweens used)') self.out('') - ordering.append((tweens.implicit, '')) + ordering.append((implicit, '')) for L, title in ordering: if title: self.out(title) self.out('') - fmt = '%-8s %-30s' + fmt = '%-10s %-30s' self.out(fmt % ('Position', 'Name')) self.out(fmt % ('-'*len('Position'), '-'*len('Name'))) + self.out(fmt % ('(implied)', 'main')) for pos, (name, item) in enumerate(L): self.out(fmt % (pos, name)) + self.out(fmt % ('(implied)', 'ingress')) self.out('') diff --git a/pyramid/tests/test_config.py b/pyramid/tests/test_config.py index d73fd7f7d..598362ccb 100644 --- a/pyramid/tests/test_config.py +++ b/pyramid/tests/test_config.py @@ -592,6 +592,7 @@ pyramid.tests.test_config.dummy_include2""", 'pyramid.tweens': 'pyramid.tests.test_config.dummy_tween_factory' } config.setup_registry(settings=settings) + config.commit() tweens = config.registry.getUtility(ITweens) self.assertEqual(tweens.explicit, [('pyramid.tests.test_config.dummy_tween_factory', @@ -639,7 +640,7 @@ pyramid.tests.test_config.dummy_include2""", config.commit() tweens = config.registry.queryUtility(ITweens) self.assertEqual( - tweens.implicit, + tweens.implicit(), [('pyramid.tweens.excview_tween_factory', excview_tween_factory), ('pyramid.tests.test_config.factory1', factory1), ('pyramid.tests.test_config.factory2', factory2)]) @@ -652,7 +653,7 @@ pyramid.tests.test_config.dummy_include2""", config.commit() tweens = config.registry.queryUtility(ITweens) self.assertEqual( - tweens.implicit, + tweens.implicit(), [ ('pyramid.tweens.excview_tween_factory', excview_tween_factory), ('pyramid.tests.test_config.dummy_tween_factory', @@ -668,13 +669,15 @@ pyramid.tests.test_config.dummy_include2""", config.add_tween(atween) config.commit() tweens = config.registry.queryUtility(ITweens) - self.assertEqual(len(tweens.implicit), 2) + implicit = tweens.implicit() + self.assertEqual(len(implicit), 2) self.assertEqual( - tweens.implicit[0], + implicit[0], ('pyramid.tweens.excview_tween_factory', excview_tween_factory)) self.assertTrue( - tweens.implicit[1][0].startswith('pyramid.tests.test_config.ATween.')) - self.assertEqual(tweens.implicit[1][1], atween) + implicit[1][0].startswith( + 'pyramid.tests.test_config.ATween.')) + self.assertEqual(implicit[1][1], atween) def test_add_tween_unsuitable(self): from pyramid.exceptions import ConfigurationError @@ -5511,45 +5514,6 @@ class Test_isexception(unittest.TestCase): pass self.assertEqual(self._callFUT(ISubException), True) -class TestTweens(unittest.TestCase): - def _makeOne(self): - from pyramid.config import Tweens - return Tweens() - - def test_add_explicit(self): - tweens = self._makeOne() - tweens.add('name', 'factory', explicit=True) - self.assertEqual(tweens.explicit, [('name', 'factory')]) - tweens.add('name2', 'factory2', explicit=True) - self.assertEqual(tweens.explicit, [('name', 'factory'), - ('name2', 'factory2')]) - - def test_add_implicit(self): - tweens = self._makeOne() - tweens.add('name', 'factory', explicit=False) - self.assertEqual(tweens.implicit, [('name', 'factory')]) - tweens.add('name2', 'factory2', explicit=False) - self.assertEqual(tweens.implicit, [('name', 'factory'), - ('name2', 'factory2')]) - - def test___call___explicit(self): - tweens = self._makeOne() - def factory1(handler, registry): - return handler - def factory2(handler, registry): - return '123' - tweens.explicit = [('name', factory1), ('name', factory2)] - self.assertEqual(tweens(None, None), '123') - - def test___call___implicit(self): - tweens = self._makeOne() - def factory1(handler, registry): - return handler - def factory2(handler, registry): - return '123' - tweens.implicit = [('name', factory1), ('name', factory2)] - self.assertEqual(tweens(None, None), '123') - class DummyRequest: subpath = () matchdict = None diff --git a/pyramid/tests/test_paster.py b/pyramid/tests/test_paster.py index 58ed73d2c..be7aa386e 100644 --- a/pyramid/tests/test_paster.py +++ b/pyramid/tests/test_paster.py @@ -856,9 +856,11 @@ class TestPTweensCommand(unittest.TestCase): L, ['"pyramid.tweens" config value NOT set (implicitly ordered tweens used)', '', - 'Position Name ', - '-------- ---- ', - '0 name ', + 'Position Name ', + '-------- ---- ', + '(implied) main ', + '0 name ', + '(implied) ingress ', '']) def test_command_implicit_and_explicit_tweens(self): @@ -875,17 +877,20 @@ class TestPTweensCommand(unittest.TestCase): '', 'Explicit Tween Chain (used)', '', - 'Position Name ', - '-------- ---- ', - '0 name2 ', + 'Position Name ', + '-------- ---- ', + '(implied) main ', + '0 name2 ', + '(implied) ingress ', '', 'Implicit Tween Chain (not used)', '', - 'Position Name ', - '-------- ---- ', - '0 name ', - '' - ]) + 'Position Name ', + '-------- ---- ', + '(implied) main ', + '0 name ', + '(implied) ingress ', + '']) def test__get_tweens(self): command = self._makeOne() @@ -894,8 +899,10 @@ class TestPTweensCommand(unittest.TestCase): class DummyTweens(object): def __init__(self, implicit, explicit): - self.implicit = implicit + self._implicit = implicit self.explicit = explicit + def implicit(self): + return self._implicit class Dummy: pass diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 6b0354468..19134813f 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -159,8 +159,8 @@ class TestRouter(unittest.TestCase): wrapper.name = 'two' wrapper.child = handler return wrapper - tweens.add('one', tween_factory1) - tweens.add('two', tween_factory2) + tweens.add_implicit('one', tween_factory1) + tweens.add_implicit('two', tween_factory2) router = self._makeOne() self.assertEqual(router.handle_request.name, 'two') self.assertEqual(router.handle_request.child.name, 'one') diff --git a/pyramid/tests/test_tweens.py b/pyramid/tests/test_tweens.py new file mode 100644 index 000000000..7d5ae11b2 --- /dev/null +++ b/pyramid/tests/test_tweens.py @@ -0,0 +1,180 @@ +import unittest + +class TestTweens(unittest.TestCase): + def _makeOne(self): + from pyramid.config import Tweens + return Tweens() + + def test_add_explicit(self): + tweens = self._makeOne() + tweens.add_explicit('name', 'factory') + self.assertEqual(tweens.explicit, [('name', 'factory')]) + tweens.add_explicit('name2', 'factory2') + self.assertEqual(tweens.explicit, [('name', 'factory'), + ('name2', 'factory2')]) + + def test_add_implicit(self): + from pyramid.tweens import INGRESS + tweens = self._makeOne() + tweens.add_implicit('name', 'factory') + self.assertEqual(tweens.implicit_names, ['name']) + self.assertEqual(tweens.implicit_factories, + {'name':'factory'}) + self.assertEqual(tweens.implicit_ingress_names, ['name']) + self.assertEqual(tweens.implicit_order, [('name', INGRESS)]) + tweens.add_implicit('name2', 'factory2') + self.assertEqual(tweens.implicit_names, ['name', 'name2']) + self.assertEqual(tweens.implicit_factories, + {'name':'factory', 'name2':'factory2'}) + self.assertEqual(tweens.implicit_ingress_names, ['name', 'name2']) + self.assertEqual(tweens.implicit_order, + [('name', INGRESS), ('name2', INGRESS)]) + tweens.add_implicit('name3', 'factory3', below='name2') + self.assertEqual(tweens.implicit_names, ['name', 'name2', 'name3']) + self.assertEqual(tweens.implicit_factories, + {'name':'factory', 'name2':'factory2', + 'name3':'factory3'}) + self.assertEqual(tweens.implicit_ingress_names, ['name', 'name2']) + self.assertEqual(tweens.implicit_order, + [('name', INGRESS), ('name2', INGRESS), + ('name2', 'name3')]) + + def test___call___explicit(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.explicit = [('name', factory1), ('name', factory2)] + self.assertEqual(tweens(None, None), '123') + + def test___call___implicit(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.implicit_names = ['name', 'name2'] + tweens.implicit_factories = {'name':factory1, 'name2':factory2} + self.assertEqual(tweens(None, None), '123') + + def test_implicit_ordering_1(self): + tweens = self._makeOne() + tweens.add_implicit('name1', 'factory1') + tweens.add_implicit('name2', 'factory2') + self.assertEqual(tweens.implicit(), [('name1', 'factory1'), + ('name2', 'factory2')]) + + def test_implicit_ordering_2(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + tweens.add_implicit('name1', 'factory1') + tweens.add_implicit('name2', 'factory2', below=MAIN) + self.assertEqual(tweens.implicit(), + [('name2', 'factory2'), ('name1', 'factory1')]) + + def test_implicit_ordering_3(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('auth', 'auth_factory', atop='browserid') + add('dbt', 'dbt_factory') + add('retry', 'retry_factory', below='txnmgr', atop='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', atop='exceptionview') + add('exceptionview', 'excview_factory', below=MAIN) + self.assertEqual(tweens.implicit(), + [('txnmgr', 'txnmgr_factory'), + ('retry', 'retry_factory'), + ('exceptionview', 'excview_factory'), + ('auth', 'auth_factory'), + ('browserid', 'browserid_factory'), + ('dbt', 'dbt_factory')]) + + def test_implicit_ordering_4(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', below=MAIN) + add('auth', 'auth_factory', atop='browserid') + add('retry', 'retry_factory', below='txnmgr', atop='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', atop='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [('txnmgr', 'txnmgr_factory'), + ('retry', 'retry_factory'), + ('exceptionview', 'excview_factory'), + ('auth', 'auth_factory'), + ('browserid', 'browserid_factory'), + ('dbt', 'dbt_factory')]) + + def test_implicit_ordering_missing_partial(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', below=MAIN) + add('auth', 'auth_factory', atop='browserid') + add('retry', 'retry_factory', below='txnmgr', atop='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [('retry', 'retry_factory'), + ('exceptionview', 'excview_factory'), + ('auth', 'auth_factory'), + ('browserid', 'browserid_factory'), + ('dbt', 'dbt_factory')]) + + def test_implicit_ordering_missing_partial2(self): + tweens = self._makeOne() + add = tweens.add_implicit + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', atop='browserid') + add('retry', 'retry_factory', below='txnmgr', atop='exceptionview') + add('browserid', 'browserid_factory') + self.assertEqual(tweens.implicit(), + [('dbt', 'dbt_factory'), + ('auth', 'auth_factory'), + ('browserid', 'browserid_factory'), + ('retry', 'retry_factory')]) + + def test_implicit_ordering_missing_partial3(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', below=MAIN) + add('retry', 'retry_factory', below='txnmgr', atop='exceptionview') + add('browserid', 'browserid_factory') + self.assertEqual(tweens.implicit(), + [('retry', 'retry_factory'), + ('exceptionview', 'excview_factory'), + ('browserid', 'browserid_factory')]) + + def test_implicit_ordering_conflict_direct(self): + from pyramid.tweens import CyclicDependencyError + tweens = self._makeOne() + add = tweens.add_implicit + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', atop='browserid', below='browserid') + self.assertRaises(CyclicDependencyError, tweens.implicit) + + def test_implicit_ordering_conflict_indirect(self): + from pyramid.tweens import CyclicDependencyError + tweens = self._makeOne() + add = tweens.add_implicit + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', atop='browserid') + add('dbt', 'dbt_factory', below='browserid', atop='auth') + self.assertRaises(CyclicDependencyError, tweens.implicit) + +class TestCyclicDependencyError(unittest.TestCase): + def _makeOne(self, cycles): + from pyramid.tweens import CyclicDependencyError + return CyclicDependencyError(cycles) + + def test___str__(self): + exc = self._makeOne({'a':['c', 'd'], 'c':['a']}) + result = str(exc) + self.assertEqual(result, + "'a' sorts atop ['c', 'd']; 'c' sorts atop ['a']") + diff --git a/pyramid/tweens.py b/pyramid/tweens.py index f7673a738..93a564cfc 100644 --- a/pyramid/tweens.py +++ b/pyramid/tweens.py @@ -1,9 +1,12 @@ import sys +from pyramid.exceptions import ConfigurationError from pyramid.interfaces import IExceptionViewClassifier from pyramid.interfaces import IView +from pyramid.interfaces import ITweens from pyramid.events import NewResponse from zope.interface import providedBy +from zope.interface import implements def excview_tween_factory(handler, registry): """ A :term:`tween` factory which produces a tween that catches an @@ -43,3 +46,147 @@ def excview_tween_factory(handler, registry): return excview_tween +class CyclicDependencyError(Exception): + def __init__(self, cycles): + self.cycles = cycles + + def __str__(self): + L = [] + cycles = self.cycles + for cycle in cycles: + dependent = cycle + dependees = cycles[cycle] + L.append('%r sorts atop %r' % (dependent, dependees)) + msg = '; '.join(L) + return msg + +MAIN = 'main' +INGRESS = 'ingress' + +class Tweens(object): + implements(ITweens) + def __init__(self): + self.explicit = [] + self.implicit_names = [] + self.implicit_factories = {} + self.implicit_order = [] + self.implicit_ingress_names = [] + + def add_explicit(self, name, factory): + self.explicit.append((name, factory)) + + def add_implicit(self, name, factory, below=None, atop=None): + if below is None and atop is None: + atop = 'ingress' + self.implicit_ingress_names.append(name) + if below is not None: + order = (below, name) + self.implicit_order.append(order) + if atop is not None: + order = (name, atop) + self.implicit_order.append(order) + self.implicit_names.append(name) + self.implicit_factories[name] = factory + + def implicit(self): + roots = [] + graph = {} + + def add_node(graph, node): + if not graph.has_key(node): + roots.append(node) + graph[node] = [0] # 0 = number of arcs coming into this node + + def add_arc(graph, fromnode, tonode): + graph[fromnode].append(tonode) + graph[tonode][0] += 1 + if tonode in roots: + roots.remove(tonode) + + names = [MAIN, INGRESS] + self.implicit_names + + orders = {} + + for pos, (first, second) in enumerate(self.implicit_order): + has_first = first in names + has_second = second in names + if (not has_first) or (not has_second): + self.implicit_order[pos] = None, None # FFF + else: + orders[first] = orders[second] = True + + for v in names: + # any name that doesn't have an ordering after we detect all + # nodes with orders should get an ordering relative to INGRESS, + # as if it were added with no below or atop + if (not v in orders) and (v not in (INGRESS, MAIN)): + self.implicit_order.append((v, INGRESS)) + self.implicit_ingress_names.append(v) + add_node(graph, v) + + for a, b in self.implicit_order: + if a is not None and b is not None: # see FFF above + add_arc(graph, a, b) + + def sortroots(name): + # sort roots so that roots (and their children) that depend only on + # the ingress sort nearer the end (nearer the ingress) + if name in self.implicit_ingress_names: + return 1 + children = graph[name][1:] + for child in children: + if sortroots(child) == 1: + return 1 + return -1 + + roots.sort(key=sortroots) + + sorted_tweens = [] + + while roots: + root = roots.pop(0) + sorted_tweens.append(root) + children = graph[root][1:] + for child in children: + arcs = graph[child][0] + arcs -= 1 + graph[child][0] = arcs + if arcs == 0: + roots.insert(0, child) + del graph[root] + + if graph: + # loop in input + cycledeps = {} + for k, v in graph.items(): + cycledeps[k] = v[1:] + raise CyclicDependencyError(cycledeps) + + return [ (name, self.implicit_factories[name]) for name in + sorted_tweens if name not in (MAIN, INGRESS) ] + + def __call__(self, handler, registry): + if self.explicit: + factories = self.explicit + else: + factories = self.implicit() + for name, factory in factories: + handler = factory(handler, registry) + return handler + +def tween_factory_name(factory): + if (hasattr(factory, '__name__') and + hasattr(factory, '__module__')): + # function or class + name = '.'.join([factory.__module__, + factory.__name__]) + elif hasattr(factory, '__module__'): + # instance + name = '.'.join([factory.__module__, + factory.__class__.__name__, + str(id(factory))]) + else: + raise ConfigurationError( + 'A tween factory must be a class, an instance, or a function; ' + '%s is not a suitable tween factory' % factory) + return name -- cgit v1.2.3