diff options
-rw-r--r-- | CHANGES.rst | 12 | ||||
-rw-r--r-- | src/zope/interface/adapter.py | 26 | ||||
-rw-r--r-- | src/zope/interface/interfaces.py | 51 | ||||
-rw-r--r-- | src/zope/interface/registry.py | 66 | ||||
-rw-r--r-- | src/zope/interface/tests/test_adapter.py | 23 | ||||
-rw-r--r-- | src/zope/interface/tests/test_registry.py | 86 |
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): |