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