From 3b5ccb38eb98e05604a5c19d89e8c77fb6104429 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 23 Nov 2011 20:45:10 -0500 Subject: first cut at introspection feature --- pyramid/config/__init__.py | 88 +++++++++++++------ pyramid/config/adapters.py | 11 ++- pyramid/config/assets.py | 10 ++- pyramid/config/factories.py | 13 ++- pyramid/config/i18n.py | 53 +++++++----- pyramid/config/introspection.py | 151 +++++++++++++++++++++++++++++++++ pyramid/config/routes.py | 20 ++++- pyramid/config/tweens.py | 1 - pyramid/config/views.py | 15 +++- pyramid/interfaces.py | 86 +++++++++++++++++++ pyramid/registry.py | 18 +++- pyramid/tests/test_config/test_init.py | 4 +- 12 files changed, 411 insertions(+), 59 deletions(-) create mode 100644 pyramid/config/introspection.py diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 40c3c037b..951f9b7ab 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -8,8 +8,11 @@ import venusian from webob.exc import WSGIHTTPException as WebobWSGIHTTPException -from pyramid.interfaces import IDebugLogger -from pyramid.interfaces import IExceptionResponse +from pyramid.interfaces import ( + IDebugLogger, + IExceptionResponse, + IIntrospector, + ) from pyramid.asset import resolve_asset_spec from pyramid.authorization import ACLAuthorizationPolicy @@ -45,6 +48,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_('') @@ -63,6 +67,7 @@ class Configurator( SettingsConfiguratorMixin, FactoriesConfiguratorMixin, AdaptersConfiguratorMixin, + IntrospectionConfiguratorMixin, ): """ A Configurator is used to configure a :app:`Pyramid` @@ -415,9 +420,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=0, + introspectables=()): """ Register an action which will be executed when :meth:`pyramid.config.Configurator.commit` is called (or executed immediately if ``autocommit`` is ``True``). @@ -442,27 +475,24 @@ class Configurator( kw = {} autocommit = self.autocommit + action_info = self.action_info if autocommit: if callable is not None: callable(*args, **kw) + for introspectable in introspectables: + introspectable(self.introspector, 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, + info=action_info, includepath=self.includepath, + introspectables=introspectables, ) def _get_action_state(self): @@ -488,7 +518,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): @@ -841,7 +871,7 @@ class ActionState(object): return True def action(self, discriminator, callable=None, args=(), kw=None, order=0, - includepath=(), info=''): + 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 @@ -850,13 +880,14 @@ class ActionState(object): # the composition and ordering is). if kw is None: kw = {} - action = (discriminator, callable, args, kw, includepath, info, order) + action = (discriminator, callable, args, kw, includepath, info, order, + introspectables) # remove trailing false items while (len(action) > 2) and not action[-1]: action = action[:-1] 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 @@ -907,7 +938,8 @@ class ActionState(object): """ try: for action in resolveConflicts(self.actions): - _, callable, args, kw, _, info, _ = expand_action(*action) + (_, callable, args, kw, _, info, _, + introspectables) = expand_action(*action) if callable is None: continue try: @@ -922,6 +954,9 @@ class ActionState(object): tb) finally: del t, v, tb + for introspectable in introspectables: + introspectable(introspector, info) + finally: if clear: del self.actions[:] @@ -980,7 +1015,8 @@ def resolveConflicts(actions): unique = {} output = [] for i in range(len(actions)): - (discriminator, callable, args, kw, includepath, info, order + (discriminator, callable, args, kw, includepath, info, order, + introspectables ) = expand_action(*(actions[i])) order = order or i @@ -989,14 +1025,15 @@ def resolveConflicts(actions): # never conflict. We can add it directly to the # configuration actions. output.append( - (order, discriminator, callable, args, kw, includepath, info) + (order, discriminator, callable, args, kw, includepath, info, + introspectables) ) continue a = unique.setdefault(discriminator, []) a.append( - (includepath, order, callable, args, kw, info) + (includepath, order, callable, args, kw, info, introspectables) ) # Check for conflicts @@ -1010,11 +1047,13 @@ def resolveConflicts(actions): # callable function is in the list return stupid[0:2] + stupid[3:] dups.sort(key=allbutfunc) - (basepath, i, callable, args, kw, baseinfo) = dups[0] + (basepath, i, callable, args, kw, baseinfo, introspectables) = dups[0] output.append( - (i, discriminator, callable, args, kw, basepath, baseinfo) + (i, discriminator, callable, args, kw, basepath, baseinfo, + introspectables) ) - for includepath, i, callable, args, kw, info in dups[1:]: + for (includepath, i, callable, args, kw, info, + introspectables) in dups[1:]: # Test whether path is a prefix of opath if (includepath[:len(basepath)] != basepath # not a prefix or @@ -1041,10 +1080,11 @@ def resolveConflicts(actions): # 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=0, introspectables=()): if kw is None: kw = {} - return (discriminator, callable, args, kw, includepath, info, order) + return (discriminator, callable, args, kw, includepath, info, order, + introspectables) global_registries = WeakOrderedSet() diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index f022e7f08..9efe29848 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -27,7 +27,10 @@ class AdaptersConfiguratorMixin(object): iface = (iface,) def register(): self.registry.registerHandler(subscriber, iface) - self.action(None, register) + intr = self.introspectable('subscriber', id(subscriber)) + intr['subscriber'] = subscriber + intr['interfaces'] = iface + self.action(None, register, introspectables=(intr,)) return subscriber @action_method @@ -52,7 +55,11 @@ 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 adapter', discriminator) + 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..42f36fc22 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -236,7 +236,15 @@ 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 override', + (package, override_package, path, override_prefix) + ) + 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 0e59f9286..1b06a6b60 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -26,7 +26,10 @@ 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 factory', None) + intr['factory'] = factory + self.action(IRootFactory, register, introspectables=(intr,)) _set_root_factory = set_root_factory # bw compat @@ -46,7 +49,9 @@ class FactoriesConfiguratorMixin(object): session_factory = self.maybe_dotted(session_factory) def register(): self.registry.registerUtility(session_factory, ISessionFactory) - self.action(ISessionFactory, register) + intr = self.introspectable('session factory', None) + intr['factory'] = session_factory + self.action(ISessionFactory, register, introspectables=(intr,)) @action_method def set_request_factory(self, factory): @@ -67,5 +72,7 @@ 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) + intr['factory'] = factory + self.action(IRequestFactory, register, introspectables=(intr,)) diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index 6eed99191..d7875f56b 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -38,7 +38,9 @@ class I18NConfiguratorMixin(object): """ def register(): self._set_locale_negotiator(negotiator) - self.action(ILocaleNegotiator, register) + intr = self.introspectable('locale negotiator', None) + intr['negotiator'] = negotiator + self.action(ILocaleNegotiator, register, introspectables=(intr,)) def _set_locale_negotiator(self, negotiator): locale_negotiator = self.maybe_dotted(negotiator) @@ -69,8 +71,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 @@ -80,25 +84,34 @@ 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 directory', 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..b30b8c4a1 --- /dev/null +++ b/pyramid/config/introspection.py @@ -0,0 +1,151 @@ +import operator + +from zope.interface import implementer + +from pyramid.interfaces import ( + IIntrospector, + IIntrospectable + ) + +@implementer(IIntrospector) +class Introspector(object): + action_info = None + def __init__(self): + self._refs = {} + self._categories = {} + self._counter = 0 + + def add(self, category_name, discriminator): + category = self._categories.setdefault(category_name, {}) + intr = category.get(discriminator) + if intr is None: + intr = Introspectable(category_name, discriminator) + category[intr.discriminator] = intr + category[intr.discriminator_hash] = intr + intr.order = self._counter + self._counter += 1 + return intr + + # for adding custom introspectables (instead of using .add) + + def add_intr(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 getall(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) + + 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: + import pdb; pdb.set_trace() + 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, []) + +@implementer(IIntrospectable) +class Introspectable(dict): + + order = 0 # mutated by .add/.add_intr + action_info = '' + + def __init__(self, category_name, discriminator): + self.category_name = category_name + self.discriminator = discriminator + 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)) + + def __call__(self, introspector, action_info): + self.action_info = action_info + introspector.add_intr(self) + for category_name, discriminator in self.relations: + introspector.relate((self.category_name, self.discriminator), + (category_name, discriminator)) + + for category_name, discriminator in self.unrelations: + introspector.unrelate((self.category_name, self.discriminator), + (category_name, discriminator)) + + @property + def discriminator_hash(self): + return hash(self.discriminator) + + @property + def related(self, introspector): + return introspector.related(self) + + 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) + +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/routes.py b/pyramid/config/routes.py index 4008a2e08..2e5f617af 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -365,6 +365,15 @@ class RoutesConfiguratorMixin(object): mapper = self.get_routes_mapper() + intr = self.introspectable('route', name) + 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: @@ -377,9 +386,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 @@ -389,7 +401,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/tweens.py b/pyramid/config/tweens.py index 048309451..39ac018d0 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -4,7 +4,6 @@ from pyramid.interfaces import ITweens from pyramid.compat import string_types from pyramid.compat import is_nonstr_iter -from pyramid.compat import string_types from pyramid.exceptions import ConfigurationError from pyramid.tweens import excview_tween_factory from pyramid.tweens import MAIN, INGRESS, EXCVIEW diff --git a/pyramid/config/views.py b/pyramid/config/views.py index cf27c3514..4f11ed1ea 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1068,7 +1068,20 @@ class ViewsConfiguratorMixin(object): xhr, accept, header, path_info, match_param] discriminator.extend(sorted([hash(x) for x in custom_predicates])) discriminator = tuple(discriminator) - self.action(discriminator, register) + introspectables = [] + view_intr = self.introspectable('view', discriminator) + introspectables.append(view_intr) + if route_name: + view_intr.relate('route', route_name) # see add_route + if renderer is not None and renderer.name and '.' in renderer.name: + tmpl_intr = self.introspectable('template', discriminator) + tmpl_intr.relate('view', discriminator) + introspectables.append(tmpl_intr) + if permission is not None: + perm_intr = self.introspectable('permission', permission) + perm_intr.relate('view', 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 fcdf72d01..3f26c4386 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -857,9 +857,95 @@ class IRendererInfo(Interface): 'to the current application') +class IIntrospector(Interface): + def add(category_name, discriminator): + """ If the introspectable of category_name with ``discriminator`` + already exists, return it; otherwise create an IIntrospectable object + and return it""" + + def add_intr(intr): + """ Add the IIntrospectable ``intr`` (use + instead of :meth:`pyramid.interfaces.IIntrospector.add` when you have + a custom IIntrospectable). """ + + 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 ``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 + 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. This method will not raise + an error if the intrpsectable does not exist. """ + + 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.""" + + 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.""" + + 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.""" + + +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").""" + + order = Attribute('order added to introspector') + category_name = Attribute('introspection category name') + discriminator = Attribute('introspectable discriminator (within category) ' + '(must be hashable)') + discriminator_hash = Attribute('an integer hash of the discriminator') + + def relate(category_name, discriminator): + """ Relate this IIntrospectable with another IIntrospectable (the one + associated with the ``category_name`` and ``discriminator``). The + introspectable you wish to relate to must have already been added via + :meth:`pyramid.interfaces.IIntrospector.add` or + :meth:`pyramid.interfaces.IIntrospector.add_intr`; a :exc:`KeyError` + will result if this is not true. + """ + + def unrelate(self, category_name, discriminator): + """ Break any relationship between this IIntrospectable and another + IIntrospectable (the one associated with the ``category_name`` and + ``discriminator``). The introspectable you wish to unrelate from must + have already been added via + :meth:`pyramid.interfaces.IIntrospector.add` or + :meth:`pyramid.interfaces.IIntrospector.add_intr`; a :exc:`KeyError` + will result if this is not true. """ + + def related(self): + """ Return a set of related IIntrospectables """ + # 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/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index ca1508295..a65032143 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -750,7 +750,7 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual( state.actions, [(('discrim', None, (), {'a': 1}, 0), - {'info': 'abc', 'includepath':()})] + {'info': 'abc', 'includepath':(), 'introspectables':()})] ) def test_action_branching_nonautocommit_without_config_info(self): @@ -764,7 +764,7 @@ pyramid.tests.test_config.dummy_include2""", self.assertEqual( state.actions, [(('discrim', None, (), {'a': 1}, 0), - {'info': 'z', 'includepath':()})] + {'info': 'z', 'includepath':(), 'introspectables':()})] ) def test_scan_integration(self): -- cgit v1.2.3 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 From 8f6b247dceb923f6f8656fdf256b60578acd4af8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 24 Nov 2011 03:04:37 -0500 Subject: fix interface --- pyramid/interfaces.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 9cae9e642..f241ddfb9 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -943,7 +943,7 @@ class IIntrospectable(Interface): will result if this is not true. """ - def unrelate(self, category_name, discriminator): + def unrelate(category_name, discriminator): """ Break any relationship between this IIntrospectable and another IIntrospectable (the one associated with the ``category_name`` and ``discriminator``). The introspectable you wish to unrelate from must @@ -952,8 +952,11 @@ class IIntrospectable(Interface): :meth:`pyramid.interfaces.IIntrospector.add_intr`; a :exc:`KeyError` will result if this is not true. """ - def related(self): - """ Return a set of related IIntrospectables """ + def related(introspector): + """ Return a sequence of related IIntrospectables """ + + def __call__(introspector, action_info): + """ Register this IIntrospectable with the introspector """ # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase -- cgit v1.2.3 From fe5be1b1368a8a602a5a6151156dd465daa1ffd1 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 24 Nov 2011 03:05:05 -0500 Subject: fix interface --- pyramid/interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index f241ddfb9..572f96908 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -928,7 +928,7 @@ class IIntrospectable(Interface): must also implement all the methods of Python's ``collections.MutableMapping`` (the "dictionary interface").""" - order = Attribute('order added to introspector') + order = Attribute('integer order in which registered with introspector') category_name = Attribute('introspection category name') discriminator = Attribute('introspectable discriminator (within category) ' '(must be hashable)') -- cgit v1.2.3 From 8a32e380d933ba015efe4de0d11f9c5971ef9d06 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 24 Nov 2011 06:10:02 -0500 Subject: require a title --- pyramid/config/adapters.py | 5 +++-- pyramid/config/assets.py | 3 ++- pyramid/config/factories.py | 6 +++--- pyramid/config/i18n.py | 5 +++-- pyramid/config/introspection.py | 10 ++------- pyramid/config/routes.py | 2 +- pyramid/config/views.py | 48 +++++++++++++++++++++++++++++++---------- pyramid/interfaces.py | 2 +- pyramid/router.py | 2 +- 9 files changed, 53 insertions(+), 30 deletions(-) diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index 9efe29848..df6b64fa8 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -27,7 +27,7 @@ class AdaptersConfiguratorMixin(object): iface = (iface,) def register(): self.registry.registerHandler(subscriber, iface) - intr = self.introspectable('subscriber', id(subscriber)) + intr = self.introspectable('subscriber', id(subscriber), 'subscriber') intr['subscriber'] = subscriber intr['interfaces'] = iface self.action(None, register, introspectables=(intr,)) @@ -56,7 +56,8 @@ class AdaptersConfiguratorMixin(object): else: reg.registerAdapter(adapter, (type_or_iface,), IResponse) discriminator = (IResponse, type_or_iface) - intr = self.introspectable('response adapter', discriminator) + intr = self.introspectable('response adapter', discriminator, + 'response adapter') intr['adapter'] = adapter intr['type'] = type_or_iface self.action(discriminator, register, introspectables=(intr,)) diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index 42f36fc22..b37f11865 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -238,7 +238,8 @@ class AssetsConfiguratorMixin(object): intr = self.introspectable( 'asset override', - (package, override_package, path, override_prefix) + (package, override_package, path, override_prefix), + 'asset override', ) intr['package'] = package intr['override_package'] = package diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 1b06a6b60..c557ff72e 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -27,7 +27,7 @@ class FactoriesConfiguratorMixin(object): self.registry.registerUtility(factory, IRootFactory) self.registry.registerUtility(factory, IDefaultRootFactory) # b/c - intr = self.introspectable('root factory', None) + intr = self.introspectable('root factory', None, 'root factory') intr['factory'] = factory self.action(IRootFactory, register, introspectables=(intr,)) @@ -49,7 +49,7 @@ class FactoriesConfiguratorMixin(object): session_factory = self.maybe_dotted(session_factory) def register(): self.registry.registerUtility(session_factory, ISessionFactory) - intr = self.introspectable('session factory', None) + intr = self.introspectable('session factory', None, 'session factory') intr['factory'] = session_factory self.action(ISessionFactory, register, introspectables=(intr,)) @@ -72,7 +72,7 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, IRequestFactory) - intr = self.introspectable('request factory', None) + intr = self.introspectable('request factory', None, 'request factory') intr['factory'] = factory self.action(IRequestFactory, register, introspectables=(intr,)) diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index d7875f56b..5b60f1d99 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -38,7 +38,8 @@ class I18NConfiguratorMixin(object): """ def register(): self._set_locale_negotiator(negotiator) - intr = self.introspectable('locale negotiator', None) + intr = self.introspectable('locale negotiator', None, + 'locale negotiator') intr['negotiator'] = negotiator self.action(ILocaleNegotiator, register, introspectables=(intr,)) @@ -86,7 +87,7 @@ class I18NConfiguratorMixin(object): if not os.path.isdir(os.path.realpath(directory)): raise ConfigurationError('"%s" is not a directory' % directory) - intr = self.introspectable('translation directory', directory) + intr = self.introspectable('translation directory', directory, spec) intr['directory'] = directory introspectables.append(intr) directories.append(directory) diff --git a/pyramid/config/introspection.py b/pyramid/config/introspection.py index 18e9f94b1..1d766fb5d 100644 --- a/pyramid/config/introspection.py +++ b/pyramid/config/introspection.py @@ -107,9 +107,10 @@ class Introspectable(dict): order = 0 # mutated by .add/.add_intr action_info = '' - def __init__(self, category_name, discriminator): + def __init__(self, category_name, discriminator, title): self.category_name = category_name self.discriminator = discriminator + self.title = title self.relations = [] self.unrelations = [] @@ -138,13 +139,6 @@ 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,)) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 2e5f617af..d4e8a94ab 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -365,7 +365,7 @@ class RoutesConfiguratorMixin(object): mapper = self.get_routes_mapper() - intr = self.introspectable('route', name) + intr = self.introspectable('route', name, name) intr['name'] = name intr['pattern'] = pattern intr['factory'] = factory diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 4f11ed1ea..df0a64231 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -924,6 +924,34 @@ 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) + view_intr = self.introspectable('view', discriminator, '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: @@ -965,6 +993,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 @@ -1062,23 +1091,20 @@ 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) - introspectables = [] - view_intr = self.introspectable('view', discriminator) - introspectables.append(view_intr) if route_name: view_intr.relate('route', route_name) # see add_route if renderer is not None and renderer.name and '.' in renderer.name: - tmpl_intr = self.introspectable('template', discriminator) + tmpl_intr = self.introspectable('template', discriminator, + renderer.name) tmpl_intr.relate('view', discriminator) + tmpl_intr['name'] = renderer.name + tmpl_intr['type'] = renderer.type + tmpl_intr['renderer'] = renderer introspectables.append(tmpl_intr) if permission is not None: - perm_intr = self.introspectable('permission', permission) + perm_intr = self.introspectable('permission', permission, + permission) + perm_intr['value'] = permission perm_intr.relate('view', discriminator) introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 572f96908..fd66ebca5 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -871,7 +871,7 @@ 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 ``default`` """ + 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 diff --git a/pyramid/router.py b/pyramid/router.py index fb309eb03..fdb55779b 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -178,7 +178,7 @@ class Router(object): if request.response_callbacks: request._process_response_callbacks(response) - + return response(request.environ, start_response) finally: -- cgit v1.2.3 From 5e92f34f019e2e6e06ff4c0b5c019349591f9a43 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 24 Nov 2011 17:37:19 -0500 Subject: add type name to iintrospectable constructor; rejigger responsibilities between iintrospectable and iintrospector --- pyramid/config/__init__.py | 9 ++- pyramid/config/adapters.py | 12 +++- pyramid/config/assets.py | 4 +- pyramid/config/factories.py | 19 +++--- pyramid/config/i18n.py | 5 +- pyramid/config/introspection.py | 61 ++++++++------------ pyramid/config/routes.py | 2 +- pyramid/config/views.py | 17 +++--- pyramid/interfaces.py | 125 +++++++++++++++++++++++++++------------- 9 files changed, 152 insertions(+), 102 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index d78d46cb8..d7e95b7e9 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -477,12 +477,14 @@ class Configurator( 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: - introspectable(self.introspector, action_info) + if introspector is not None: + introspector.register(introspectable, action_info) else: self.action_state.action( @@ -960,8 +962,9 @@ class ActionState(object): tb) finally: del t, v, tb - for introspectable in introspectables: - introspectable(introspector, info) + if introspector is not None: + for introspectable in introspectables: + introspector.register(introspectable, info) finally: if clear: diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index df6b64fa8..620464ed3 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -27,7 +27,10 @@ class AdaptersConfiguratorMixin(object): iface = (iface,) def register(): self.registry.registerHandler(subscriber, iface) - intr = self.introspectable('subscriber', id(subscriber), 'subscriber') + intr = self.introspectable('subscribers', + id(subscriber), + repr(subscriber), + 'subscriber') intr['subscriber'] = subscriber intr['interfaces'] = iface self.action(None, register, introspectables=(intr,)) @@ -56,8 +59,11 @@ class AdaptersConfiguratorMixin(object): else: reg.registerAdapter(adapter, (type_or_iface,), IResponse) discriminator = (IResponse, type_or_iface) - intr = self.introspectable('response adapter', discriminator, - 'response adapter') + intr = self.introspectable( + 'response adapters', + discriminator, + repr(adapter), + 'response adapter') intr['adapter'] = adapter intr['type'] = type_or_iface self.action(discriminator, register, introspectables=(intr,)) diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index b37f11865..7080e5e7c 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -237,8 +237,10 @@ class AssetsConfiguratorMixin(object): override(from_package, path, to_package, override_prefix) intr = self.introspectable( - 'asset override', + 'asset overrides', (package, override_package, path, override_prefix), + '%s/%s -> %s/%s' % (package, path, override_package, + override_prefix), 'asset override', ) intr['package'] = package diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index c557ff72e..6cc90a80b 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -27,17 +27,18 @@ class FactoriesConfiguratorMixin(object): self.registry.registerUtility(factory, IRootFactory) self.registry.registerUtility(factory, IDefaultRootFactory) # b/c - intr = self.introspectable('root factory', None, 'root factory') + intr = self.introspectable('root factories', None, repr(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:: @@ -46,11 +47,12 @@ 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) - intr = self.introspectable('session factory', None, 'session factory') - intr['factory'] = session_factory + self.registry.registerUtility(factory, ISessionFactory) + intr = self.introspectable('session factory', None, repr(factory), + 'session factory') + intr['factory'] = factory self.action(ISessionFactory, register, introspectables=(intr,)) @action_method @@ -72,7 +74,8 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, IRequestFactory) - intr = self.introspectable('request factory', None, 'request factory') + intr = self.introspectable('request factory', None, repr(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 5b60f1d99..b405f3683 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -38,7 +38,7 @@ class I18NConfiguratorMixin(object): """ def register(): self._set_locale_negotiator(negotiator) - intr = self.introspectable('locale negotiator', None, + intr = self.introspectable('locale negotiator', None, repr(negotiator), 'locale negotiator') intr['negotiator'] = negotiator self.action(ILocaleNegotiator, register, introspectables=(intr,)) @@ -87,7 +87,8 @@ class I18NConfiguratorMixin(object): if not os.path.isdir(os.path.realpath(directory)): raise ConfigurationError('"%s" is not a directory' % directory) - intr = self.introspectable('translation directory', directory, spec) + intr = self.introspectable('translation directories', directory, + spec, 'translation directory') intr['directory'] = directory introspectables.append(intr) directories.append(directory) diff --git a/pyramid/config/introspection.py b/pyramid/config/introspection.py index 1d766fb5d..666728fc5 100644 --- a/pyramid/config/introspection.py +++ b/pyramid/config/introspection.py @@ -3,32 +3,18 @@ import operator from zope.interface import implementer from pyramid.interfaces import ( - IIntrospector, - IIntrospectable + IIntrospectable, + IIntrospector ) @implementer(IIntrospector) class Introspector(object): - action_info = None def __init__(self): self._refs = {} self._categories = {} self._counter = 0 - def add(self, category_name, discriminator): - category = self._categories.setdefault(category_name, {}) - intr = category.get(discriminator) - if intr is None: - intr = Introspectable(category_name, discriminator) - category[intr.discriminator] = intr - category[intr.discriminator_hash] = intr - intr.order = self._counter - self._counter += 1 - return intr - - # for adding custom introspectables (instead of using .add) - - def add_intr(self, intr): + def add(self, intr): category = self._categories.setdefault(intr.category_name, {}) category[intr.discriminator] = intr category[intr.discriminator_hash] = intr @@ -49,9 +35,12 @@ class Introspector(object): 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 sorted(self._categories.keys()): + for category_name in self.categories(): L.append((category_name, self.get_category(category_name, sort_fn))) return L @@ -73,7 +62,6 @@ class Introspector(object): category_name, discriminator = pair intr = self._categories.get(category_name, {}).get(discriminator) if intr is None: - import pdb; pdb.set_trace() raise KeyError((category_name, discriminator)) introspectables.append(intr) return introspectables @@ -101,16 +89,30 @@ class Introspector(object): 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 .add/.add_intr - action_info = '' + order = 0 # mutated by introspector.add/introspector.add_intr + action_info = '' # mutated by introspector.register - def __init__(self, category_name, discriminator, title): + 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 = [] @@ -120,25 +122,10 @@ class Introspectable(dict): def unrelate(self, category_name, discriminator): self.unrelations.append((category_name, discriminator)) - def __call__(self, introspector, action_info): - self.action_info = action_info - introspector.add_intr(self) - for category_name, discriminator in self.relations: - introspector.relate((self.category_name, self.discriminator), - (category_name, discriminator)) - - for category_name, discriminator in self.unrelations: - introspector.unrelate((self.category_name, self.discriminator), - (category_name, discriminator)) - @property def discriminator_hash(self): return hash(self.discriminator) - @property - def related(self, introspector): - return introspector.related(self) - def __hash__(self): return hash((self.category_name,) + (self.discriminator,)) diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index d4e8a94ab..46f69300d 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -365,7 +365,7 @@ class RoutesConfiguratorMixin(object): mapper = self.get_routes_mapper() - intr = self.introspectable('route', name, name) + intr = self.introspectable('routes', name, name, 'route') intr['name'] = name intr['pattern'] = pattern intr['factory'] = factory diff --git a/pyramid/config/views.py b/pyramid/config/views.py index df0a64231..f4e7bb1db 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -931,7 +931,8 @@ class ViewsConfiguratorMixin(object): xhr, accept, header, path_info, match_param] discriminator.extend(sorted([hash(x) for x in custom_predicates])) discriminator = tuple(discriminator) - view_intr = self.introspectable('view', discriminator, 'view') + view_intr = self.introspectable('views', discriminator, repr(view), + 'view') view_intr.update( dict(name=name, context=context, @@ -1092,20 +1093,20 @@ class ViewsConfiguratorMixin(object): IMultiView, name=name) if route_name: - view_intr.relate('route', route_name) # see add_route + 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('template', discriminator, - renderer.name) - tmpl_intr.relate('view', discriminator) + 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 introspectables.append(tmpl_intr) if permission is not None: - perm_intr = self.introspectable('permission', permission, - permission) + perm_intr = self.introspectable('permissions', permission, + permission, 'permission') perm_intr['value'] = permission - perm_intr.relate('view', discriminator) + perm_intr.relate('views', discriminator) introspectables.append(perm_intr) self.action(discriminator, register, introspectables=introspectables) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index fd66ebca5..81ff6df1e 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -858,16 +858,6 @@ class IRendererInfo(Interface): class IIntrospector(Interface): - def add(category_name, discriminator): - """ If the introspectable of category_name with ``discriminator`` - already exists, return it; otherwise create an IIntrospectable object - and return it""" - - def add_intr(intr): - """ Add the IIntrospectable ``intr`` (use - instead of :meth:`pyramid.interfaces.IIntrospector.add` when you have - a custom IIntrospectable). """ - 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 @@ -884,6 +874,10 @@ class IIntrospector(Interface): 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 @@ -896,8 +890,57 @@ class IIntrospector(Interface): def remove(category_name, discriminator): """ Remove the IIntrospectable related to ``category_name`` and - ``discriminator`` from the introspector. This method will not raise - an error if the intrpsectable does not exist. """ + ``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 @@ -905,7 +948,11 @@ class IIntrospector(Interface): 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.""" + 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 @@ -913,13 +960,11 @@ class IIntrospector(Interface): 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.""" + pair is not already related to another. - 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.""" + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register` + """ class IIntrospectable(Interface): @@ -928,35 +973,37 @@ class IIntrospectable(Interface): must also implement all the methods of Python's ``collections.MutableMapping`` (the "dictionary interface").""" - order = Attribute('integer order in which registered with introspector') + 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): - """ Relate this IIntrospectable with another IIntrospectable (the one - associated with the ``category_name`` and ``discriminator``). The - introspectable you wish to relate to must have already been added via - :meth:`pyramid.interfaces.IIntrospector.add` or - :meth:`pyramid.interfaces.IIntrospector.add_intr`; a :exc:`KeyError` - will result if this is not true. + """ 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): - """ Break any relationship between this IIntrospectable and another - IIntrospectable (the one associated with the ``category_name`` and - ``discriminator``). The introspectable you wish to unrelate from must - have already been added via - :meth:`pyramid.interfaces.IIntrospector.add` or - :meth:`pyramid.interfaces.IIntrospector.add_intr`; a :exc:`KeyError` - will result if this is not true. """ - - def related(introspector): - """ Return a sequence of related IIntrospectables """ - - def __call__(introspector, action_info): - """ Register this IIntrospectable with the introspector """ + """ 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 -- cgit v1.2.3 From 8b6f09d965a6e637b795a8268c310c81fcb43a10 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 25 Nov 2011 21:52:11 -0500 Subject: add rudimentary object description code --- TODO.txt | 14 ++++++++++ pyramid/config/__init__.py | 2 ++ pyramid/config/adapters.py | 4 +-- pyramid/config/factories.py | 9 ++++--- pyramid/config/routes.py | 3 ++- pyramid/config/views.py | 9 ++++++- pyramid/util.py | 66 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 100 insertions(+), 7 deletions(-) diff --git a/TODO.txt b/TODO.txt index f13cd5c6c..ed215138e 100644 --- a/TODO.txt +++ b/TODO.txt @@ -8,6 +8,20 @@ Must-Have - 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. + Nice-to-Have ------------ diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index d7e95b7e9..e993d0700 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -34,6 +34,7 @@ from pyramid.settings import aslist from pyramid.threadlocal import manager from pyramid.util import DottedNameResolver from pyramid.util import WeakOrderedSet +from pyramid.util import object_description from pyramid.config.adapters import AdaptersConfiguratorMixin from pyramid.config.assets import AssetsConfiguratorMixin @@ -219,6 +220,7 @@ class Configurator( basepath = None includepath = () info = '' + object_description = staticmethod(object_description) def __init__(self, registry=None, diff --git a/pyramid/config/adapters.py b/pyramid/config/adapters.py index 620464ed3..04571bec3 100644 --- a/pyramid/config/adapters.py +++ b/pyramid/config/adapters.py @@ -29,7 +29,7 @@ class AdaptersConfiguratorMixin(object): self.registry.registerHandler(subscriber, iface) intr = self.introspectable('subscribers', id(subscriber), - repr(subscriber), + self.object_description(subscriber), 'subscriber') intr['subscriber'] = subscriber intr['interfaces'] = iface @@ -62,7 +62,7 @@ class AdaptersConfiguratorMixin(object): intr = self.introspectable( 'response adapters', discriminator, - repr(adapter), + self.object_description(adapter), 'response adapter') intr['adapter'] = adapter intr['type'] = type_or_iface diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 6cc90a80b..2ab9fc7db 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -27,7 +27,8 @@ class FactoriesConfiguratorMixin(object): self.registry.registerUtility(factory, IRootFactory) self.registry.registerUtility(factory, IDefaultRootFactory) # b/c - intr = self.introspectable('root factories', None, repr(factory), + intr = self.introspectable('root factories', None, + self.object_description(factory), 'root factory') intr['factory'] = factory self.action(IRootFactory, register, introspectables=(intr,)) @@ -50,7 +51,8 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, ISessionFactory) - intr = self.introspectable('session factory', None, repr(factory), + intr = self.introspectable('session factory', None, + self.object_description(factory), 'session factory') intr['factory'] = factory self.action(ISessionFactory, register, introspectables=(intr,)) @@ -74,7 +76,8 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) def register(): self.registry.registerUtility(factory, IRequestFactory) - intr = self.introspectable('request factory', None, repr(factory), + 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/routes.py b/pyramid/config/routes.py index 46f69300d..0ab380ec1 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -365,7 +365,8 @@ class RoutesConfiguratorMixin(object): mapper = self.get_routes_mapper() - intr = self.introspectable('routes', name, name, 'route') + intr = self.introspectable('routes', name, + '%s (%s)' % (name, pattern), 'route') intr['name'] = name intr['pattern'] = pattern intr['factory'] = factory diff --git a/pyramid/config/views.py b/pyramid/config/views.py index f4e7bb1db..12193b478 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -931,7 +931,14 @@ class ViewsConfiguratorMixin(object): xhr, accept, header, path_info, match_param] discriminator.extend(sorted([hash(x) for x in custom_predicates])) discriminator = tuple(discriminator) - view_intr = self.introspectable('views', discriminator, repr(view), + 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, diff --git a/pyramid/util.py b/pyramid/util.py index 3eb4b3fed..fd3bcd48d 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,67 @@ 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 `` 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) + 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) + oself.__class__ + 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 -- cgit v1.2.3 From 7f72f8d8607607a15b1c460db5fdf25ee9692a3c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 25 Nov 2011 22:09:41 -0500 Subject: add more introspectables, fix object_description so tests pass --- pyramid/config/rendering.py | 12 +++++++++++- pyramid/config/security.py | 24 +++++++++++++++++++++--- pyramid/config/views.py | 1 + pyramid/util.py | 7 ++++--- 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py index f70dea118..f828f453b 100644 --- a/pyramid/config/rendering.py +++ b/pyramid/config/rendering.py @@ -43,9 +43,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): @@ -83,4 +89,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/security.py b/pyramid/config/security.py index 935e2fd95..20c830e75 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -29,8 +29,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) @@ -60,6 +65,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) @@ -108,9 +117,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 12193b478..00ea2fdd2 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1108,6 +1108,7 @@ class ViewsConfiguratorMixin(object): 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, diff --git a/pyramid/util.py b/pyramid/util.py index fd3bcd48d..ee909c316 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -265,6 +265,8 @@ def object_description(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 @@ -272,9 +274,8 @@ def object_description(object): oself = getattr(object, '__self__', None) if oself is None: oself = getattr(object, 'im_self', None) - oself.__class__ - return 'method %s of class %s.%s' (object.__name__, modulename, - oself.__class__.__name___) + return 'method %s of class %s.%s' % (object.__name__, modulename, + oself.__class__.__name__) if inspect.isclass(object): dottedname = '%s.%s' % (modulename, object.__name__) -- cgit v1.2.3 From 8786a5719950900ba7a8390323476076e737ac9e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 26 Nov 2011 12:02:48 -0500 Subject: note --- TODO.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.txt b/TODO.txt index ed215138e..d13b504ef 100644 --- a/TODO.txt +++ b/TODO.txt @@ -22,6 +22,8 @@ Must-Have * usage docs. + * make it possible to disuse introspection? + Nice-to-Have ------------ -- cgit v1.2.3 From 8a4c36cdca2c512e9f0be8a862774c89367cf323 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Nov 2011 16:27:36 -0500 Subject: fix on python 3 --- pyramid/util.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyramid/util.py b/pyramid/util.py index ee909c316..c439456b6 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -4,7 +4,11 @@ import sys import types import weakref -from pyramid.compat import string_types +from pyramid.compat import ( + integer_types, + string_types, + ) + from pyramid.exceptions import ConfigurationError from pyramid.path import package_of @@ -256,7 +260,9 @@ def object_description(object): """ if isinstance(object, string_types): return object - if isinstance(object, (bool, int, float, long, types.NoneType)): + if isinstance(object, integer_types): + return object + if isinstance(object, (bool, float, type(None))): return str(object) if isinstance(object, (tuple, set)): return shortrepr(object, ')') -- cgit v1.2.3 From e496386fd5e9e98c0479f39d67c092d61720c29c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Nov 2011 19:06:07 -0500 Subject: rejigger how registry is assigned an introspector; add tests for object_description --- pyramid/config/__init__.py | 79 +++++++++++++-------- pyramid/config/introspection.py | 152 ---------------------------------------- pyramid/registry.py | 146 +++++++++++++++++++++++++++++++++++--- pyramid/tests/test_events.py | 5 +- pyramid/tests/test_util.py | 70 ++++++++++++++++++ pyramid/util.py | 44 ++++++------ 6 files changed, 279 insertions(+), 217 deletions(-) delete mode 100644 pyramid/config/introspection.py diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index b0c86f0cd..3039abedd 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -12,7 +12,6 @@ from webob.exc import WSGIHTTPException as WebobWSGIHTTPException from pyramid.interfaces import ( IDebugLogger, IExceptionResponse, - IIntrospector, ) from pyramid.asset import resolve_asset_spec @@ -41,7 +40,11 @@ from pyramid.path import ( package_of, ) -from pyramid.registry import Registry +from pyramid.registry import ( + Introspectable, + Introspector, + Registry, + ) from pyramid.router import Router @@ -69,9 +72,9 @@ 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_('') +_marker = object() ConfigurationError = ConfigurationError # pyflakes @@ -88,7 +91,6 @@ class Configurator( SettingsConfiguratorMixin, FactoriesConfiguratorMixin, AdaptersConfiguratorMixin, - IntrospectionConfiguratorMixin, ): """ A Configurator is used to configure a :app:`Pyramid` @@ -231,8 +233,13 @@ class Configurator( If ``route_prefix`` is passed, all routes added with :meth:`pyramid.config.Configurator.add_route` will have the specified path - prepended to their pattern. This parameter is new in Pyramid 1.2.""" + prepended to their pattern. This parameter is new in Pyramid 1.2. + If ``introspector`` is passed, it must be an instance implementing the + :class:`pyramid.interfaces.IIntrospector` interface. If no + ``introspector`` is passed, the default IIntrospector implementation will + be used. + """ manager = manager # for testing injection venusian = venusian # for testing injection _ainfo = None @@ -240,6 +247,7 @@ class Configurator( includepath = () info = '' object_description = staticmethod(object_description) + introspectable = Introspectable def __init__(self, registry=None, @@ -259,6 +267,7 @@ class Configurator( autocommit=False, exceptionresponse_view=default_exceptionresponse_view, route_prefix=None, + introspector=None, ): if package is None: package = caller_package() @@ -286,15 +295,24 @@ class Configurator( session_factory=session_factory, default_view_mapper=default_view_mapper, exceptionresponse_view=exceptionresponse_view, + introspector=introspector, ) - def setup_registry(self, settings=None, root_factory=None, - authentication_policy=None, authorization_policy=None, - renderers=None, debug_logger=None, - locale_negotiator=None, request_factory=None, - renderer_globals_factory=None, default_permission=None, - session_factory=None, default_view_mapper=None, - exceptionresponse_view=default_exceptionresponse_view): + def setup_registry(self, + settings=None, + root_factory=None, + authentication_policy=None, + authorization_policy=None, + renderers=None, + debug_logger=None, + locale_negotiator=None, + request_factory=None, + renderer_globals_factory=None, + default_permission=None, + session_factory=None, + default_view_mapper=None, + exceptionresponse_view=default_exceptionresponse_view, + introspector=None): """ When you pass a non-``None`` ``registry`` argument to the :term:`Configurator` constructor, no initial setup is performed against the registry. This is because the registry you pass in may @@ -314,6 +332,10 @@ class Configurator( registry = self.registry self._fix_registry() + + if introspector is not None: + self.introspector = introspector + self._set_settings(settings) self._register_response_adapters() @@ -442,23 +464,24 @@ 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 _get_introspector(self): + introspector = getattr(self.registry, 'introspector', _marker) + if introspector is _marker: + introspector = Introspector() + self._set_introspector(introspector) + return introspector + + def _set_introspector(self, introspector): + self.registry.introspector = introspector + + def _del_introspector(self): + del self.registry.introspector + + introspector = property(_get_introspector, + _set_introspector, + _del_introspector) @property def action_info(self): diff --git a/pyramid/config/introspection.py b/pyramid/config/introspection.py deleted file mode 100644 index 666728fc5..000000000 --- a/pyramid/config/introspection.py +++ /dev/null @@ -1,152 +0,0 @@ -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/registry.py b/pyramid/registry.py index 99f5ee843..947643252 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -1,9 +1,15 @@ +import operator + +from zope.interface import implementer + from zope.interface.registry import Components from pyramid.compat import text_ + from pyramid.interfaces import ( ISettings, - IIntrospector + IIntrospector, + IIntrospectable, ) empty = text_('') @@ -78,16 +84,136 @@ class Registry(Components, dict): settings = property(_get_settings, _set_settings) - def _get_introspector(self): - return self.queryUtility(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 _set_introspector(self, introspector): - self.registerUtility(introspector, IIntrospector) - - def _del_introspector(self): - self.unregisterUtility(IIntrospector) + def __nonzero__(self): + return True - introspector = property(_get_introspector, _set_introspector, - _del_introspector) + __bool__ = __nonzero__ # py3 global_registry = Registry('global') diff --git a/pyramid/tests/test_events.py b/pyramid/tests/test_events.py index 108a5d2d9..4b58a129c 100644 --- a/pyramid/tests/test_events.py +++ b/pyramid/tests/test_events.py @@ -122,11 +122,10 @@ class ContextFoundEventTests(unittest.TestCase): class TestSubscriber(unittest.TestCase): def setUp(self): - registry = DummyRegistry() - self.config = testing.setUp(registry=registry) + self.config = testing.setUp() def tearDown(self): - self.config.end() + testing.tearDown() def _makeOne(self, *ifaces): from pyramid.events import subscriber diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 57bcd08d7..f45c75535 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -255,5 +255,75 @@ class Test_WeakOrderedSet(unittest.TestCase): self.assertEqual(list(wos), []) self.assertEqual(wos.last, None) +class Test_object_description(unittest.TestCase): + def _callFUT(self, object): + from pyramid.util import object_description + return object_description(object) + + def test_string(self): + self.assertEqual(self._callFUT('abc'), 'abc') + + def test_int(self): + self.assertEqual(self._callFUT(1), '1') + + def test_bool(self): + self.assertEqual(self._callFUT(True), 'True') + + def test_None(self): + self.assertEqual(self._callFUT(None), 'None') + + def test_float(self): + self.assertEqual(self._callFUT(1.2), '1.2') + + def test_tuple(self): + self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')") + + def test_set(self): + self.assertEqual(self._callFUT(set(['a'])), "set(['a'])") + + def test_list(self): + self.assertEqual(self._callFUT(['a']), "['a']") + + def test_dict(self): + self.assertEqual(self._callFUT({'a':1}), "{'a': 1}") + + def test_nomodule(self): + o = object() + self.assertEqual(self._callFUT(o), 'object %s' % str(o)) + + def test_module(self): + import pyramid + self.assertEqual(self._callFUT(pyramid), 'module pyramid') + + def test_method(self): + self.assertEqual( + self._callFUT(self.test_method), + 'method test_method of class pyramid.tests.test_util.' + 'Test_object_description') + + def test_class(self): + self.assertEqual( + self._callFUT(self.__class__), + 'class pyramid.tests.test_util.Test_object_description') + + def test_function(self): + self.assertEqual( + self._callFUT(dummyfunc), + 'function pyramid.tests.test_util.dummyfunc') + + def test_instance(self): + inst = Dummy() + self.assertEqual( + self._callFUT(inst), + "object %s" % str(inst)) + + def test_shortened_repr(self): + inst = ['1'] * 1000 + self.assertEqual( + self._callFUT(inst), + str(inst)[:100] + ' ... ]') + +def dummyfunc(): pass + class Dummy(object): pass diff --git a/pyramid/util.py b/pyramid/util.py index c439456b6..1fd612d09 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,12 +1,12 @@ import inspect import pkg_resources import sys -import types import weakref from pyramid.compat import ( integer_types, string_types, + text_, ) from pyramid.exceptions import ConfigurationError @@ -235,20 +235,20 @@ 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: + """ Produce a human-consumable text description of ``object``, + usually involving a Python dotted name. For example: .. code-block:: python >>> object_description(None) - 'None' + u'None' >>> from xml.dom import minidom >>> object_description(minidom) - 'module xml.dom.minidom' + u'module xml.dom.minidom' >>> object_description(minidom.Attr) - 'class xml.dom.minidom.Attr' + u'class xml.dom.minidom.Attr' >>> object_description(minidom.Attr.appendChild) - 'method appendChild of class xml.dom.minidom.Attr' + u'method appendChild of class xml.dom.minidom.Attr' >>> If this method cannot identify the type of the object, a generic @@ -259,11 +259,11 @@ def object_description(object): (possibly shortened) string representation is returned. """ if isinstance(object, string_types): - return object + return text_(object) if isinstance(object, integer_types): - return object + return text_(str(object)) if isinstance(object, (bool, float, type(None))): - return str(object) + return text_(str(object)) if isinstance(object, (tuple, set)): return shortrepr(object, ')') if isinstance(object, list): @@ -272,32 +272,28 @@ def object_description(object): return shortrepr(object, '}') module = inspect.getmodule(object) if module is None: - return 'object %s' % str(object) + return text_('object %s' % str(object)) modulename = module.__name__ if inspect.ismodule(object): - return 'module %s' % modulename + return text_('module %s' % modulename) if inspect.ismethod(object): oself = getattr(object, '__self__', None) - if oself is None: + if oself is None: # pragma: no cover oself = getattr(object, 'im_self', None) - return 'method %s of class %s.%s' % (object.__name__, modulename, - oself.__class__.__name__) + return text_('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 + return text_('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) + return text_('function %s' % dottedname) + return text_('object %s' % str(object)) def shortrepr(object, closer): r = str(object) if len(r) > 100: - r = r[:100] + '... %s' % closer + r = r[:100] + ' ... %s' % closer return r -- cgit v1.2.3 From ce5c42f1b832b21405ffd40f61c74a5cfa040e8d Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Mon, 28 Nov 2011 20:27:14 -0500 Subject: docs --- docs/api/config.rst | 10 ++++++++++ pyramid/config/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/api/config.rst b/docs/api/config.rst index a8c193b60..9f130b7dc 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -94,6 +94,16 @@ .. automethod:: set_renderer_globals_factory(factory) + .. attribute:: introspector + + The :term:`introspector` associated with this configuration. + + .. attribute:: introspectable + + A shortcut attribute which points to the + :class:`pyramid.registry.Introspectable` class (used during + directives to provide introspection to actions). + .. attribute:: global_registries diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 3039abedd..c2f004896 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -238,7 +238,7 @@ class Configurator( If ``introspector`` is passed, it must be an instance implementing the :class:`pyramid.interfaces.IIntrospector` interface. If no ``introspector`` is passed, the default IIntrospector implementation will - be used. + be used. This parameter is new in Pyramid 1.3. """ manager = manager # for testing injection venusian = venusian # for testing injection -- cgit v1.2.3 From 35ad08ba9c66b900fe0c537516b390a92cb2a8cd Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 29 Nov 2011 08:31:33 -0500 Subject: move register method from IIntrospector back to IIntrospectable; provide better conflict reporting and a more useful ActionInfo object --- CHANGES.txt | 3 +++ docs/narr/advconfig.rst | 4 ++-- pyramid/config/__init__.py | 8 +++---- pyramid/config/util.py | 19 +++++++++++++-- pyramid/interfaces.py | 60 ++++++++++++++++------------------------------ pyramid/registry.py | 37 ++++++++++++++-------------- 6 files changed, 64 insertions(+), 67 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index c1a4f0216..6f30d506c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -25,6 +25,9 @@ Features This function sets up Python logging according to the logging configuration in a PasteDeploy ini file. +- Configuration conflict reporting is reported in a more understandable way + ("Line 11 in file..." vs. a repr of a tuple of similar info). + Bug Fixes --------- diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 7b62b1a73..a6db5f58e 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -87,8 +87,8 @@ that ends something like this: Conflicting configuration actions For: ('view', None, '', None, , None, None, None, None, None, False, None, None, None) - ('app.py', 14, '', 'config.add_view(hello_world)') - ('app.py', 17, '', 'config.add_view(hello_world)') + Line 14 of file app.py in : 'config.add_view(hello_world)' + Line 17 of file app.py in : 'config.add_view(hello_world)' This traceback is trying to tell us: diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index c2f004896..d4dc0247f 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -526,9 +526,9 @@ class Configurator( if autocommit: if callable is not None: callable(*args, **kw) - for introspectable in introspectables: - if introspector is not None: - introspector.register(introspectable, action_info) + if introspector is not None: + for introspectable in introspectables: + introspectable.register(introspector, action_info) else: self.action_state.action( @@ -1008,7 +1008,7 @@ class ActionState(object): del t, v, tb if introspector is not None: for introspectable in introspectables: - introspector.register(introspectable, info) + introspectable.register(introspector, info) finally: if clear: diff --git a/pyramid/config/util.py b/pyramid/config/util.py index cbec2e0c2..3fcb5d154 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,3 +1,4 @@ +import collections import re import traceback @@ -19,6 +20,20 @@ from hashlib import md5 MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() +_ActionInfo = collections.namedtuple( + 'ActionInfo', + ('filename', 'lineno', 'function', 'linerepr') + ) + +class ActionInfo(_ActionInfo): + # this is a namedtuple subclass for (minor) backwards compat + slots = () + def __str__(self): + return ( + 'Line %s of file %s in %s: %r' % ( + self.lineno, self.filename, self.function, self.linerepr) + ) + def action_method(wrapped): """ Wrapper to provide the right conflict info report data when a method that calls Configurator.action calls another that does the same""" @@ -29,9 +44,9 @@ def action_method(wrapped): if info is None: try: f = traceback.extract_stack(limit=3) - info = f[-2] + info = ActionInfo(*f[-2]) except: # pragma: no cover - info = '' + info = ActionInfo('', 0, '', '') self._ainfo.append(info) try: result = wrapped(self, *arg, **kw) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 475b52313..2ff74e40b 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -903,38 +903,6 @@ class IIntrospector(Interface): ``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 @@ -983,14 +951,6 @@ class IIntrospectable(Interface): 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)') @@ -1007,6 +967,26 @@ class IIntrospectable(Interface): the ``category_name`` and ``discriminator``) during action execution. """ + def register(introspector, action_info=''): + """ Register this IIntrospectable with an introspector. This method + is invoked during action execution. Adds the introspectable and its + relations to the introspector. ``introspector`` should be an + object implementing IIntrospector. ``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, introspector, action_info): + self.action_info = action_info + introspector.add(self) + for methodname, category_name, discriminator in self._relations: + method = getattr(introspector, methodname) + method((i.category_name, i.discriminator), + (category_name, discriminator)) + """ + # 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. diff --git a/pyramid/registry.py b/pyramid/registry.py index 947643252..b081980b0 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -166,38 +166,37 @@ class Introspector(object): 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 + order = 0 # mutated by introspector.add + action_info = '' # mutated by introspectable.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 = [] + self._relations = [] def relate(self, category_name, discriminator): - self.relations.append((category_name, discriminator)) + self._relations.append((True, category_name, discriminator)) def unrelate(self, category_name, discriminator): - self.unrelations.append((category_name, discriminator)) + self._relations.append((False, category_name, discriminator)) + + def register(self, introspector, action_info): + self.action_info = action_info + introspector.add(self) + for relate, category_name, discriminator in self._relations: + if relate: + method = introspector.relate + else: + method = introspector.unrelate + method( + (self.category_name, self.discriminator), + (category_name, discriminator) + ) @property def discriminator_hash(self): -- cgit v1.2.3 From 56bfd68215d656cb5039bc842b030b5ab447a7f7 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 29 Nov 2011 08:33:24 -0500 Subject: get repr right --- docs/narr/advconfig.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index a6db5f58e..4d4f82197 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -88,7 +88,7 @@ that ends something like this: For: ('view', None, '', None, , None, None, None, None, None, False, None, None, None) Line 14 of file app.py in : 'config.add_view(hello_world)' - Line 17 of file app.py in : 'config.add_view(hello_world)' + Line 17 of file app.py in : 'config.add_view(goodbye_world)' This traceback is trying to tell us: -- cgit v1.2.3 From 9f341ea78cfae3a33ef250acee49fa9709a7d7fe Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 29 Nov 2011 08:37:49 -0500 Subject: add note about allowing extension via custom directives --- docs/narr/advconfig.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 4d4f82197..3b6f7669a 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -13,6 +13,8 @@ also, by default, performs configuration in two separate phases. This allows you to ignore relative configuration statement ordering in some circumstances. +Pyramid also allows you to extend its Configurator with custom directives. + .. index:: pair: configuration; conflict detection -- cgit v1.2.3 From 87f8d21d487a67068347e5f8c18343cd7c12b121 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 29 Nov 2011 09:52:23 -0500 Subject: docs and add more introspection info --- TODO.txt | 6 ++---- pyramid/config/routes.py | 12 ++++++++++-- pyramid/config/tweens.py | 11 ++++++++++- pyramid/config/views.py | 6 +++++- pyramid/interfaces.py | 4 ++-- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/TODO.txt b/TODO.txt index d13b504ef..bf1c7773b 100644 --- a/TODO.txt +++ b/TODO.txt @@ -4,10 +4,6 @@ Pyramid TODOs 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 @@ -24,6 +20,8 @@ Must-Have * make it possible to disuse introspection? +- Give discriminators a nicer repr for conflict reporting? + Nice-to-Have ------------ diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index ab62d4c75..35ad0f8c4 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -370,11 +370,19 @@ class RoutesConfiguratorMixin(object): mapper = self.get_routes_mapper() intr = self.introspectable('routes', name, - '%s (%s)' % (name, pattern), 'route') + '%s (pattern: %r)' % (name, pattern), + 'route') intr['name'] = name intr['pattern'] = pattern intr['factory'] = factory - intr['predicates'] = predicates + intr['xhr'] = xhr + intr['request_method'] = request_method + intr['path_info'] = path_info + intr['request_param'] = request_param + intr['header'] = header + intr['accept'] = accept + intr['traverse'] = traverse + intr['custom_predicates'] = custom_predicates intr['pregenerator'] = pregenerator intr['static'] = static intr['use_global_views'] = use_global_views diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 76efe4af5..399e2f68a 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -151,7 +151,16 @@ class TweensConfiguratorMixin(object): else: tweens.add_implicit(name, tween_factory, under=under, over=over) - self.action(('tween', name, explicit), register) + discriminator = ('tween', name, explicit) + tween_type = explicit and 'explicit' or 'implicit' + + intr = self.introspectable('tweens', discriminator, + 'name', 'tween') + intr['factory'] = tween_factory + intr['type'] = tween_type + intr['under'] = under + intr['over'] = over + self.action(discriminator, register, introspectables=(intr,)) class CyclicDependencyError(Exception): def __init__(self, cycles): diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 16c0a8253..5c4470834 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1360,7 +1360,11 @@ class ViewsConfiguratorMixin(object): self.registry.registerUtility(mapper, IViewMapperFactory) # IViewMapperFactory is looked up as the result of view config # in phase 3 - self.action(IViewMapperFactory, register, order=PHASE1_CONFIG) + intr = self.introspectable('view mapper', IViewMapperFactory, + self.object_description(mapper), + 'view mapper') + self.action(IViewMapperFactory, register, order=PHASE1_CONFIG, + introspectables=(intr,)) @action_method def add_static_view(self, name, path, **kw): diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 2ff74e40b..d46a46af0 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -951,9 +951,9 @@ class IIntrospectable(Interface): discriminator = Attribute('introspectable discriminator (within category) ' '(must be hashable)') discriminator_hash = Attribute('an integer hash of the discriminator') - action_info = Attribute('A string representing the caller that invoked ' + action_info = Attribute('An object representing the caller that invoked ' 'the creation of this introspectable (usually ' - 'managed by IIntrospector during registration)') + 'a sentinel until updated during self.register)') def relate(category_name, discriminator): """ Indicate an intent to relate this IIntrospectable with another -- cgit v1.2.3 From 47e1f7336d2d954182cb439a0327d93044cfa244 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Tue, 29 Nov 2011 10:13:12 -0500 Subject: introspection categories differ for implicit and explicit tweens --- pyramid/config/tweens.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 399e2f68a..22ea21a57 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -138,11 +138,20 @@ class TweensConfiguratorMixin(object): raise ConfigurationError('%s cannot be under MAIN' % name) registry = self.registry + introspectables = [] tweens = registry.queryUtility(ITweens) if tweens is None: tweens = Tweens() registry.registerUtility(tweens, ITweens) + ex_intr = self.introspectable('tweens (implicit)', + ('tween', EXCVIEW, False), + EXCVIEW, 'implicit tween') + ex_intr['factory'] = excview_tween_factory + ex_intr['type'] = 'implicit' + ex_intr['under'] = None + ex_intr['over'] = MAIN + introspectables.append(ex_intr) tweens.add_implicit(EXCVIEW, excview_tween_factory, over=MAIN) def register(): @@ -154,13 +163,14 @@ class TweensConfiguratorMixin(object): discriminator = ('tween', name, explicit) tween_type = explicit and 'explicit' or 'implicit' - intr = self.introspectable('tweens', discriminator, - 'name', 'tween') + intr = self.introspectable('tweens (%s)' % tween_type, discriminator, + name, '%s tween' % tween_type) intr['factory'] = tween_factory intr['type'] = tween_type intr['under'] = under intr['over'] = over - self.action(discriminator, register, introspectables=(intr,)) + introspectables.append(intr) + self.action(discriminator, register, introspectables=introspectables) class CyclicDependencyError(Exception): def __init__(self, cycles): @@ -200,7 +210,7 @@ class Tweens(object): self.order += [(u, name) for u in under] self.req_under.add(name) if over is not None: - if not is_nonstr_iter(over): #hasattr(over, '__iter__'): + if not is_nonstr_iter(over): over = (over,) self.order += [(name, o) for o in over] self.req_over.add(name) -- cgit v1.2.3 From 57a0d7765c54031e6ac83881b536712316f22c45 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Nov 2011 12:55:41 -0500 Subject: docs; todo; coverage for Introspector --- CHANGES.txt | 6 ++ TODO.txt | 5 +- docs/api/config.rst | 15 ++- docs/api/interfaces.rst | 6 ++ docs/api/registry.rst | 25 +++++ pyramid/interfaces.py | 23 +++-- pyramid/registry.py | 24 ++--- pyramid/tests/test_registry.py | 212 +++++++++++++++++++++++++++++++++++++++++ 8 files changed, 291 insertions(+), 25 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6f30d506c..6fdb03635 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -28,6 +28,12 @@ Features - Configuration conflict reporting is reported in a more understandable way ("Line 11 in file..." vs. a repr of a tuple of similar info). +- New APIs: ``pyramid.registry.Introspectable``, + ``pyramid.config.Configurator.introspector``, + ``pyramid.config.Configurator.introspectable``, + ``pyramid.registry.Registry.introspector``. See API docs of related + modules for more info. + Bug Fixes --------- diff --git a/TODO.txt b/TODO.txt index bf1c7773b..fbe77789a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,8 +6,9 @@ Must-Have - Introspection: - * More specific filename/lineno info instead of opaque string (or a way to - parse the opaque string into filename/lineno info). + * Narrative docs. + + * Test with pyramid_zcml (wrt action_info / actions.append). * categorize() return value ordering not right yet. diff --git a/docs/api/config.rst b/docs/api/config.rst index 9f130b7dc..dbfbb1761 100644 --- a/docs/api/config.rst +++ b/docs/api/config.rst @@ -94,16 +94,23 @@ .. automethod:: set_renderer_globals_factory(factory) - .. attribute:: introspector - - The :term:`introspector` associated with this configuration. - .. attribute:: introspectable A shortcut attribute which points to the :class:`pyramid.registry.Introspectable` class (used during directives to provide introspection to actions). + This attribute is new as of :app:`Pyramid` 1.3. + + .. attribute:: introspector + + The :term:`introspector` related to this configuration. It is an + instance implementing the :class:`pyramid.interfaces.IIntrospector` + interface. If the Configurator constructor was supplied with an + ``introspector`` argument, this attribute will be that value. + Otherwise, it will be an instance of a default introspector type. + + This attribute is new as of :app:`Pyramid` 1.3. .. attribute:: global_registries diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index b336e549d..64f2773d3 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -68,3 +68,9 @@ Other Interfaces .. autointerface:: IResponse :members: + .. autointerface:: IIntrospectable + :members: + + .. autointerface:: IIntrospector + :members: + diff --git a/docs/api/registry.rst b/docs/api/registry.rst index 4d327370a..3dbf73a67 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -14,3 +14,28 @@ accessed as ``request.registry.settings`` or ``config.registry.settings`` in a typical Pyramid application. + .. attribute:: introspector + + When a registry is set up (or created) by a :term:`Configurator`, the + registry will be decorated with an instance named ``introspector`` + implementing the :class:`pyramid.interfaces.IIntrospector` interface. + See also :attr:`pyramid.config.Configurator.introspector``. + + When a registry is created "by hand", however, this attribute will not + exist until set up by a configurator. + + This attribute is often accessed as ``request.registry.introspector`` in + a typical Pyramid application. + + This attribute is new as of :app:`Pyramid` 1.3. + +.. class:: Introspectable + + The default implementation of the interface + :class:`pyramid.interfaces.IIntrospectable` used by framework exenders. + An instance of this class is is created when + :attr:`pyramid.config.Configurator.introspectable` is called. + + This class is new as of :app:`Pyramid` 1.3. + + diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index d46a46af0..6bb0c6738 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -865,13 +865,13 @@ class IIntrospector(Interface): 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): + def get_category(category_name, sort_key=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 + category associated with ``category_name`` . If ``sort_key`` is ``None``, the sequence will be returned in the order the - introspectables were added to the introspector. Otherwise, sort_fn + introspectables were added to the introspector. Otherwise, sort_key should be a function that accepts an IIntrospectable and returns a value from it (ala the ``key`` function of Python's ``sorted`` callable).""" @@ -880,13 +880,13 @@ class IIntrospector(Interface): """ Return a sorted sequence of category names known by this introspector """ - def categorized(sort_fn=None): + def categorized(sort_key=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 + introspectables. If ``sort_key`` 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 + to the introspector. Otherwise, sort_key should be a function that accepts an IIntrospectable and returns a value from it (ala the ``key`` function of Python's ``sorted`` callable).""" @@ -941,7 +941,8 @@ 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").""" + ``collections.MutableMapping`` (the "dictionary interface"), and must be + hashable.""" title = Attribute('Text title describing this introspectable') type_name = Attribute('Text type name describing this introspectable') @@ -987,6 +988,14 @@ class IIntrospectable(Interface): (category_name, discriminator)) """ + def __hash__(): + + """ Introspectables must be hashable. The typical implementation of + an introsepectable's __hash__ is:: + + return hash((self.category_name,) + (self.discriminator,)) + """ + # 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. diff --git a/pyramid/registry.py b/pyramid/registry.py index b081980b0..813cde715 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -103,32 +103,32 @@ class Introspector(object): 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') + def get_category(self, category_name, sort_key=None): + if sort_key is None: + sort_key = operator.attrgetter('order') category = self._categories[category_name] values = category.values() - values = sorted(set(values), key=sort_fn) + values = sorted(set(values), key=sort_key) 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): + def categorized(self, sort_key=None): L = [] for category_name in self.categories(): - L.append((category_name, self.get_category(category_name, sort_fn))) + L.append((category_name, self.get_category(category_name,sort_key))) return L + def categories(self): + return sorted(self._categories.keys()) + def remove(self, category_name, discriminator): intr = self.get(category_name, discriminator) if intr is None: return - L = self._refs.pop((category_name, discriminator), []) + L = self._refs.pop(intr, []) for d in L: L2 = self._refs[d] - L2.remove((category_name, discriminator)) + L2.remove(intr) category = self._categories[intr.category_name] del category[intr.discriminator] del category[intr.discriminator_hash] @@ -170,7 +170,7 @@ class Introspector(object): class Introspectable(dict): order = 0 # mutated by introspector.add - action_info = '' # mutated by introspectable.register + action_info = None # mutated by introspectable.register def __init__(self, category_name, discriminator, title, type_name): self.category_name = category_name diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py index c3104bd31..5b2152d3a 100644 --- a/pyramid/tests/test_registry.py +++ b/pyramid/tests/test_registry.py @@ -42,11 +42,223 @@ class TestRegistry(unittest.TestCase): registry.settings = 'foo' self.assertEqual(registry._settings, 'foo') +class TestIntrospector(unittest.TestCase): + def _makeOne(self): + from pyramid.registry import Introspector + return Introspector() + + def test_add(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(intr.order, 0) + category = {'discriminator':intr, 'discriminator_hash':intr} + self.assertEqual(inst._categories, {'category':category}) + + def test_get_success(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'discriminator'), intr) + + def test_get_success_byhash(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'discriminator_hash'), intr) + + def test_get_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'wontexist', 'foo'), 'foo') + + def test_get_category(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr2) + inst.add(intr) + expected = [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ] + self.assertEqual(inst.get_category('category'), expected) + + def test_get_category_with_sortkey(self): + import operator + inst = self._makeOne() + intr = DummyIntrospectable() + intr.foo = 2 + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + intr2.foo = 1 + inst.add(intr) + inst.add(intr2) + expected = [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ] + self.assertEqual( + inst.get_category('category', operator.attrgetter('foo')), + expected) + + def test_categorized(self): + import operator + inst = self._makeOne() + intr = DummyIntrospectable() + intr.foo = 2 + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + intr2.foo = 1 + inst.add(intr) + inst.add(intr2) + expected = [('category', [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ])] + self.assertEqual( + inst.categorized(operator.attrgetter('foo')), expected) + + def test_categories(self): + inst = self._makeOne() + inst._categories['a'] = 1 + inst._categories['b'] = 2 + self.assertEqual(list(inst.categories()), ['a', 'b']) + + def test_remove(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + inst.remove('category', 'discriminator') + self.assertEqual(inst._categories, + {'category': + {}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs.get(intr), None) + self.assertEqual(inst._refs[intr2], []) + + def test_remove_fail(self): + inst = self._makeOne() + self.assertEqual(inst.remove('a', 'b'), None) + + def test_relate(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst._categories, + {'category': + {'discriminator':intr, + 'discriminator_hash':intr}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs[intr], [intr2]) + self.assertEqual(inst._refs[intr2], [intr]) + + def test_relate_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertRaises( + KeyError, + inst.relate, + ('category', 'discriminator'), + ('category2', 'discriminator2') + ) + + def test_unrelate(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + inst.unrelate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst._categories, + {'category': + {'discriminator':intr, + 'discriminator_hash':intr}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs[intr], []) + self.assertEqual(inst._refs[intr2], []) + + def test_related(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst.related(intr), [intr2]) + + def test_related_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + del inst._categories['category'] + self.assertRaises(KeyError, inst.related, intr) + + class DummyModule: __path__ = "foo" __name__ = "dummy" __file__ = '' +class DummyIntrospectable(object): + category_name = 'category' + discriminator = 'discriminator' + title = 'title' + type_name = 'type' + order = None + action_info = None + discriminator_hash = 'discriminator_hash' + + def __hash__(self): + return hash((self.category_name,) + (self.discriminator,)) + + from zope.interface import Interface from zope.interface import implementer class IDummyEvent(Interface): -- cgit v1.2.3 From 82ba1088e98d6e33136b5e78f87ac02d0daa7879 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Nov 2011 13:00:43 -0500 Subject: fix set repr on py3 --- pyramid/tests/test_util.py | 5 ++++- pyramid/util.py | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index f45c75535..d010e6653 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -279,7 +279,10 @@ class Test_object_description(unittest.TestCase): self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')") def test_set(self): - self.assertEqual(self._callFUT(set(['a'])), "set(['a'])") + if PY3: + self.assertEqual(self._callFUT(set(['a'])), "{'a'}") + else: + self.assertEqual(self._callFUT(set(['a'])), "set(['a'])") def test_list(self): self.assertEqual(self._callFUT(['a']), "['a']") diff --git a/pyramid/util.py b/pyramid/util.py index 1fd612d09..f22f847c4 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -7,6 +7,7 @@ from pyramid.compat import ( integer_types, string_types, text_, + PY3, ) from pyramid.exceptions import ConfigurationError @@ -264,7 +265,12 @@ def object_description(object): return text_(str(object)) if isinstance(object, (bool, float, type(None))): return text_(str(object)) - if isinstance(object, (tuple, set)): + if isinstance(object, set): + if PY3: # pragma: no cover + return shortrepr(object, '}') + else: + return shortrepr(object, ')') + if isinstance(object, tuple): return shortrepr(object, ')') if isinstance(object, list): return shortrepr(object, ']') -- cgit v1.2.3 From 57a9d679eb78e774c271bf68f6e805dc2b8186c4 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Nov 2011 13:55:02 -0500 Subject: add tests for introspectable; add more interface docs and expose actioninfo --- docs/api/interfaces.rst | 2 + pyramid/interfaces.py | 27 +++++++----- pyramid/registry.py | 29 ++++++------- pyramid/tests/test_registry.py | 93 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 125 insertions(+), 26 deletions(-) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index 64f2773d3..5b190b53b 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -74,3 +74,5 @@ Other Interfaces .. autointerface:: IIntrospector :members: + .. autointerface:: IActionInfo + :members: diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 6bb0c6738..2576b4e35 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -897,11 +897,10 @@ class IIntrospector(Interface): an error if an introspectable related to the category name and discriminator does not exist.""" - def related(category_name, discriminator): + def related(intr): """ Return a sequence of IIntrospectables related to the - IIntrospectable associated with (``category_name``, - ``discriminator``). Return the empty sequence if no relations for - exist.""" + IIntrospectable ``intr``. Return the empty sequence if no relations + for exist.""" def add(intr): """ Add the IIntrospectable ``intr`` (use instead of @@ -968,14 +967,14 @@ class IIntrospectable(Interface): the ``category_name`` and ``discriminator``) during action execution. """ - def register(introspector, action_info=''): + def register(introspector, action_info): """ Register this IIntrospectable with an introspector. This method is invoked during action execution. Adds the introspectable and its - relations to the introspector. ``introspector`` should be an - object implementing IIntrospector. ``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: + relations to the introspector. ``introspector`` should be an object + implementing IIntrospector. ``action_info`` should be a object + implementing the interface :class:`pyramid.interfaces.IActionInfo` + representing the call that registered this introspectable. + Pseudocode for an implementation of this method: .. code-block:: python @@ -996,6 +995,14 @@ class IIntrospectable(Interface): return hash((self.category_name,) + (self.discriminator,)) """ +class IActionInfo(Interface): + filename = Attribute('filename as a string') + lineno = Attribute('line number in file as an integer') + function = Attribute('a string representing the function or method ' + 'that was executing') + linerepr = Attribute('a string representing the call site ' + 'which caused the action to be executed') + # 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. diff --git a/pyramid/registry.py b/pyramid/registry.py index 813cde715..a6d70bfa8 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -170,7 +170,7 @@ class Introspector(object): class Introspectable(dict): order = 0 # mutated by introspector.add - action_info = None # mutated by introspectable.register + action_info = None # mutated by self.register def __init__(self, category_name, discriminator, title, type_name): self.category_name = category_name @@ -185,19 +185,6 @@ class Introspectable(dict): def unrelate(self, category_name, discriminator): self._relations.append((False, category_name, discriminator)) - def register(self, introspector, action_info): - self.action_info = action_info - introspector.add(self) - for relate, category_name, discriminator in self._relations: - if relate: - method = introspector.relate - else: - method = introspector.unrelate - method( - (self.category_name, self.discriminator), - (category_name, discriminator) - ) - @property def discriminator_hash(self): return hash(self.discriminator) @@ -215,4 +202,18 @@ class Introspectable(dict): __bool__ = __nonzero__ # py3 + def register(self, introspector, action_info): + self.action_info = action_info + introspector.add(self) + for relate, category_name, discriminator in self._relations: + if relate: + method = introspector.relate + else: + method = introspector.unrelate + method( + (self.category_name, self.discriminator), + (category_name, discriminator) + ) + + global_registry = Registry('global') diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py index 5b2152d3a..3d68688d1 100644 --- a/pyramid/tests/test_registry.py +++ b/pyramid/tests/test_registry.py @@ -43,9 +43,19 @@ class TestRegistry(unittest.TestCase): self.assertEqual(registry._settings, 'foo') class TestIntrospector(unittest.TestCase): - def _makeOne(self): + def _getTargetClass(slf): from pyramid.registry import Introspector - return Introspector() + return Introspector + + def _makeOne(self): + return self._getTargetClass()() + + def test_conformance(self): + from zope.interface.verify import verifyClass + from zope.interface.verify import verifyObject + from pyramid.interfaces import IIntrospector + verifyClass(IIntrospector, self._getTargetClass()) + verifyObject(IIntrospector, self._makeOne()) def test_add(self): inst = self._makeOne() @@ -240,6 +250,85 @@ class TestIntrospector(unittest.TestCase): del inst._categories['category'] self.assertRaises(KeyError, inst.related, intr) +class TestIntrospectable(unittest.TestCase): + def _getTargetClass(slf): + from pyramid.registry import Introspectable + return Introspectable + + def _makeOne(self, *arg, **kw): + return self._getTargetClass()(*arg, **kw) + + def _makeOnePopulated(self): + return self._makeOne('category', 'discrim', 'title', 'type') + + def test_conformance(self): + from zope.interface.verify import verifyClass + from zope.interface.verify import verifyObject + from pyramid.interfaces import IIntrospectable + verifyClass(IIntrospectable, self._getTargetClass()) + verifyObject(IIntrospectable, self._makeOnePopulated()) + + def test_relate(self): + inst = self._makeOnePopulated() + inst.relate('a', 'b') + self.assertEqual(inst._relations, [(True, 'a', 'b')]) + + def test_unrelate(self): + inst = self._makeOnePopulated() + inst.unrelate('a', 'b') + self.assertEqual(inst._relations, [(False, 'a', 'b')]) + + def test_discriminator_hash(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.discriminator_hash, hash(inst.discriminator)) + + def test___hash__(self): + inst = self._makeOnePopulated() + self.assertEqual(hash(inst), + hash((inst.category_name,) + (inst.discriminator,))) + + def test___repr__(self): + inst = self._makeOnePopulated() + self.assertEqual( + repr(inst), + "") + + def test___nonzero__(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.__nonzero__(), True) + + def test___bool__(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.__bool__(), True) + + def test_register(self): + introspector = DummyIntrospector() + action_info = object() + inst = self._makeOnePopulated() + inst._relations.append((True, 'category1', 'discrim1')) + inst._relations.append((False, 'category2', 'discrim2')) + inst.register(introspector, action_info) + self.assertEqual(inst.action_info, action_info) + self.assertEqual(introspector.intrs, [inst]) + self.assertEqual(introspector.relations, + [(('category', 'discrim'), ('category1', 'discrim1'))]) + self.assertEqual(introspector.unrelations, + [(('category', 'discrim'), ('category2', 'discrim2'))]) + +class DummyIntrospector(object): + def __init__(self): + self.intrs = [] + self.relations = [] + self.unrelations = [] + + def add(self, intr): + self.intrs.append(intr) + + def relate(self, *pairs): + self.relations.append(pairs) + + def unrelate(self, *pairs): + self.unrelations.append(pairs) class DummyModule: __path__ = "foo" -- cgit v1.2.3 From 2e651ed3c6cae62f1d7e9829e3f86f258dce133b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Nov 2011 17:32:55 -0500 Subject: more tests for introspectables --- pyramid/config/__init__.py | 18 +-- pyramid/interfaces.py | 13 ++- pyramid/tests/test_config/test_init.py | 205 +++++++++++++++++++++++++++++++-- pyramid/tests/test_config/test_util.py | 10 ++ pyramid/tests/test_util.py | 4 +- 5 files changed, 221 insertions(+), 29 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index d4dc0247f..31f35e5ff 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -69,7 +69,10 @@ from pyramid.config.security import SecurityConfiguratorMixin from pyramid.config.settings import SettingsConfiguratorMixin from pyramid.config.testing import TestingConfiguratorMixin from pyramid.config.tweens import TweensConfiguratorMixin -from pyramid.config.util import action_method +from pyramid.config.util import ( + action_method, + ActionInfo, + ) from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin @@ -491,7 +494,7 @@ class Configurator( if self._ainfo: info = self._ainfo[0] else: - info = '' + info = ActionInfo('', 0, '', '') return info def action(self, discriminator, callable=None, args=(), kw=None, order=None, @@ -918,7 +921,7 @@ class ActionState(object): return True def action(self, discriminator, callable=None, args=(), kw=None, order=None, - includepath=(), info='', introspectables=()): + includepath=(), info=None, introspectables=()): """Add an action with the given discriminator, callable and arguments """ if kw is None: @@ -976,13 +979,11 @@ class ActionState(object): in: oops - Note that actions executed before the error still have an effect: >>> output [('f', (1,), {}), ('f', (2,), {})] - """ try: @@ -992,10 +993,9 @@ class ActionState(object): kw = action['kw'] info = action['info'] introspectables = action['introspectables'] - if callable is None: - continue try: - callable(*args, **kw) + if callable is not None: + callable(*args, **kw) except (KeyboardInterrupt, SystemExit): # pragma: no cover raise except: @@ -1073,7 +1073,7 @@ def resolveConflicts(actions): return output def expand_action(discriminator, callable=None, args=(), kw=None, - includepath=(), info='', order=None, introspectables=()): + includepath=(), info=None, order=None, introspectables=()): if kw is None: kw = {} return dict( diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 2576b4e35..05881571e 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -996,12 +996,13 @@ class IIntrospectable(Interface): """ class IActionInfo(Interface): - filename = Attribute('filename as a string') - lineno = Attribute('line number in file as an integer') - function = Attribute('a string representing the function or method ' - 'that was executing') - linerepr = Attribute('a string representing the call site ' - 'which caused the action to be executed') + filename = Attribute('filename of action-invoking code as a string') + lineno = Attribute('line number in file (as an integer) of action-invoking ' + 'code') + function = Attribute('a string representing the module, function or method ' + 'that enclosed the line which invoked the action') + linerepr = Attribute('a string representing the source code line ' + 'which invoked the action') # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index e80557096..86dace822 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -228,6 +228,12 @@ class ConfiguratorTests(unittest.TestCase): request_iface=IRequest) self.assertTrue(view.__wraps__ is exceptionresponse_view) + def test_ctor_with_introspector(self): + introspector = DummyIntrospector() + config = self._makeOne(introspector=introspector) + self.assertEqual(config.introspector, introspector) + self.assertEqual(config.registry.introspector, introspector) + def test_with_package_module(self): from pyramid.tests.test_config import test_init import pyramid.tests @@ -637,6 +643,21 @@ pyramid.tests.test_config.dummy_include2""", [('pyramid.tests.test_config.dummy_tween_factory', dummy_tween_factory)]) + def test_introspector_decorator(self): + inst = self._makeOne() + default = inst.introspector + self.failUnless(hasattr(default, 'add')) + self.assertEqual(inst.introspector, inst.registry.introspector) + introspector = DummyIntrospector() + inst.introspector = introspector + new = inst.introspector + self.failUnless(new is introspector) + self.assertEqual(inst.introspector, inst.registry.introspector) + del inst.introspector + default = inst.introspector + self.failIf(default is new) + self.failUnless(hasattr(default, 'add')) + def test_make_wsgi_app(self): import pyramid.config from pyramid.router import Router @@ -740,6 +761,15 @@ pyramid.tests.test_config.dummy_include2""", config = self._makeOne(autocommit=True) self.assertEqual(config.action('discrim', kw={'a':1}), None) + def test_action_autocommit_with_introspectables(self): + from pyramid.config.util import ActionInfo + config = self._makeOne(autocommit=True) + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(len(intr.registered), 1) + self.assertEqual(intr.registered[0][0], config.introspector) + self.assertEqual(intr.registered[0][1].__class__, ActionInfo) + def test_action_branching_nonautocommit_with_config_info(self): config = self._makeOne(autocommit=False) config.info = 'abc' @@ -779,6 +809,18 @@ pyramid.tests.test_config.dummy_include2""", 'kw': {'a': 1}, 'order': None})]) + def test_action_branching_nonautocommit_with_introspectables(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = [] + state = DummyActionState() + config.action_state = state + state.autocommit = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual( + state.actions[0][1]['introspectables'], (intr,)) + def test_scan_integration(self): from zope.interface import alsoProvides from pyramid.interfaces import IRequest @@ -1398,7 +1440,7 @@ class TestActionState(unittest.TestCase): 'callable': f, 'discriminator': 1, 'includepath': (), - 'info': '', + 'info': None, 'introspectables': (), 'kw': {'x': 1}, 'order': None}]) @@ -1409,7 +1451,7 @@ class TestActionState(unittest.TestCase): 'callable': f, 'discriminator': 1, 'includepath': (), - 'info': '', + 'info': None, 'introspectables': (), 'kw': {'x': 1}, 'order': None}, @@ -1418,7 +1460,7 @@ class TestActionState(unittest.TestCase): 'callable': None, 'discriminator': None, 'includepath': (), - 'info': '', + 'info': None, 'introspectables': (), 'kw': {}, 'order': None},]) @@ -1433,7 +1475,7 @@ class TestActionState(unittest.TestCase): 'callable': None, 'discriminator': None, 'includepath': ('abc',), - 'info': '', + 'info': None, 'introspectables': (), 'kw': {}, 'order': None}]) @@ -1476,20 +1518,36 @@ class TestActionState(unittest.TestCase): 'callable': None, 'discriminator': None, 'includepath': (), - 'info': '', + 'info': None, 'introspectables': (), 'kw': {}, 'order': 99999}]) + def test_action_with_introspectables(self): + c = self._makeOne() + c.actions = [] + intr = DummyIntrospectable() + c.action(None, introspectables=(intr,)) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (intr,), + 'kw': {}, + 'order': None}]) + def test_processSpec(self): c = self._makeOne() self.assertTrue(c.processSpec('spec')) self.assertFalse(c.processSpec('spec')) - def test_execute_actions_simple(self): + def test_execute_actions_tuples(self): output = [] def f(*a, **k): - output.append(('f', a, k)) + output.append((a, k)) c = self._makeOne() c.actions = [ (1, f, (1,)), @@ -1498,7 +1556,57 @@ class TestActionState(unittest.TestCase): (None, None), ] c.execute_actions() - self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})]) + self.assertEqual(output, [((1,), {}), ((2,), {})]) + + def test_execute_actions_dicts(self): + output = [] + def f(*a, **k): + output.append((a, k)) + c = self._makeOne() + c.actions = [ + {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{}, + 'order':None, 'includepath':(), 'info':None, + 'introspectables':()}, + {'discriminator':1, 'callable':f, 'args':(11,), 'kw':{}, + 'includepath':('x',), 'order': None, 'info':None, + 'introspectables':()}, + {'discriminator':2, 'callable':f, 'args':(2,), 'kw':{}, + 'order':None, 'includepath':(), 'info':None, + 'introspectables':()}, + {'discriminator':None, 'callable':None, 'args':(), 'kw':{}, + 'order':None, 'includepath':(), 'info':None, + 'introspectables':()}, + ] + c.execute_actions() + self.assertEqual(output, [((1,), {}), ((2,), {})]) + + def test_execute_actions_with_introspectables(self): + output = [] + def f(*a, **k): + output.append((a, k)) + c = self._makeOne() + intr = DummyIntrospectable() + c.actions = [ + {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{}, + 'order':None, 'includepath':(), 'info':None, + 'introspectables':(intr,)}, + ] + introspector = DummyIntrospector() + c.execute_actions(introspector=introspector) + self.assertEqual(output, [((1,), {})]) + self.assertEqual(intr.registered, [(introspector, None)]) + + def test_execute_actions_with_introspectable_no_callable(self): + c = self._makeOne() + intr = DummyIntrospectable() + c.actions = [ + {'discriminator':1, 'callable':None, 'args':(1,), 'kw':{}, + 'order':None, 'includepath':(), 'info':None, + 'introspectables':(intr,)}, + ] + introspector = DummyIntrospector() + c.execute_actions(introspector=introspector) + self.assertEqual(intr.registered, [(introspector, None)]) def test_execute_actions_error(self): output = [] @@ -1521,7 +1629,7 @@ class Test_resolveConflicts(unittest.TestCase): from pyramid.config import resolveConflicts return resolveConflicts(actions) - def test_it_success(self): + def test_it_success_tuples(self): from pyramid.tests.test_config import dummyfactory as f result = self._callFUT([ (None, f), @@ -1534,7 +1642,68 @@ class Test_resolveConflicts(unittest.TestCase): ]) self.assertEqual( result, - [{'info': '', + [{'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 1}, + + {'info': None, + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': ('y',), + 'order': 5}, + + {'info': None, + '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_success_dicts(self): + from pyramid.tests.test_config import dummyfactory as f + from pyramid.config import expand_action + result = self._callFUT([ + expand_action(None, f), + expand_action(1, f, (1,), {}, (), 'first'), + expand_action(1, f, (2,), {}, ('x',), 'second'), + expand_action(1, f, (3,), {}, ('y',), 'third'), + expand_action(4, f, (4,), {}, ('y',), 'should be last', 99999), + expand_action(3, f, (3,), {}, ('y',)), + expand_action(None, f, (5,), {}, ('y',)), + ]) + self.assertEqual( + result, + [{'info': None, 'args': (), 'callable': f, 'introspectables': (), @@ -1552,7 +1721,7 @@ class Test_resolveConflicts(unittest.TestCase): 'includepath': (), 'order': 1}, - {'info': '', + {'info': None, 'args': (3,), 'callable': f, 'introspectables': (), @@ -1561,7 +1730,7 @@ class Test_resolveConflicts(unittest.TestCase): 'includepath': ('y',), 'order': 5}, - {'info': '', + {'info': None, 'args': (5,), 'callable': f, 'introspectables': (), @@ -1700,3 +1869,15 @@ class DummyZCMLContext(object): includepath = () info = '' +class DummyIntrospector(object): + def __init__(self): + self.intrs = [] + def add(self, intr): + self.intrs.append(intr) + +class DummyIntrospectable(object): + def __init__(self): + self.registered = [] + def register(self, introspector, action_info): + self.registered.append((introspector, action_info)) + diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index bc7cf0a82..1225b3e21 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -312,6 +312,16 @@ class Test__make_predicates(unittest.TestCase): hash2, _, __= self._callFUT(request_method='GET') self.assertEqual(hash1, hash2) +class TestActionInfo(unittest.TestCase): + def _makeOne(self, filename, lineno, function, linerepr): + from pyramid.config.util import ActionInfo + return ActionInfo(filename, lineno, function, linerepr) + + def test___str__(self): + inst = self._makeOne('filename', 'lineno', 'function', 'linerepr') + self.assertEqual(str(inst), + "Line lineno of file filename in function: 'linerepr'") + class DummyCustomPredicate(object): def __init__(self): self.__text__ = 'custom predicate' diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index d010e6653..61e372417 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -279,9 +279,9 @@ class Test_object_description(unittest.TestCase): self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')") def test_set(self): - if PY3: + if PY3: # pragma: no cover self.assertEqual(self._callFUT(set(['a'])), "{'a'}") - else: + else: # pragma: no cover self.assertEqual(self._callFUT(set(['a'])), "set(['a'])") def test_list(self): -- cgit v1.2.3 From 2cb381ceadc26ebc5cf51c28ecba572a05300d8a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Wed, 30 Nov 2011 17:58:44 -0500 Subject: garden --- TODO.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index fbe77789a..abcf08dd0 100644 --- a/TODO.txt +++ b/TODO.txt @@ -17,8 +17,6 @@ Must-Have * introspection hiding for directives? - * usage docs. - * make it possible to disuse introspection? - Give discriminators a nicer repr for conflict reporting? -- cgit v1.2.3 From ae0ff29c220bf7f4cd253ae94855c3a33bf2a497 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 1 Dec 2011 13:30:44 -0500 Subject: minor cleanups --- pyramid/config/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 31f35e5ff..ff0e3581d 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -993,6 +993,7 @@ class ActionState(object): kw = action['kw'] info = action['info'] introspectables = action['introspectables'] + try: if callable is not None: callable(*args, **kw) @@ -1006,6 +1007,7 @@ class ActionState(object): tb) finally: del t, v, tb + if introspector is not None: for introspectable in introspectables: introspectable.register(introspector, info) @@ -1054,17 +1056,18 @@ def resolveConflicts(actions): 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'] + first = dups[0] + output.append(first) + basepath = first['includepath'] + baseinfo = first['info'] + discriminator = first['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): - L = conflicts.setdefault(discriminator, [baseinfo]) - L.append(dup['info']) + infos = conflicts.setdefault(discriminator, [baseinfo]) + infos.append(dup['info']) if conflicts: raise ConfigurationConflictError(conflicts) -- cgit v1.2.3 From 73b206decf517c806fb16de4b7e3b404e596b81c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Fri, 2 Dec 2011 13:19:59 -0500 Subject: fbo pyramid_zcml: default order back to 0, don't rely on existence of introspectables --- pyramid/config/__init__.py | 12 +++++++----- pyramid/tests/test_config/test_init.py | 30 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index ff0e3581d..40f061897 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -497,7 +497,7 @@ class Configurator( info = ActionInfo('', 0, '', '') return info - def action(self, discriminator, callable=None, args=(), kw=None, order=None, + def action(self, discriminator, callable=None, args=(), kw=None, order=0, introspectables=()): """ Register an action which will be executed when :meth:`pyramid.config.Configurator.commit` is called (or executed @@ -920,7 +920,7 @@ class ActionState(object): self._seen_files.add(spec) return True - def action(self, discriminator, callable=None, args=(), kw=None, order=None, + def action(self, discriminator, callable=None, args=(), kw=None, order=0, includepath=(), info=None, introspectables=()): """Add an action with the given discriminator, callable and arguments """ @@ -992,7 +992,9 @@ class ActionState(object): args = action['args'] kw = action['kw'] info = action['info'] - introspectables = action['introspectables'] + # we use "get" below in case an action was added via a ZCML + # directive that did not know about introspectables + introspectables = action.get('introspectables', ()) try: if callable is not None: @@ -1037,7 +1039,7 @@ def resolveConflicts(actions): # old-style ZCML tuple action action = expand_action(*action) order = action['order'] - if order is None: + if not order: action['order'] = i discriminator = action['discriminator'] if discriminator is None: @@ -1076,7 +1078,7 @@ def resolveConflicts(actions): return output def expand_action(discriminator, callable=None, args=(), kw=None, - includepath=(), info=None, order=None, introspectables=()): + includepath=(), info=None, order=0, introspectables=()): if kw is None: kw = {} return dict( diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 86dace822..94ff4348a 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -787,7 +787,7 @@ pyramid.tests.test_config.dummy_include2""", 'info': 'abc', 'introspectables': (), 'kw': {'a': 1}, - 'order': None})]) + 'order': 0})]) def test_action_branching_nonautocommit_without_config_info(self): config = self._makeOne(autocommit=False) @@ -807,7 +807,7 @@ pyramid.tests.test_config.dummy_include2""", 'info': 'z', 'introspectables': (), 'kw': {'a': 1}, - 'order': None})]) + 'order': 0})]) def test_action_branching_nonautocommit_with_introspectables(self): config = self._makeOne(autocommit=False) @@ -1443,7 +1443,7 @@ class TestActionState(unittest.TestCase): 'info': None, 'introspectables': (), 'kw': {'x': 1}, - 'order': None}]) + 'order': 0}]) c.action(None) self.assertEqual( c.actions, @@ -1454,7 +1454,7 @@ class TestActionState(unittest.TestCase): 'info': None, 'introspectables': (), 'kw': {'x': 1}, - 'order': None}, + 'order': 0}, {'args': (), 'callable': None, @@ -1463,7 +1463,7 @@ class TestActionState(unittest.TestCase): 'info': None, 'introspectables': (), 'kw': {}, - 'order': None},]) + 'order': 0},]) def test_action_with_includepath(self): c = self._makeOne() @@ -1478,7 +1478,7 @@ class TestActionState(unittest.TestCase): 'info': None, 'introspectables': (), 'kw': {}, - 'order': None}]) + 'order': 0}]) def test_action_with_info(self): c = self._makeOne() @@ -1492,7 +1492,7 @@ class TestActionState(unittest.TestCase): 'info': 'abc', 'introspectables': (), 'kw': {}, - 'order': None}]) + 'order': 0}]) def test_action_with_includepath_and_info(self): c = self._makeOne() @@ -1506,7 +1506,7 @@ class TestActionState(unittest.TestCase): 'info': 'bleh', 'introspectables': (), 'kw': {}, - 'order': None}]) + 'order': 0}]) def test_action_with_order(self): c = self._makeOne() @@ -1537,7 +1537,7 @@ class TestActionState(unittest.TestCase): 'info': None, 'introspectables': (intr,), 'kw': {}, - 'order': None}]) + 'order': 0}]) def test_processSpec(self): c = self._makeOne() @@ -1565,16 +1565,16 @@ class TestActionState(unittest.TestCase): c = self._makeOne() c.actions = [ {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{}, - 'order':None, 'includepath':(), 'info':None, + 'order':0, 'includepath':(), 'info':None, 'introspectables':()}, {'discriminator':1, 'callable':f, 'args':(11,), 'kw':{}, - 'includepath':('x',), 'order': None, 'info':None, + 'includepath':('x',), 'order': 0, 'info':None, 'introspectables':()}, {'discriminator':2, 'callable':f, 'args':(2,), 'kw':{}, - 'order':None, 'includepath':(), 'info':None, + 'order':0, 'includepath':(), 'info':None, 'introspectables':()}, {'discriminator':None, 'callable':None, 'args':(), 'kw':{}, - 'order':None, 'includepath':(), 'info':None, + 'order':0, 'includepath':(), 'info':None, 'introspectables':()}, ] c.execute_actions() @@ -1588,7 +1588,7 @@ class TestActionState(unittest.TestCase): intr = DummyIntrospectable() c.actions = [ {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{}, - 'order':None, 'includepath':(), 'info':None, + 'order':0, 'includepath':(), 'info':None, 'introspectables':(intr,)}, ] introspector = DummyIntrospector() @@ -1601,7 +1601,7 @@ class TestActionState(unittest.TestCase): intr = DummyIntrospectable() c.actions = [ {'discriminator':1, 'callable':None, 'args':(1,), 'kw':{}, - 'order':None, 'includepath':(), 'info':None, + 'order':0, 'includepath':(), 'info':None, 'introspectables':(intr,)}, ] introspector = DummyIntrospector() -- cgit v1.2.3 From c4503bf117e43f780c269e64edbde71fc3d6d72b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 01:56:17 -0500 Subject: break out 'extending config' into exconfig and add stuff about the action method; move startup and router chapters to earlier in toc --- docs/glossary.rst | 42 +++++++++- docs/index.rst | 5 +- docs/latexindex.rst | 6 +- docs/narr/advconfig.rst | 77 +---------------- docs/narr/extconfig.rst | 219 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 267 insertions(+), 82 deletions(-) create mode 100644 docs/narr/extconfig.rst diff --git a/docs/glossary.rst b/docs/glossary.rst index 39933cf4c..95ca1f20a 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -563,9 +563,8 @@ Glossary also `PEP 318 `_. configuration declaration - An individual method call made to an instance of a :app:`Pyramid` - :term:`Configurator` object which performs an arbitrary action, such as - registering a :term:`view configuration` (via the + An individual method call made to a :term:`configuration directive`, + such as registering a :term:`view configuration` (via the :meth:`~pyramid.config.Configurator.add_view` method of the configurator) or :term:`route configuration` (via the :meth:`~pyramid.config.Configurator.add_route` method of the @@ -941,3 +940,40 @@ Glossary directory of a Python installation or virtualenv as the result of running ``setup.py install`` or ``setup.py develop``. + introspector + An object with the methods described by + :class:`pyramid.interfaces.IIntrospector` that is available in both + configuration code (for registration) and at runtime (for querying) that + allows a developer to introspect configuration statements and + relationships between those statements. + + conflict resolution + Pyramid attempts to resolve ambiguous configuration statements made by + application developers via automatic conflict resolution. Automatic + conflict resolution is described in + :ref:`automatic_conflict_resolution`. If Pyramid cannot resolve + ambiguous configuration statements, it is possible to manually resolve + them as described in :ref:`manually_resolving_conflicts`. + + configuration directive + A method of the :term:`Configurator` which causes a configuration action + to occur. The method :meth:`pyramid.config.Configurator.add_view` is a + configuration directive, and application developers can add their own + directives as necessary (see :ref:`add_directive`). + + action + Represents a pending configuration statement generated by a call to a + :term:`configuration directive`. The set of pending configuration + actions are processed when :meth:`pyramid.config.Configurator.commit` is + called. + + discriminator + The unique identifier of an :term:`action`. + + introspectable + An object which implements the attributes and methods described in + :class:`pyramid.interfaces.IIntrospectable`. Introspectables are used + by the :term:`introspector` to display configuration information about + a running Pyramid application. An introspectable is associated with a + :term:`action` by virtue of the + :meth:`pyramid.config.Configurator.action` method. diff --git a/docs/index.rst b/docs/index.rst index e4de8b0c8..f07e8eac2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -65,6 +65,7 @@ Narrative documentation in chapter form explaining how to use narr/configuration narr/project narr/startup + narr/router narr/urldispatch narr/views narr/renderers @@ -87,9 +88,9 @@ Narrative documentation in chapter form explaining how to use narr/security narr/hybrid narr/hooks - narr/advconfig narr/extending - narr/router + narr/advconfig + narr/extconfig narr/threadlocals narr/zca diff --git a/docs/latexindex.rst b/docs/latexindex.rst index 584dd3825..d74dbba79 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -31,6 +31,8 @@ Narrative Documentation narr/configuration narr/firstapp narr/project + narr/startup + narr/router narr/urldispatch narr/views narr/renderers @@ -53,9 +55,9 @@ Narrative Documentation narr/security narr/hybrid narr/hooks - narr/advconfig narr/extending - narr/startup + narr/advconfig + narr/extconfig narr/threadlocals narr/zca diff --git a/docs/narr/advconfig.rst b/docs/narr/advconfig.rst index 3b6f7669a..3a7bf2805 100644 --- a/docs/narr/advconfig.rst +++ b/docs/narr/advconfig.rst @@ -13,8 +13,6 @@ also, by default, performs configuration in two separate phases. This allows you to ignore relative configuration statement ordering in some circumstances. -Pyramid also allows you to extend its Configurator with custom directives. - .. index:: pair: configuration; conflict detection @@ -117,6 +115,8 @@ Conflict detection happens for any kind of configuration: imperative configuration or configuration that results from the execution of a :term:`scan`. +.. _manually_resolving_conflicts: + Manually Resolving Conflicts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -399,76 +399,3 @@ constraints: the routes they imply require relative ordering. Such ordering constraints are not absolved by two-phase configuration. Routes are still added in configuration execution order. -.. index:: - single: add_directive - pair: configurator; adding directives - -.. _add_directive: - -Adding Methods to the Configurator via ``add_directive`` --------------------------------------------------------- - -Framework extension writers can add arbitrary methods to a -:term:`Configurator` by using the -:meth:`pyramid.config.Configurator.add_directive` method of the configurator. -This makes it possible to extend a Pyramid configurator in arbitrary ways, -and allows it to perform application-specific tasks more succinctly. - -The :meth:`~pyramid.config.Configurator.add_directive` method accepts two -positional arguments: a method name and a callable object. The callable -object is usually a function that takes the configurator instance as its -first argument and accepts other arbitrary positional and keyword arguments. -For example: - -.. code-block:: python - :linenos: - - from pyramid.events import NewRequest - from pyramid.config import Configurator - - def add_newrequest_subscriber(config, subscriber): - config.add_subscriber(subscriber, NewRequest). - - if __name__ == '__main__': - config = Configurator() - config.add_directive('add_newrequest_subscriber', - add_newrequest_subscriber) - -Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can -then call the method by its given name as if it were a built-in method of the -Configurator: - -.. code-block:: python - :linenos: - - def mysubscriber(event): - print event.request - - config.add_newrequest_subscriber(mysubscriber) - -A call to :meth:`~pyramid.config.Configurator.add_directive` is often -"hidden" within an ``includeme`` function within a "frameworky" package meant -to be included as per :ref:`including_configuration` via -:meth:`~pyramid.config.Configurator.include`. For example, if you put this -code in a package named ``pyramid_subscriberhelpers``: - -.. code-block:: python - :linenos: - - def includeme(config) - config.add_directive('add_newrequest_subscriber', - add_newrequest_subscriber) - -The user of the add-on package ``pyramid_subscriberhelpers`` would then be -able to install it and subsequently do: - -.. code-block:: python - :linenos: - - def mysubscriber(event): - print event.request - - from pyramid.config import Configurator - config = Configurator() - config.include('pyramid_subscriberhelpers') - config.add_newrequest_subscriber(mysubscriber) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst new file mode 100644 index 000000000..4a0db85de --- /dev/null +++ b/docs/narr/extconfig.rst @@ -0,0 +1,219 @@ +.. index:: + single: extending configuration + +.. _extconfig_narr: + +Extending Pyramid Configuration +=============================== + +Pyramid allows you to extend its Configurator with custom directives. These +directives can add an :term:`action`, participate in :term:`conflict +resolution`, and can provide some number of :term:`introspectable` objects. + +.. index:: + single: add_directive + pair: configurator; adding directives + +.. _add_directive: + +Adding Methods to the Configurator via ``add_directive`` +-------------------------------------------------------- + +Framework extension writers can add arbitrary methods to a +:term:`Configurator` by using the +:meth:`pyramid.config.Configurator.add_directive` method of the configurator. +Using :meth:`~pyramid.config.Configurator.add_directive` makes it possible to +extend a Pyramid configurator in arbitrary ways, and allows it to perform +application-specific tasks more succinctly. + +The :meth:`~pyramid.config.Configurator.add_directive` method accepts two +positional arguments: a method name and a callable object. The callable +object is usually a function that takes the configurator instance as its +first argument and accepts other arbitrary positional and keyword arguments. +For example: + +.. code-block:: python + :linenos: + + from pyramid.events import NewRequest + from pyramid.config import Configurator + + def add_newrequest_subscriber(config, subscriber): + config.add_subscriber(subscriber, NewRequest). + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_newrequest_subscriber', + add_newrequest_subscriber) + +Once :meth:`~pyramid.config.Configurator.add_directive` is called, a user can +then call the added directive by its given name as if it were a built-in +method of the Configurator: + +.. code-block:: python + :linenos: + + def mysubscriber(event): + print event.request + + config.add_newrequest_subscriber(mysubscriber) + +A call to :meth:`~pyramid.config.Configurator.add_directive` is often +"hidden" within an ``includeme`` function within a "frameworky" package meant +to be included as per :ref:`including_configuration` via +:meth:`~pyramid.config.Configurator.include`. For example, if you put this +code in a package named ``pyramid_subscriberhelpers``: + +.. code-block:: python + :linenos: + + def includeme(config) + config.add_directive('add_newrequest_subscriber', + add_newrequest_subscriber) + +The user of the add-on package ``pyramid_subscriberhelpers`` would then be +able to install it and subsequently do: + +.. code-block:: python + :linenos: + + def mysubscriber(event): + print event.request + + from pyramid.config import Configurator + config = Configurator() + config.include('pyramid_subscriberhelpers') + config.add_newrequest_subscriber(mysubscriber) + +Using ``config.action`` in a Directive +-------------------------------------- + +If a custom directive can't do its work exclusively in terms of existing +configurator methods (such as +:meth:`pyramid.config.Configurator.add_subscriber`, as above), the directive +may need to make use of the :meth:`pyramid.config.Configurator.action` +method. This method adds an entry to the list of "actions" that Pyramid will +attempt to process when :meth:`pyramid.config.Configurator.commit` is called. +An action is simply a dictionary that includes a :term:`discriminator`, +possibly a callback function, and possibly other metadata used by Pyramid's +action system. + +Here's an example directive which uses the "action" method: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, jammyjam): + def register(): + config.registry.jammyjam = jammyjam + config.action('jammyjam', register) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +Fancy, but what does it do? The action method accepts a number of arguments. +In the above directive named ``add_jammyjam``, we call +:meth:`~pyramid.config.Configurator.action` with two arguments: the string +``jammyjam`` is passed as the first argument, ``discriminator`` and the +closure function named ``register`` is passed as the second argument, +named ``callable``. + +When the :meth:`~pyramid.config.Configurator.action` method is called, it +appends an action to the list of pending configuration actions. All pending +actions with the same discriminator value are potentially in conflict with +one another (see :ref:`conflict_detection`). When the +:meth:`~pyramid.config.Configurator.commit` method of the Configurator is +called (either explicitly or as the result of calling +:meth:`~pyramid.config.Configurator.make_wsgi_app`), conflicting actions are +potentially automatically resolved as per +:ref:`automatic_conflict_resolution`. If a conflict cannot be automatically +resolved, a ConfigurationConflictError is raised and application startup is +prevented. + +In our above example, therefore, if a consumer of our ``add_jammyjam`` +directive did this: + +.. code-block:: python + :linenos: + + config.add_jammyjam('first') + config.add_jammyjam('second') + +When the action list was committed, the user's application would not start, +because the discriminators of the actions generated by the two calls are in +direct conflict. Automatic conflict resolution cannot resolve the conflict, +and the user provided no intermediate +:meth:`pyramid.config.Configurator.commit` call between the calls to +``add_jammyjam`` to ensure that the successive calls did not conflict with +each other. This is the purpose of the discriminator argument to the action +method: it's used to indicate a uniqueness constraint for an action. Two +actions with the same discriminator will conflict unless the conflict is +automatically or manually resolved. A discriminator can be any hashable +object, but it is generally a string or a tuple. + +But let's imagine that a consumer of ``add_jammyjam`` used it in such a way +that no configuration conflicts are generated. + +.. code-block:: python + :linenos: + + config.add_jammyjam('first') + +What happens then? When the ``add_jammyjam`` method is called, an action is +appended to the pending actions list. When the pending configuration actions +are processed during :meth:`~pyramid.config.Configurator.commit`, and no +conflicts occur, the *callable* provided as the second argument to the +:meth:`~pyramid.config.Configurator.action` method within ``add_jammyjam`` is +called with no arguments. The callable in ``add_jammyjam`` is the +``register`` closure function. It simply sets the value +``config.registry.jammyjam`` to whatever the user passed in as the +``jammyjam`` argument to the ``add_jammyjam`` function. Therefore, the +result of the user's call to our directive will set the ``jammyjam`` +attribute of the registry to the string ``first``. A callable is used by a +directive to defer the result of a user's call to a directive until conflict +detection has had a chance to do its job. + +Other arguments exist to the :meth:`~pyramid.config.Configurator.action` +method, including ``args``, ``kw``, ``order``, and ``introspectables``. + +``args`` and ``kw`` exist as values, which, if passed, will be used as +arguments to the ``callable`` function when it is called back. For example +our directive might use them like so: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, jammyjam): + def register(*arg, **kw): + config.registry.jammyjam_args = arg + config.registry.jammyjam_kw = kw + config.registry.jammyjam = jammyjam + config.action('jammyjam', register, args=('one',), kw={'two':'two'}) + +In the above example, when this directive is used to generate an action, and +that action is committed, ``config.registry.jammyjam_args`` will be set to +``('one',)`` and ``config.registry.jammyjam_kw`` will be set to +``{'two':'two'}``. ``args`` and ``kw`` are honestly not very useful when +your ``callable`` is a closure function, because you already usually have +access to every local in the directive without needing them to be passed +back. They can be useful, however, if you don't use a closure as a callable. + +``order`` is a crude order control mechanism. ``order`` defaults to the +integer ``0``; it can be set to any other integer. All actions that share an +order will be called before other actions that share a higher order. This +makes it possible to write a directive with callable logic that relies on the +execution of the callable of another directive being done first. For +example, Pyramid's :meth:`pyramid.config.Configurator.add_view` directive +registers an action with a higher order than the +:meth:`pyramid.config.Configurator.add_route` method. Due to this, the +``add_view`` method's callable can assume that, if a ``route_name`` was +passed to it, that a route by this name was already registered by +``add_route``, and if such a route has not already been registered, it's a +configuration error (a view that names a nonexistent route via its +``route_name`` parameter will never be called). + +``introspectables`` is a sequence of :term:`introspectable` objects. Using +``introspectables`` allows you to plug into Pyramid's configuration +introspection system. + -- cgit v1.2.3 From efd490e1c5bc17da49700f2d4572e64d0a3c0c9a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 01:58:49 -0500 Subject: reword --- docs/narr/extconfig.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 4a0db85de..4468e95b4 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -115,9 +115,9 @@ Here's an example directive which uses the "action" method: Fancy, but what does it do? The action method accepts a number of arguments. In the above directive named ``add_jammyjam``, we call :meth:`~pyramid.config.Configurator.action` with two arguments: the string -``jammyjam`` is passed as the first argument, ``discriminator`` and the -closure function named ``register`` is passed as the second argument, -named ``callable``. +``jammyjam`` is passed as the first argument named ``discriminator``, and the +closure function named ``register`` is passed as the second argument named +``callable``. When the :meth:`~pyramid.config.Configurator.action` method is called, it appends an action to the list of pending configuration actions. All pending -- cgit v1.2.3 From 1c4631864bcb7eb4b81a72fb305f72edc86f56ed Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 02:02:16 -0500 Subject: wording --- docs/narr/extconfig.rst | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 4468e95b4..570d20ec7 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -140,17 +140,22 @@ directive did this: config.add_jammyjam('first') config.add_jammyjam('second') -When the action list was committed, the user's application would not start, -because the discriminators of the actions generated by the two calls are in -direct conflict. Automatic conflict resolution cannot resolve the conflict, -and the user provided no intermediate +When the action list was committed resulting from the set of calls above, our +user's application would not start, because the discriminators of the actions +generated by the two calls are in direct conflict. Automatic conflict +resolution cannot resolve the conflict (because no ``config.include`` is +involved), and the user provided no intermediate :meth:`pyramid.config.Configurator.commit` call between the calls to ``add_jammyjam`` to ensure that the successive calls did not conflict with -each other. This is the purpose of the discriminator argument to the action +each other. + +This demonstrates the purpose of the discriminator argument to the action method: it's used to indicate a uniqueness constraint for an action. Two actions with the same discriminator will conflict unless the conflict is automatically or manually resolved. A discriminator can be any hashable -object, but it is generally a string or a tuple. +object, but it is generally a string or a tuple. *You use a discriminator to +declaratively ensure that the user doesn't provide ambiguous configuration +statements.* But let's imagine that a consumer of ``add_jammyjam`` used it in such a way that no configuration conflicts are generated. @@ -160,7 +165,7 @@ that no configuration conflicts are generated. config.add_jammyjam('first') -What happens then? When the ``add_jammyjam`` method is called, an action is +What happens now? When the ``add_jammyjam`` method is called, an action is appended to the pending actions list. When the pending configuration actions are processed during :meth:`~pyramid.config.Configurator.commit`, and no conflicts occur, the *callable* provided as the second argument to the @@ -170,9 +175,9 @@ called with no arguments. The callable in ``add_jammyjam`` is the ``config.registry.jammyjam`` to whatever the user passed in as the ``jammyjam`` argument to the ``add_jammyjam`` function. Therefore, the result of the user's call to our directive will set the ``jammyjam`` -attribute of the registry to the string ``first``. A callable is used by a -directive to defer the result of a user's call to a directive until conflict -detection has had a chance to do its job. +attribute of the registry to the string ``first``. *A callable is used by a +directive to defer the result of a user's call to the directive until +conflict detection has had a chance to do its job*. Other arguments exist to the :meth:`~pyramid.config.Configurator.action` method, including ``args``, ``kw``, ``order``, and ``introspectables``. -- cgit v1.2.3 From 8015a9f1e05526a4990997f58aab19a5f06b1737 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 02:03:06 -0500 Subject: wording --- docs/narr/extconfig.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index 570d20ec7..edc120e39 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -6,9 +6,10 @@ Extending Pyramid Configuration =============================== -Pyramid allows you to extend its Configurator with custom directives. These -directives can add an :term:`action`, participate in :term:`conflict -resolution`, and can provide some number of :term:`introspectable` objects. +Pyramid allows you to extend its Configurator with custom directives. Custom +directives can use other directives, they can add a custom :term:`action`, +they can participate in :term:`conflict resolution`, and they can provide +some number of :term:`introspectable` objects. .. index:: single: add_directive -- cgit v1.2.3 From 6ab90035628ab282ba4e5433f5b9549c98a6df13 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 02:34:39 -0500 Subject: add blather about introspectables --- docs/narr/extconfig.rst | 140 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index edc120e39..ac8b83baa 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -219,7 +219,141 @@ passed to it, that a route by this name was already registered by configuration error (a view that names a nonexistent route via its ``route_name`` parameter will never be called). -``introspectables`` is a sequence of :term:`introspectable` objects. Using -``introspectables`` allows you to plug into Pyramid's configuration -introspection system. +``introspectables`` is a sequence of :term:`introspectable` objects. You can +pass a sequence of introspectables to the +:meth:`~pyramid.config.Configurator.action` method, which allows you to +augment Pyramid's configuration introspection system. + +.. _introspection: + +Configuration Introspection +--------------------------- + +.. warning:: + + The introspection subsystem is new in Pyramid 1.3. + +Pyramid provides a configuration introspection system that can be used by +debugging tools to provide visibility into the configuration of a running +application. + +All built-in Pyramid directives (such as +:meth:`pyramid.config.Configurator.add_view` and +:meth:`pyramid.config.Configurator.add_route`) register a set of +introspectables when called. For example, when you register a view via +``add_view``, the directive registers at least one introspectable: an +introspectable about the view registration itself, providing human-consumable +values for the arguments it was passed. You can later use the introspection +query system to determine whether a particular view uses a renderer, or +whether a particular view is limited to a particular request method, or which +routes a particular view is registered against. The Pyramid "debug toolbar" +makes use of the introspection system in various ways to display information +to Pyramid developers. + +Introspection values are set when a sequence of :term:`introspectable` +objects is passed to the :meth:`~pyramid.config.Configurator.action` method. +Here's an example of a directive which uses introspectables: + +.. code-block:: python + :linenos: + + def add_jammyjam(config, value): + def register(): + config.registry.jammyjam = value + intr = config.introspectable(category_name='jammyjams', + discriminator='jammyjam', + title='a jammyjam', + type_name=None) + intr['value'] = value + config.action('jammyjam', register, introspectables=(intr,)) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +If you notice, the above directive uses the ``introspectable`` attribute of a +Configurator (:attr:`pyramid.config.Configurator.introspectable`) to create +an introspectable object. The introspectable object's constructor requires +at least four arguments: the ``category_name``, the ``discriminator``, the +``title``, and the ``type_name``. + +The ``category_name`` is a string representing the logical category for this +introspectable. Usually the category_name is a pluralization of the type of +object being added via the action. + +The ``discriminator`` is a value unique **within the category** (unlike the +action discriminator, which must be unique within the entire set of actions). +It is typically a string or tuple representing the values unique to this +introspectable within the category. It is used to generate links and as part +of a relationship-forming target for other introspectables. + +The ``title`` is a human-consumable string that can be used by introspection +system frontends to show a friendly summary of this introspectable. + +The ``type_name`` is a value that can be used to subtype this introspectable +within its category for for sorting and presentation purposes. It can be any +value. + +An introspectable is also dictionary-like. It can contain any set of +key/value pairs, typically related to the arguments passed to its related +directive. While the category_name, discriminator, title and type_name are +*metadata* about the introspectable, the values provided as key/value pairs +are the actual data provided by the introspectable. In the above example, we +set the ``value`` key to the value of the ``value`` argument passed to the +directive. + +Our directive above mutates the introspectable, and passes it in to the +``action`` method as the first element of a tuple as the value of the +``introspectable`` keyword argument. This associates this introspectable +with the action. Introspection tools will then display this introspectable +in their index. + +Introspectable Relationships +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two introspectables may have relationships between each other. + +.. code-block:: python + :linenos: + + def add_jammyjam(config, value, template): + def register(): + config.registry.jammyjam = (value, template) + intr = config.introspectable(category_name='jammyjams', + discriminator='jammyjam', + title='a jammyjam', + type_name=None) + intr['value'] = value + tmpl_intr = config.introspectable(category_name='jammyjam templates', + discriminator=template, + title=template, + type_name=None) + tmpl_intr['value'] = template + intr.relate('jammyjam templates', template) + config.action('jammyjam', register, introspectables=(intr, tmpl_intr)) + + if __name__ == '__main__': + config = Configurator() + config.add_directive('add_jammyjam', add_jammyjam) + +In the above example, the ``add_jammyjam`` directive registers *two* +introspectables. The first is related to the ``value`` passed to the +directive; the second is related to the ``template`` passed to the directive. +If you believe a concept within a directive is important enough to have its +own introspectable, you can cause the same directive to register more than +one introspectable, registering one introspectable for the "main idea" and +another for a related concept. + +The call to ``intr.relate`` above +(:meth:`pyramid.interfaces.IIntrospectable.relate`) is passed two arguments: +a category name and a directive. The example above effectively indicates +that the directive wishes to form a relationship between the ``intr`` +introspectable and the ``tmpl_intr`` introspectable; the arguments passed to +``relate`` are the category name and discriminator of the ``tmpl_intr`` +introspectable. + +Introspectable relationships will show up in frontend system renderings of +introspection values. For example, if a view registration names a route +name, the introspectable related to the view callable will show a reference +to the route it relates to and vice versa. -- cgit v1.2.3 From 9d97b654057e621c4928fe597053d54aa5f63a8c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 03:00:15 -0500 Subject: add skeleton for using introspection chapter --- CHANGES.txt | 9 +++++ docs/index.rst | 1 + docs/latexindex.rst | 1 + docs/narr/introspector.rst | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+) create mode 100644 docs/narr/introspector.rst diff --git a/CHANGES.txt b/CHANGES.txt index 6fdb03635..6ba72fc06 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -28,6 +28,10 @@ Features - Configuration conflict reporting is reported in a more understandable way ("Line 11 in file..." vs. a repr of a tuple of similar info). +- An configuration introspection system was added; see the narrative + documentation chapter entitled "Pyramid Configuration Introspection" for + more information. + - New APIs: ``pyramid.registry.Introspectable``, ``pyramid.config.Configurator.introspector``, ``pyramid.config.Configurator.introspectable``, @@ -103,6 +107,11 @@ Documentation - Minor updates to the ZODB Wiki tutorial. +- A narrative documentation chapter named "Extending Pyramid Configuration" + was added; it describes how to add a new directive, and how use the + ``pyramid.config.Configurator.action`` method within custom directives. It + also describes how to add introspectable objects. + Scaffolds --------- diff --git a/docs/index.rst b/docs/index.rst index f07e8eac2..ceb29d108 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -88,6 +88,7 @@ Narrative documentation in chapter form explaining how to use narr/security narr/hybrid narr/hooks + narr/introspector narr/extending narr/advconfig narr/extconfig diff --git a/docs/latexindex.rst b/docs/latexindex.rst index d74dbba79..4db5b64b2 100644 --- a/docs/latexindex.rst +++ b/docs/latexindex.rst @@ -55,6 +55,7 @@ Narrative Documentation narr/security narr/hybrid narr/hooks + narr/introspector narr/extending narr/advconfig narr/extconfig diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst new file mode 100644 index 000000000..3cbafa010 --- /dev/null +++ b/docs/narr/introspector.rst @@ -0,0 +1,89 @@ +.. index:: + single: introspection + single: introspector + +.. _using_introspection: + +Pyramid Configuration Introspection +=================================== + +When Pyramid starts up, each call to a :term:`configuration directive` causes +one or more :term:`introspectable` objects to be registered with an +:term:`introspector`. This introspector can be queried by application code +to obtain information about the configuration of the running application. +This feature is useful for debug toolbars, command-line scripts which show +some aspect of configuration, and for runtime reporting of startup-time +configuration settings. + +Using the Introspector +---------------------- + +Here's an example of using Pyramid's introspector: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + @view_config(route_name='foo') + @view_config(route_name='bar') + def route_accepts(request): + introspector = request.registry.introspector + route_name = request.matched_route.name + route_intr = introspector.get('routes', route_name) + return Response(str(route_intr['accept'])) + +This view will return a response that contains the "accept" argument provided +to the ``add_route`` method of the route which matched when the view was +called. It used the :meth:`pyramid.interfaces.IIntrospector.get` method to +return an introspectable in the category ``routes`` with a +:term:`discriminator` equal to the matched route name. It then used the +returned introspectable to obtain an "accept" value. + +The introspector has a number of other query-related methods: see +:class:`pyramid.interfaces.IIntrospector` for more information. The +introspectable returned by the query methods of the introspector has methods +and attributes described by :class:`pyramid.interfaces.IIntrospectable`. + +Concrete Introspection Categories +--------------------------------- + +This is a list of concrete introspection categories provided by Pyramid. + +``subscribers`` + +``response adapters`` + +``asset overrides`` + +``root factories`` + +``session factory`` + +``request factory`` + +``locale negotiator`` + +``translation directories`` + +``renderer factories`` + +``routes`` + +``authentication policy`` + +``authorization policy`` + +``default permission`` + +``tweens (implicit)`` + +``views`` + +``templates`` + +``permissions`` + +``view mapper`` + -- cgit v1.2.3 From 5224059f71d0ad592a611c196a3af7cbd1dc828f Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 16:24:34 -0500 Subject: add more content to the introspectables narr chapter; adjust introspection registrations while doing so --- docs/narr/extconfig.rst | 13 +- docs/narr/introspector.rst | 425 ++++++++++++++++++++++++++++++-- pyramid/config/factories.py | 3 +- pyramid/config/i18n.py | 3 +- pyramid/config/rendering.py | 7 +- pyramid/config/routes.py | 21 +- pyramid/config/security.py | 6 +- pyramid/config/tweens.py | 9 +- pyramid/config/views.py | 17 +- pyramid/interfaces.py | 9 +- pyramid/tests/test_config/test_views.py | 14 ++ 11 files changed, 481 insertions(+), 46 deletions(-) diff --git a/docs/narr/extconfig.rst b/docs/narr/extconfig.rst index ac8b83baa..856654377 100644 --- a/docs/narr/extconfig.rst +++ b/docs/narr/extconfig.rst @@ -336,7 +336,7 @@ Two introspectables may have relationships between each other. config = Configurator() config.add_directive('add_jammyjam', add_jammyjam) -In the above example, the ``add_jammyjam`` directive registers *two* +In the above example, the ``add_jammyjam`` directive registers two introspectables. The first is related to the ``value`` passed to the directive; the second is related to the ``template`` passed to the directive. If you believe a concept within a directive is important enough to have its @@ -352,8 +352,15 @@ introspectable and the ``tmpl_intr`` introspectable; the arguments passed to ``relate`` are the category name and discriminator of the ``tmpl_intr`` introspectable. +Relationships need not be made between two introspectables created by the +same directive. Instead, a relationship can be formed between an +introspectable created in one directive and another introspectable created in +another by calling ``relate`` on either side with the other directive's +category name and discriminator. An error will be raised at configuration +commit time if you attempt to relate an introspectable with another +nonexistent introspectable, however. + Introspectable relationships will show up in frontend system renderings of introspection values. For example, if a view registration names a route name, the introspectable related to the view callable will show a reference -to the route it relates to and vice versa. - +to the route to which it relates to and vice versa. diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 3cbafa010..8adfde7d1 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -9,16 +9,17 @@ Pyramid Configuration Introspection When Pyramid starts up, each call to a :term:`configuration directive` causes one or more :term:`introspectable` objects to be registered with an -:term:`introspector`. This introspector can be queried by application code -to obtain information about the configuration of the running application. -This feature is useful for debug toolbars, command-line scripts which show -some aspect of configuration, and for runtime reporting of startup-time +:term:`introspector`. The introspector can be queried by application code to +obtain information about the configuration of the running application. This +feature is useful for debug toolbars, command-line scripts which show some +aspect of configuration, and for runtime reporting of startup-time configuration settings. Using the Introspector ---------------------- -Here's an example of using Pyramid's introspector: +Here's an example of using Pyramid's introspector from within a view +callable: .. code-block:: python :linenos: @@ -26,64 +27,440 @@ Here's an example of using Pyramid's introspector: from pyramid.view import view_config from pyramid.response import Response - @view_config(route_name='foo') @view_config(route_name='bar') def route_accepts(request): introspector = request.registry.introspector route_name = request.matched_route.name route_intr = introspector.get('routes', route_name) - return Response(str(route_intr['accept'])) + return Response(str(route_intr['pattern'])) -This view will return a response that contains the "accept" argument provided -to the ``add_route`` method of the route which matched when the view was -called. It used the :meth:`pyramid.interfaces.IIntrospector.get` method to -return an introspectable in the category ``routes`` with a -:term:`discriminator` equal to the matched route name. It then used the -returned introspectable to obtain an "accept" value. +This view will return a response that contains the "pattern" argument +provided to the ``add_route`` method of the route which matched when the view +was called. It uses the :meth:`pyramid.interfaces.IIntrospector.get` method +to return an introspectable in the category ``routes`` with a +:term:`discriminator` equal to the matched route name. It then uses the +returned introspectable to obtain an "pattern" value. -The introspector has a number of other query-related methods: see -:class:`pyramid.interfaces.IIntrospector` for more information. The -introspectable returned by the query methods of the introspector has methods -and attributes described by :class:`pyramid.interfaces.IIntrospectable`. +The introspectable returned by the query methods of the introspector has +methods and attributes described by +:class:`pyramid.interfaces.IIntrospectable`. In particular, the +:meth:`~pyramid.interfaces.IIntrospector.get`, +:meth:`~pyramid.interfaces.IIntrospector.get_category`, +:meth:`~pyramid.interfaces.IIntrospector.categories`, +:meth:`~pyramid.interfaces.IIntrospector.categorized`, and +:meth:`~pyramid.interfaces.IIntrospector.related` methods of an introspector +can be used to query for introspectables. -Concrete Introspection Categories ---------------------------------- +Introspectable Objects +---------------------- + +Introspectable objects are returned from query methods of an introspector. +Each introspectable object implements the attributes and methods the +documented at :class:`pyramid.interfaces.IIntrospectable`. + +The important attributes shared by all introspectables are the following: + +``title`` + + A human-readable text title describing the introspectable + +``category_name`` + + A text category name describing the introspection category to which this + introspectable belongs. It is often a plural if there are expected to be + more than one introspectable registered within the category. + +``discriminator`` + + A hashable object representing the unique value of this introspectable + within its category. + +``discriminator_hash`` -This is a list of concrete introspection categories provided by Pyramid. + The integer hash of the discriminator (useful for using in HTML links). + +``type_name`` + + The text name of a subtype within this introspectable's category. If there + is only one type name in this introspectable's category, this value will + often be a singular version of the category name but it can be an arbitrary + value. + +Besides having the attributes described above, an introspectable is a +dictionary-like object. An introspectable can be queried for data values via +its ``__getitem__``, ``get``, ``keys``, ``values``, or ``items`` methods. +For example: + +.. code-block:: python + :linenos: + + route_intr = introspector.get('routes', 'edit_user') + pattern = route_intr['pattern'] + +Pyramid Introspection Categories +-------------------------------- + +The list of concrete introspection categories provided by built-in Pyramid +configuration directives follows. Add-on packages may supply other +introspectables in categories not described here. ``subscribers`` + Each introspectable in the ``subscribers`` category represents a call to + :meth:`pryamid.config.Configurator.add_subscriber` (or the decorator + equivalent); each will have the following data. + + ``subscriber`` + + The subscriber callable object (the resolution of the ``subscriber`` + argument passed to ``add_susbcriber``). + + ``interfaces`` + + A sequence of interfaces (or classes) that are subscribed to (the + resolution of the ``ifaces`` argument passed to ``add_subscriber``). + ``response adapters`` -``asset overrides`` + Each introspectable in the ``response adapters`` category represents a call + to :meth:`pyramid.config.Configurator.add_response_adapter` (or a decorator + equivalent); each will have the following data. + + ``adapter`` + + The adapter object (the resolved ``adapter`` argument to + ``add_response_adapter``). + + ``type`` + + The resolved ``type_or_iface`` argument passed to + ``add_response_adapter``. ``root factories`` + XXX ``default root factory`` category? + + Each introspectable in the ``root factories`` category represents a call to + :meth:`pyramid.config.Configurator.set_root_factory` (or the Configurator + constructor equivalent) *or* a ``factory`` argument passed to + :meth:`pyramid.config.Configurator.add_route`; each will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_root_factory``). + + ``route_name`` + + The name of the route which will use this factory. If this is the + *default* root factory (if it's registered during a call to + ``set_root_factory``), this value will be ``None``. + ``session factory`` + Only one introspectable will exist in the ``session factory`` category. It + represents a call to :meth:`pyramid.config.Configurator.set_session_factory` + (or the Configurator constructor equivalent); it will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_session_factory``). + ``request factory`` + Only one introspectable will exist in the ``request factory`` category. It + represents a call to :meth:`pyramid.config.Configurator.set_request_factory` + (or the Configurator constructor equivalent); it will have the following + data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_request_factory``). + ``locale negotiator`` -``translation directories`` + Only one introspectable will exist in the ``locale negotiator`` category. + It represents a call to + :meth:`pyramid.config.Configurator.set_locale_negotiator` (or the + Configurator constructor equivalent); it will have the following data. + + ``negotiator`` + + The factory object (the resolved ``negotiator`` argument to + ``set_locale_negotiator``). ``renderer factories`` + Each introspectable in the ``renderer factories`` category represents a + call to :meth:`pyramid.config.Configurator.add_renderer` (or the + Configurator constructor equivalent); each will have the following data. + + ``name`` + + The name of the renderer (the value of the ``name`` argument to + ``add_renderer``). + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``add_renderer``). + +``renderer globals factory`` + + There will be one and only one introspectable in the ``renderer globals + factory`` category. It represents a call to + :meth:`pyramid.config.Configurator.set_renderer_globals_factory`; it will + have the following data. + + ``factory`` + + The factory object (the resolved ``factory`` argument to + ``set_renderer_globals_factory``). + ``routes`` + Each introspectable in the ``routes`` category represents a call to + :meth:`pyramid.config.Configurator.add_route`; each will have the following + data. + + ``name`` + + The ``name`` argument passed to ``add_route``. + + ``pattern`` + + The ``pattern`` argument passed to ``add_route``. + + ``factory`` + + The (resolved) ``factory`` argument passed to ``add_route``. + + ``xhr`` + + The ``xhr`` argument passed to ``add_route``. + + ``request_method`` + + The ``request_method`` argument passed to ``add_route``. + + ``request_methods`` + + A sequence of request method names implied by the ``request_method`` + argument passed to ``add_route``. + + ``path_info`` + + The ``path_info`` argument passed to ``add_route``. + + ``request_param`` + + The ``request_param`` argument passed to ``add_route``. + + ``header`` + + The ``header`` argument passed to ``add_route``. + + ``accept`` + + The ``accept`` argument passed to ``add_route``. + + ``traverse`` + + The ``traverse`` argument passed to ``add_route``. + + ``custom_predicates`` + + The ``custom_predicates`` argument passed to ``add_route``. + + ``pregenerator`` + + The ``pregenerator`` argument passed to ``add_route``. + + ``pregenerator`` + + The ``static`` argument passed to ``add_route``. + + ``pregenerator`` + + The ``use_global_views`` argument passed to ``add_route``. + + ``object`` + + The :class:`pyramid.interfaces.IRoute` object that is used to perform + matching and generation for this route. + ``authentication policy`` + There will be one and only one introspectable in the ``authentication + policy`` category. It represents a call to the + :meth:`pyramid.config.Configurator.set_authentication_policy` method (or + its Configurator constructor equivalent); it will have the following data. + + ``policy`` + + The policy object (the resolved ``policy`` argument to + ``set_authentication_policy``). + ``authorization policy`` + There will be one and only one introspectable in the ``authorization + policy`` category. It represents a call to the + :meth:`pyramid.config.Configurator.set_authorization_policy` method (or its + Configurator constructor equivalent); it will have the following data. + + ``policy`` + + The policy object (the resolved ``policy`` argument to + ``set_authorization_policy``). + ``default permission`` -``tweens (implicit)`` + There will be one and only one introspectable in the ``default permission`` + category. It represents a call to the + :meth:`pyramid.config.Configurator.set_default_permission` method (or its + Configurator constructor equivalent); it will have the following data. + + ``value`` + + The permission name passed to ``set_default_permission``. ``views`` -``templates`` + Each introspectable in the ``views`` category represents a call to + :meth:`pyramid.config.Configurator.add_view`; each will have the following + data. + + ``name`` + + The ``name`` argument passed to ``add_view``. + + ``context`` + + The (resolved) ``context`` argument passed to ``add_view``. + + ``containment`` + + The (resolved) ``containment`` argument passed to ``add_view``. + + ``request_param`` + + The ``request_param`` argument passed to ``add_view``. + + ``request_methods`` + + A sequence of request method names implied by the ``request_method`` + argument passed to ``add_view``. + + ``route_name`` + + The ``route_name`` argument passed to ``add_view``. + + ``attr`` + + The ``attr`` argument passed to ``add_view``. + + ``xhr`` + + The ``xhr`` argument passed to ``add_view``. + + ``accept`` + + The ``accept`` argument passed to ``add_view``. + + ``header`` + + The ``header`` argument passed to ``add_view``. + + ``path_info`` + + The ``path_info`` argument passed to ``add_view``. + + ``match_param`` + + The ``match_param`` argument passed to ``add_view``. + + ``callable`` + + The (resolved) ``view`` argument passed to ``add_view``. Represents the + "raw" view callable. + + ``derived_callable`` + + The view callable derived from the ``view`` argument passed to + ``add_view``. Represents the view callable which Pyramid itself calls + (wrapped in security and other wrappers). + + ``mapper`` + + The (resolved) ``mapper`` argument passed to ``add_view``. + + ``decorator`` + + The (resolved) ``decorator`` argument passed to ``add_view``. ``permissions`` + Each introspectable in the ``permissions`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has an explicit + ``permission`` argument to *or* a call to + :meth:`pyramid.config.Configurator.set_default_permission`; each will have + the following data. + + ``value`` + + The permission name passed to ``add_view`` or ``set_default_permission``. + +``templates`` + + Each introspectable in the ``templates`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has a ``renderer`` + argument which points to a template; each will have the following data. + + ``name`` + + The renderer's name (a string). + + ``type`` + + The renderer's type (a string). + + ``renderer`` + + The :class:`pyramid.interfaces.IRendererInfo` object which represents + this template's renderer. + ``view mapper`` + XXX default view mapper category? + + Each introspectable in the ``permissions`` category represents a call to + :meth:`pyramid.config.Configurator.add_view` that has an explicit + ``mapper`` argument to *or* a call to + :meth:`pyramid.config.Configurator.set_view_mapper`; each will have + the following data. + + ``mapper`` + + The (resolved) ``mapper`` argument passed to ``add_view`` or + ``set_view_mapper``. + +``asset overrides`` + + XXX + +``translation directories`` + + XXX + +``tweens (implicit)`` + + XXX + +``tweens (explicit)`` + + XXX + diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index cfa91d6d9..530b6cc28 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -29,7 +29,8 @@ class FactoriesConfiguratorMixin(object): self.registry.registerUtility(factory, IRootFactory) self.registry.registerUtility(factory, IDefaultRootFactory) # b/c - intr = self.introspectable('root factories', None, + intr = self.introspectable('root factories', + None, self.object_description(factory), 'root factory') intr['factory'] = factory diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index 2c149923c..5770f84c8 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -40,7 +40,8 @@ class I18NConfiguratorMixin(object): """ def register(): self._set_locale_negotiator(negotiator) - intr = self.introspectable('locale negotiator', None, repr(negotiator), + intr = self.introspectable('locale negotiator', None, + self.object_description(negotiator), 'locale negotiator') intr['negotiator'] = negotiator self.action(ILocaleNegotiator, register, introspectables=(intr,)) diff --git a/pyramid/config/rendering.py b/pyramid/config/rendering.py index 0d37e201f..926511b7b 100644 --- a/pyramid/config/rendering.py +++ b/pyramid/config/rendering.py @@ -48,7 +48,8 @@ class RenderingConfiguratorMixin(object): name = '' def register(): self.registry.registerUtility(factory, IRendererFactory, name=name) - intr = self.introspectable('renderer factories', name, + intr = self.introspectable('renderer factories', + name, self.object_description(factory), 'renderer factory') intr['factory'] = factory @@ -74,7 +75,9 @@ class RenderingConfiguratorMixin(object): .. warning:: - This method is deprecated as of Pyramid 1.1. + This method is deprecated as of Pyramid 1.1. Use a BeforeRender + event subscriber as documented in the :ref:`hooks_chapter` chapter + instead. .. note:: diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 35ad0f8c4..2628f9cac 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -14,6 +14,7 @@ from pyramid.urldispatch import RoutesMapper from pyramid.config.util import ( action_method, make_predicates, + as_sorted_tuple, ) class RoutesConfiguratorMixin(object): @@ -369,14 +370,17 @@ class RoutesConfiguratorMixin(object): mapper = self.get_routes_mapper() - intr = self.introspectable('routes', name, + introspectables = [] + + intr = self.introspectable('routes', + name, '%s (pattern: %r)' % (name, pattern), 'route') intr['name'] = name intr['pattern'] = pattern intr['factory'] = factory intr['xhr'] = xhr - intr['request_method'] = request_method + intr['request_methods'] = as_sorted_tuple(request_method) intr['path_info'] = path_info intr['request_param'] = request_param intr['header'] = header @@ -386,6 +390,17 @@ class RoutesConfiguratorMixin(object): intr['pregenerator'] = pregenerator intr['static'] = static intr['use_global_views'] = use_global_views + introspectables.append(intr) + + if factory: + factory_intr = self.introspectable('root factories', + name, + self.object_description(factory), + 'root factory') + factory_intr['factory'] = factory + factory_intr['route_name'] = name + factory_intr.relate('routes', name) + introspectables.append(factory_intr) def register_route_request_iface(): request_iface = self.registry.queryUtility(IRouteRequest, name=name) @@ -414,7 +429,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, introspectables=(intr,)) + order=PHASE2_CONFIG, introspectables=introspectables) # 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 1830fb900..a0ea173ba 100644 --- a/pyramid/config/security.py +++ b/pyramid/config/security.py @@ -126,8 +126,10 @@ class SecurityConfiguratorMixin(object): permission, 'default permission') intr['value'] = permission - perm_intr = self.introspectable('permissions', permission, - permission, '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, diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index 22ea21a57..e36e9e84e 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -146,7 +146,8 @@ class TweensConfiguratorMixin(object): registry.registerUtility(tweens, ITweens) ex_intr = self.introspectable('tweens (implicit)', ('tween', EXCVIEW, False), - EXCVIEW, 'implicit tween') + EXCVIEW, + 'implicit tween') ex_intr['factory'] = excview_tween_factory ex_intr['type'] = 'implicit' ex_intr['under'] = None @@ -163,8 +164,10 @@ class TweensConfiguratorMixin(object): discriminator = ('tween', name, explicit) tween_type = explicit and 'explicit' or 'implicit' - intr = self.introspectable('tweens (%s)' % tween_type, discriminator, - name, '%s tween' % tween_type) + intr = self.introspectable('tweens (%s)' % tween_type, + discriminator, + name, + '%s tween' % tween_type) intr['factory'] = tween_factory intr['type'] = tween_type intr['under'] = under diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 5c4470834..02dcbf2ee 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -962,7 +962,7 @@ class ViewsConfiguratorMixin(object): context=context, containment=containment, request_param=request_param, - request_method=request_method, + request_methods=request_method, route_name=route_name, attr=attr, xhr=xhr, @@ -1116,9 +1116,18 @@ class ViewsConfiguratorMixin(object): (IExceptionViewClassifier, request_iface, context), IMultiView, name=name) + if mapper: + mapper_intr = self.introspectable('view mappers', + discriminator, + view_desc, + 'view mapper') + mapper_intr['mapper'] = mapper + mapper_intr.relate('views', discriminator) + introspectables.append(mapper_intr) if route_name: view_intr.relate('routes', route_name) # see add_route if renderer is not None and renderer.name and '.' in renderer.name: + # it's a template tmpl_intr = self.introspectable('templates', discriminator, renderer.name, 'template') tmpl_intr.relate('views', discriminator) @@ -1360,9 +1369,11 @@ class ViewsConfiguratorMixin(object): self.registry.registerUtility(mapper, IViewMapperFactory) # IViewMapperFactory is looked up as the result of view config # in phase 3 - intr = self.introspectable('view mapper', IViewMapperFactory, + intr = self.introspectable('view mappers', + IViewMapperFactory, self.object_description(mapper), - 'view mapper') + 'default view mapper') + intr['mapper'] = mapper self.action(IViewMapperFactory, register, order=PHASE1_CONFIG, introspectables=(intr,)) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 05881571e..a8a9cc55a 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -946,14 +946,15 @@ class IIntrospectable(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') + '(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') - action_info = Attribute('An object representing the caller that invoked ' - 'the creation of this introspectable (usually ' - 'a sentinel until updated during self.register)') + action_info = Attribute('An IActionInfo object representing the caller ' + 'that invoked the creation of this introspectable ' + '(usually a sentinel until updated during ' + 'self.register)') def relate(category_name, discriminator): """ Indicate an intent to relate this IIntrospectable with another diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index a9a4d5836..d80a6bb64 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1362,6 +1362,20 @@ class TestViewsConfigurationMixin(unittest.TestCase): request = self._makeRequest(config) self.assertEqual(view(None, request), 'OK') + def test_add_view_with_mapper(self): + from pyramid.renderers import null_renderer + class Mapper(object): + def __init__(self, **kw): + self.__class__.kw = kw + def __call__(self, view): + return view + config = self._makeOne(autocommit=True) + def view(context, request): return 'OK' + config.add_view(view=view, mapper=Mapper, renderer=null_renderer) + view = self._getViewCallable(config) + self.assertEqual(view(None, None), 'OK') + self.assertEqual(Mapper.kw['mapper'], Mapper) + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): -- cgit v1.2.3 From 735df7f3b62a831c9fb7d44872027a5c4d0eb328 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 16:52:01 -0500 Subject: unify resolveConflicts implementation with the same impl on chrism-dictactions branch of zope.configuration --- pyramid/config/__init__.py | 41 +++++++++++++++++----------------- pyramid/tests/test_config/test_init.py | 12 +++++----- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 40f061897..f95c876e0 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1033,50 +1033,49 @@ def resolveConflicts(actions): # organize actions by discriminators unique = {} output = [] - for i in range(len(actions)): - action = actions[i] + for i, action in enumerate(actions): if not isinstance(action, dict): - # old-style ZCML tuple action + # old-style tuple action action = expand_action(*action) - order = action['order'] - if not order: - action['order'] = i + order = action['order'] or i discriminator = action['discriminator'] if discriminator is None: # The discriminator is None, so this action can never # conflict. We can add it directly to the result. - output.append(action) + output.append((order, action)) continue + L = unique.setdefault(discriminator, []) - L.append(action) + L.append((order, 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 bypath(action): - return (action['includepath'], action['order']) + def bypath(tup): + return tup[1]['includepath'], tup[0] dups.sort(key=bypath) - first = dups[0] - output.append(first) - basepath = first['includepath'] - baseinfo = first['info'] - discriminator = first['discriminator'] - for dup in dups[1:]: + order, first = dups[0] + output.append(dups[0]) + basepath, baseinfo, discriminator = (first['includepath'], + first['info'], + first['discriminator']) + + for order, 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): - infos = conflicts.setdefault(discriminator, [baseinfo]) - infos.append(dup['info']) + L = conflicts.setdefault(discriminator, [baseinfo]) + L.append(dup['info']) if conflicts: raise ConfigurationConflictError(conflicts) - output.sort(key=operator.itemgetter('order')) - return output - + output.sort(key=operator.itemgetter(0)) + return [ x[1] for x in output ] + def expand_action(discriminator, callable=None, args=(), kw=None, includepath=(), info=None, order=0, introspectables=()): if kw is None: diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 94ff4348a..27a8c9306 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1658,7 +1658,7 @@ class Test_resolveConflicts(unittest.TestCase): 'kw': {}, 'discriminator': 1, 'includepath': (), - 'order': 1}, + 'order': 0}, {'info': None, 'args': (3,), @@ -1667,7 +1667,7 @@ class Test_resolveConflicts(unittest.TestCase): 'kw': {}, 'discriminator': 3, 'includepath': ('y',), - 'order': 5}, + 'order': 0}, {'info': None, 'args': (5,), @@ -1676,7 +1676,7 @@ class Test_resolveConflicts(unittest.TestCase): 'kw': {}, 'discriminator': None, 'includepath': ('y',), - 'order': 6}, + 'order': 0}, {'info': 'should be last', 'args': (4,), @@ -1719,7 +1719,7 @@ class Test_resolveConflicts(unittest.TestCase): 'kw': {}, 'discriminator': 1, 'includepath': (), - 'order': 1}, + 'order': 0}, {'info': None, 'args': (3,), @@ -1728,7 +1728,7 @@ class Test_resolveConflicts(unittest.TestCase): 'kw': {}, 'discriminator': 3, 'includepath': ('y',), - 'order': 5}, + 'order': 0}, {'info': None, 'args': (5,), @@ -1737,7 +1737,7 @@ class Test_resolveConflicts(unittest.TestCase): 'kw': {}, 'discriminator': None, 'includepath': ('y',), - 'order': 6}, + 'order': 0}, {'info': 'should be last', 'args': (4,), -- cgit v1.2.3 From d83396604337a1f3a09319ba7d37baeb67f50c8a Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 18:07:05 -0500 Subject: add -t b/c alias for -s/--scaffold --- pyramid/scripts/pcreate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index f559e4f17..dacebd6ea 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -24,6 +24,12 @@ class PCreateCommand(object): action='append', help=("Add a scaffold to the create process " "(multiple -s args accepted)")) + parser.add_option('-t', '--template', + dest='scaffold_name', + action='append', + help=('A backwards compatibility alias for ' + '-s/--scaffold. Add a scaffold to the ' + 'create process (multiple -t args accepted)')) parser.add_option('-l', '--list', dest='list', action='store_true', -- cgit v1.2.3 From 422bf50d7c3b99ab7c4655f81d373eb5ca29301b Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 18:07:15 -0500 Subject: garden --- TODO.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index abcf08dd0..0c6692f09 100644 --- a/TODO.txt +++ b/TODO.txt @@ -8,7 +8,9 @@ Must-Have * Narrative docs. - * Test with pyramid_zcml (wrt action_info / actions.append). + * ActionInfo for ZCML actions (begin/end lineno/cols?) + + * Document ActionInfo. * categorize() return value ordering not right yet. -- cgit v1.2.3 From 58c01ff8863971f81db59d437d49fd9e97b59e5c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 19:37:38 -0500 Subject: flesh out categories more --- TODO.txt | 4 +++ docs/narr/introspector.rst | 80 ++++++++++++++++++++++++++++++++++++------ docs/narr/tb_introspector.png | Bin 0 -> 46164 bytes pyramid/config/assets.py | 9 ++--- pyramid/config/i18n.py | 2 +- pyramid/config/tweens.py | 6 ++-- pyramid/config/views.py | 2 +- 7 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 docs/narr/tb_introspector.png diff --git a/TODO.txt b/TODO.txt index 0c6692f09..0a15d8db0 100644 --- a/TODO.txt +++ b/TODO.txt @@ -8,6 +8,10 @@ Must-Have * Narrative docs. + * ``default root factory`` category? + + * ``default view mapper`` category? + * ActionInfo for ZCML actions (begin/end lineno/cols?) * Document ActionInfo. diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 8adfde7d1..ac0859164 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -15,6 +15,10 @@ feature is useful for debug toolbars, command-line scripts which show some aspect of configuration, and for runtime reporting of startup-time configuration settings. +.. warning:: + + Introspection is new in Pyramid 1.3. + Using the Introspector ---------------------- @@ -86,6 +90,12 @@ The important attributes shared by all introspectables are the following: often be a singular version of the category name but it can be an arbitrary value. +``action_info`` + + An object describing the directive call site which caused this + introspectable to be registered; contains attributes described in + :class:`pyramid.interfaces.IActionInfo`. + Besides having the attributes described above, an introspectable is a dictionary-like object. An introspectable can be queried for data values via its ``__getitem__``, ``get``, ``keys``, ``values``, or ``items`` methods. @@ -107,7 +117,7 @@ introspectables in categories not described here. ``subscribers`` Each introspectable in the ``subscribers`` category represents a call to - :meth:`pryamid.config.Configurator.add_subscriber` (or the decorator + :meth:`pyramid.config.Configurator.add_subscriber` (or the decorator equivalent); each will have the following data. ``subscriber`` @@ -138,8 +148,6 @@ introspectables in categories not described here. ``root factories`` - XXX ``default root factory`` category? - Each introspectable in the ``root factories`` category represents a call to :meth:`pyramid.config.Configurator.set_root_factory` (or the Configurator constructor equivalent) *or* a ``factory`` argument passed to @@ -435,8 +443,6 @@ introspectables in categories not described here. ``view mapper`` - XXX default view mapper category? - Each introspectable in the ``permissions`` category represents a call to :meth:`pyramid.config.Configurator.add_view` that has an explicit ``mapper`` argument to *or* a call to @@ -450,17 +456,69 @@ introspectables in categories not described here. ``asset overrides`` - XXX + Each introspectable in the ``asset overrides`` category represents a call + to :meth:`pyramid.config.Configurator.override_asset`; each will have the + following data. + + ``to_override`` + + The ``to_override`` argument (an asset spec) passed to + ``override_asset``. + + ``override_with`` + + The ``override_with`` argument (an asset spec) passed to + ``override_asset``. ``translation directories`` - XXX + Each introspectable in the ``asset overrides`` category represents an + individual element in a ``specs`` argument passed to to + :meth:`pyramid.config.Configurator.add_translation_dirs`; each will have + the following data. + + ``directory`` + + The absolute path of the translation directory. + + ``spec`` + + The asset specification passed to ``add_translation_dirs``. + +``tweens`` + + Each introspectable in the ``tweens`` category represents a call to + :meth:`pyramid.config.Configurator.add_tween`; each will have the following + data. + + ``name`` + + The dotted name to the tween factory as a string (passed as + the ``tween_factory`` argument to ``add_tween``). + + ``factory`` + + The (resolved) tween factory object. + + ``type`` + + ``implict`` or ``explicit`` as a string. + + ``under`` + + The ``under`` argument passed to ``add_tween`` (a string). + + ``over`` + + The ``over`` argument passed to ``add_tween`` (a string). -``tweens (implicit)`` +Toolbar Introspection +--------------------- - XXX +The Pyramid debug toolbar (part of the ``pyramid_debugtoolbar`` package) +provides a canned view of all registered introspectables and their +relationships. It looks something like this: -``tweens (explicit)`` +.. image:: tb_introspector.png - XXX diff --git a/docs/narr/tb_introspector.png b/docs/narr/tb_introspector.png new file mode 100644 index 000000000..231a094f7 Binary files /dev/null and b/docs/narr/tb_introspector.png differ diff --git a/pyramid/config/assets.py b/pyramid/config/assets.py index 7080e5e7c..c93431987 100644 --- a/pyramid/config/assets.py +++ b/pyramid/config/assets.py @@ -239,14 +239,11 @@ class AssetsConfiguratorMixin(object): intr = self.introspectable( 'asset overrides', (package, override_package, path, override_prefix), - '%s/%s -> %s/%s' % (package, path, override_package, - override_prefix), + '%s -> %s' % (to_override, override_with), 'asset override', ) - intr['package'] = package - intr['override_package'] = package - intr['override_prefix'] = override_prefix - intr['path'] = path + intr['to_override'] = to_override + intr['override_with'] = override_with self.action(None, register, introspectables=(intr,)) override_resource = override_asset # bw compat diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index 5770f84c8..60c7a71c1 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -93,6 +93,7 @@ class I18NConfiguratorMixin(object): intr = self.introspectable('translation directories', directory, spec, 'translation directory') intr['directory'] = directory + intr['spec'] = spec introspectables.append(intr) directories.append(directory) @@ -106,7 +107,6 @@ class I18NConfiguratorMixin(object): ITranslationDirectories) tdirs.insert(0, directory) - # XXX no action? if directories: # We actually only need an IChameleonTranslate function diff --git a/pyramid/config/tweens.py b/pyramid/config/tweens.py index e36e9e84e..1a83f0de9 100644 --- a/pyramid/config/tweens.py +++ b/pyramid/config/tweens.py @@ -144,10 +144,11 @@ class TweensConfiguratorMixin(object): if tweens is None: tweens = Tweens() registry.registerUtility(tweens, ITweens) - ex_intr = self.introspectable('tweens (implicit)', + ex_intr = self.introspectable('tweens', ('tween', EXCVIEW, False), EXCVIEW, 'implicit tween') + ex_intr['name'] = EXCVIEW ex_intr['factory'] = excview_tween_factory ex_intr['type'] = 'implicit' ex_intr['under'] = None @@ -164,10 +165,11 @@ class TweensConfiguratorMixin(object): discriminator = ('tween', name, explicit) tween_type = explicit and 'explicit' or 'implicit' - intr = self.introspectable('tweens (%s)' % tween_type, + intr = self.introspectable('tweens', discriminator, name, '%s tween' % tween_type) + intr['name'] = name intr['factory'] = tween_factory intr['type'] = tween_type intr['under'] = under diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 02dcbf2ee..0b6c6070f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1119,7 +1119,7 @@ class ViewsConfiguratorMixin(object): if mapper: mapper_intr = self.introspectable('view mappers', discriminator, - view_desc, + 'view mapper for %s' % view_desc, 'view mapper') mapper_intr['mapper'] = mapper mapper_intr.relate('views', discriminator) -- cgit v1.2.3 From 79f34b817580f1043270b467a3c79aeeaebd9233 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 20:44:48 -0500 Subject: change interface of get_category --- pyramid/interfaces.py | 18 +++++++++++------- pyramid/registry.py | 16 +++++++++++----- pyramid/tests/test_registry.py | 8 ++++++-- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index a8a9cc55a..559d3c110 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -865,16 +865,20 @@ class IIntrospector(Interface): discriminator (or discriminator hash) ``discriminator``. If it does not exist in the introspector, return the value of ``default`` """ - def get_category(category_name, sort_key=None): + def get_category(category_name, default=None, sort_key=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_key`` is - ``None``, the sequence will be returned in the order the - introspectables were added to the introspector. Otherwise, sort_key - should be a function that accepts an IIntrospectable and returns a - value from it (ala the ``key`` function of Python's ``sorted`` - callable).""" + category associated with ``category_name`` . + + If the category named ``category_name`` does not exist in the + introspector the value passed as ``default`` will be returned. + + If ``sort_key`` is ``None``, the sequence will be returned in the + order the introspectables were added to the introspector. Otherwise, + sort_key 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 diff --git a/pyramid/registry.py b/pyramid/registry.py index a6d70bfa8..295d6c6b9 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -103,19 +103,25 @@ class Introspector(object): intr = category.get(discriminator, default) return intr - def get_category(self, category_name, sort_key=None): + def get_category(self, category_name, default=None, sort_key=None): if sort_key is None: sort_key = operator.attrgetter('order') - category = self._categories[category_name] + category = self._categories.get(category_name) + if category is None: + return default values = category.values() values = sorted(set(values), key=sort_key) - return [{'introspectable':intr, 'related':self.related(intr)} for - intr in values] + return [ + {'introspectable':intr, + 'related':self.related(intr)} + for intr in values + ] def categorized(self, sort_key=None): L = [] for category_name in self.categories(): - L.append((category_name, self.get_category(category_name,sort_key))) + L.append((category_name, self.get_category(category_name, + sort_key=sort_key))) return L def categories(self): diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py index 3d68688d1..11019b852 100644 --- a/pyramid/tests/test_registry.py +++ b/pyramid/tests/test_registry.py @@ -97,6 +97,10 @@ class TestIntrospector(unittest.TestCase): ] self.assertEqual(inst.get_category('category'), expected) + def test_get_category_returns_default_on_miss(self): + inst = self._makeOne() + self.assertEqual(inst.get_category('category', '123'), '123') + def test_get_category_with_sortkey(self): import operator inst = self._makeOne() @@ -113,7 +117,7 @@ class TestIntrospector(unittest.TestCase): {'introspectable':intr, 'related':[]}, ] self.assertEqual( - inst.get_category('category', operator.attrgetter('foo')), + inst.get_category('category', sort_key=operator.attrgetter('foo')), expected) def test_categorized(self): @@ -132,7 +136,7 @@ class TestIntrospector(unittest.TestCase): {'introspectable':intr, 'related':[]}, ])] self.assertEqual( - inst.categorized(operator.attrgetter('foo')), expected) + inst.categorized(sort_key=operator.attrgetter('foo')), expected) def test_categories(self): inst = self._makeOne() -- cgit v1.2.3 From b40fb8b26fe37102b076cd2310ea7a3fe8b79311 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 21:04:24 -0500 Subject: add a noop introspector (allow introspection to be turned off) --- TODO.txt | 2 -- docs/api/registry.rst | 5 +++++ docs/narr/introspector.rst | 20 ++++++++++++++++-- pyramid/registry.py | 22 ++++++++++++++++++++ pyramid/tests/test_registry.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/TODO.txt b/TODO.txt index 0a15d8db0..4692b073b 100644 --- a/TODO.txt +++ b/TODO.txt @@ -23,8 +23,6 @@ Must-Have * introspection hiding for directives? - * make it possible to disuse introspection? - - Give discriminators a nicer repr for conflict reporting? Nice-to-Have diff --git a/docs/api/registry.rst b/docs/api/registry.rst index 3dbf73a67..25192f3ed 100644 --- a/docs/api/registry.rst +++ b/docs/api/registry.rst @@ -38,4 +38,9 @@ This class is new as of :app:`Pyramid` 1.3. +.. class:: noop_introspector + + An introspector which throws away all registrations, useful for disabling + introspection altogether (pass as ``introspector`` to the + :term:`Configurator` constructor). diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index ac0859164..71b41773c 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -512,8 +512,8 @@ introspectables in categories not described here. The ``over`` argument passed to ``add_tween`` (a string). -Toolbar Introspection ---------------------- +Introspection in the Toolbar +---------------------------- The Pyramid debug toolbar (part of the ``pyramid_debugtoolbar`` package) provides a canned view of all registered introspectables and their @@ -521,4 +521,20 @@ relationships. It looks something like this: .. image:: tb_introspector.png +Disabling Introspection +----------------------- +You can disable Pyramid introspection by passing the object +:attr:`pyramid.registry.noop_introspector` to the :term:`Configurator` +constructor in your application setup: + +.. code-block:: python + + from pyramid.config import Configurator + from pyramid.registry import noop_introspector + config = Configurator(..., introspector=noop_introspector) + +When the noop introspector is active, all introspectables generated by the +framework are thrown away. A noop introspector behaves just like a "real" +introspector, but the methods of a noop introspector do nothing and return +null values. diff --git a/pyramid/registry.py b/pyramid/registry.py index 295d6c6b9..d594ae910 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -172,6 +172,28 @@ class Introspector(object): raise KeyError((category_name, discriminator)) return self._refs.get(intr, []) +@implementer(IIntrospector) +class _NoopIntrospector(object): + def add(self, intr): + pass + def get(self, category_name, discriminator, default=None): + return default + def get_category(self, category_name, default=None, sort_key=None): + return default + def categorized(self, sort_key=None): + return [] + def categories(self): + return [] + def remove(self, category_name, discriminator): + return + def relate(self, *pairs): + return + unrelate = relate + def related(self, intr): + return [] + +noop_introspector = _NoopIntrospector() + @implementer(IIntrospectable) class Introspectable(dict): diff --git a/pyramid/tests/test_registry.py b/pyramid/tests/test_registry.py index 11019b852..29803346a 100644 --- a/pyramid/tests/test_registry.py +++ b/pyramid/tests/test_registry.py @@ -254,6 +254,52 @@ class TestIntrospector(unittest.TestCase): del inst._categories['category'] self.assertRaises(KeyError, inst.related, intr) +class Test_noop_introspector(unittest.TestCase): + def _makeOne(self): + from pyramid.registry import noop_introspector + return noop_introspector + + def test_conformance(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IIntrospector + verifyObject(IIntrospector, self._makeOne()) + + def test_add(self): + inst = self._makeOne() + self.assertEqual(inst.add('a'), None) + + def test_get(self): + inst = self._makeOne() + self.assertEqual(inst.get('category', 'd', default='123'), '123') + + def test_get_category(self): + inst = self._makeOne() + self.assertEqual(inst.get_category('category', default='123'), '123') + + def test_categorized(self): + inst = self._makeOne() + self.assertEqual(inst.categorized(), []) + + def test_categories(self): + inst = self._makeOne() + self.assertEqual(inst.categories(), []) + + def test_remove(self): + inst = self._makeOne() + self.assertEqual(inst.remove('cat', 'discrim'), None) + + def test_relate(self): + inst = self._makeOne() + self.assertEqual(inst.relate(), None) + + def test_unrelate(self): + inst = self._makeOne() + self.assertEqual(inst.unrelate(), None) + + def test_related(self): + inst = self._makeOne() + self.assertEqual(inst.related('a'), []) + class TestIntrospectable(unittest.TestCase): def _getTargetClass(slf): from pyramid.registry import Introspectable -- cgit v1.2.3 From d8e504cb1d486b9cd1caea7437ff212f92ed4fdb Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 21:25:43 -0500 Subject: make add_route generate the right request methods introspection value --- docs/narr/introspector.rst | 6 ++++-- pyramid/config/routes.py | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 71b41773c..1285a4cf1 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -258,7 +258,8 @@ introspectables in categories not described here. ``request_methods`` A sequence of request method names implied by the ``request_method`` - argument passed to ``add_route``. + argument passed to ``add_route`` or the value ``None`` if a + ``request_method`` argument was not supplied. ``path_info`` @@ -361,7 +362,8 @@ introspectables in categories not described here. ``request_methods`` A sequence of request method names implied by the ``request_method`` - argument passed to ``add_view``. + argument passed to ``add_view`` or the value ``None`` if a + ``request_method`` argument was not supplied. ``route_name`` diff --git a/pyramid/config/routes.py b/pyramid/config/routes.py index 2628f9cac..ea39b6805 100644 --- a/pyramid/config/routes.py +++ b/pyramid/config/routes.py @@ -348,6 +348,9 @@ class RoutesConfiguratorMixin(object): """ # these are route predicates; if they do not match, the next route # in the routelist will be tried + if request_method is not None: + request_method = as_sorted_tuple(request_method) + ignored, predicates, ignored = make_predicates( xhr=xhr, request_method=request_method, @@ -380,7 +383,7 @@ class RoutesConfiguratorMixin(object): intr['pattern'] = pattern intr['factory'] = factory intr['xhr'] = xhr - intr['request_methods'] = as_sorted_tuple(request_method) + intr['request_methods'] = request_method intr['path_info'] = path_info intr['request_param'] = request_param intr['header'] = header -- cgit v1.2.3 From fc3e425e64420fc05dc4fa1ebf83951934a2f005 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sat, 3 Dec 2011 21:29:25 -0500 Subject: wording --- docs/narr/introspector.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/narr/introspector.rst b/docs/narr/introspector.rst index 1285a4cf1..cfc6144dd 100644 --- a/docs/narr/introspector.rst +++ b/docs/narr/introspector.rst @@ -536,7 +536,7 @@ constructor in your application setup: from pyramid.registry import noop_introspector config = Configurator(..., introspector=noop_introspector) -When the noop introspector is active, all introspectables generated by the -framework are thrown away. A noop introspector behaves just like a "real" -introspector, but the methods of a noop introspector do nothing and return -null values. +When the noop introspector is active, all introspectables generated by +configuration directives are thrown away. A noop introspector behaves just +like a "real" introspector, but the methods of a noop introspector do nothing +and return null values. -- cgit v1.2.3 From 82f67769c3f32b60ddec8bb16285cec99f86994c Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Dec 2011 15:04:32 -0500 Subject: resolved actions must be ordered via (order, i) to capture the intent --- pyramid/config/__init__.py | 63 ++++++++++++++++++---------- pyramid/tests/test_config/test_init.py | 75 ++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 21 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index f95c876e0..52a7024a2 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1037,44 +1037,65 @@ def resolveConflicts(actions): if not isinstance(action, dict): # old-style tuple action action = expand_action(*action) + + # "order" is an integer grouping. Actions in a lower order will be + # executed before actions in a higher order. Within an order, + # actions are executed sequentially based on original action ordering + # ("i"). order = action['order'] or i discriminator = action['discriminator'] + + # "ainfo" is a tuple of (order, i, action) where "order" is a + # user-supplied grouping, "i" is an integer expressing the relative + # position of this action in the action list being resolved, and + # "action" is an action dictionary. The purpose of an ainfo is to + # associate an "order" and an "i" with a particular action; "order" + # and "i" exist for sorting purposes after conflict resolution. + ainfo = (order, i, action) + if discriminator is None: - # The discriminator is None, so this action can never - # conflict. We can add it directly to the result. - output.append((order, action)) + # The discriminator is None, so this action can never conflict. + # We can add it directly to the result. + output.append(ainfo) continue L = unique.setdefault(discriminator, []) - L.append((order, action)) + L.append(ainfo) # 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 bypath(tup): - return tup[1]['includepath'], tup[0] - dups.sort(key=bypath) - order, first = dups[0] - output.append(dups[0]) - basepath, baseinfo, discriminator = (first['includepath'], - first['info'], - first['discriminator']) - - for order, dup in dups[1:]: - includepath = dup['includepath'] + + for discriminator, ainfos in unique.items(): + + # We use (includepath, order, i) as a sort key because we need to + # sort the actions by the paths so that the shortest path with a + # given prefix comes first. The "first" action is the one with the + # shortest include path. We break sorting ties using "order", then + # "i". + def bypath(ainfo): + return ainfo[2]['includepath'], ainfo[0], ainfo[1] + + ainfos.sort(key=bypath) + ainfo, rest = ainfos[0], ainfos[1:] + output.append(ainfo) + _, _, action = ainfo + basepath, baseinfo, discriminator = (action['includepath'], + action['info'], + action['discriminator']) + + for _, _, action in rest: + includepath = action['includepath'] # Test whether path is a prefix of opath if (includepath[:len(basepath)] != basepath # not a prefix or includepath == basepath): L = conflicts.setdefault(discriminator, [baseinfo]) - L.append(dup['info']) + L.append(action['info']) if conflicts: raise ConfigurationConflictError(conflicts) - output.sort(key=operator.itemgetter(0)) - return [ x[1] for x in output ] + # sort conflict-resolved actions by (order, i) and return them + return [ x[2] for x in sorted(output, key=operator.itemgetter(0, 1))] def expand_action(discriminator, callable=None, args=(), kw=None, includepath=(), info=None, order=0, introspectables=()): diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index 27a8c9306..fc44908d7 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1764,6 +1764,81 @@ class Test_resolveConflicts(unittest.TestCase): ] ) + def test_it_with_actions_grouped_by_order(self): + from pyramid.tests.test_config import dummyfactory as f + from pyramid.config import expand_action + result = self._callFUT([ + expand_action(None, f), + expand_action(1, f, (1,), {}, (), 'third', 10), + expand_action(1, f, (2,), {}, ('x',), 'fourth', 10), + expand_action(1, f, (3,), {}, ('y',), 'fifth', 10), + expand_action(2, f, (1,), {}, (), 'sixth', 10), + expand_action(3, f, (1,), {}, (), 'seventh', 10), + expand_action(5, f, (4,), {}, ('y',), 'eighth', 99999), + expand_action(4, f, (3,), {}, (), 'first', 5), + expand_action(4, f, (5,), {}, ('y',), 'second', 5), + ]) + self.assertEqual(len(result), 6) + # resolved actions should be grouped by (order, i) + self.assertEqual( + result, + [{'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': (), + 'order': 5}, + + {'info': 'third', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 10}, + + {'info': 'sixth', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 2, + 'includepath': (), + 'order': 10}, + + {'info': 'seventh', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': (), + 'order': 10}, + + {'info': 'eighth', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 5, + 'includepath': ('y',), + 'order': 99999} + ] + ) + + class TestGlobalRegistriesIntegration(unittest.TestCase): def setUp(self): from pyramid.config import global_registries -- cgit v1.2.3 From 4b0dc407f46a4a2330f756c422719fe5961373cf Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Dec 2011 15:08:00 -0500 Subject: treat order exclusively as a grouping --- pyramid/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 52a7024a2..67df733d9 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1042,7 +1042,7 @@ def resolveConflicts(actions): # executed before actions in a higher order. Within an order, # actions are executed sequentially based on original action ordering # ("i"). - order = action['order'] or i + order = action['order'] or 0 discriminator = action['discriminator'] # "ainfo" is a tuple of (order, i, action) where "order" is a -- cgit v1.2.3 From 38e6b4012164eec480ca9604e68d737bff83b68e Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Dec 2011 15:11:37 -0500 Subject: make code clearer by using aliases --- pyramid/config/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 67df733d9..3ffbdbb47 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -1073,7 +1073,8 @@ def resolveConflicts(actions): # shortest include path. We break sorting ties using "order", then # "i". def bypath(ainfo): - return ainfo[2]['includepath'], ainfo[0], ainfo[1] + path, order, i = ainfo[2]['includepath'], ainfo[0], ainfo[1] + return path, order, i ainfos.sort(key=bypath) ainfo, rest = ainfos[0], ainfos[1:] -- cgit v1.2.3 From 549cf70449226539bd5b5db48fce3a6095c26cd9 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Dec 2011 16:16:44 -0500 Subject: change the ActionInfo interface to match ZCML's ParserInfo interface --- pyramid/config/__init__.py | 4 ++-- pyramid/config/util.py | 37 ++++++++++++++++++++++------------ pyramid/interfaces.py | 23 ++++++++++++++------- pyramid/tests/test_config/test_init.py | 2 +- pyramid/tests/test_config/test_util.py | 28 +++++++++++++++++++++---- 5 files changed, 67 insertions(+), 27 deletions(-) diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 3ffbdbb47..3b15e8ef2 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -488,13 +488,13 @@ class Configurator( @property def action_info(self): - info = self.info # usually a ZCML action if self.info has data + info = self.info # usually a ZCML action (ParserInfo) if self.info if not info: # Try to provide more accurate info for conflict reports if self._ainfo: info = self._ainfo[0] else: - info = ActionInfo('', 0, '', '') + info = ActionInfo(None, 0, '', '') return info def action(self, discriminator, callable=None, args=(), kw=None, order=0, diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 3fcb5d154..b65e44725 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -1,7 +1,10 @@ -import collections import re import traceback +from zope.interface import implementer + +from pyramid.interfaces import IActionInfo + from pyramid.compat import ( string_types, bytes_, @@ -20,19 +23,27 @@ from hashlib import md5 MAX_ORDER = 1 << 30 DEFAULT_PHASH = md5().hexdigest() -_ActionInfo = collections.namedtuple( - 'ActionInfo', - ('filename', 'lineno', 'function', 'linerepr') - ) +@implementer(IActionInfo) +class ActionInfo(object): + def __init__(self, file, line, function, src): + line = line or 0 + src = src or '' + ssrc = src.strip() + column = src.rfind(ssrc) + eline = line + len(src.split('\n')) + ecolumn = len(src.split('\n')[-1]) + srclines = src.split('\n') + src = '\n'.join(' %s' % x for x in srclines) + self._src = src + self.file = file + self.line = line + self.column = column + self.eline = eline + self.ecolumn = ecolumn + self.function = function -class ActionInfo(_ActionInfo): - # this is a namedtuple subclass for (minor) backwards compat - slots = () def __str__(self): - return ( - 'Line %s of file %s in %s: %r' % ( - self.lineno, self.filename, self.function, self.linerepr) - ) + return 'Line %s of file %s:\n%s' % (self.line, self.file, self._src) def action_method(wrapped): """ Wrapper to provide the right conflict info report data when a method @@ -46,7 +57,7 @@ def action_method(wrapped): f = traceback.extract_stack(limit=3) info = ActionInfo(*f[-2]) except: # pragma: no cover - info = ActionInfo('', 0, '', '') + info = ActionInfo(None, 0, '', '') self._ainfo.append(info) try: result = wrapped(self, *arg, **kw) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 559d3c110..2c096cf40 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1001,13 +1001,22 @@ class IIntrospectable(Interface): """ class IActionInfo(Interface): - filename = Attribute('filename of action-invoking code as a string') - lineno = Attribute('line number in file (as an integer) of action-invoking ' - 'code') - function = Attribute('a string representing the module, function or method ' - 'that enclosed the line which invoked the action') - linerepr = Attribute('a string representing the source code line ' - 'which invoked the action') + """ Class which provides code introspection capability associated with an + action. The ParserInfo class used by ZCML implements the same interface.""" + file = Attribute( + 'filename of action-invoking code as a string') + line = Attribute( + 'starting line number in file (as an integer) of action-invoking code') + column = Attribute( + 'start column number in file (as an integer) of action-invoking code') + eline = Attribute( + 'ending line number in file (as an integer) of action-invoking code') + ecolumn = Attribute( + 'ending column number in file (as an integer) of action-invoking code') + + def __str__(): + """ Return a representation of the action information (including + source code from file, if possible) """ # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index fc44908d7..c2b63dfc0 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -1925,7 +1925,7 @@ def _conflictFunctions(e): conflicts = e._conflicts.values() for conflict in conflicts: for confinst in conflict: - yield confinst[2] + yield confinst.function class DummyActionState(object): autocommit = False diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 1225b3e21..31aa7f77a 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -313,14 +313,34 @@ class Test__make_predicates(unittest.TestCase): self.assertEqual(hash1, hash2) class TestActionInfo(unittest.TestCase): - def _makeOne(self, filename, lineno, function, linerepr): + def _getTargetClass(self): from pyramid.config.util import ActionInfo - return ActionInfo(filename, lineno, function, linerepr) + return ActionInfo + + def _makeOne(self, filename, lineno, function, linerepr): + return self._getTargetClass()(filename, lineno, function, linerepr) + + def test_class_conforms(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IActionInfo + verifyClass(IActionInfo, self._getTargetClass()) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IActionInfo + verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) + + def test_ctor(self): + inst = self._makeOne('filename', 10, 'function', ' linerepr\n\nfoo') + self.assertEqual(inst.line, 10) + self.assertEqual(inst.column, 2) + self.assertEqual(inst.eline, 13) + self.assertEqual(inst.ecolumn, 3) def test___str__(self): - inst = self._makeOne('filename', 'lineno', 'function', 'linerepr') + inst = self._makeOne('filename', 0, 'function', ' linerepr ') self.assertEqual(str(inst), - "Line lineno of file filename in function: 'linerepr'") + "Line 0 of file filename:\n linerepr ") class DummyCustomPredicate(object): def __init__(self): -- cgit v1.2.3 From d2ed7edee5991a795597a4d8e14a7bcf84113748 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Dec 2011 16:36:31 -0500 Subject: dont try so hard --- pyramid/config/util.py | 10 +++------- pyramid/interfaces.py | 14 +++++++++----- pyramid/tests/test_config/test_util.py | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pyramid/config/util.py b/pyramid/config/util.py index b65e44725..81c5d8176 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -28,18 +28,14 @@ class ActionInfo(object): def __init__(self, file, line, function, src): line = line or 0 src = src or '' - ssrc = src.strip() - column = src.rfind(ssrc) - eline = line + len(src.split('\n')) - ecolumn = len(src.split('\n')[-1]) srclines = src.split('\n') src = '\n'.join(' %s' % x for x in srclines) self._src = src self.file = file self.line = line - self.column = column - self.eline = eline - self.ecolumn = ecolumn + self.column = None + self.eline = None + self.ecolumn = None self.function = function def __str__(self): diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 2c096cf40..c656c3510 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1004,15 +1004,19 @@ class IActionInfo(Interface): """ Class which provides code introspection capability associated with an action. The ParserInfo class used by ZCML implements the same interface.""" file = Attribute( - 'filename of action-invoking code as a string') + 'Filename of action-invoking code as a string') line = Attribute( - 'starting line number in file (as an integer) of action-invoking code') + 'Starting line number in file (as an integer) of action-invoking code.' + 'This will be ``None`` if the value could not be determined.') column = Attribute( - 'start column number in file (as an integer) of action-invoking code') + 'Starting column number in file (as an integer) of action-invoking ' + 'code. This will be ``None`` if the value could not be determined.') eline = Attribute( - 'ending line number in file (as an integer) of action-invoking code') + 'Ending line number in file (as an integer) of action-invoking code.' + 'This will be ``None`` if the value could not be determined.') ecolumn = Attribute( - 'ending column number in file (as an integer) of action-invoking code') + 'Ending column number in file (as an integer) of action-invoking code.' + 'This will be ``None`` if the value could not be determined.') def __str__(): """ Return a representation of the action information (including diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 31aa7f77a..12d3055d0 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -333,9 +333,9 @@ class TestActionInfo(unittest.TestCase): def test_ctor(self): inst = self._makeOne('filename', 10, 'function', ' linerepr\n\nfoo') self.assertEqual(inst.line, 10) - self.assertEqual(inst.column, 2) - self.assertEqual(inst.eline, 13) - self.assertEqual(inst.ecolumn, 3) + self.assertEqual(inst.column, None) + self.assertEqual(inst.eline, None) + self.assertEqual(inst.ecolumn, None) def test___str__(self): inst = self._makeOne('filename', 0, 'function', ' linerepr ') -- cgit v1.2.3 From 7d109d6522353bf5f5f3ca4f29bc2b27542f2ef2 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Dec 2011 17:01:04 -0500 Subject: allow config.action to take arbitrary kw args --- CHANGES.txt | 10 +++--- TODO.txt | 6 ---- pyramid/config/__init__.py | 77 +++++++++++++++++++++++++++++----------------- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 6ba72fc06..44f948180 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -30,13 +30,13 @@ Features - An configuration introspection system was added; see the narrative documentation chapter entitled "Pyramid Configuration Introspection" for - more information. - -- New APIs: ``pyramid.registry.Introspectable``, + more information. New APIs: ``pyramid.registry.Introspectable``, ``pyramid.config.Configurator.introspector``, ``pyramid.config.Configurator.introspectable``, - ``pyramid.registry.Registry.introspector``. See API docs of related - modules for more info. + ``pyramid.registry.Registry.introspector``. + +- Allow extra keyword arguments to be passed to the + ``pyramid.config.Configurator.action`` method. Bug Fixes --------- diff --git a/TODO.txt b/TODO.txt index 4692b073b..1253b9b16 100644 --- a/TODO.txt +++ b/TODO.txt @@ -12,12 +12,6 @@ Must-Have * ``default view mapper`` category? - * ActionInfo for ZCML actions (begin/end lineno/cols?) - - * Document ActionInfo. - - * categorize() return value ordering not right yet. - * implement ptweens and proutes based on introspection instead of current state of affairs. diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 3b15e8ef2..315cdef07 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -239,9 +239,10 @@ class Configurator( prepended to their pattern. This parameter is new in Pyramid 1.2. If ``introspector`` is passed, it must be an instance implementing the - :class:`pyramid.interfaces.IIntrospector` interface. If no - ``introspector`` is passed, the default IIntrospector implementation will - be used. This parameter is new in Pyramid 1.3. + attributes and methods of :class:`pyramid.interfaces.IIntrospector`. If + ``introspector`` is not passed (or is passed as ``None``), the default + introspector implementation will be used. This parameter is new in + Pyramid 1.3. """ manager = manager # for testing injection venusian = venusian # for testing injection @@ -498,7 +499,7 @@ class Configurator( return info def action(self, discriminator, callable=None, args=(), kw=None, order=0, - introspectables=()): + introspectables=(), **extra): """ Register an action which will be executed when :meth:`pyramid.config.Configurator.commit` is called (or executed immediately if ``autocommit`` is ``True``). @@ -511,13 +512,24 @@ class Configurator( given, but it can be ``None``, to indicate that the action never conflicts. It must be a hashable value. - The ``callable`` is a callable object which performs the action. It - is optional. ``args`` and ``kw`` are tuple and dict objects - respectively, which are passed to ``callable`` when this action is - executed. + The ``callable`` is a callable object which performs the task + associated with the action when the action is executed. It is + optional. - ``order`` is a crude order control mechanism, only rarely used (has - no effect when autocommit is ``True``). + ``args`` and ``kw`` are tuple and dict objects respectively, which + are passed to ``callable`` when this action is executed. Both are + optional. + + ``order`` is a grouping mechanism; an action with a lower order will + be executed before an action with a higher order (has no effect when + autocommit is ``True``). + + ``introspectables`` is a sequence of :term:`introspectable` objects + (or the empty sequence if no introspectable objects are associated + with this action). + + ``extra`` provides a facility for inserting extra keys and values + into an action dictionary. """ if kw is None: kw = {} @@ -534,16 +546,20 @@ class Configurator( introspectable.register(introspector, action_info) else: - self.action_state.action( - discriminator=discriminator, - callable=callable, - args=args, - kw=kw, - order=order, - info=action_info, - includepath=self.includepath, - introspectables=introspectables, + action = extra + action.update( + dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + order=order, + info=action_info, + includepath=self.includepath, + introspectables=introspectables, + ) ) + self.action_state.action(**action) def _get_action_state(self): registry = self.registry @@ -921,20 +937,23 @@ class ActionState(object): return True def action(self, discriminator, callable=None, args=(), kw=None, order=0, - includepath=(), info=None, introspectables=()): + includepath=(), info=None, introspectables=(), **extra): """Add an action with the given discriminator, callable and arguments """ if kw is None: kw = {} - action = dict( - discriminator=discriminator, - callable=callable, - args=args, - kw=kw, - includepath=includepath, - info=info, - order=order, - introspectables=introspectables, + action = extra + action.update( + dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + includepath=includepath, + info=info, + order=order, + introspectables=introspectables, + ) ) self.actions.append(action) -- cgit v1.2.3 From 4a4ef4f7ac6d94e00b6beb2a97472ed6d7bdddd8 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Dec 2011 19:11:14 -0500 Subject: simplify actioninfo interface; fix actioninfos passed as tuple via _info --- TODO.txt | 6 +++++- pyramid/config/util.py | 16 +++++++--------- pyramid/interfaces.py | 9 --------- pyramid/registry.py | 1 - pyramid/tests/test_config/test_init.py | 2 +- pyramid/tests/test_config/test_util.py | 8 ++++---- 6 files changed, 17 insertions(+), 25 deletions(-) diff --git a/TODO.txt b/TODO.txt index 1253b9b16..baed9e03f 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,12 +6,16 @@ Must-Have - Introspection: - * Narrative docs. + * Why do translation directories report weird actioninfo? + + * Review narrative docs. * ``default root factory`` category? * ``default view mapper`` category? + * get rid of "tweens" category (can't sort properly?) + * implement ptweens and proutes based on introspection instead of current state of affairs. diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 81c5d8176..3a2f911dc 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -26,20 +26,15 @@ DEFAULT_PHASH = md5().hexdigest() @implementer(IActionInfo) class ActionInfo(object): def __init__(self, file, line, function, src): - line = line or 0 - src = src or '' - srclines = src.split('\n') - src = '\n'.join(' %s' % x for x in srclines) - self._src = src self.file = file self.line = line - self.column = None - self.eline = None - self.ecolumn = None self.function = function + self.src = src def __str__(self): - return 'Line %s of file %s:\n%s' % (self.line, self.file, self._src) + srclines = self.src.split('\n') + src = '\n'.join(' %s' % x for x in srclines) + return 'Line %s of file %s:\n%s' % (self.line, self.file, src) def action_method(wrapped): """ Wrapper to provide the right conflict info report data when a method @@ -48,6 +43,9 @@ def action_method(wrapped): if self._ainfo is None: self._ainfo = [] info = kw.pop('_info', None) + if is_nonstr_iter(info) and len(info) == 4: + # _info permitted as extract_stack tuple + info = ActionInfo(*info) if info is None: try: f = traceback.extract_stack(limit=3) diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index c656c3510..0261ae3db 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1008,15 +1008,6 @@ class IActionInfo(Interface): line = Attribute( 'Starting line number in file (as an integer) of action-invoking code.' 'This will be ``None`` if the value could not be determined.') - column = Attribute( - 'Starting column number in file (as an integer) of action-invoking ' - 'code. This will be ``None`` if the value could not be determined.') - eline = Attribute( - 'Ending line number in file (as an integer) of action-invoking code.' - 'This will be ``None`` if the value could not be determined.') - ecolumn = Attribute( - 'Ending column number in file (as an integer) of action-invoking code.' - 'This will be ``None`` if the value could not be determined.') def __str__(): """ Return a representation of the action information (including diff --git a/pyramid/registry.py b/pyramid/registry.py index d594ae910..7e373b58a 100644 --- a/pyramid/registry.py +++ b/pyramid/registry.py @@ -243,5 +243,4 @@ class Introspectable(dict): (category_name, discriminator) ) - global_registry = Registry('global') diff --git a/pyramid/tests/test_config/test_init.py b/pyramid/tests/test_config/test_init.py index c2b63dfc0..17dacdc5b 100644 --- a/pyramid/tests/test_config/test_init.py +++ b/pyramid/tests/test_config/test_init.py @@ -976,7 +976,7 @@ pyramid.tests.test_config.dummy_include2""", conflicts = e._conflicts.values() for conflict in conflicts: for confinst in conflict: - yield confinst[3] + yield confinst.src which = list(scanconflicts(why)) self.assertEqual(len(which), 4) self.assertTrue("@view_config(renderer='string')" in which) diff --git a/pyramid/tests/test_config/test_util.py b/pyramid/tests/test_config/test_util.py index 12d3055d0..1180e7e29 100644 --- a/pyramid/tests/test_config/test_util.py +++ b/pyramid/tests/test_config/test_util.py @@ -331,11 +331,11 @@ class TestActionInfo(unittest.TestCase): verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) def test_ctor(self): - inst = self._makeOne('filename', 10, 'function', ' linerepr\n\nfoo') + inst = self._makeOne('filename', 10, 'function', 'src') + self.assertEqual(inst.file, 'filename') self.assertEqual(inst.line, 10) - self.assertEqual(inst.column, None) - self.assertEqual(inst.eline, None) - self.assertEqual(inst.ecolumn, None) + self.assertEqual(inst.function, 'function') + self.assertEqual(inst.src, 'src') def test___str__(self): inst = self._makeOne('filename', 0, 'function', ' linerepr ') -- cgit v1.2.3 From a78b58dd5cf665f7a7aaa18e9e7f6cae3fc7f749 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Sun, 4 Dec 2011 19:26:02 -0500 Subject: add_translation_dirs is now an action method --- TODO.txt | 2 -- pyramid/config/i18n.py | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index baed9e03f..b7ea305c6 100644 --- a/TODO.txt +++ b/TODO.txt @@ -6,8 +6,6 @@ Must-Have - Introspection: - * Why do translation directories report weird actioninfo? - * Review narrative docs. * ``default root factory`` category? diff --git a/pyramid/config/i18n.py b/pyramid/config/i18n.py index 60c7a71c1..67a7e2018 100644 --- a/pyramid/config/i18n.py +++ b/pyramid/config/i18n.py @@ -50,6 +50,7 @@ class I18NConfiguratorMixin(object): locale_negotiator = self.maybe_dotted(negotiator) self.registry.registerUtility(locale_negotiator, ILocaleNegotiator) + @action_method def add_translation_dirs(self, *specs): """ Add one or more :term:`translation directory` paths to the current configuration state. The ``specs`` argument is a -- cgit v1.2.3