From 412b4abe9bb05aa0509508bead2498dfa2bd5f41 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 24 Nov 2011 02:56:40 -0500 Subject: ditch the miserable tuple-reordering stupidity of zope-style action execution (use a dictionary for all Pyramid-related actions and don't try to minimize them); add a text method to iintrospectable (temporarily); methd name changes wrt iintrospectable --- pyramid/config/__init__.py | 172 ++++++++++--------------- pyramid/config/introspection.py | 24 +++- pyramid/interfaces.py | 21 +++- pyramid/tests/test_config/test_init.py | 210 ++++++++++++++++++++++++------- pyramid/tests/test_config/test_routes.py | 3 +- 5 files changed, 267 insertions(+), 163 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 951f9b7ab..d78d46cb8 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1,5 +1,6 @@ import inspect import logging +import operator import os import sys import types @@ -449,7 +450,7 @@ class Configurator( info = '' return info - def action(self, discriminator, callable=None, args=(), kw=None, order=0, + def action(self, discriminator, callable=None, args=(), kw=None, order=None, introspectables=()): """ Register an action which will be executed when :meth:`pyramid.config.Configurator.commit` is called (or executed @@ -485,11 +486,11 @@ class Configurator( else: self.action_state.action( - discriminator, - callable, - args, - kw, - order, + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + order=order, info=action_info, includepath=self.includepath, introspectables=introspectables, @@ -870,21 +871,22 @@ class ActionState(object): self._seen_files.add(spec) return True - def action(self, discriminator, callable=None, args=(), kw=None, order=0, + def action(self, discriminator, callable=None, args=(), kw=None, order=None, includepath=(), info='', introspectables=()): """Add an action with the given discriminator, callable and arguments """ - # NB: note that the ordering and composition of the action tuple should - # not change without first ensuring that ``pyramid_zcml`` appends - # similarly-composed actions to our .actions variable (as silly as - # the composition and ordering is). if kw is None: kw = {} - action = (discriminator, callable, args, kw, includepath, info, order, - introspectables) - # remove trailing false items - while (len(action) > 2) and not action[-1]: - action = action[:-1] + action = dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + includepath=includepath, + info=info, + order=order, + introspectables=introspectables, + ) self.actions.append(action) def execute_actions(self, clear=True, introspector=None): @@ -936,10 +938,14 @@ class ActionState(object): """ + try: for action in resolveConflicts(self.actions): - (_, callable, args, kw, _, info, _, - introspectables) = expand_action(*action) + callable = action['callable'] + args = action['args'] + kw = action['kw'] + info = action['info'] + introspectables = action['introspectables'] if callable is None: continue try: @@ -966,125 +972,73 @@ def resolveConflicts(actions): """Resolve conflicting actions Given an actions list, identify and try to resolve conflicting actions. - Actions conflict if they have the same non-null discriminator. + 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. - - Here are some examples to illustrate how this works: - - >>> from zope.configmachine.tests.directives import f - >>> from pprint import PrettyPrinter - >>> pprint=PrettyPrinter(width=60).pprint - >>> pprint(resolveConflicts([ - ... (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',)), - ... ])) - [(None, f), - (1, f, (1,), {}, (), 'first'), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - (4, f, (4,), {}, ('y',), 'should be last')] - - >>> try: - ... v = resolveConflicts([ - ... (None, f), - ... (1, f, (2,), {}, ('x',), 'eek'), - ... (1, f, (3,), {}, ('y',), 'ack'), - ... (4, f, (4,), {}, ('y',)), - ... (3, f, (3,), {}, ('y',)), - ... (None, f, (5,), {}, ('y',)), - ... ]) - ... except ConfigurationConflictError, v: - ... pass - >>> print v - Conflicting configuration actions - For: 1 - eek - ack - """ # organize actions by discriminators unique = {} output = [] for i in range(len(actions)): - (discriminator, callable, args, kw, includepath, info, order, - introspectables - ) = expand_action(*(actions[i])) - - order = order or i + action = actions[i] + if not isinstance(action, dict): + # old-style ZCML tuple action + action = expand_action(*action) + order = action['order'] + if order is None: + action['order'] = i + discriminator = action['discriminator'] if discriminator is None: - # The discriminator is None, so this directive can - # never conflict. We can add it directly to the - # configuration actions. - output.append( - (order, discriminator, callable, args, kw, includepath, info, - introspectables) - ) + # The discriminator is None, so this action can never + # conflict. We can add it directly to the result. + output.append(action) continue - - - a = unique.setdefault(discriminator, []) - a.append( - (includepath, order, callable, args, kw, info, introspectables) - ) + L = unique.setdefault(discriminator, []) + L.append(action) # Check for conflicts conflicts = {} for discriminator, dups in unique.items(): - # We need to sort the actions by the paths so that the shortest # path with a given prefix comes first: - def allbutfunc(stupid): - # f me with a shovel, py3 cant cope with sorting when the - # callable function is in the list - return stupid[0:2] + stupid[3:] - dups.sort(key=allbutfunc) - (basepath, i, callable, args, kw, baseinfo, introspectables) = dups[0] - output.append( - (i, discriminator, callable, args, kw, basepath, baseinfo, - introspectables) - ) - for (includepath, i, callable, args, kw, info, - introspectables) in dups[1:]: + def bypath(action): + return (action['includepath'], action['order']) + dups.sort(key=bypath) + output.append(dups[0]) + basepath = dups[0]['includepath'] + baseinfo = dups[0]['info'] + discriminator = dups[0]['discriminator'] + for dup in dups[1:]: + includepath = dup['includepath'] # Test whether path is a prefix of opath if (includepath[:len(basepath)] != basepath # not a prefix - or - (includepath == basepath) - ): - if discriminator not in conflicts: - conflicts[discriminator] = [baseinfo] - conflicts[discriminator].append(info) - + or includepath == basepath): + L = conflicts.setdefault(discriminator, [baseinfo]) + L.append(dup['info']) if conflicts: raise ConfigurationConflictError(conflicts) - # Now put the output back in the original order, and return it: - output.sort() - r = [] - for o in output: - action = o[1:] - while len(action) > 2 and not action[-1]: - action = action[:-1] - r.append(action) - - return r + output.sort(key=operator.itemgetter('order')) + return output -# this function is licensed under the ZPL (stolen from Zope) def expand_action(discriminator, callable=None, args=(), kw=None, - includepath=(), info='', order=0, introspectables=()): + includepath=(), info='', order=None, introspectables=()): if kw is None: kw = {} - return (discriminator, callable, args, kw, includepath, info, order, - introspectables) + return dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + includepath=includepath, + info=info, + order=order, + introspectables=introspectables, + ) global_registries = WeakOrderedSet() diff --git a/pyramid/config/introspection.py b/pyramid/config/introspection.py index b30b8c4a1..18e9f94b1 100644 --- a/pyramid/config/introspection.py +++ b/pyramid/config/introspection.py @@ -40,12 +40,20 @@ class Introspector(object): intr = category.get(discriminator, default) return intr - def getall(self, category_name, sort_fn=None): + def get_category(self, category_name, sort_fn=None): if sort_fn is None: sort_fn = operator.attrgetter('order') category = self._categories[category_name] values = category.values() - return sorted(set(values), key=sort_fn) + values = sorted(set(values), key=sort_fn) + return [{'introspectable':intr, 'related':self.related(intr)} for + intr in values] + + def categorized(self, sort_fn=None): + L = [] + for category_name in sorted(self._categories.keys()): + L.append((category_name, self.get_category(category_name, sort_fn))) + return L def remove(self, category_name, discriminator): intr = self.get(category_name, discriminator) @@ -130,6 +138,13 @@ class Introspectable(dict): def related(self, introspector): return introspector.related(self) + def text(self): + result = [repr(self.discriminator)] + for k, v in self.items(): + result.append('%s: %s' % (k, v)) + result.append('action_info: %s' % (self.action_info,)) + return '\n'.join(result) + def __hash__(self): return hash((self.category_name,) + (self.discriminator,)) @@ -137,6 +152,11 @@ class Introspectable(dict): return '<%s category %r, discriminator %r>' % (self.__class__.__name__, self.category_name, self.discriminator) + + def __nonzero__(self): + return True + + __bool__ = __nonzero__ # py3 class IntrospectionConfiguratorMixin(object): introspectable = Introspectable diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 3f26c4386..9cae9e642 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -873,10 +873,23 @@ class IIntrospector(Interface): discriminator (or discriminator hash) ``discriminator``. If it does not exist in the introspector, return ``default`` """ - def getall(category_name, sort_fn=None): - """ Get a sequence of IIntrospectable objects related to - ``category_name`` . If ``sort_fn`` is ``None``, the sequence of - introspectable objects will be returned in the order they were added + def get_category(category_name, sort_fn=None): + """ Get a sequence of dictionaries in the form + ``[{'introspectable':IIntrospectable, 'related':[sequence of related + IIntrospectables]}, ...]`` where each introspectable is part of the + category associated with ``category_name`` . If ``sort_fn`` is + ``None``, the sequence will be returned in the order the + introspectables were added to the introspector. Otherwise, sort_fn + should be a function that accepts an IIntrospectable and returns a + value from it (ala the ``key`` function of Python's ``sorted`` + callable).""" + + def categorized(sort_fn=None): + """ Get a sequence of tuples in the form ``[(category_name, + [{'introspectable':IIntrospectable, 'related':[sequence of related + IIntrospectables]}, ...])]`` representing all known + introspectables. If ``sort_fn`` is ``None``, each introspectables + sequence will be returned in the order the introspectables were added to the introspector. Otherwise, sort_fn should be a function that accepts an IIntrospectable and returns a value from it (ala the ``key`` function of Python's ``sorted`` callable).""" diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index a65032143..e80557096 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -662,10 +662,10 @@ pyramid.tests.test_config.dummy_include2""", after = config.action_state actions = after.actions self.assertEqual(len(actions), 1) - self.assertEqual( - after.actions[0][:3], - ('discrim', None, test_config), - ) + action = after.actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_include_with_python_callable(self): from pyramid.tests import test_config @@ -674,10 +674,10 @@ pyramid.tests.test_config.dummy_include2""", after = config.action_state actions = after.actions self.assertEqual(len(actions), 1) - self.assertEqual( - actions[0][:3], - ('discrim', None, test_config), - ) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_include_with_module_defaults_to_includeme(self): from pyramid.tests import test_config @@ -686,10 +686,10 @@ pyramid.tests.test_config.dummy_include2""", after = config.action_state actions = after.actions self.assertEqual(len(actions), 1) - self.assertEqual( - actions[0][:3], - ('discrim', None, test_config), - ) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_include_with_route_prefix(self): root_config = self._makeOne(autocommit=True) @@ -749,9 +749,15 @@ pyramid.tests.test_config.dummy_include2""", config.action('discrim', kw={'a':1}) self.assertEqual( state.actions, - [(('discrim', None, (), {'a': 1}, 0), - {'info': 'abc', 'includepath':(), 'introspectables':()})] - ) + [((), + {'args': (), + 'callable': None, + 'discriminator': 'discrim', + 'includepath': (), + 'info': 'abc', + 'introspectables': (), + 'kw': {'a': 1}, + 'order': None})]) def test_action_branching_nonautocommit_without_config_info(self): config = self._makeOne(autocommit=False) @@ -763,9 +769,15 @@ pyramid.tests.test_config.dummy_include2""", config.action('discrim', kw={'a':1}) self.assertEqual( state.actions, - [(('discrim', None, (), {'a': 1}, 0), - {'info': 'z', 'includepath':(), 'introspectables':()})] - ) + [((), + {'args': (), + 'callable': None, + 'discriminator': 'discrim', + 'includepath': (), + 'info': 'z', + 'introspectables': (), + 'kw': {'a': 1}, + 'order': None})]) def test_scan_integration(self): from zope.interface import alsoProvides @@ -1313,10 +1325,10 @@ class TestConfigurator_add_directive(unittest.TestCase): self.assertTrue(hasattr(config, 'dummy_extend')) config.dummy_extend('discrim') after = config.action_state - self.assertEqual( - after.actions[-1][:3], - ('discrim', None, test_config), - ) + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_extend_with_python_callable(self): from pyramid.tests import test_config @@ -1326,10 +1338,10 @@ class TestConfigurator_add_directive(unittest.TestCase): self.assertTrue(hasattr(config, 'dummy_extend')) config.dummy_extend('discrim') after = config.action_state - self.assertEqual( - after.actions[-1][:3], - ('discrim', None, test_config), - ) + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) def test_extend_same_name_doesnt_conflict(self): config = self.config @@ -1340,10 +1352,10 @@ class TestConfigurator_add_directive(unittest.TestCase): self.assertTrue(hasattr(config, 'dummy_extend')) config.dummy_extend('discrim') after = config.action_state - self.assertEqual( - after.actions[-1][:3], - ('discrim', None, config.registry), - ) + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], config.registry) def test_extend_action_method_successful(self): config = self.config @@ -1361,10 +1373,10 @@ class TestConfigurator_add_directive(unittest.TestCase): after = config2.action_state actions = after.actions self.assertEqual(len(actions), 1) - self.assertEqual( - after.actions[0][:3], - ('discrim', None, config2.package), - ) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], config2.package) class TestActionState(unittest.TestCase): def _makeOne(self): @@ -1380,32 +1392,94 @@ class TestActionState(unittest.TestCase): c = self._makeOne() c.actions = [] c.action(1, f, (1,), {'x':1}) - self.assertEqual(c.actions, [(1, f, (1,), {'x': 1})]) + self.assertEqual( + c.actions, + [{'args': (1,), + 'callable': f, + 'discriminator': 1, + 'includepath': (), + 'info': '', + 'introspectables': (), + 'kw': {'x': 1}, + 'order': None}]) c.action(None) - self.assertEqual(c.actions, [(1, f, (1,), {'x': 1}), (None, None)]) + self.assertEqual( + c.actions, + [{'args': (1,), + 'callable': f, + 'discriminator': 1, + 'includepath': (), + 'info': '', + 'introspectables': (), + 'kw': {'x': 1}, + 'order': None}, + + {'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': '', + 'introspectables': (), + 'kw': {}, + 'order': None},]) def test_action_with_includepath(self): c = self._makeOne() c.actions = [] c.action(None, includepath=('abc',)) - self.assertEqual(c.actions, [(None, None, (), {}, ('abc',))]) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': ('abc',), + 'info': '', + 'introspectables': (), + 'kw': {}, + 'order': None}]) def test_action_with_info(self): c = self._makeOne() c.action(None, info='abc') - self.assertEqual(c.actions, [(None, None, (), {}, (), 'abc')]) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': 'abc', + 'introspectables': (), + 'kw': {}, + 'order': None}]) def test_action_with_includepath_and_info(self): c = self._makeOne() c.action(None, includepath=('spec',), info='bleh') - self.assertEqual(c.actions, - [(None, None, (), {}, ('spec',), 'bleh')]) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': ('spec',), + 'info': 'bleh', + 'introspectables': (), + 'kw': {}, + 'order': None}]) def test_action_with_order(self): c = self._makeOne() c.actions = [] c.action(None, order=99999) - self.assertEqual(c.actions, [(None, None, (), {}, (), '', 99999)]) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': '', + 'introspectables': (), + 'kw': {}, + 'order': 99999}]) def test_processSpec(self): c = self._makeOne() @@ -1458,12 +1532,54 @@ class Test_resolveConflicts(unittest.TestCase): (3, f, (3,), {}, ('y',)), (None, f, (5,), {}, ('y',)), ]) - self.assertEqual(result, - [(None, f), - (1, f, (1,), {}, (), 'first'), - (3, f, (3,), {}, ('y',)), - (None, f, (5,), {}, ('y',)), - (4, f, (4,), {}, ('y',), 'should be last')]) + self.assertEqual( + result, + [{'info': '', + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 1}, + + {'info': '', + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': ('y',), + 'order': 5}, + + {'info': '', + 'args': (5,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': ('y',), + 'order': 6}, + + {'info': 'should be last', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': ('y',), + 'order': 99999} + ] + ) def test_it_conflict(self): from pyramid.tests.test_config import dummyfactory as f diff --git a/pyramid/tests/test_config/test_routes.py b/pyramid/tests/test_config/test_routes.py index 1646561cd..140a4aa73 100644 --- a/pyramid/tests/test_config/test_routes.py +++ b/pyramid/tests/test_config/test_routes.py @@ -52,7 +52,8 @@ class RoutesConfiguratorMixinTests(unittest.TestCase): def test_add_route_discriminator(self): config = self._makeOne() config.add_route('name', 'path') - self.assertEqual(config.action_state.actions[-1][0], ('route', 'name')) + self.assertEqual(config.action_state.actions[-1]['discriminator'], + ('route', 'name')) def test_add_route_with_factory(self): config = self._makeOne(autocommit=True) -- cgit v1.2.3