diff options
| -rw-r--r-- | CHANGES.txt | 6 | ||||
| -rw-r--r-- | TODO.txt | 5 | ||||
| -rw-r--r-- | docs/api/config.rst | 15 | ||||
| -rw-r--r-- | docs/api/interfaces.rst | 6 | ||||
| -rw-r--r-- | docs/api/registry.rst | 25 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 23 | ||||
| -rw-r--r-- | pyramid/registry.py | 24 | ||||
| -rw-r--r-- | 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 --------- @@ -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): |
