Add IAdapterRegistry.subscribed and Components.rebuildUtilityRegistryFromLocalCache

Fixes #230
This commit is contained in:
Jason Madden 2021-03-15 09:43:24 -05:00
parent dd69666aae
commit 50c73de0a8
No known key found for this signature in database
GPG Key ID: 349F84431A08B99E
6 changed files with 255 additions and 5 deletions

View File

@ -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)
==================

View File

@ -430,6 +430,26 @@ class BaseAdapterRegistry(object):
self.changed(self)
def subscribed(self, required, provided, subscriber):
required = tuple([_convert_None_to_Interface(r) for r in required])
name = u''
order = len(required)
byorder = self._subscribers
if len(byorder) <= order:
return None
components = byorder[order]
key = required + (provided,)
for k in key:
d = components.get(k)
if d is None:
return None
components = d
subscribers = components.get(name, ())
return subscriber if subscriber in subscribers else None
def allSubscriptions(self):
"""
Yields tuples ``(required, provided, value)`` for all the

View File

@ -997,11 +997,35 @@ class IAdapterRegistry(Interface):
Subscribers have no names.
"""
def subscriptions(required, provided):
"""Get a sequence of subscribers
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)``.
Subscribers for a **sequence** of *required* interfaces, and a *provided*
interface are returned.
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.
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.

View File

@ -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:

View File

@ -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()

View File

@ -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):