summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2017-06-14 08:51:43 -0500
committerJason Madden <jamadden@gmail.com>2017-06-14 08:51:43 -0500
commit751a95088567e4d219ecac6f2145f26b5753e424 (patch)
treeae1305d3c81ddf6dbdc4b7298cc32941084ec54b
parentbf0565350d8973f8ba260d0824678248d053de11 (diff)
downloadzope-interface-issue93.tar.gz
Fix the cache getting out of sync with _utility_registrations.issue93
Fixes #93
-rw-r--r--CHANGES.rst4
-rw-r--r--src/zope/interface/registry.py29
-rw-r--r--src/zope/interface/tests/test_registry.py53
3 files changed, 73 insertions, 13 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index f94f3c2..64a4eb7 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -8,6 +8,10 @@ Changes
``zope.component.persistentregistry.PersistentRegistry`` instances.
See `issue 85 <https://github.com/zopefoundation/zope.interface/issues/85>`_.
+- Fix a regression that could lead to the utility registration cache
+ of ``Components`` getting out of sync. See `issue 93
+ <https://github.com/zopefoundation/zope.interface/issues/93>`_.
+
4.4.1 (2017-05-13)
------------------
diff --git a/src/zope/interface/registry.py b/src/zope/interface/registry.py
index 26855ca..bba0267 100644
--- a/src/zope/interface/registry.py
+++ b/src/zope/interface/registry.py
@@ -67,12 +67,14 @@ class _UnhashableComponentCounter(object):
return
raise KeyError(component) # pragma: no cover
+def _defaultdict_int():
+ return defaultdict(int)
class _UtilityRegistrations(object):
def __init__(self, utilities, utility_registrations):
# {provided -> {component: count}}
- self._cache = defaultdict(lambda: defaultdict(int))
+ self._cache = defaultdict(_defaultdict_int)
self._utilities = utilities
self._utility_registrations = utility_registrations
@@ -138,18 +140,17 @@ class _UtilityRegistrations(object):
@implementer(IComponents)
class Components(object):
+ _v_utility_registrations_cache = None
+
def __init__(self, name='', bases=()):
+ # __init__ is used for test cleanup as well as initialization.
+ # XXX add a separate API for test cleanup.
assert isinstance(name, STRING_TYPES)
self.__name__ = name
self._init_registries()
self._init_registrations()
self.__bases__ = tuple(bases)
-
- # __init__ is used for test cleanup as well as initialization.
- # XXX add a separate API for test cleanup.
- # See _utility_registrations below.
- if hasattr(self, '_v_utility_registrations_cache'):
- del self._v_utility_registrations_cache
+ self._v_utility_registrations_cache = None
def __repr__(self):
return "<%s %s>" % (self.__class__.__name__, self.__name__)
@@ -185,12 +186,14 @@ class Components(object):
def _utility_registrations_cache(self):
# We use a _v_ attribute internally so that data aren't saved in ZODB,
# because this object cannot be pickled.
- try:
- return self._v_utility_registrations_cache
- except AttributeError:
- self._v_utility_registrations_cache = _UtilityRegistrations(
- self.utilities, self._utility_registrations)
- return self._v_utility_registrations_cache
+ cache = self._v_utility_registrations_cache
+ if (cache is None
+ or cache._utilities is not self.utilities
+ or cache._utility_registrations is not self._utility_registrations):
+ cache = self._v_utility_registrations_cache = _UtilityRegistrations(
+ self.utilities,
+ self._utility_registrations)
+ return cache
def _getBases(self):
# Subclasses might override
diff --git a/src/zope/interface/tests/test_registry.py b/src/zope/interface/tests/test_registry.py
index dd3cbbe..e5a8eb0 100644
--- a/src/zope/interface/tests/test_registry.py
+++ b/src/zope/interface/tests/test_registry.py
@@ -342,6 +342,59 @@ class ComponentsTests(unittest.TestCase):
comp.registerUtility(_to_reg, ifoo, _name, _info, False)
self.assertEqual(len(_events), 0)
+ def test_registerUtility_changes_object_identity_after(self):
+ # If a subclass changes the identity of the _utility_registrations,
+ # the cache is updated and the right thing still happens.
+ class CompThatChangesAfter1Reg(self._getTargetClass()):
+ reg_count = 0
+ def registerUtility(self, *args):
+ self.reg_count += 1
+ super(CompThatChangesAfter1Reg, self).registerUtility(*args)
+ if self.reg_count == 1:
+ self._utility_registrations = dict(self._utility_registrations)
+
+ comp = CompThatChangesAfter1Reg()
+ comp.registerUtility(object(), Interface)
+
+ self.assertEqual(len(list(comp.registeredUtilities())), 1)
+
+ class IFoo(Interface):
+ pass
+
+ comp.registerUtility(object(), IFoo)
+ self.assertEqual(len(list(comp.registeredUtilities())), 2)
+
+ def test_registerUtility_changes_object_identity_before(self):
+ # If a subclass changes the identity of the _utility_registrations,
+ # the cache is updated and the right thing still happens.
+ class CompThatChangesAfter2Reg(self._getTargetClass()):
+ reg_count = 0
+ def registerUtility(self, *args):
+ self.reg_count += 1
+ if self.reg_count == 2:
+ self._utility_registrations = dict(self._utility_registrations)
+
+ super(CompThatChangesAfter2Reg, self).registerUtility(*args)
+
+ comp = CompThatChangesAfter2Reg()
+ comp.registerUtility(object(), Interface)
+
+ self.assertEqual(len(list(comp.registeredUtilities())), 1)
+
+ class IFoo(Interface):
+ pass
+
+ comp.registerUtility(object(), IFoo)
+ self.assertEqual(len(list(comp.registeredUtilities())), 2)
+
+
+ class IBar(Interface):
+ pass
+
+ comp.registerUtility(object(), IBar)
+ self.assertEqual(len(list(comp.registeredUtilities())), 3)
+
+
def test_unregisterUtility_neither_factory_nor_component_nor_provided(self):
comp = self._makeOne()
self.assertRaises(TypeError, comp.unregisterUtility,