Merge lp:~zyga/checkbox/fix-developer-decorators into lp:checkbox

Proposed by Zygmunt Krynicki
Status: Merged
Approved by: Maciej Kisielewski
Approved revision: 3959
Merged at revision: 3963
Proposed branch: lp:~zyga/checkbox/fix-developer-decorators
Merge into: lp:checkbox
Diff against target: 117 lines (+57/-21)
2 files modified
plainbox/plainbox/impl/developer.py (+53/-21)
plainbox/plainbox/impl/session/assistant.py (+4/-0)
To merge this branch: bzr merge lp:~zyga/checkbox/fix-developer-decorators
Reviewer Review Type Date Requested Status
Maciej Kisielewski Approve
Review via email: mp+269484@code.launchpad.net
To post a comment you must log in.
3958. By Zygmunt Krynicki

plainbox:session:assistant: allow calls to transport creation methods

This patch grants permissions to call two transport creation methods.
They are currently working because of a bug in the developer usage
expectation system that is fixed with the subsequent commit.

Signed-off-by: Zygmunt Krynicki <email address hidden>

3959. By Zygmunt Krynicki

plainbox:developer: fix support for decorated functions

This patch fixes an interesting issue where the
UsageExpection.of(self).enforce(back=2) code would misbehave on
decorated functions. In general, the code really allowed the decorator,
not the function hiding behind it.

The fix involves a number of operations.

First the UsageExpectation class no longer caches anything about the
allowed_calls dictionary. Since the dictionary can be mutated
externally (without re-assignment) it's simply useless to do that.
Instead everything is computed on demand in enforce().

Next, the code uses the __wrapped__ attribute (if present) to discover
the true function that the user is allowed to call. This actually breaks
all the existing code (that relied on the bug that made it work before)
so another change is made.

Lastly, an off-by-one bug is now detected. When
UsageExpectation.of(self).enforce(back=2) is used when plain .enforce()
is actually correct a warning is logged, pointing to the appropriate
location with instructions on what to change.

Signed-off-by: Zygmunt Krynicki <email address hidden>

Revision history for this message
Maciej Kisielewski (kissiel) wrote :

+1, Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'plainbox/plainbox/impl/developer.py'
--- plainbox/plainbox/impl/developer.py 2015-07-30 11:31:35 +0000
+++ plainbox/plainbox/impl/developer.py 2015-08-28 09:55:48 +0000
@@ -19,9 +19,18 @@
19"""Support code for enforcing usage expectations on public API."""19"""Support code for enforcing usage expectations on public API."""
2020
21import inspect21import inspect
22import logging
23import warnings
2224
23__all__ = ('UsageExpectation',)25__all__ = ('UsageExpectation',)
2426
27_logger = logging.getLogger("plainbox.developer")
28
29
30class OffByOneBackWarning(UserWarning):
31
32 """Warning on incorrect use of UsageExpectations(self).enforce(back=2)."""
33
2534
26class DeveloperError(Exception):35class DeveloperError(Exception):
2736
@@ -149,18 +158,7 @@
149 something goes wrong.158 something goes wrong.
150 """159 """
151 self.cls = cls160 self.cls = cls
152 self._allowed_calls = {}161 self.allowed_calls = {}
153
154 @property
155 def allowed_calls(self):
156 """Get the mapping of possible methods to call."""
157 return self._allowed_calls
158
159 @allowed_calls.setter
160 def allowed_calls(self, value):
161 """Set new mapping of possible methods to call."""
162 self._allowed_calls = value
163 self._allowed_code = frozenset(func.__code__ for func in value)
164162
165 def enforce(self, back=1):163 def enforce(self, back=1):
166 """164 """
@@ -179,16 +177,50 @@
179 :raises DeveloperError:177 :raises DeveloperError:
180 If the expectations are not met.178 If the expectations are not met.
181 """179 """
180 # XXX: Allowed calls is a dictionary that may be freely changed by the
181 # outside caller. We're unable to protect against it. Therefore the
182 # optimized values (for computing what is really allowed) must be
183 # obtained each time we are about to check, in enforce()
184 allowed_code = frozenset(
185 func.__wrapped__.__code__
186 if hasattr(func, '__wrapped__') else func.__code__
187 for func in self.allowed_calls
188 )
182 caller_frame = inspect.stack(0)[back][0]189 caller_frame = inspect.stack(0)[back][0]
190 if back > 1:
191 alt_caller_frame = inspect.stack(0)[back - 1][0]
192 else:
193 alt_caller_frame = None
194 _logger.debug("Caller code: %r", caller_frame.f_code)
195 _logger.debug("Alternate code: %r",
196 alt_caller_frame.f_code if alt_caller_frame else None)
197 _logger.debug("Allowed code: %r", allowed_code)
183 try:198 try:
184 if caller_frame.f_code not in self._allowed_code:199 if caller_frame.f_code in allowed_code:
185 fn_name = caller_frame.f_code.co_name200 return
186 allowed_pairs = tuple(201 # This can be removed later, it allows the caller to make an
187 (fn.__code__.co_name, why)202 # off-by-one mistake and go away with it.
188 for fn, why in sorted(203 if (alt_caller_frame is not None
189 self.allowed_calls.items(),204 and alt_caller_frame.f_code in allowed_code):
190 key=lambda fn_why: fn_why[0].__code__.co_name)205 warnings.warn(
191 )206 "Please back={}. Properly constructed decorators are"
192 raise UnexpectedMethodCall(self.cls, fn_name, allowed_pairs)207 " automatically handled and do not require the use of the"
208 " back argument.".format(back - 1), OffByOneBackWarning,
209 back)
210 return
211 fn_name = caller_frame.f_code.co_name
212 allowed_undecorated_calls = {
213 func.__wrapped__ if hasattr(func, '__wrapped__') else func: msg
214 for func, msg in self.allowed_calls.items()
215 }
216 allowed_pairs = tuple(
217 (fn.__code__.co_name, why)
218 for fn, why in sorted(
219 allowed_undecorated_calls.items(),
220 key=lambda fn_why: fn_why[0].__code__.co_name)
221 )
222 raise UnexpectedMethodCall(self.cls, fn_name, allowed_pairs)
193 finally:223 finally:
194 del caller_frame224 del caller_frame
225 if alt_caller_frame is not None:
226 del alt_caller_frame
195227
=== modified file 'plainbox/plainbox/impl/session/assistant.py'
--- plainbox/plainbox/impl/session/assistant.py 2015-08-03 14:10:44 +0000
+++ plainbox/plainbox/impl/session/assistant.py 2015-08-28 09:55:48 +0000
@@ -122,6 +122,10 @@
122 "use an alternate execution controllers"),122 "use an alternate execution controllers"),
123 self.select_providers: (123 self.select_providers: (
124 "select the providers to work with"),124 "select the providers to work with"),
125 self.get_canonical_certification_transport: (
126 "create a transport for the C3 system"),
127 self.get_canonical_hexr_transport: (
128 "create a transport for the HEXR system"),
125 }129 }
126130
127 @raises(UnexpectedMethodCall)131 @raises(UnexpectedMethodCall)

Subscribers

People subscribed via source and target branches