summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst12
-rw-r--r--src/zope/interface/adapter.py26
-rw-r--r--src/zope/interface/interfaces.py51
-rw-r--r--src/zope/interface/registry.py66
-rw-r--r--src/zope/interface/tests/test_adapter.py23
-rw-r--r--src/zope/interface/tests/test_registry.py86
6 files changed, 257 insertions, 7 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index ff3254c..94f07b1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -24,6 +24,18 @@
to fix the reference counting issue mentioned above, as well as to
update the data structures when custom data types have changed.
+- Add the interface method ``IAdapterRegistry.subscribed()`` and
+ implementation ``BaseAdapterRegistry.subscribed()`` for querying
+ directly registered subscribers. See `issue 230
+ <https://github.com/zopefoundation/zope.interface/issues/230>`_.
+
+- Add the maintenance method
+ ``Components.rebuildUtilityRegistryFromLocalCache()``. Most users
+ will not need this, but it can be useful if the ``Components.utilities``
+ registry is suspected to be out of sync with the ``Components``
+ object itself (this might happen to persistent ``Components``
+ implementations in the face of bugs).
+
5.2.0 (2020-11-05)
==================
diff --git a/src/zope/interface/adapter.py b/src/zope/interface/adapter.py
index b9836d9..d85ed8d 100644
--- a/src/zope/interface/adapter.py
+++ b/src/zope/interface/adapter.py
@@ -296,11 +296,14 @@ class BaseAdapterRegistry(object):
self.changed(self)
- def registered(self, required, provided, name=u''):
+ def _find_leaf(self, byorder, required, provided, name):
+ # Find the leaf value, if any, in the *byorder* list
+ # for the interface sequence *required* and the interface
+ # *provided*, given the already normalized *name*.
+ #
+ # If no such leaf value exists, returns ``None``
required = tuple([_convert_None_to_Interface(r) for r in required])
- name = _normalize_name(name)
order = len(required)
- byorder = self._adapters
if len(byorder) <= order:
return None
@@ -315,6 +318,14 @@ class BaseAdapterRegistry(object):
return components.get(name)
+ def registered(self, required, provided, name=u''):
+ return self._find_leaf(
+ self._adapters,
+ required,
+ provided,
+ _normalize_name(name)
+ )
+
@classmethod
def _allKeys(cls, components, i, parent_k=()):
if i == 0:
@@ -433,6 +444,15 @@ class BaseAdapterRegistry(object):
self.changed(self)
+ def subscribed(self, required, provided, subscriber):
+ subscribers = self._find_leaf(
+ self._subscribers,
+ required,
+ provided,
+ u''
+ ) or ()
+ return subscriber if subscriber in subscribers else None
+
def allSubscriptions(self):
"""
Yields tuples ``(required, provided, value)`` for all the
diff --git a/src/zope/interface/interfaces.py b/src/zope/interface/interfaces.py
index eb08657..77bc3a0 100644
--- a/src/zope/interface/interfaces.py
+++ b/src/zope/interface/interfaces.py
@@ -997,11 +997,35 @@ class IAdapterRegistry(Interface):
Subscribers have no names.
"""
+ def subscribed(required, provided, subscriber):
+ """
+ Check whether the object *subscriber* is registered directly
+ with this object via a previous call to
+ ``subscribe(required, provided, subscriber)``.
+
+ If the *subscriber*, or one equal to it, has been subscribed,
+ for the given *required* sequence and *provided* interface,
+ return that object. (This does not guarantee whether the *subscriber*
+ itself is returned, or an object equal to it.)
+
+ If it has not, return ``None``.
+
+ Unlike :meth:`subscriptions`, this method won't retrieve
+ components registered for more specific required interfaces or
+ less specific provided interfaces.
+
+ .. versionadded:: 5.3.0
+ """
+
def subscriptions(required, provided):
- """Get a sequence of subscribers
+ """
+ Get a sequence of subscribers.
- Subscribers for a **sequence** of *required* interfaces, and a *provided*
- interface are returned.
+ Subscribers for a sequence of *required* interfaces, and a *provided*
+ interface are returned. This takes into account subscribers
+ registered with this object, as well as those registered with
+ base adapter registries in the resolution order, and interfaces that
+ extend *provided*.
.. versionchanged:: 5.1.1
Correct the method signature to remove the ``name`` parameter.
@@ -1009,7 +1033,26 @@ class IAdapterRegistry(Interface):
"""
def subscribers(objects, provided):
- """Get a sequence of subscription adapters
+ """
+ Get a sequence of subscription **adapters**.
+
+ This is like :meth:`subscriptions`, but calls the returned
+ subscribers with *objects* (and optionally returns the results
+ of those calls), instead of returning the subscribers directly.
+
+ :param objects: A sequence of objects; they will be used to
+ determine the *required* argument to :meth:`subscriptions`.
+ :param provided: A single interface, or ``None``, to pass
+ as the *provided* parameter to :meth:`subscriptions`.
+ If an interface is given, the results of calling each returned
+ subscriber with the the *objects* are collected and returned
+ from this method; each result should be an object implementing
+ the *provided* interface. If ``None``, the resulting subscribers
+ are still called, but the results are ignored.
+ :return: A sequence of the results of calling the subscribers
+ if *provided* is not ``None``. If there are no registered
+ subscribers, or *provided* is ``None``, this will be an empty
+ sequence.
.. versionchanged:: 5.1.1
Correct the method signature to remove the ``name`` parameter.
diff --git a/src/zope/interface/registry.py b/src/zope/interface/registry.py
index 90ae1ad..4fdb120 100644
--- a/src/zope/interface/registry.py
+++ b/src/zope/interface/registry.py
@@ -505,6 +505,72 @@ class Components(object):
def handle(self, *objects):
self.adapters.subscribers(objects, None)
+ def rebuildUtilityRegistryFromLocalCache(self, rebuild=False):
+ """
+ Emergency maintenance method to rebuild the ``.utilities``
+ registry from the local copy maintained in this object, or
+ detect the need to do so.
+
+ Most users will never need to call this, but it can be helpful
+ in the event of suspected corruption.
+
+ By default, this method only checks for corruption. To make it
+ actually rebuild the registry, pass `True` for *rebuild*.
+
+ :param bool rebuild: If set to `True` (not the default),
+ this method will actually register and subscribe utilities
+ in the registry as needed to synchronize with the local cache.
+
+ :return: A dictionary that's meant as diagnostic data. The keys
+ and values may change over time. When called with a false *rebuild*,
+ the keys ``"needed_registered"`` and ``"needed_subscribed"`` will be
+ non-zero if any corruption was detected, but that will not be corrected.
+
+ .. versionadded:: 5.3.0
+ """
+ regs = dict(self._utility_registrations)
+ utils = self.utilities
+ needed_registered = 0
+ did_not_register = 0
+ needed_subscribed = 0
+ did_not_subscribe = 0
+
+
+ # Avoid the expensive change process during this; we'll call
+ # it once at the end if needed.
+ assert 'changed' not in utils.__dict__
+ utils.changed = lambda _: None
+
+ if rebuild:
+ register = utils.register
+ subscribe = utils.subscribe
+ else:
+ register = subscribe = lambda *args: None
+
+ try:
+ for (provided, name), (value, _info, _factory) in regs.items():
+ if utils.registered((), provided, name) != value:
+ register((), provided, name, value)
+ needed_registered += 1
+ else:
+ did_not_register += 1
+
+ if utils.subscribed((), provided, value) is None:
+ needed_subscribed += 1
+ subscribe((), provided, value)
+ else:
+ did_not_subscribe += 1
+ finally:
+ del utils.changed
+ if rebuild and (needed_subscribed or needed_registered):
+ utils.changed(utils)
+
+ return {
+ 'needed_registered': needed_registered,
+ 'did_not_register': did_not_register,
+ 'needed_subscribed': needed_subscribed,
+ 'did_not_subscribe': did_not_subscribe
+ }
def _getName(component):
try:
diff --git a/src/zope/interface/tests/test_adapter.py b/src/zope/interface/tests/test_adapter.py
index 2412f41..2ab84ca 100644
--- a/src/zope/interface/tests/test_adapter.py
+++ b/src/zope/interface/tests/test_adapter.py
@@ -621,6 +621,29 @@ class BaseAdapterRegistryTests(unittest.TestCase):
self.assertEqual(len(registry._subscribers), 0)
self.assertEqual(registry._provided, PT())
+ def test_subscribed_empty(self):
+ registry = self._makeOne()
+ self.assertIsNone(registry.subscribed([None], None, ''))
+ subscribed = list(registry.allSubscriptions())
+ self.assertEqual(subscribed, [])
+
+ def test_subscribed_non_empty_miss(self):
+ IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable
+ registry = self._makeOne()
+ registry.subscribe([IB1], IF0, 'A1')
+ # Mismatch required
+ self.assertIsNone(registry.subscribed([IB2], IF0, ''))
+ # Mismatch provided
+ self.assertIsNone(registry.subscribed([IB1], IF1, ''))
+ # Mismatch value
+ self.assertIsNone(registry.subscribed([IB1], IF0, ''))
+
+ def test_subscribed_non_empty_hit(self):
+ IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable
+ registry = self._makeOne()
+ registry.subscribe([IB0], IF0, 'A1')
+ self.assertEqual(registry.subscribed([IB0], IF0, 'A1'), 'A1')
+
def test_unsubscribe_w_None_after_multiple(self):
IB0, IB1, IB2, IB3, IB4, IF0, IF1, IR0, IR1 = _makeInterfaces() # pylint:disable=unused-variable
registry = self._makeOne()
diff --git a/src/zope/interface/tests/test_registry.py b/src/zope/interface/tests/test_registry.py
index e9e89ad..81bb58a 100644
--- a/src/zope/interface/tests/test_registry.py
+++ b/src/zope/interface/tests/test_registry.py
@@ -2351,6 +2351,92 @@ class ComponentsTests(unittest.TestCase):
def test_register_unregister_nonequal_objects_provided(self):
self.test_register_unregister_identical_objects_provided(identical=False)
+ def test_rebuildUtilityRegistryFromLocalCache(self):
+ class IFoo(Interface):
+ "Does nothing"
+
+ class UtilityImplementingFoo(object):
+ "Does nothing"
+
+ comps = self._makeOne()
+
+ for i in range(30):
+ comps.registerUtility(UtilityImplementingFoo(), IFoo, name=u'%s' % (i,))
+
+ orig_generation = comps.utilities._generation
+
+ orig_adapters = comps.utilities._adapters
+ self.assertEqual(len(orig_adapters), 1)
+ self.assertEqual(len(orig_adapters[0]), 1)
+ self.assertEqual(len(orig_adapters[0][IFoo]), 30)
+
+ orig_subscribers = comps.utilities._subscribers
+ self.assertEqual(len(orig_subscribers), 1)
+ self.assertEqual(len(orig_subscribers[0]), 1)
+ self.assertEqual(len(orig_subscribers[0][IFoo]), 1)
+ self.assertEqual(len(orig_subscribers[0][IFoo][u'']), 30)
+
+ # Blow a bunch of them away, creating artificial corruption
+ new_adapters = comps.utilities._adapters = type(orig_adapters)()
+ new_adapters.append({})
+ d = new_adapters[0][IFoo] = {}
+ for name in range(10):
+ name = type(u'')(str(name))
+ d[name] = orig_adapters[0][IFoo][name]
+
+ self.assertNotEqual(orig_adapters, new_adapters)
+
+ new_subscribers = comps.utilities._subscribers = type(orig_subscribers)()
+ new_subscribers.append({})
+ d = new_subscribers[0][IFoo] = {}
+ d[u''] = ()
+
+ for name in range(5, 12): # 12 - 5 = 7
+ name = type(u'')(str(name))
+ comp = orig_adapters[0][IFoo][name]
+ d[u''] += (comp,)
+
+ # We can preflight (by default) and nothing changes
+ rebuild_results_preflight = comps.rebuildUtilityRegistryFromLocalCache()
+
+ self.assertEqual(comps.utilities._generation, orig_generation)
+ self.assertEqual(rebuild_results_preflight, {
+ 'did_not_register': 10,
+ 'needed_registered': 20,
+
+ 'did_not_subscribe': 7,
+ 'needed_subscribed': 23,
+ })
+
+ # Now for real
+ rebuild_results = comps.rebuildUtilityRegistryFromLocalCache(rebuild=True)
+
+ # The generation only got incremented once
+ self.assertEqual(comps.utilities._generation, orig_generation + 1)
+ # The result was the same
+ self.assertEqual(rebuild_results_preflight, rebuild_results)
+ self.assertEqual(new_adapters, orig_adapters)
+ self.assertEqual(
+ len(new_subscribers[0][IFoo][u'']),
+ len(orig_subscribers[0][IFoo][u'']))
+
+ for orig_subscriber in orig_subscribers[0][IFoo][u'']:
+ self.assertIn(orig_subscriber, new_subscribers[0][IFoo][u''])
+
+ # Preflighting, rebuilding again produce no changes.
+ preflight_after = comps.rebuildUtilityRegistryFromLocalCache()
+ self.assertEqual(preflight_after, {
+ 'did_not_register': 30,
+ 'needed_registered': 0,
+
+ 'did_not_subscribe': 30,
+ 'needed_subscribed': 0,
+ })
+
+ rebuild_after = comps.rebuildUtilityRegistryFromLocalCache(rebuild=True)
+ self.assertEqual(rebuild_after, preflight_after)
+ self.assertEqual(comps.utilities._generation, orig_generation + 1)
+
class UnhashableComponentsTests(ComponentsTests):