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
1=== modified file 'plainbox/plainbox/impl/developer.py'
2--- plainbox/plainbox/impl/developer.py 2015-07-30 11:31:35 +0000
3+++ plainbox/plainbox/impl/developer.py 2015-08-28 09:55:48 +0000
4@@ -19,9 +19,18 @@
5 """Support code for enforcing usage expectations on public API."""
6
7 import inspect
8+import logging
9+import warnings
10
11 __all__ = ('UsageExpectation',)
12
13+_logger = logging.getLogger("plainbox.developer")
14+
15+
16+class OffByOneBackWarning(UserWarning):
17+
18+ """Warning on incorrect use of UsageExpectations(self).enforce(back=2)."""
19+
20
21 class DeveloperError(Exception):
22
23@@ -149,18 +158,7 @@
24 something goes wrong.
25 """
26 self.cls = cls
27- self._allowed_calls = {}
28-
29- @property
30- def allowed_calls(self):
31- """Get the mapping of possible methods to call."""
32- return self._allowed_calls
33-
34- @allowed_calls.setter
35- def allowed_calls(self, value):
36- """Set new mapping of possible methods to call."""
37- self._allowed_calls = value
38- self._allowed_code = frozenset(func.__code__ for func in value)
39+ self.allowed_calls = {}
40
41 def enforce(self, back=1):
42 """
43@@ -179,16 +177,50 @@
44 :raises DeveloperError:
45 If the expectations are not met.
46 """
47+ # XXX: Allowed calls is a dictionary that may be freely changed by the
48+ # outside caller. We're unable to protect against it. Therefore the
49+ # optimized values (for computing what is really allowed) must be
50+ # obtained each time we are about to check, in enforce()
51+ allowed_code = frozenset(
52+ func.__wrapped__.__code__
53+ if hasattr(func, '__wrapped__') else func.__code__
54+ for func in self.allowed_calls
55+ )
56 caller_frame = inspect.stack(0)[back][0]
57+ if back > 1:
58+ alt_caller_frame = inspect.stack(0)[back - 1][0]
59+ else:
60+ alt_caller_frame = None
61+ _logger.debug("Caller code: %r", caller_frame.f_code)
62+ _logger.debug("Alternate code: %r",
63+ alt_caller_frame.f_code if alt_caller_frame else None)
64+ _logger.debug("Allowed code: %r", allowed_code)
65 try:
66- if caller_frame.f_code not in self._allowed_code:
67- fn_name = caller_frame.f_code.co_name
68- allowed_pairs = tuple(
69- (fn.__code__.co_name, why)
70- for fn, why in sorted(
71- self.allowed_calls.items(),
72- key=lambda fn_why: fn_why[0].__code__.co_name)
73- )
74- raise UnexpectedMethodCall(self.cls, fn_name, allowed_pairs)
75+ if caller_frame.f_code in allowed_code:
76+ return
77+ # This can be removed later, it allows the caller to make an
78+ # off-by-one mistake and go away with it.
79+ if (alt_caller_frame is not None
80+ and alt_caller_frame.f_code in allowed_code):
81+ warnings.warn(
82+ "Please back={}. Properly constructed decorators are"
83+ " automatically handled and do not require the use of the"
84+ " back argument.".format(back - 1), OffByOneBackWarning,
85+ back)
86+ return
87+ fn_name = caller_frame.f_code.co_name
88+ allowed_undecorated_calls = {
89+ func.__wrapped__ if hasattr(func, '__wrapped__') else func: msg
90+ for func, msg in self.allowed_calls.items()
91+ }
92+ allowed_pairs = tuple(
93+ (fn.__code__.co_name, why)
94+ for fn, why in sorted(
95+ allowed_undecorated_calls.items(),
96+ key=lambda fn_why: fn_why[0].__code__.co_name)
97+ )
98+ raise UnexpectedMethodCall(self.cls, fn_name, allowed_pairs)
99 finally:
100 del caller_frame
101+ if alt_caller_frame is not None:
102+ del alt_caller_frame
103
104=== modified file 'plainbox/plainbox/impl/session/assistant.py'
105--- plainbox/plainbox/impl/session/assistant.py 2015-08-03 14:10:44 +0000
106+++ plainbox/plainbox/impl/session/assistant.py 2015-08-28 09:55:48 +0000
107@@ -122,6 +122,10 @@
108 "use an alternate execution controllers"),
109 self.select_providers: (
110 "select the providers to work with"),
111+ self.get_canonical_certification_transport: (
112+ "create a transport for the C3 system"),
113+ self.get_canonical_hexr_transport: (
114+ "create a transport for the HEXR system"),
115 }
116
117 @raises(UnexpectedMethodCall)

Subscribers

People subscribed via source and target branches