diff options
| author | Jason Madden <jamadden@gmail.com> | 2016-08-19 08:18:39 -0500 |
|---|---|---|
| committer | Jason Madden <jamadden@gmail.com> | 2016-08-19 08:18:39 -0500 |
| commit | 58ef4872922803fac3429d88fb3c58ae509b17c5 (patch) | |
| tree | 2781bb0f904b30e2c81f62de0957a4cbc5cc03a6 /src | |
| parent | b0244a9140b5f3feb968326dfff5d99d303b61a3 (diff) | |
| download | zope-interface-58ef4872922803fac3429d88fb3c58ae509b17c5.tar.gz | |
Use dictionary lookups for testing subscribed status.
Fixes #46.
Benchmarks show a dramatic improvement; not quite as good as the
demonstration in #46 because the implementation had to be a bit more
complex to properly handle unregistration, but still very good. Here it
is with 20,000 items already registered:
```
%time add_to_reg(reg, 1000)
CPU times: user 190 ms, sys: 19.3 ms, total: 209 ms
Wall time: 203 ms
```
Here's a profile with about 100,000 utilities registered:
```
%prun add_to_reg(reg, 1000)
80005 function calls (79005 primitive calls) in 0.713 seconds
Ordered by: internal time
ncalls tottime percall cumtime percall filename:lineno(function)
1000 0.596 0.001 0.621 0.001 adapter.py:202(subscribe)
12000 0.016 0.000 0.019 0.000 interface.py:518(__hash__)
1000 0.010 0.000 0.709 0.001 registry.py:206(registerUtility)
3000/2000 0.009 0.000 0.014 0.000 interface.py:255(interfaces)
2000 0.008 0.000 0.022 0.000 adapter.py:637(changed)
1000 0.008 0.000 0.025 0.000 registry.py:498(_getUtilityProvided)
9000 0.008 0.000 0.017 0.000 {method 'get' of 'dict' objects}
1000 0.008 0.000 0.027 0.000 adapter.py:102(register)
2000 0.006 0.000 0.008 0.000 adapter.py:450(changed)
1000 0.005 0.000 0.663 0.001 registry.py:145(registerUtility)
```
I was very careful not to change the pickle at all.
zope.interface and zope.component tests have been run and both pass. (It
was necessary to account for the underlying objects changing because of
the way zope.component cleans up after tests.)
Diffstat (limited to 'src')
| -rw-r--r-- | src/zope/interface/registry.py | 152 | ||||
| -rw-r--r-- | src/zope/interface/tests/test_registry.py | 190 |
2 files changed, 268 insertions, 74 deletions
diff --git a/src/zope/interface/registry.py b/src/zope/interface/registry.py index 83fcea2..5f945f7 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,128 @@ from zope.interface._compat import CLASS_TYPES from zope.interface._compat import STRING_TYPES +class _CacheList(object): + # defaultdict(int)-like object for unhashable components + + def __init__(self, otherdict): + self._data = [] + for comp, count in otherdict.items(): + self._data.append((comp, count)) + + def __getitem__(self, key): + for comp, count in self._data: + if comp == 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, comps): + # 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[comps] + 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 comps.utilities + or regs._utility_registrations is not comps._utility_registrations): + regs = None + + if regs is None: + regs = cls(comps.utilities, comps._utility_registrations) + cls._regs_for_components[comps] = 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: + # Not hashable, and we have a dict. Switch to a list. + self._cache[provided] = _CacheList(self._cache[provided]) + self._cache[provided][component] += 1 + + def __uncache_utility(self, provided, component): + # It seems like this line could raise a TypeError if component isn't + # hashable and we haven't yet switched to _CacheList. 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 = self._cache[provided][component] + count -= 1 + if count == 0: + del self._cache[provided][component] + else: + self._cache[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 +223,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 +253,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') |
