Skip to content

Commit

Permalink
Merge 820b9b0 into dd69666
Browse files Browse the repository at this point in the history
  • Loading branch information
jamadden committed Mar 15, 2021
2 parents dd69666 + 820b9b0 commit 15703b0
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 7 deletions.
12 changes: 12 additions & 0 deletions CHANGES.rst
Expand Up @@ -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)
==================

Expand Down
26 changes: 23 additions & 3 deletions src/zope/interface/adapter.py
Expand Up @@ -293,11 +293,14 @@ def register(self, required, provided, name, value):

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

Expand All @@ -312,6 +315,14 @@ def registered(self, required, provided, name=u''):

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:
Expand Down Expand Up @@ -430,6 +441,15 @@ def subscribe(self, required, provided, value):

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
Expand Down
51 changes: 47 additions & 4 deletions src/zope/interface/interfaces.py
Expand Up @@ -997,19 +997,62 @@ def subscribe(required, provided, subscriber): # pylint:disable=arguments-differ
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.
Subscribers have no names.
"""

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.
Expand Down
66 changes: 66 additions & 0 deletions src/zope/interface/registry.py
Expand Up @@ -505,6 +505,72 @@ def unregisterHandler(self, factory=None, required=None, name=u''):
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:
Expand Down
23 changes: 23 additions & 0 deletions src/zope/interface/tests/test_adapter.py
Expand Up @@ -621,6 +621,29 @@ def test_subscribe_unsubscribe_nonequal_objects_provided(self):
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()
Expand Down
86 changes: 86 additions & 0 deletions src/zope/interface/tests/test_registry.py
Expand Up @@ -2351,6 +2351,92 @@ class IFoo(Interface):
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):

Expand Down

0 comments on commit 15703b0

Please sign in to comment.