diff options
| -rw-r--r-- | TODO.txt | 18 | ||||
| -rw-r--r-- | pyramid/config/__init__.py | 223 | ||||
| -rw-r--r-- | pyramid/config/adapters.py | 18 | ||||
| -rw-r--r-- | pyramid/config/assets.py | 13 | ||||
| -rw-r--r-- | pyramid/config/factories.py | 27 | ||||
| -rw-r--r-- | pyramid/config/i18n.py | 55 | ||||
| -rw-r--r-- | pyramid/config/introspection.py | 152 | ||||
| -rw-r--r-- | pyramid/config/rendering.py | 12 | ||||
| -rw-r--r-- | pyramid/config/routes.py | 21 | ||||
| -rw-r--r-- | pyramid/config/security.py | 24 | ||||
| -rw-r--r-- | pyramid/config/views.py | 62 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 149 | ||||
| -rw-r--r-- | pyramid/registry.py | 18 | ||||
| -rw-r--r-- | pyramid/router.py | 2 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_init.py | 210 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_routes.py | 3 | ||||
| -rw-r--r-- | pyramid/util.py | 67 |
17 files changed, 866 insertions, 208 deletions
@@ -6,6 +6,24 @@ Must-Have - Fix SQLA tutorial to match ZODB tutorial. +- Fix SQLA tutorial to match alchemy scaffold. + +- Introspection: + + * More specific filename/lineno info instead of opaque string (or a way to + parse the opaque string into filename/lineno info). + + * categorize() return value ordering not right yet. + + * implement ptweens and proutes based on introspection instead of current + state of affairs. + + * introspection hiding for directives? + + * usage docs. + + * make it possible to disuse introspection? + Nice-to-Have ------------ diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 67269954c..b0c86f0cd 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 @@ -11,6 +12,7 @@ from webob.exc import WSGIHTTPException as WebobWSGIHTTPException from pyramid.interfaces import ( IDebugLogger, IExceptionResponse, + IIntrospector, ) from pyramid.asset import resolve_asset_spec @@ -50,6 +52,7 @@ from pyramid.threadlocal import manager from pyramid.util import ( DottedNameResolver, WeakOrderedSet, + object_description, ) from pyramid.config.adapters import AdaptersConfiguratorMixin @@ -66,6 +69,7 @@ from pyramid.config.tweens import TweensConfiguratorMixin from pyramid.config.util import action_method from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin +from pyramid.config.introspection import IntrospectionConfiguratorMixin empty = text_('') @@ -84,6 +88,7 @@ class Configurator( SettingsConfiguratorMixin, FactoriesConfiguratorMixin, AdaptersConfiguratorMixin, + IntrospectionConfiguratorMixin, ): """ A Configurator is used to configure a :app:`Pyramid` @@ -234,6 +239,7 @@ class Configurator( basepath = None includepath = () info = '' + object_description = staticmethod(object_description) def __init__(self, registry=None, @@ -436,9 +442,37 @@ class Configurator( info=info, event=event) _registry.registerSelfAdapter = registerSelfAdapter + if not hasattr(_registry, 'introspector'): + def _get_introspector(reg): + return reg.queryUtility(IIntrospector) + + def _set_introspector(reg, introspector): + reg.registerUtility(introspector, IIntrospector) + + def _del_introspector(reg): + reg.unregisterUtility(IIntrospector) + + introspector = property(_get_introspector, _set_introspector, + _del_introspector) + + _registry.__class__.introspector = introspector + + # API - def action(self, discriminator, callable=None, args=(), kw=None, order=0): + @property + def action_info(self): + info = self.info # usually a ZCML action if self.info has data + if not info: + # Try to provide more accurate info for conflict reports + if self._ainfo: + info = self._ainfo[0] + else: + info = '' + return info + + 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 immediately if ``autocommit`` is ``True``). @@ -463,27 +497,26 @@ class Configurator( kw = {} autocommit = self.autocommit + action_info = self.action_info + introspector = self.introspector if autocommit: if callable is not None: callable(*args, **kw) + for introspectable in introspectables: + if introspector is not None: + introspector.register(introspectable, action_info) else: - info = self.info # usually a ZCML action if self.info has data - if not info: - # Try to provide more accurate info for conflict reports - if self._ainfo: - info = self._ainfo[0] - else: - info = '' self.action_state.action( - discriminator, - callable, - args, - kw, - order, - info=info, + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + order=order, + info=action_info, includepath=self.includepath, + introspectables=introspectables, ) def _get_action_state(self): @@ -509,7 +542,7 @@ class Configurator( of this error will be information about the source of the conflict, usually including file names and line numbers of the cause of the configuration conflicts.""" - self.action_state.execute_actions() + self.action_state.execute_actions(introspector=self.introspector) self.action_state = ActionState() # old actions have been processed def include(self, callable, route_prefix=None): @@ -861,23 +894,25 @@ class ActionState(object): self._seen_files.add(spec) return True - def action(self, discriminator, callable=None, args=(), kw=None, order=0, - includepath=(), info=''): + 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) - # 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): + def execute_actions(self, clear=True, introspector=None): """Execute the configuration actions This calls the action callables after resolving conflicts @@ -926,9 +961,14 @@ class ActionState(object): """ + try: for action in resolveConflicts(self.actions): - _, callable, args, kw, _, info, _ = expand_action(*action) + callable = action['callable'] + args = action['args'] + kw = action['kw'] + info = action['info'] + introspectables = action['introspectables'] if callable is None: continue try: @@ -943,6 +983,10 @@ class ActionState(object): tb) finally: del t, v, tb + if introspector is not None: + for introspectable in introspectables: + introspector.register(introspectable, info) + finally: if clear: del self.actions[:] @@ -952,120 +996,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 - ) = 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) - ) + # 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) - ) + 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) = dups[0] - output.append( - (i, discriminator, callable, args, kw, basepath, baseinfo) - ) - for includepath, i, callable, args, kw, info 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) + output.sort(key=operator.itemgetter('order')) + return output - return r - -# this function is licensed under the ZPL (stolen from Zope) def expand_action(discriminator, callable=None, args=(), kw=None, - includepath=(), info='', order=0): + includepath=(), info='', order=None, introspectables=()): if kw is None: kw = {} - return (discriminator, callable, args, kw, includepath, info, order) + 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/adapters.py b/pyramid/config/adapters.py index f022e7f08..04571bec3 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -27,7 +27,13 @@ class AdaptersConfiguratorMixin(object): iface = (iface,) def register(): self.registry.registerHandler(subscriber, iface) - self.action(None, register) + intr = self.introspectable('subscribers', + id(subscriber), + self.object_description(subscriber), + 'subscriber') + intr['subscriber'] = subscriber + intr['interfaces'] = iface + self.action(None, register, introspectables=(intr,)) return subscriber @action_method @@ -52,7 +58,15 @@ class AdaptersConfiguratorMixin(object): reg.registerSelfAdapter((type_or_iface,), IResponse) else: reg.registerAdapter(adapter, (type_or_iface,), IResponse) - self.action((IResponse, type_or_iface), register) + discriminator = (IResponse, type_or_iface) + intr = self.introspectable( + 'response adapters', + discriminator, + self.object_description(adapter), + 'response adapter') + intr['adapter'] = adapter + intr['type'] = type_or_iface + self.action(discriminator, register, introspectables=(intr,)) def _register_response_adapters(self): # cope with WebOb response objects that aren't decorated with IResponse diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index 08cc6dc38..7080e5e7c 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -236,7 +236,18 @@ class AssetsConfiguratorMixin(object): to_package = sys.modules[override_package] override(from_package, path, to_package, override_prefix) - self.action(None, register) + intr = self.introspectable( + 'asset overrides', + (package, override_package, path, override_prefix), + '%s/%s -> %s/%s' % (package, path, override_package, + override_prefix), + 'asset override', + ) + intr['package'] = package + intr['override_package'] = package + intr['override_prefix'] = override_prefix + intr['path'] = path + self.action(None, register, introspectables=(intr,)) override_resource = override_asset # bw compat diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index a5a797a47..cfa91d6d9 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -28,15 +28,20 @@ class FactoriesConfiguratorMixin(object): def register(): self.registry.registerUtility(factory, IRootFactory) self.registry.registerUtility(factory, IDefaultRootFactory) # b/c - self.action(IRootFactory, register) + + intr = self.introspectable('root factories', None, + self.object_description(factory), + 'root factory') + intr['factory'] = factory + self.action(IRootFactory, register, introspectables=(intr,)) _set_root_factory = set_root_factory # bw compat @action_method - def set_session_factory(self, session_factory): + def set_session_factory(self, factory): """ Configure the application with a :term:`session factory`. If this - method is called, the ``session_factory`` argument must be a session + method is called, the ``factory`` argument must be a session factory callable or a :term:`dotted Python name` to that factory. .. note:: @@ -45,10 +50,14 @@ class FactoriesConfiguratorMixin(object): :class:`pyramid.config.Configurator` constructor can be used to achieve the same purpose. """ - session_factory = self.maybe_dotted(session_factory) + factory = self.maybe_dotted(factory) def register(): - self.registry.registerUtility(session_factory, ISessionFactory) - self.action(ISessionFactory, register) + self.registry.registerUtility(factory, ISessionFactory) + intr = self.introspectable('session factory', None, + self.object_description(factory), + 'session factory') + intr['factory'] = factory + self.action(ISessionFactory, register, introspectables=(intr,)) @action_method def set_request_factory(self, factory): @@ -69,5 +78,9 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, IRequestFactory) - self.action(IRequestFactory, register) + intr = self.introspectable('request factory', None, + self.object_description(factory), + 'request factory') + intr['factory'] = factory + self.action(IRequestFactory, register, introspectables=(intr,)) diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index 34df1bb47..2c149923c 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -40,7 +40,10 @@ class I18NConfiguratorMixin(object): """ def register(): self._set_locale_negotiator(negotiator) - self.action(ILocaleNegotiator, register) + intr = self.introspectable('locale negotiator', None, repr(negotiator), + 'locale negotiator') + intr['negotiator'] = negotiator + self.action(ILocaleNegotiator, register, introspectables=(intr,)) def _set_locale_negotiator(self, negotiator): locale_negotiator = self.maybe_dotted(negotiator) @@ -71,8 +74,10 @@ class I18NConfiguratorMixin(object): in the order they're provided in the ``*specs`` list argument (items earlier in the list trump ones later in the list). """ - for spec in specs[::-1]: # reversed + directories = [] + introspectables = [] + for spec in specs[::-1]: # reversed package_name, filename = self._split_spec(spec) if package_name is None: # absolute filename directory = filename @@ -82,25 +87,35 @@ class I18NConfiguratorMixin(object): directory = os.path.join(package_path(package), filename) if not os.path.isdir(os.path.realpath(directory)): - raise ConfigurationError('"%s" is not a directory' % directory) - - tdirs = self.registry.queryUtility(ITranslationDirectories) - if tdirs is None: - tdirs = [] - self.registry.registerUtility(tdirs, ITranslationDirectories) - - tdirs.insert(0, directory) - # XXX no action? + raise ConfigurationError('"%s" is not a directory' % + directory) + intr = self.introspectable('translation directories', directory, + spec, 'translation directory') + intr['directory'] = directory + introspectables.append(intr) + directories.append(directory) - if specs: - - # We actually only need an IChameleonTranslate function - # utility to be registered zero or one times. We register the - # same function once for each added translation directory, - # which does too much work, but has the same effect. - - ctranslate = ChameleonTranslate(translator) - self.registry.registerUtility(ctranslate, IChameleonTranslate) + def register(): + for directory in directories: + + tdirs = self.registry.queryUtility(ITranslationDirectories) + if tdirs is None: + tdirs = [] + self.registry.registerUtility(tdirs, + ITranslationDirectories) + + tdirs.insert(0, directory) + # XXX no action? + + if directories: + # We actually only need an IChameleonTranslate function + # utility to be registered zero or one times. We register the + # same function once for each added translation directory, + # which does too much work, but has the same effect. + ctranslate = ChameleonTranslate(translator) + self.registry.registerUtility(ctranslate, IChameleonTranslate) + + self.action(None, register, introspectables=introspectables) def translator(msg): request = get_current_request() diff --git a/pyramid/config/introspection.py b/pyramid/config/introspection.py new file mode 100644 index 000000000..666728fc5 --- /dev/null +++ b/pyramid/config/introspection.py @@ -0,0 +1,152 @@ +import operator + +from zope.interface import implementer + +from pyramid.interfaces import ( + IIntrospectable, + IIntrospector + ) + +@implementer(IIntrospector) +class Introspector(object): + def __init__(self): + self._refs = {} + self._categories = {} + self._counter = 0 + + def add(self, intr): + category = self._categories.setdefault(intr.category_name, {}) + category[intr.discriminator] = intr + category[intr.discriminator_hash] = intr + intr.order = self._counter + self._counter += 1 + + def get(self, category_name, discriminator, default=None): + category = self._categories.setdefault(category_name, {}) + intr = category.get(discriminator, default) + return intr + + 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() + values = sorted(set(values), key=sort_fn) + return [{'introspectable':intr, 'related':self.related(intr)} for + intr in values] + + def categories(self): + return sorted(self._categories.keys()) + + def categorized(self, sort_fn=None): + L = [] + for category_name in self.categories(): + 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) + if intr is None: + return + L = self._refs.pop((category_name, discriminator), []) + for d in L: + L2 = self._refs[d] + L2.remove((category_name, discriminator)) + category = self._categories[intr.category_name] + del category[intr.discriminator] + del category[intr.discriminator_hash] + + def _get_intrs_by_pairs(self, pairs): + introspectables = [] + for pair in pairs: + category_name, discriminator = pair + intr = self._categories.get(category_name, {}).get(discriminator) + if intr is None: + raise KeyError((category_name, discriminator)) + introspectables.append(intr) + return introspectables + + def relate(self, *pairs): + introspectables = self._get_intrs_by_pairs(pairs) + relatable = ((x,y) for x in introspectables for y in introspectables) + for x, y in relatable: + L = self._refs.setdefault(x, []) + if x is not y and y not in L: + L.append(y) + + def unrelate(self, *pairs): + introspectables = self._get_intrs_by_pairs(pairs) + relatable = ((x,y) for x in introspectables for y in introspectables) + for x, y in relatable: + L = self._refs.get(x, []) + if y in L: + L.remove(y) + + def related(self, intr): + category_name, discriminator = intr.category_name, intr.discriminator + intr = self._categories.get(category_name, {}).get(discriminator) + if intr is None: + raise KeyError((category_name, discriminator)) + return self._refs.get(intr, []) + + def register(self, introspectable, action_info=''): + introspectable.action_info = action_info + self.add(introspectable) + for category_name, discriminator in introspectable.relations: + self.relate(( + introspectable.category_name, introspectable.discriminator), + (category_name, discriminator)) + + for category_name, discriminator in introspectable.unrelations: + self.unrelate(( + introspectable.category_name, introspectable.discriminator), + (category_name, discriminator)) + +@implementer(IIntrospectable) +class Introspectable(dict): + + order = 0 # mutated by introspector.add/introspector.add_intr + action_info = '' # mutated by introspector.register + + def __init__(self, category_name, discriminator, title, type_name): + self.category_name = category_name + self.discriminator = discriminator + self.title = title + self.type_name = type_name + self.relations = [] + self.unrelations = [] + + def relate(self, category_name, discriminator): + self.relations.append((category_name, discriminator)) + + def unrelate(self, category_name, discriminator): + self.unrelations.append((category_name, discriminator)) + + @property + def discriminator_hash(self): + return hash(self.discriminator) + + def __hash__(self): + return hash((self.category_name,) + (self.discriminator,)) + + def __repr__(self): + 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 + + @property + def introspector(self): + introspector = getattr(self.registry, 'introspector', None) + if introspector is None: + introspector = Introspector() + self.registry.introspector = introspector + return introspector + diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py index a18a9b196..0d37e201f 100644 --- a/pyramid/config/rendering.py +++ b/pyramid/config/rendering.py @@ -48,9 +48,15 @@ class RenderingConfiguratorMixin(object): name = '' def register(): self.registry.registerUtility(factory, IRendererFactory, name=name) + intr = self.introspectable('renderer factories', name, + self.object_description(factory), + 'renderer factory') + intr['factory'] = factory + intr['name'] = name # we need to register renderers early (in phase 1) because they are # used during view configuration (which happens in phase 3) - self.action((IRendererFactory, name), register, order=PHASE1_CONFIG) + self.action((IRendererFactory, name), register, order=PHASE1_CONFIG, + introspectables=(intr,)) @action_method def set_renderer_globals_factory(self, factory, warn=True): @@ -88,4 +94,8 @@ class RenderingConfiguratorMixin(object): factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, IRendererGlobalsFactory) + intr = self.introspectable('renderer globals factory', None, + self.object_description(factory), + 'renderer globals factory') + intr['factory'] = factory self.action(IRendererGlobalsFactory, register) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index e190e56ee..ab62d4c75 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -369,6 +369,16 @@ class RoutesConfiguratorMixin(object): mapper = self.get_routes_mapper() + intr = self.introspectable('routes', name, + '%s (%s)' % (name, pattern), 'route') + intr['name'] = name + intr['pattern'] = pattern + intr['factory'] = factory + intr['predicates'] = predicates + intr['pregenerator'] = pregenerator + intr['static'] = static + intr['use_global_views'] = use_global_views + def register_route_request_iface(): request_iface = self.registry.queryUtility(IRouteRequest, name=name) if request_iface is None: @@ -381,9 +391,12 @@ class RoutesConfiguratorMixin(object): request_iface, IRouteRequest, name=name) def register_connect(): - return mapper.connect(name, pattern, factory, predicates=predicates, - pregenerator=pregenerator, static=static) - + route = mapper.connect( + name, pattern, factory, predicates=predicates, + pregenerator=pregenerator, static=static + ) + intr['object'] = route + return route # We have to connect routes in the order they were provided; # we can't use a phase to do that, because when the actions are @@ -393,7 +406,7 @@ class RoutesConfiguratorMixin(object): # But IRouteRequest interfaces must be registered before we begin to # process view registrations (in phase 3) self.action(('route', name), register_route_request_iface, - order=PHASE2_CONFIG) + order=PHASE2_CONFIG, introspectables=(intr,)) # deprecated adding views from add_route; must come after # route registration for purposes of autocommit ordering diff --git a/pyramid/config/security.py b/pyramid/config/security.py index 23cd5f27f..1830fb900 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -31,8 +31,13 @@ class SecurityConfiguratorMixin(object): 'Cannot configure an authentication policy without ' 'also configuring an authorization policy ' '(use the set_authorization_policy method)') + intr = self.introspectable('authentication policy', None, + self.object_description(policy), + 'authentication policy') + intr['policy'] = policy # authentication policy used by view config (phase 3) - self.action(IAuthenticationPolicy, register, order=PHASE2_CONFIG) + self.action(IAuthenticationPolicy, register, order=PHASE2_CONFIG, + introspectables=(intr,)) def _set_authentication_policy(self, policy): policy = self.maybe_dotted(policy) @@ -62,6 +67,10 @@ class SecurityConfiguratorMixin(object): 'also configuring an authentication policy ' '(use the set_authorization_policy method)') + intr = self.introspectable('authorization policy', None, + self.object_description(policy), + 'authorization policy') + intr['policy'] = policy # authorization policy used by view config (phase 3) and # authentication policy (phase 2) self.action(IAuthorizationPolicy, register, order=PHASE1_CONFIG) @@ -110,9 +119,18 @@ class SecurityConfiguratorMixin(object): :class:`pyramid.config.Configurator` constructor can be used to achieve the same purpose. """ - # default permission used during view registration (phase 3) def register(): self.registry.registerUtility(permission, IDefaultPermission) - self.action(IDefaultPermission, register, order=PHASE1_CONFIG) + intr = self.introspectable('default permission', + None, + permission, + 'default permission') + intr['value'] = permission + perm_intr = self.introspectable('permissions', permission, + permission, 'permission') + perm_intr['value'] = permission + # default permission used during view registration (phase 3) + self.action(IDefaultPermission, register, order=PHASE1_CONFIG, + introspectables=(intr, perm_intr,)) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 2a6157ffb..16c0a8253 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -941,6 +941,42 @@ class ViewsConfiguratorMixin(object): name=renderer, package=self.package, registry = self.registry) + introspectables = [] + discriminator = [ + 'view', context, name, request_type, IView, containment, + request_param, request_method, route_name, attr, + xhr, accept, header, path_info, match_param] + discriminator.extend(sorted([hash(x) for x in custom_predicates])) + discriminator = tuple(discriminator) + if inspect.isclass(view) and attr: + view_desc = 'method %r of %s' % ( + attr, self.object_description(view)) + else: + view_desc = self.object_description(view) + view_intr = self.introspectable('views', + discriminator, + view_desc, + 'view') + view_intr.update( + dict(name=name, + context=context, + containment=containment, + request_param=request_param, + request_method=request_method, + route_name=route_name, + attr=attr, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + match_param=match_param, + callable=view, + mapper=mapper, + decorator=decorator, + ) + ) + introspectables.append(view_intr) + def register(permission=permission, renderer=renderer): request_iface = IRequest if route_name is not None: @@ -982,6 +1018,7 @@ class ViewsConfiguratorMixin(object): decorator=decorator, http_cache=http_cache) derived_view = deriver(view) + view_intr['derived_callable'] = derived_view registered = self.registry.adapters.registered @@ -1079,13 +1116,24 @@ class ViewsConfiguratorMixin(object): (IExceptionViewClassifier, request_iface, context), IMultiView, name=name) - discriminator = [ - 'view', context, name, request_type, IView, containment, - request_param, request_method, route_name, attr, - xhr, accept, header, path_info, match_param] - discriminator.extend(sorted([hash(x) for x in custom_predicates])) - discriminator = tuple(discriminator) - self.action(discriminator, register) + if route_name: + view_intr.relate('routes', route_name) # see add_route + if renderer is not None and renderer.name and '.' in renderer.name: + tmpl_intr = self.introspectable('templates', discriminator, + renderer.name, 'template') + tmpl_intr.relate('views', discriminator) + tmpl_intr['name'] = renderer.name + tmpl_intr['type'] = renderer.type + tmpl_intr['renderer'] = renderer + tmpl_intr.relate('renderer factories', renderer.type) + introspectables.append(tmpl_intr) + if permission is not None: + perm_intr = self.introspectable('permissions', permission, + permission, 'permission') + perm_intr['value'] = permission + perm_intr.relate('views', discriminator) + introspectables.append(perm_intr) + self.action(discriminator, register, introspectables=introspectables) def derive_view(self, view, attr=None, renderer=None): """ diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 5e7137345..475b52313 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -859,9 +859,158 @@ class IRendererInfo(Interface): 'to the current application') +class IIntrospector(Interface): + def get(category_name, discriminator, default=None): + """ Get the IIntrospectable related to the category_name and the + discriminator (or discriminator hash) ``discriminator``. If it does + not exist in the introspector, return the value of ``default`` """ + + 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 categories(): + """ Return a sorted sequence of category names known by + this introspector """ + + 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).""" + + def remove(category_name, discriminator): + """ Remove the IIntrospectable related to ``category_name`` and + ``discriminator`` from the introspector, and fix up any relations + that the introspectable participates in. This method will not raise + an error if an introspectable related to the category name and + discriminator does not exist.""" + + def related(category_name, discriminator): + """ Return a sequence of IIntrospectables related to the + IIntrospectable associated with (``category_name``, + ``discriminator``). Return the empty sequence if no relations for + exist.""" + + def register(introspectable, action_info=''): + """ Register an IIntrospectable with this introspector. This method + is invoked during action execution. Adds the introspectable and its + relations to the introspector. ``introspectable`` should be an + object implementing IIntrospectable. ``action_info`` should be a + string representing the call that registered this introspectable + (e.g. with line numbers, etc). Pseudocode for an implementation of + this method: + + .. code-block:: python + + def register(self, introspectable, action_info=''): + i = introspectable + i.action_info = action_info + self.add(introspectable) + for category_name, discriminator in i.relations: + self.relate(( + i.category_name, i.discriminator), + (category_name, discriminator)) + + for category_name, discriminator in i.unrelations: + self.unrelate(( + i.category_name, i.discriminator), + (category_name, discriminator)) + + The introspectable you wished to be related to or unrelated from must + have already been added via + :meth:`pyramid.interfaces.IIntrospector.add` (or by this method, + which implies an add) before this method is called; a :exc:`KeyError` + will result if this is not true. + """ + + def add(intr): + """ Add the IIntrospectable ``intr`` (use instead of + :meth:`pyramid.interfaces.IIntrospector.add` when you have a custom + IIntrospectable). Replaces any existing introspectable registered + using the same category/discriminator. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register`""" + + def relate(*pairs): + """ Given any number of of ``(category_name, discriminator)`` pairs + passed as positional arguments, relate the associated introspectables + to each other. The introspectable related to each pair must have + already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError` + will result if this is not true. An error will not be raised if any + pair has already been associated with another. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register` + """ + + def unrelate(*pairs): + """ Given any number of of ``(category_name, discriminator)`` pairs + passed as positional arguments, unrelate the associated introspectables + from each other. The introspectable related to each pair must have + already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError` + will result if this is not true. An error will not be raised if any + pair is not already related to another. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register` + """ + + +class IIntrospectable(Interface): + """ An introspectable object used for configuration introspection. In + addition to the methods below, objects which implement this interface + must also implement all the methods of Python's + ``collections.MutableMapping`` (the "dictionary interface").""" + + title = Attribute('Text title describing this introspectable') + type_name = Attribute('Text type name describing this introspectable') + order = Attribute('integer order in which registered with introspector ' + '(managed by introspector, usually') + category_name = Attribute('introspection category name') + discriminator = Attribute('introspectable discriminator (within category) ' + '(must be hashable)') + discriminator_hash = Attribute('an integer hash of the discriminator') + relations = Attribute('A sequence of ``(category_name, discriminator)`` ' + 'pairs indicating the relations that this ' + 'introspectable wants to establish when registered ' + 'with the introspector') + unrelations = Attribute('A sequence of ``(category_name, discriminator)`` ' + 'pairs indicating the relations that this ' + 'introspectable wants to break when registered ' + 'with the introspector') + action_info = Attribute('A string representing the caller that invoked ' + 'the creation of this introspectable (usually ' + 'managed by IIntrospector during registration)') + + def relate(category_name, discriminator): + """ Indicate an intent to relate this IIntrospectable with another + IIntrospectable (the one associated with the ``category_name`` and + ``discriminator``) during action execution. + """ + + def unrelate(category_name, discriminator): + """ Indicate an intent to break the relationship between this + IIntrospectable with another IIntrospectable (the one associated with + the ``category_name`` and ``discriminator``) during action execution. + """ + # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. PHASE1_CONFIG = -20 PHASE2_CONFIG = -10 + diff --git a/pyramid/registry.py b/pyramid/registry.py index ac706595e..99f5ee843 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -1,7 +1,10 @@ from zope.interface.registry import Components from pyramid.compat import text_ -from pyramid.interfaces import ISettings +from pyramid.interfaces import ( + ISettings, + IIntrospector + ) empty = text_('') @@ -26,6 +29,7 @@ class Registry(Components, dict): # for optimization purposes, if no listeners are listening, don't try # to notify them has_listeners = False + _settings = None def __nonzero__(self): @@ -74,4 +78,16 @@ class Registry(Components, dict): settings = property(_get_settings, _set_settings) + def _get_introspector(self): + return self.queryUtility(IIntrospector) + + def _set_introspector(self, introspector): + self.registerUtility(introspector, IIntrospector) + + def _del_introspector(self): + self.unregisterUtility(IIntrospector) + + introspector = property(_get_introspector, _set_introspector, + _del_introspector) + global_registry = Registry('global') diff --git a/pyramid/router.py b/pyramid/router.py index 608a66756..0c115a1ac 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -189,7 +189,7 @@ class Router(object): if request.response_callbacks: request._process_response_callbacks(response) - + return response(request.environ, start_response) finally: diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index ca1508295..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':()})] - ) + [((), + {'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':()})] - ) + [((), + {'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) diff --git a/pyramid/util.py b/pyramid/util.py index 3eb4b3fed..ee909c316 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,5 +1,7 @@ +import inspect import pkg_resources import sys +import types import weakref from pyramid.compat import string_types @@ -228,3 +230,68 @@ def strings_differ(string1, string2): return invalid_bits != 0 +def object_description(object): + """ Produce a human-consumable string description of ``object``, usually + involving a Python dotted name. For example: + + .. code-block:: python + + >>> object_description(None) + 'None' + >>> from xml.dom import minidom + >>> object_description(minidom) + 'module xml.dom.minidom' + >>> object_description(minidom.Attr) + 'class xml.dom.minidom.Attr' + >>> object_description(minidom.Attr.appendChild) + 'method appendChild of class xml.dom.minidom.Attr' + >>> + + If this method cannot identify the type of the object, a generic + description ala ``object <object.__name__>`` will be returned. + + If the object passed is already a string, it is simply returned. If it + is a boolean, an integer, a list, a tuple, a set, or ``None``, a + (possibly shortened) string representation is returned. + """ + if isinstance(object, string_types): + return object + if isinstance(object, (bool, int, float, long, types.NoneType)): + return str(object) + if isinstance(object, (tuple, set)): + return shortrepr(object, ')') + if isinstance(object, list): + return shortrepr(object, ']') + if isinstance(object, dict): + return shortrepr(object, '}') + module = inspect.getmodule(object) + if module is None: + return 'object %s' % str(object) + modulename = module.__name__ + if inspect.ismodule(object): + return 'module %s' % modulename + if inspect.ismethod(object): + oself = getattr(object, '__self__', None) + if oself is None: + oself = getattr(object, 'im_self', None) + return 'method %s of class %s.%s' % (object.__name__, modulename, + oself.__class__.__name__) + + if inspect.isclass(object): + dottedname = '%s.%s' % (modulename, object.__name__) + return 'class %s' % dottedname + if inspect.isfunction(object): + dottedname = '%s.%s' % (modulename, object.__name__) + return 'function %s' % dottedname + if inspect.isbuiltin(object): + dottedname = '%s.%s' % (modulename, object.__name__) + return 'builtin %s' % dottedname + if hasattr(object, '__name__'): + return 'object %s' % object.__name__ + return 'object %s' % str(object) + +def shortrepr(object, closer): + r = str(object) + if len(r) > 100: + r = r[:100] + '... %s' % closer + return r |
