summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2011-11-30 12:55:41 -0500
committerChris McDonough <chrism@plope.com>2011-11-30 12:55:41 -0500
commit57a0d7765c54031e6ac83881b536712316f22c45 (patch)
tree2b708572847ab35aa5172d5d5255daf8f5f07485
parent47e1f7336d2d954182cb439a0327d93044cfa244 (diff)
downloadpyramid-57a0d7765c54031e6ac83881b536712316f22c45.tar.gz
pyramid-57a0d7765c54031e6ac83881b536712316f22c45.tar.bz2
pyramid-57a0d7765c54031e6ac83881b536712316f22c45.zip
docs; todo; coverage for Introspector
-rw-r--r--CHANGES.txt6
-rw-r--r--TODO.txt5
-rw-r--r--docs/api/config.rst15
-rw-r--r--docs/api/interfaces.rst6
-rw-r--r--docs/api/registry.rst25
-rw-r--r--pyramid/interfaces.py23
-rw-r--r--pyramid/registry.py24
-rw-r--r--pyramid/tests/test_registry.py212
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):