diff options
| author | Jason Madden <jason+github@nextthought.com> | 2016-08-30 09:50:16 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2016-08-30 09:50:16 -0500 |
| commit | 4ad360e44ec52b742750c8201676d7fe934f5f2a (patch) | |
| tree | fbff0354b5618d43094f8bd382321933589c6d08 | |
| parent | 8138c73db26b17ca1967daa2908845b2d2f21cb8 (diff) | |
| parent | 1e2b756500b7b8518884ff4c3bd8bd64e62641a4 (diff) | |
| download | zope-interface-4ad360e44ec52b742750c8201676d7fe934f5f2a.tar.gz | |
Merge pull request #48 from zopefoundation/fix-46
Use dictionary lookups for testing subscribed status.
| -rw-r--r-- | .coveragerc | 2 | ||||
| -rw-r--r-- | CHANGES.rst | 6 | ||||
| -rw-r--r-- | src/zope/interface/registry.py | 153 | ||||
| -rw-r--r-- | src/zope/interface/tests/test_registry.py | 190 |
4 files changed, 276 insertions, 75 deletions
diff --git a/.coveragerc b/.coveragerc index 58d9a3a..507ddc7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,4 +1,4 @@ [report] exclude_lines = - # pragma: no cover + pragma: no cover class I[A-Z]\w+\((Interface|I[A-Z].*)\): diff --git a/CHANGES.rst b/CHANGES.rst index 191f2fe..643b5f4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -12,6 +12,12 @@ Changes - Make ``setuptools`` a hard dependency of ``setup.py``. (https://github.com/zopefoundation/zope.interface/issues/13) +- Change a linear algorithm (O(n)) in ``Components.registerUtility`` and + ``Components.unregisterUtility`` into a dictionary lookup (O(1)) for + hashable components. This substantially improves the time taken to + manipulate utilities in large registries at the cost of some + additional memory usage. (https://github.com/zopefoundation/zope.interface/issues/46) + 4.2.0 (2016-06-10) ------------------ diff --git a/src/zope/interface/registry.py b/src/zope/interface/registry.py index 83fcea2..2e1b085 100644 --- a/src/zope/interface/registry.py +++ b/src/zope/interface/registry.py @@ -13,9 +13,12 @@ ############################################################################## """Basic components support """ +from collections import defaultdict +from weakref import WeakKeyDictionary + try: from zope.event import notify -except ImportError: #pragma NO COVER +except ImportError: # pragma: no cover def notify(*arg, **kw): pass from zope.interface.interfaces import ISpecification @@ -38,6 +41,129 @@ from zope.interface._compat import CLASS_TYPES from zope.interface._compat import STRING_TYPES +class _UnhashableComponentCounter(object): + # defaultdict(int)-like object for unhashable components + + def __init__(self, otherdict): + # [(component, count)] + self._data = [item for item in otherdict.items()] + + def __getitem__(self, key): + for component, count in self._data: + if component == key: + return count + return 0 + + def __setitem__(self, component, count): + for i, data in enumerate(self._data): + if data[0] == component: + self._data[i] = component, count + return + self._data.append((component, count)) + + def __delitem__(self, component): + for i, data in enumerate(self._data): + if data[0] == component: + del self._data[i] + return + raise KeyError(component) # pragma: no cover + + +class _UtilityRegistrations(object): + + _regs_for_components = WeakKeyDictionary() + + @classmethod + def for_components(cls, components): + # We manage these utility/subscription registrations as associated + # objects with a weakref to avoid making any changes to + # the pickle format + try: + regs = cls._regs_for_components[components] + except KeyError: + regs = None + else: + # In case the components have been re-initted, clear the cache + # (zope.component.testing does this between tests) + if (regs._utilities is not components.utilities + or regs._utility_registrations is not components._utility_registrations): + regs = None + + if regs is None: + regs = cls(components.utilities, components._utility_registrations) + cls._regs_for_components[components] = regs + + return regs + + @classmethod + def clear_cache(cls): + cls._regs_for_components.clear() + + def __init__(self, utilities, utility_registrations): + # {provided -> {component: count}} + self._cache = defaultdict(lambda: defaultdict(int)) + self._utilities = utilities + self._utility_registrations = utility_registrations + + self.__populate_cache() + + def __populate_cache(self): + for ((p, _), data) in iter(self._utility_registrations.items()): + component = data[0] + self.__cache_utility(p, component) + + def __cache_utility(self, provided, component): + try: + self._cache[provided][component] += 1 + except TypeError: + # The component is not hashable, and we have a dict. Switch to a strategy + # that doesn't use hashing. + prov = self._cache[provided] = _UnhashableComponentCounter(self._cache[provided]) + prov[component] += 1 + + def __uncache_utility(self, provided, component): + provided = self._cache[provided] + # It seems like this line could raise a TypeError if component isn't + # hashable and we haven't yet switched to _UnhashableComponentCounter. However, + # we can't actually get in that situation. In order to get here, we would + # have had to cache the utility already which would have switched + # the datastructure if needed. + count = provided[component] + count -= 1 + if count == 0: + del provided[component] + else: + provided[component] = count + return count > 0 + + def _is_utility_subscribed(self, provided, component): + try: + return self._cache[provided][component] > 0 + except TypeError: + # Not hashable and we're still using a dict + return False + + def registerUtility(self, provided, name, component, info, factory): + subscribed = self._is_utility_subscribed(provided, component) + + self._utility_registrations[(provided, name)] = component, info, factory + self._utilities.register((), provided, name, component) + + if not subscribed: + self._utilities.subscribe((), provided, component) + + self.__cache_utility(provided, component) + + def unregisterUtility(self, provided, name, component): + del self._utility_registrations[(provided, name)] + self._utilities.unregister((), provided, name) + + subscribed = self.__uncache_utility(provided, component) + + if not subscribed: + self._utilities.unsubscribe((), provided, component) + + @implementer(IComponents) class Components(object): @@ -98,17 +224,7 @@ class Components(object): return self.unregisterUtility(reg[0], provided, name) - subscribed = False - for ((p, _), data) in iter(self._utility_registrations.items()): - if p == provided and data[0] == component: - subscribed = True - break - - self._utility_registrations[(provided, name)] = component, info, factory - self.utilities.register((), provided, name, component) - - if not subscribed: - self.utilities.subscribe((), provided, component) + _UtilityRegistrations.for_components(self).registerUtility(provided, name, component, info, factory) if event: notify(Registered( @@ -138,18 +254,7 @@ class Components(object): component = old[0] # Note that component is now the old thing registered - - del self._utility_registrations[(provided, name)] - self.utilities.unregister((), provided, name) - - subscribed = False - for ((p, _), data) in iter(self._utility_registrations.items()): - if p == provided and data[0] == component: - subscribed = True - break - - if not subscribed: - self.utilities.unsubscribe((), provided, component) + _UtilityRegistrations.for_components(self).unregisterUtility(provided, name, component) notify(Unregistered( UtilityRegistration(self, provided, name, component, *old[1:]) diff --git a/src/zope/interface/tests/test_registry.py b/src/zope/interface/tests/test_registry.py index 571bab3..c2a940a 100644 --- a/src/zope/interface/tests/test_registry.py +++ b/src/zope/interface/tests/test_registry.py @@ -73,7 +73,7 @@ class ComponentsTests(unittest.TestCase): def test_registerUtility_with_component_name(self): from zope.interface.declarations import named, InterfaceClass - + class IFoo(InterfaceClass): pass @@ -103,7 +103,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Registered from zope.interface.registry import UtilityRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -135,7 +135,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Registered from zope.interface.registry import UtilityRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -163,7 +163,7 @@ class ComponentsTests(unittest.TestCase): def test_registerUtility_no_provided_available(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass class Foo(object): @@ -181,7 +181,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Registered from zope.interface.registry import UtilityRegistration - + class IFoo(InterfaceClass): pass class Foo(object): @@ -210,7 +210,7 @@ class ComponentsTests(unittest.TestCase): def test_registerUtility_duplicates_existing_reg(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -226,7 +226,7 @@ class ComponentsTests(unittest.TestCase): def test_registerUtility_w_different_info(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -247,7 +247,7 @@ class ComponentsTests(unittest.TestCase): def test_registerUtility_w_different_names_same_component(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -274,7 +274,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.interfaces import Unregistered from zope.interface.interfaces import Registered from zope.interface.registry import UtilityRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -312,7 +312,7 @@ class ComponentsTests(unittest.TestCase): def test_registerUtility_w_existing_subscr(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -329,7 +329,7 @@ class ComponentsTests(unittest.TestCase): def test_registerUtility_wo_event(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -357,7 +357,7 @@ class ComponentsTests(unittest.TestCase): def test_unregisterUtility_w_component_miss(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -374,7 +374,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Unregistered from zope.interface.registry import UtilityRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -405,7 +405,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Unregistered from zope.interface.registry import UtilityRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -437,7 +437,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Unregistered from zope.interface.registry import UtilityRegistration - + class IFoo(InterfaceClass): pass class Foo(object): @@ -471,7 +471,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Unregistered from zope.interface.registry import UtilityRegistration - + class IFoo(InterfaceClass): pass class Foo(object): @@ -503,7 +503,7 @@ class ComponentsTests(unittest.TestCase): def test_unregisterUtility_w_existing_subscr(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -519,9 +519,77 @@ class ComponentsTests(unittest.TestCase): comp.unregisterUtility(_to_reg, ifoo, _name2) self.assertEqual(comp.utilities._subscribers[0][ifoo][''], (_to_reg,)) + def test_unregisterUtility_w_existing_subscr_non_hashable(self): + from zope.interface.declarations import InterfaceClass + + class IFoo(InterfaceClass): + pass + ifoo = IFoo('IFoo') + _info = u'info' + _name1 = u'name1' + _name2 = u'name2' + _to_reg = dict() + comp = self._makeOne() + comp.registerUtility(_to_reg, ifoo, _name1, _info) + comp.registerUtility(_to_reg, ifoo, _name2, _info) + _monkey, _events = self._wrapEvents() + with _monkey: + comp.unregisterUtility(_to_reg, ifoo, _name2) + self.assertEqual(comp.utilities._subscribers[0][ifoo][''], (_to_reg,)) + + def test_unregisterUtility_w_existing_subscr_non_hashable_fresh_cache(self): + # We correctly populate the cache of registrations if it has gone away + # (for example, the Components was unpickled) + from zope.interface.declarations import InterfaceClass + from zope.interface.registry import _UtilityRegistrations + + class IFoo(InterfaceClass): + pass + ifoo = IFoo('IFoo') + _info = u'info' + _name1 = u'name1' + _name2 = u'name2' + _to_reg = dict() + comp = self._makeOne() + comp.registerUtility(_to_reg, ifoo, _name1, _info) + comp.registerUtility(_to_reg, ifoo, _name2, _info) + + _UtilityRegistrations.clear_cache() + + _monkey, _events = self._wrapEvents() + with _monkey: + comp.unregisterUtility(_to_reg, ifoo, _name2) + self.assertEqual(comp.utilities._subscribers[0][ifoo][''], (_to_reg,)) + + def test_unregisterUtility_w_existing_subscr_non_hashable_reinitted(self): + # We correctly populate the cache of registrations if the base objects change + # out from under us + from zope.interface.declarations import InterfaceClass + + class IFoo(InterfaceClass): + pass + ifoo = IFoo('IFoo') + _info = u'info' + _name1 = u'name1' + _name2 = u'name2' + _to_reg = dict() + comp = self._makeOne() + comp.registerUtility(_to_reg, ifoo, _name1, _info) + comp.registerUtility(_to_reg, ifoo, _name2, _info) + + # zope.component.testing does this + comp.__init__('base') + comp.registerUtility(_to_reg, ifoo, _name2, _info) + + _monkey, _events = self._wrapEvents() + with _monkey: + # Nothing to do, but we don't break either + comp.unregisterUtility(_to_reg, ifoo, _name2) + self.assertEqual(0, len(comp.utilities._subscribers)) + def test_unregisterUtility_w_existing_subscr_other_component(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -539,13 +607,35 @@ class ComponentsTests(unittest.TestCase): self.assertEqual(comp.utilities._subscribers[0][ifoo][''], (_other_reg,)) + def test_unregisterUtility_w_existing_subscr_other_component_mixed_hash(self): + from zope.interface.declarations import InterfaceClass + + class IFoo(InterfaceClass): + pass + ifoo = IFoo('IFoo') + _info = u'info' + _name1 = u'name1' + _name2 = u'name2' + # First register something hashable + _other_reg = object() + # Then it transfers to something unhashable + _to_reg = dict() + comp = self._makeOne() + comp.registerUtility(_other_reg, ifoo, _name1, _info) + comp.registerUtility(_to_reg, ifoo, _name2, _info) + _monkey, _events = self._wrapEvents() + with _monkey: + comp.unregisterUtility(_to_reg, ifoo, _name2) + self.assertEqual(comp.utilities._subscribers[0][ifoo][''], + (_other_reg,)) + def test_registeredUtilities_empty(self): comp = self._makeOne() self.assertEqual(list(comp.registeredUtilities()), []) def test_registeredUtilities_notempty(self): from zope.interface.declarations import InterfaceClass - + from zope.interface.registry import UtilityRegistration class IFoo(InterfaceClass): pass @@ -630,7 +720,7 @@ class ComponentsTests(unittest.TestCase): def test_getUtilitiesFor_hit(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -653,7 +743,7 @@ class ComponentsTests(unittest.TestCase): def test_getAllUtilitiesRegisteredFor_hit(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -668,7 +758,7 @@ class ComponentsTests(unittest.TestCase): def test_registerAdapter_with_component_name(self): from zope.interface.declarations import named, InterfaceClass - + class IFoo(InterfaceClass): pass @@ -691,7 +781,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Registered from zope.interface.registry import AdapterRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -724,7 +814,7 @@ class ComponentsTests(unittest.TestCase): def test_registerAdapter_no_provided_available(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -744,7 +834,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import implementer from zope.interface.interfaces import Registered from zope.interface.registry import AdapterRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -779,7 +869,7 @@ class ComponentsTests(unittest.TestCase): def test_registerAdapter_no_required_available(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -795,7 +885,7 @@ class ComponentsTests(unittest.TestCase): def test_registerAdapter_w_invalid_required(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -814,7 +904,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.interface import Interface from zope.interface.interfaces import Registered from zope.interface.registry import AdapterRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -851,7 +941,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import implementedBy from zope.interface.interfaces import Registered from zope.interface.registry import AdapterRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -889,7 +979,7 @@ class ComponentsTests(unittest.TestCase): def test_registerAdapter_w_required_containing_junk(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -907,7 +997,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Registered from zope.interface.registry import AdapterRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -942,7 +1032,7 @@ class ComponentsTests(unittest.TestCase): def test_registerAdapter_wo_event(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1089,7 +1179,7 @@ class ComponentsTests(unittest.TestCase): def test_registeredAdapters_notempty(self): from zope.interface.declarations import InterfaceClass - + from zope.interface.registry import AdapterRegistration class IFoo(InterfaceClass): pass @@ -1359,7 +1449,7 @@ class ComponentsTests(unittest.TestCase): def test_getAdapters_non_empty(self): from zope.interface.declarations import InterfaceClass from zope.interface.declarations import implementer - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1393,7 +1483,7 @@ class ComponentsTests(unittest.TestCase): def test_registerSubscriptionAdapter_w_nonblank_name(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1411,7 +1501,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Registered from zope.interface.registry import SubscriptionRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1449,7 +1539,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import implementer from zope.interface.interfaces import Registered from zope.interface.registry import SubscriptionRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1487,7 +1577,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Registered from zope.interface.registry import SubscriptionRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1523,7 +1613,7 @@ class ComponentsTests(unittest.TestCase): def test_registerSubscriptionAdapter_wo_event(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1546,7 +1636,7 @@ class ComponentsTests(unittest.TestCase): def test_registeredSubscriptionAdapters_notempty(self): from zope.interface.declarations import InterfaceClass - + from zope.interface.registry import SubscriptionRegistration class IFoo(InterfaceClass): pass @@ -1579,7 +1669,7 @@ class ComponentsTests(unittest.TestCase): def test_unregisterSubscriptionAdapter_w_nonblank_name(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1790,7 +1880,7 @@ class ComponentsTests(unittest.TestCase): def test_registerHandler_w_nonblank_name(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1805,7 +1895,7 @@ class ComponentsTests(unittest.TestCase): from zope.interface.declarations import InterfaceClass from zope.interface.interfaces import Registered from zope.interface.registry import HandlerRegistration - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1837,7 +1927,7 @@ class ComponentsTests(unittest.TestCase): def test_registerHandler_wo_explicit_required_no_event(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -1892,10 +1982,10 @@ class ComponentsTests(unittest.TestCase): self.assertEqual(subscribers[1].name, '') self.assertEqual(subscribers[1].factory, _factory2) self.assertEqual(subscribers[1].info, '') - + def test_unregisterHandler_w_nonblank_name(self): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -2055,7 +2145,7 @@ class UtilityRegistrationTests(unittest.TestCase): def _makeOne(self, component=None, factory=None): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -2240,7 +2330,7 @@ class AdapterRegistrationTests(unittest.TestCase): def _makeOne(self, component=None): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -2449,7 +2539,7 @@ class SubscriptionRegistrationTests(unittest.TestCase): def _makeOne(self, component=None): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') @@ -2486,7 +2576,7 @@ class HandlerRegistrationTests(unittest.TestCase): def _makeOne(self, component=None): from zope.interface.declarations import InterfaceClass - + class IFoo(InterfaceClass): pass ifoo = IFoo('IFoo') |
