Merge lp:~free.ekanayaka/python-zope-fixtures/adapter-fixture into lp:python-zope-fixtures

Proposed by Free Ekanayaka
Status: Merged
Merged at revision: 8
Proposed branch: lp:~free.ekanayaka/python-zope-fixtures/adapter-fixture
Merge into: lp:python-zope-fixtures
Diff against target: 239 lines (+151/-13)
3 files modified
lib/zope_fixtures/__init__.py (+3/-1)
lib/zope_fixtures/components.py (+77/-9)
lib/zope_fixtures/tests/test_components.py (+71/-3)
To merge this branch: bzr merge lp:~free.ekanayaka/python-zope-fixtures/adapter-fixture
Reviewer Review Type Date Requested Status
Robert Collins Approve
Review via email: mp+124187@code.launchpad.net

Description of the change

I realized that the ComponentsFixture might not be a good idea, or at least it probably doesn't match the typical use cases one would have in a Zope-based application.

The reason is that it uses the low-level sethook() API to override the current registry (aka site manager), while Zope actually supports an higher-level API in the zope.components.hooks module that seems precisely meant to support thread-local/request-specific overrides via the setHooks() and setSite() APIs. This higher-level API is indeed implemented on top of sethook().

I believe ComponentsFixture should be dropped and maybe replaced with a SiteFixture which would use the setHooks/setSite APIs to offer a similar behavior, but more in line with what the Zope publisher does, and hence more suited for writing unit-tests.

Said that, this branch heads into that direction and re-implements UtilityFixture without using ComponentsFixture under the hood, and merely using whatever site manager is currently in-force (thus letting test code be in control of the site manager). It also adds an AdapterFixture which pretty much does the same, but with adapters.

It should be possible to virtually get the same features of ComponentsFixture using UtilityFixture and AdapterFixture.

To post a comment you must log in.
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

FWIW more information on the canonical use of hooks in a Zope application can be found in zope/component/hooks.txt and zope/site/site.txt.

Revision history for this message
Robert Collins (lifeless) wrote :

I think this:
76 +
77 + # Check if an utility for the same interface and name exists, if so
78 + # restore it upon cleanup, otherwise just unregister our one.
79 + original = sm.queryUtility(self._provided, self._name)
80 + if original is not None:
81 + self.addCleanup(sm.registerUtility, original, self._provided,
82 + self._name)
83 + else:
84 + self.addCleanup(sm.unregisterUtility, self._component,
85 + self._provided, self._name)
86 +
87 + # Register the utility
88 + sm.registerUtility(self._component, self._provided, self._name)
might be clearer as
76 +
79 + original = sm.queryUtility(self._provided, self._name)
88 + sm.registerUtility(self._component, self._provided, self._name)
80 + if original is not None:
77 + # Restore any existing utility.
81 + self.addCleanup(sm.registerUtility, original, self._provided,
82 + self._name)
87 + # Register the utility
84 + self.addCleanup(sm.unregisterUtility, self._component,
85 + self._provided, self._name)

Which due to the LIFO nature of cleanups will just always unregister the utility we are using, and restore any previous utility - this is easier to reason about - it doesn't depend on the silent-replace aspect of registerUtility, and the perf cost is trivial (one call to unregisterUtility).

48 + The utility will be unregistered upon cleanUp, or the original utility
49 + will be restored if there was one.

Perhaps: "The registration will be reverted upon cleanUp, restoring the previous utility (or None)."

same pattern in adapterFixture. I wonder if we can pull that logic sideways and make it independent.

I'll merge what you have and see about a refactoring branch.

review: Approve
Revision history for this message
Robert Collins (lifeless) wrote :

Ok, so it comes together okish, but not enough better to worry about just yet - I think if callmany were more easily accessible it would be that little bit better and worth doing.

Revision history for this message
Robert Collins (lifeless) wrote :

Also, you forgot a NEWS entry ;).

Revision history for this message
Robert Collins (lifeless) wrote :

BTW, do you have more changes, or should we release this?

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Hi Robert,

thanks for the careful review. Yeah, I thought too about unifying the logic but it just looked not enough worth yet as you point. I'm fine with the proposed change in the addCleanup calls, if you feel like committing it go ahead. Good catch on the NEWS entry, and yes please release it, I don't have further changes.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/zope_fixtures/__init__.py'
2--- lib/zope_fixtures/__init__.py 2012-04-26 02:58:05 +0000
3+++ lib/zope_fixtures/__init__.py 2012-09-13 12:45:41 +0000
4@@ -36,9 +36,11 @@
5 __all__ = [
6 'ComponentsFixture',
7 'UtilityFixture',
8+ 'AdapterFixture',
9 ]
10
11-from zope_fixtures.components import ComponentsFixture, UtilityFixture
12+from zope_fixtures.components import (
13+ ComponentsFixture, UtilityFixture, AdapterFixture)
14
15
16 def test_suite():
17
18=== modified file 'lib/zope_fixtures/components.py'
19--- lib/zope_fixtures/components.py 2012-04-26 02:52:29 +0000
20+++ lib/zope_fixtures/components.py 2012-09-13 12:45:41 +0000
21@@ -16,6 +16,7 @@
22 __all__ = [
23 'ComponentsFixture',
24 'UtilityFixture',
25+ 'AdapterFixture',
26 ]
27
28 from fixtures import Fixture
29@@ -23,6 +24,9 @@
30
31 Components = try_import("zope.component.registry.Components")
32 getSiteManager = try_import("zope.component.getSiteManager")
33+_getUtilityProvided = try_import("zope.component.registry._getUtilityProvided")
34+_getAdapterProvided = try_import("zope.component.registry._getAdapterProvided")
35+_getAdapterRequired = try_import("zope.component.registry._getAdapterRequired")
36
37
38 class ComponentsFixture(Fixture):
39@@ -65,19 +69,83 @@
40
41
42 class UtilityFixture(Fixture):
43- """Convenience around ComponentsFixture to override a single utility."""
44-
45- def __init__(self, *args, **kwargs):
46+ """Fixture for registering or overriding a single utility.
47+
48+ The utility will be unregistered upon cleanUp, or the original utility
49+ will be restored if there was one.
50+
51+ The registration is performed agaist the current site manager.
52+ """
53+
54+ def __init__(self, component, provided=None, name=u""):
55 """Create an UtilityFixture.
56
57- The args and kwargs parameters are the same supported by
58- registerUtility and will be passed to it.
59+ The parameters are the same supported by registerUtility and will
60+ be passed to it.
61 """
62 super(UtilityFixture, self).__init__()
63- self.args = args
64- self.kwargs = kwargs
65+ self._component = component
66+ self._provided = provided or _getUtilityProvided(component)
67+ self._name = name
68
69 def setUp(self):
70 super(UtilityFixture, self).setUp()
71- components = self.useFixture(ComponentsFixture())
72- components.registry.registerUtility(*self.args, **self.kwargs)
73+ # We use the current site manager so we honor whatever hooks have
74+ # been set
75+ sm = getSiteManager()
76+
77+ # Check if an utility for the same interface and name exists, if so
78+ # restore it upon cleanup, otherwise just unregister our one.
79+ original = sm.queryUtility(self._provided, self._name)
80+ if original is not None:
81+ self.addCleanup(sm.registerUtility, original, self._provided,
82+ self._name)
83+ else:
84+ self.addCleanup(sm.unregisterUtility, self._component,
85+ self._provided, self._name)
86+
87+ # Register the utility
88+ sm.registerUtility(self._component, self._provided, self._name)
89+
90+
91+class AdapterFixture(Fixture):
92+ """Fixture for registering or overriding a single adapter.
93+
94+ The adapter will be unregistered upon cleanUp, or the original adapter
95+ will be restored if there was one.
96+
97+ The registration is performed agaist the current site manager.
98+ """
99+
100+ def __init__(self, factory, required=None, provided=None, name=u""):
101+ """Create an AdapterFixture.
102+
103+ The parameters are the same supported by registerAdpater and will
104+ be passed to it.
105+ """
106+ super(AdapterFixture, self).__init__()
107+ self._factory = factory
108+ self._required = _getAdapterRequired(factory, required)
109+ self._provided = provided or _getAdapterProvided(factory)
110+ self._name = name
111+
112+ def setUp(self):
113+ super(AdapterFixture, self).setUp()
114+ # We use the current site manager so we honor whatever hooks have
115+ # been set
116+ sm = getSiteManager()
117+
118+ # Check if an utility for the same interface and name exists, if so
119+ # restore it upon cleanup, otherwise just unregister our one.
120+ original = sm.adapters.registered(self._required, self._provided,
121+ self._name)
122+ if original is not None:
123+ self.addCleanup(sm.registerAdapter, original, self._required,
124+ self._provided, self._name)
125+ else:
126+ self.addCleanup(sm.unregisterAdapter, self._factory, self._required,
127+ self._provided, self._name)
128+
129+ # Register the utility
130+ sm.registerAdapter(self._factory, self._required, self._provided,
131+ self._name)
132
133=== modified file 'lib/zope_fixtures/tests/test_components.py'
134--- lib/zope_fixtures/tests/test_components.py 2012-04-26 02:52:29 +0000
135+++ lib/zope_fixtures/tests/test_components.py 2012-09-13 12:45:41 +0000
136@@ -16,10 +16,12 @@
137 import testtools
138
139 from zope.interface import Interface, implements
140-from zope.component import provideUtility, queryUtility, getSiteManager
141+from zope.component import (
142+ provideUtility, queryUtility, getSiteManager, queryAdapter, adapts)
143 from zope.component.registry import Components
144
145-from zope_fixtures import ComponentsFixture, UtilityFixture
146+from zope_fixtures import (
147+ ComponentsFixture, UtilityFixture, AdapterFixture)
148
149
150 class ITestUtility(Interface):
151@@ -30,6 +32,26 @@
152 implements(ITestUtility)
153
154
155+class ITestContext(Interface):
156+ pass
157+
158+
159+class TestContext(object):
160+ implements(ITestContext)
161+
162+
163+class ITestAdapter(Interface):
164+ pass
165+
166+
167+class TestAdapter(object):
168+ implements(ITestAdapter)
169+ adapts(ITestContext)
170+
171+ def __init__(self, context):
172+ self.context = context
173+
174+
175 class TestComponentsFixture(testtools.TestCase):
176
177 def test_overlays_site_manager(self):
178@@ -98,14 +120,60 @@
179 self.useFixture(fixture)
180 self.assertIs(utility, queryUtility(ITestUtility))
181
182- def test_restore_utility(self):
183+ def test_unregister_utility(self):
184 utility = TestUtility()
185 with UtilityFixture(utility):
186 self.assertIs(utility, queryUtility(ITestUtility))
187 self.assertIs(None, queryUtility(ITestUtility))
188
189+ def test_restore_original_utility(self):
190+ original = TestUtility()
191+ sm = getSiteManager()
192+ sm.registerUtility(original)
193+ self.addCleanup(sm.unregisterUtility, original)
194+ utility = TestUtility()
195+ with UtilityFixture(utility):
196+ self.assertIs(utility, queryUtility(ITestUtility))
197+ self.assertIs(original, queryUtility(ITestUtility))
198+
199 def test_arguments_passthrough(self):
200 utility = TestUtility()
201 fixture = UtilityFixture(utility, name="foo")
202 self.useFixture(fixture)
203 self.assertIs(utility, queryUtility(ITestUtility, name="foo"))
204+
205+
206+class TestAdapterFixture(testtools.TestCase):
207+
208+ def test_provide_adapter(self):
209+ fixture = AdapterFixture(TestAdapter)
210+ self.useFixture(fixture)
211+ context = TestContext()
212+ adapter = queryAdapter(context)
213+ self.assertIs(context, adapter.context)
214+
215+ def test_unregister_adapter(self):
216+ context = TestContext()
217+ with AdapterFixture(TestAdapter):
218+ adapter = queryAdapter(context)
219+ self.assertIs(context, adapter.context)
220+ self.assertIs(None, queryAdapter(context))
221+
222+ def test_restore_original_adapter(self):
223+
224+ class TestAdapter2(TestAdapter):
225+ pass
226+
227+ sm = getSiteManager()
228+ sm.registerAdapter(TestAdapter2)
229+ self.addCleanup(sm.unregisterAdapter, TestAdapter2)
230+ context = TestContext()
231+ with AdapterFixture(TestAdapter):
232+ self.assertIsInstance(queryAdapter(context), TestAdapter)
233+ self.assertIsInstance(queryAdapter(context), TestAdapter2)
234+
235+ def test_arguments_passthrough(self):
236+ fixture = AdapterFixture(TestAdapter, name="foo")
237+ self.useFixture(fixture)
238+ context = TestContext()
239+ self.assertIsInstance(queryAdapter(context, name="foo"), TestAdapter)

Subscribers

People subscribed via source and target branches