Merge lp:~sylvain-pineau/checkbox/json_load_classmethods_bis into lp:checkbox
- json_load_classmethods_bis
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Zygmunt Krynicki |
Approved revision: | 1938 |
Merged at revision: | 1927 |
Proposed branch: | lp:~sylvain-pineau/checkbox/json_load_classmethods_bis |
Merge into: | lp:checkbox |
Diff against target: |
374 lines (+179/-29) 8 files modified
plainbox/plainbox/impl/job.py (+7/-0) plainbox/plainbox/impl/resource.py (+0/-5) plainbox/plainbox/impl/result.py (+7/-0) plainbox/plainbox/impl/session.py (+55/-17) plainbox/plainbox/impl/test_job.py (+16/-0) plainbox/plainbox/impl/test_resource.py (+0/-6) plainbox/plainbox/impl/test_result.py (+26/-0) plainbox/plainbox/impl/test_session.py (+68/-1) |
To merge this branch: | bzr merge lp:~sylvain-pineau/checkbox/json_load_classmethods_bis |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Zygmunt Krynicki (community) | Approve | ||
Sylvain Pineau (community) | Needs Resubmitting | ||
Review via email: mp+146126@code.launchpad.net |
Commit message
Description of the change
Prerequisite to resume a full plainbox session
This MR adds classmethods to re create instances from JSON objects.
Zygmunt Krynicki (zyga) wrote : | # |
- 1916. By Zygmunt Krynicki
-
"[r=roadmr]
[bug=][ author= zkrynicki] automatic merge by tarmac" - 1917. By Zygmunt Krynicki
-
"[r=sylvain-
pineau, roadmr] [bug=][ author= zkrynicki] automatic merge by tarmac"
Zygmunt Krynicki (zyga) wrote : | # |
73 + #if value.job is not self.job:
74 + # raise ValueError("result job does not match")
I probably know why you did this but I don't know if just disabling that (especially this way) is okay. Could you explain this please?
- 1918. By Launchpad Translations on behalf of checkbox-dev
-
Launchpad automatic translations update.
Sylvain Pineau (sylvain-pineau) wrote : | # |
Well It won't break in this commit, only with the next one.
Deserialized objects are only a subset of the original __dict__, and the identity match will always fail.
Thus the removal of this check.
I would probably prefer a less strict comparison such as != that we can easily override if needed to match Jobdefinition based on their checksum.
This an example of object.__dict__ debug output, used in these two lines:
#################
{'_data': {'description': 'Gets CPU resource info from /proc/cpuinfo', 'command': 'cpuinfo_resource', 'name': 'cpuinfo', 'plugin': 'resource'}, '_resource_
#################
{'_data': {'name': 'cpuinfo', 'plugin': 'resource'}, '_resource_
As you can see, the "is" statement can't help us.
- 1919. By Zygmunt Krynicki
-
"[r=zkrynicki]
[bug=][ author= zkrynicki] automatic merge by tarmac" - 1920. By Zygmunt Krynicki
-
"[r=zkrynicki]
[bug=][ author= zkrynicki] automatic merge by tarmac" - 1921. By Zygmunt Krynicki
-
"[r=roadmr]
[bug=][ author= zkrynicki] automatic merge by tarmac" - 1922. By Daniel Manrique
-
Bumped rev to 0.15.3 (0.15.2 is based on rev1907)
- 1923. By Zygmunt Krynicki
-
"[r=roadmr]
[bug=][ author= zkrynicki] automatic merge by tarmac" - 1924. By Zygmunt Krynicki
-
"[r=roadmr]
[bug=][ author= zkrynicki] automatic merge by tarmac" - 1925. By Sylvain Pineau
-
box.py: removed the unneeded open()
in with session.open(), contextmanager already calls open()
- 1926. By Sylvain Pineau
-
Added classmethods to create instances from JSON
JSON objects are decoded as dictionaries, added
classmethods to populate instances from dict values - 1927. By Sylvain Pineau
-
Restore the session.open() call
The to avoid calling session.open() twice will be in __enter__()
- 1928. By Sylvain Pineau
-
Added encode/decode tests
For JobDefinition, ResourceExpression and JobResultTests
Fix broken tests where session.update_ job_state_ map() is now missing - 1929. By Sylvain Pineau
-
Session can now be resumed from the JSON file
Added the update_
job_state_ map method to avoid modifying the
job_state_map with __init__(), This is required to decode JSON objects
as dict and create session instances. Resuming/updating SessionState
with previous data requires an object_hook parameter (dict_to_object),
used to create python class instances from dictionaries.
previous_sesion_ file() check if a previous session file exists and
returns its full pathname. resume_prompt() is for now a pure CLI prompt
to get user input. - 1930. By Sylvain Pineau
-
Cleanup after cherry picking
- 1931. By Sylvain Pineau
-
Removed uneeded ResourceExpression encoder/decoder helpers
The _readiness_
inhibitor_ list will be recomputed on resume - 1932. By Sylvain Pineau
-
Don't save the _readiness_
inhibitor_ list, it will be evaluated on resume - 1933. By Sylvain Pineau
-
Added tests for JobState encode/decode to json
- 1934. By Sylvain Pineau
-
Reworked test_session.py after cherry-picking
Mainly to use _get_persistanc
e_subset( ) instead of __getstate__() - 1935. By Sylvain Pineau
-
Removed deprecated code
- 1936. By Sylvain Pineau
-
Created a class/identifier mapping
To be used in serialization operations
- 1937. By Sylvain Pineau
-
PEP8 fixes
- 1938. By Sylvain Pineau
-
Restore the job identity check in the result setter of JobState
Sylvain Pineau (sylvain-pineau) wrote : | # |
Restore the job identity check in the result setter of JobState.
Should be Ok to land now.
Preview Diff
1 | === modified file 'plainbox/plainbox/impl/job.py' |
2 | --- plainbox/plainbox/impl/job.py 2013-02-08 20:00:14 +0000 |
3 | +++ plainbox/plainbox/impl/job.py 2013-02-10 14:42:22 +0000 |
4 | @@ -249,3 +249,10 @@ |
5 | # Compute the sha256 hash of the UTF-8 encoding of the canonical form |
6 | # and return the hex digest as the checksum that can be displayed. |
7 | return hashlib.sha256(canonical_form.encode('UTF-8')).hexdigest() |
8 | + |
9 | + @classmethod |
10 | + def from_json_record(cls, record): |
11 | + """ |
12 | + Create a JobDefinition instance from JSON record |
13 | + """ |
14 | + return cls(record['data']) |
15 | |
16 | === modified file 'plainbox/plainbox/impl/resource.py' |
17 | --- plainbox/plainbox/impl/resource.py 2013-02-01 09:59:13 +0000 |
18 | +++ plainbox/plainbox/impl/resource.py 2013-02-10 14:42:22 +0000 |
19 | @@ -418,11 +418,6 @@ |
20 | def __repr__(self): |
21 | return "<ResourceExpression text:{!r}>".format(self._text) |
22 | |
23 | - def _get_persistance_subset(self): |
24 | - state = {} |
25 | - state['_text'] = self._text |
26 | - return state |
27 | - |
28 | @property |
29 | def text(self): |
30 | """ |
31 | |
32 | === modified file 'plainbox/plainbox/impl/result.py' |
33 | --- plainbox/plainbox/impl/result.py 2013-02-01 09:59:13 +0000 |
34 | +++ plainbox/plainbox/impl/result.py 2013-02-10 14:42:22 +0000 |
35 | @@ -127,3 +127,10 @@ |
36 | state['data']['return_code'] = self._data.get('return_code') |
37 | # io_log are stored on disk, see session.jobs_io_log_dir |
38 | return state |
39 | + |
40 | + @classmethod |
41 | + def from_json_record(cls, record): |
42 | + """ |
43 | + Create a JobResult instance from JSON record |
44 | + """ |
45 | + return cls(record['data']) |
46 | |
47 | === modified file 'plainbox/plainbox/impl/session.py' |
48 | --- plainbox/plainbox/impl/session.py 2013-02-08 20:00:15 +0000 |
49 | +++ plainbox/plainbox/impl/session.py 2013-02-10 14:42:22 +0000 |
50 | @@ -37,7 +37,6 @@ |
51 | from plainbox.impl.resource import ExpressionCannotEvaluateError |
52 | from plainbox.impl.resource import ExpressionFailedError |
53 | from plainbox.impl.resource import Resource |
54 | -from plainbox.impl.resource import ResourceExpression |
55 | from plainbox.impl.result import JobResult |
56 | from plainbox.impl.rfc822 import RFC822SyntaxError |
57 | from plainbox.impl.rfc822 import gen_rfc822_records |
58 | @@ -155,8 +154,6 @@ |
59 | return "resource expression {!r} evaluates to false".format( |
60 | self.related_expression.text) |
61 | |
62 | - def _get_persistance_subset(self): |
63 | - return self.__dict__ |
64 | |
65 | # A global instance of JobReadinessInhibitor with the UNDESIRED cause. |
66 | # This is used a lot and it makes no sense to instantiate all the time. |
67 | @@ -252,22 +249,30 @@ |
68 | return "job can be started" |
69 | |
70 | def _get_persistance_subset(self): |
71 | - state = {} |
72 | - state['_job'] = self._job |
73 | # Don't save resource job results, fresh data are required |
74 | # so we can't reuse the old ones |
75 | + # The inhibitor list needs to be recomputed as well, don't save it. |
76 | + state = {} |
77 | + state['_job'] = self._job |
78 | if self._job.plugin == 'resource': |
79 | - state['_readiness_inhibitor_list'] = \ |
80 | - [UndesiredJobReadinessInhibitor] |
81 | state['_result'] = JobResult({ |
82 | 'job': self._job, |
83 | 'outcome': JobResult.OUTCOME_NONE |
84 | }) |
85 | else: |
86 | - state['_readiness_inhibitor_list'] = self._readiness_inhibitor_list |
87 | state['_result'] = self._result |
88 | return state |
89 | |
90 | + @classmethod |
91 | + def from_json_record(cls, record): |
92 | + """ |
93 | + Create a JobState instance from JSON record |
94 | + """ |
95 | + obj = cls(record['_job']) |
96 | + obj._readiness_inhibitor_list = [UndesiredJobReadinessInhibitor] |
97 | + obj._result = record['_result'] |
98 | + return obj |
99 | + |
100 | |
101 | class SessionState: |
102 | """ |
103 | @@ -301,7 +306,8 @@ |
104 | # XXX: this can loose data job_list has jobs with the same name. It |
105 | # would be better to use job id as the keys here. A separate map could |
106 | # be used for the name->job lookup. |
107 | - self._job_state_map = {job.name: JobState(job) for job in job_list} |
108 | + self._job_state_map = {job.name: JobState(job) |
109 | + for job in self._job_list} |
110 | # A subset of job_list that was selected by the user for execution. |
111 | # Used to compute run_list. Can be changed at will during lifetime |
112 | # of this object |
113 | @@ -328,6 +334,16 @@ |
114 | state['_desired_job_list'] = self._desired_job_list |
115 | return state |
116 | |
117 | + @classmethod |
118 | + def from_json_record(cls, record): |
119 | + """ |
120 | + Create a SessionState instance from JSON record |
121 | + """ |
122 | + obj = cls([]) |
123 | + obj._job_state_map = record['_job_state_map'] |
124 | + obj._desired_job_list = record['_desired_job_list'] |
125 | + return obj |
126 | + |
127 | def open(self): |
128 | """ |
129 | Open session state for running jobs. |
130 | @@ -678,16 +694,38 @@ |
131 | |
132 | |
133 | class SessionStateEncoder(json.JSONEncoder): |
134 | - """ |
135 | - JSON Serialize helper to encode SessionState attributes |
136 | - Convert objects to a dictionary of their representation |
137 | - """ |
138 | + |
139 | + _class_indentifiers = { |
140 | + JobDefinition: 'JOB_DEFINITION', |
141 | + JobResult: 'JOB_RESULT', |
142 | + JobState: 'JOB_STATE', |
143 | + SessionState: 'SESSION_STATE', |
144 | + } |
145 | + |
146 | def default(self, obj): |
147 | - if (isinstance(obj, (JobDefinition, JobReadinessInhibitor, JobResult, |
148 | - ResourceExpression, JobState, SessionState))): |
149 | - d = {'__class__': obj.__class__.__name__, |
150 | - '__module__': obj.__module__} |
151 | + """ |
152 | + JSON Serialize helper to encode SessionState attributes |
153 | + Convert objects to a dictionary of their representation |
154 | + """ |
155 | + if (isinstance(obj, (JobDefinition, JobResult, JobState, |
156 | + SessionState))): |
157 | + d = {'_class_id': self._class_indentifiers[obj.__class__]} |
158 | d.update(obj._get_persistance_subset()) |
159 | return d |
160 | else: |
161 | return json.JSONEncoder.default(self, obj) |
162 | + |
163 | + def dict_to_object(self, d): |
164 | + """ |
165 | + JSON Decoder helper |
166 | + Convert dictionary to python objects |
167 | + """ |
168 | + if '_class_id' in d: |
169 | + for c, id in self._class_indentifiers.items(): |
170 | + if id == d['_class_id']: |
171 | + cls = c |
172 | + inst = cls.from_json_record(d) |
173 | + break |
174 | + else: |
175 | + inst = d |
176 | + return inst |
177 | |
178 | === modified file 'plainbox/plainbox/impl/test_job.py' |
179 | --- plainbox/plainbox/impl/test_job.py 2013-02-07 17:21:14 +0000 |
180 | +++ plainbox/plainbox/impl/test_job.py 2013-02-10 14:42:22 +0000 |
181 | @@ -24,11 +24,14 @@ |
182 | Test definitions for plainbox.impl.job module |
183 | """ |
184 | |
185 | +import json |
186 | + |
187 | from unittest import TestCase |
188 | |
189 | from plainbox.impl.job import JobDefinition |
190 | from plainbox.impl.rfc822 import RFC822Record |
191 | from plainbox.impl.rfc822 import Origin |
192 | +from plainbox.impl.session import SessionStateEncoder |
193 | |
194 | |
195 | class TestJobDefinition(TestCase): |
196 | @@ -268,3 +271,16 @@ |
197 | self.assertEqual( |
198 | job1.get_checksum(), |
199 | "ad137ba3654827cb07a254a55c5e2a8daa4de6af604e84ccdbe9b7f221014362") |
200 | + |
201 | + def test_decode(self): |
202 | + raw_json = """{ |
203 | + "_class_id": "JOB_DEFINITION", |
204 | + "data": { |
205 | + "name": "camera/still", |
206 | + "plugin": "user-verify" |
207 | + } |
208 | + }""" |
209 | + job_dec = json.loads(raw_json, object_hook=SessionStateEncoder().dict_to_object) |
210 | + self.assertIsInstance(job_dec, JobDefinition) |
211 | + self.assertEqual(job_dec.name, "camera/still") |
212 | + self.assertEqual(job_dec.plugin, "user-verify") |
213 | |
214 | === modified file 'plainbox/plainbox/impl/test_resource.py' |
215 | --- plainbox/plainbox/impl/test_resource.py 2013-02-01 09:59:13 +0000 |
216 | +++ plainbox/plainbox/impl/test_resource.py 2013-02-10 14:42:22 +0000 |
217 | @@ -273,12 +273,6 @@ |
218 | expr = ResourceExpression("obj.a == 2") |
219 | self.assertRaises(TypeError, expr.evaluate, [{'a': 2}]) |
220 | |
221 | - def test_encode(self): |
222 | - text = "device.category == 'CAPTURE'" |
223 | - expr = ResourceExpression(text) |
224 | - expr_enc = expr._get_persistance_subset() |
225 | - self.assertEqual(expr_enc['_text'], text) |
226 | - |
227 | |
228 | class ResourceProgramTests(TestCase): |
229 | |
230 | |
231 | === modified file 'plainbox/plainbox/impl/test_result.py' |
232 | --- plainbox/plainbox/impl/test_result.py 2013-02-05 11:02:35 +0000 |
233 | +++ plainbox/plainbox/impl/test_result.py 2013-02-10 14:42:22 +0000 |
234 | @@ -23,11 +23,13 @@ |
235 | |
236 | Test definitions for plainbox.impl.result module |
237 | """ |
238 | +import json |
239 | |
240 | from unittest import TestCase |
241 | |
242 | from plainbox.impl.result import JobResult |
243 | from plainbox.impl.testing_utils import make_job |
244 | +from plainbox.impl.session import SessionStateEncoder |
245 | |
246 | |
247 | class JobResultTests(TestCase): |
248 | @@ -80,3 +82,27 @@ |
249 | self.assertEqual(result_enc['data']['return_code'], result.return_code) |
250 | with self.assertRaises(KeyError): |
251 | result_enc['io_log'] |
252 | + |
253 | + def test_decode(self): |
254 | + raw_json = """{ |
255 | + "_class_id": "JOB_RESULT", |
256 | + "data": { |
257 | + "comments": null, |
258 | + "job": { |
259 | + "_class_id": "JOB_DEFINITION", |
260 | + "data": { |
261 | + "name": "__audio__", |
262 | + "plugin": "local" |
263 | + } |
264 | + }, |
265 | + "outcome": "pass", |
266 | + "return_code": 0 |
267 | + } |
268 | + }""" |
269 | + result_dec = json.loads(raw_json, object_hook=SessionStateEncoder().dict_to_object) |
270 | + self.assertIsInstance(result_dec, JobResult) |
271 | + self.assertEqual(result_dec.job.name, "__audio__") |
272 | + self.assertEqual(result_dec.outcome, JobResult.OUTCOME_PASS) |
273 | + self.assertIsNone(result_dec.comments) |
274 | + self.assertEqual(result_dec.io_log, ()) |
275 | + self.assertEqual(result_dec.return_code, 0) |
276 | |
277 | === modified file 'plainbox/plainbox/impl/test_session.py' |
278 | --- plainbox/plainbox/impl/test_session.py 2013-02-05 11:02:35 +0000 |
279 | +++ plainbox/plainbox/impl/test_session.py 2013-02-10 14:42:22 +0000 |
280 | @@ -24,6 +24,8 @@ |
281 | Test definitions for plainbox.impl.session module |
282 | """ |
283 | |
284 | +import json |
285 | + |
286 | from unittest import TestCase |
287 | |
288 | from plainbox.impl.resource import Resource |
289 | @@ -32,6 +34,7 @@ |
290 | from plainbox.impl.session import JobState |
291 | from plainbox.impl.session import SessionState |
292 | from plainbox.impl.session import UndesiredJobReadinessInhibitor |
293 | +from plainbox.impl.session import SessionStateEncoder |
294 | from plainbox.impl.testing_utils import make_job |
295 | from plainbox.impl.testing_utils import make_job_result |
296 | |
297 | @@ -175,6 +178,70 @@ |
298 | self.job_state.get_readiness_description().startswith( |
299 | "job cannot be started: ")) |
300 | |
301 | + def test_encode_resource_job(self): |
302 | + self.job_R = make_job("R", plugin="resource") |
303 | + result_R = JobResult({ |
304 | + 'job': self.job_R, |
305 | + 'outcome': JobResult.OUTCOME_PASS, |
306 | + 'io_log': ((0, 'stdout', "attr: value\n"),) |
307 | + }) |
308 | + jobstate = JobState(self.job_R) |
309 | + jobstate.result = result_R |
310 | + jobstate_enc = jobstate._get_persistance_subset() |
311 | + # The inhibitor list is not saved |
312 | + with self.assertRaises(KeyError): |
313 | + jobstate_enc['_readiness_inhibitor_list'] |
314 | + # Resource have to be re evealutated on startup, outcome of the job |
315 | + # must be reset to JobResult.OUTCOME_NONE |
316 | + self.assertEqual(jobstate_enc['_result'].outcome, |
317 | + JobResult.OUTCOME_NONE) |
318 | + |
319 | + def test_encode_normal_job(self): |
320 | + result = JobResult({ |
321 | + 'job': self.job, |
322 | + 'outcome': JobResult.OUTCOME_PASS, |
323 | + }) |
324 | + self.job_state.result = result |
325 | + jobstate_enc = self.job_state._get_persistance_subset() |
326 | + # The inhibitor list is not saved |
327 | + with self.assertRaises(KeyError): |
328 | + jobstate_enc['_readiness_inhibitor_list'] |
329 | + # Normal jobs should keep their outcome value |
330 | + self.assertEqual(jobstate_enc['_result'].outcome, |
331 | + JobResult.OUTCOME_PASS) |
332 | + |
333 | + def test_decode(self): |
334 | + raw_json = """{ |
335 | + "_class_id": "JOB_STATE", |
336 | + "_job": { |
337 | + "_class_id": "JOB_DEFINITION", |
338 | + "data": { |
339 | + "name": "X", |
340 | + "plugin": "dummy" |
341 | + } |
342 | + }, |
343 | + "_result": { |
344 | + "_class_id": "JOB_RESULT", |
345 | + "data": { |
346 | + "comments": null, |
347 | + "job": { |
348 | + "_class_id": "JOB_DEFINITION", |
349 | + "data": { |
350 | + "name": "X", |
351 | + "plugin": "dummy" |
352 | + } |
353 | + }, |
354 | + "outcome": "pass", |
355 | + "return_code": null |
356 | + } |
357 | + } |
358 | + }""" |
359 | + job_dec = json.loads(raw_json, object_hook=SessionStateEncoder().dict_to_object) |
360 | + self.assertIsInstance(job_dec, JobState) |
361 | + self.assertEqual(repr(job_dec._result), |
362 | + ("<JobResult job:<JobDefinition name:'X'" |
363 | + " plugin:'dummy'> outcome:'pass'>")) |
364 | + |
365 | |
366 | class SessionStateSmokeTests(TestCase): |
367 | |
368 | @@ -234,7 +301,7 @@ |
369 | # most of their time adding (changing) jobs in an ever-growing pile that |
370 | # they don't necessarily fully know, comprehend or remember. |
371 | |
372 | - def test_resource_job_affects_resouces(self): |
373 | + def test_resource_job_affects_resources(self): |
374 | pass |
375 | |
376 |
Just for the record, replied on IRC:
15:42 < spineau> zyga: I've pushed a new version of my branch, the one that provides the json decoders. job_state_ map() is odd
15:43 < spineau> zyga: added tests and remove the encode code of ResourceExpression now that we'll recompute the inhibitor list on resume
15:44 < zyga> spineau: hey
15:44 < spineau> zyga: the next patch will rely on this one and will provide the Session resume method and corresponding tests
15:44 < zyga> spineau: I'm reading it already
15:44 < zyga> spineau: I have some questions
15:44 < spineau> zyga: ok
15:44 < zyga> spineau: update_
15:44 < zyga> it's not updating anything, it just resets the state
15:45 < zyga> as a developer I don't know why I would ever want to call it, it really feels internal
15:45 < zyga> and the docstring is wrong, it's not updating, it is resetting the state really
15:45 < zyga> spineau: so, those are all details
15:46 < zyga> spineau: let me read the whole thing
15:46 < zyga> spineau: job_list=[] in __init__()
15:47 < spineau> zyga: ok, let me explain why I needed this, when deserializing a session state I can't find the current job_list.
15:47 < zyga> spineau: general comment about serialization and reversing classes, __class__ + __module__ == implementation detail + exploit source, use a map that uses fixed identifiers that bind to imlpementation specific classes
15:47 < zyga> spineau: ok, go, I'm listening
15:47 < spineau> zyga: so I recreate the session state with an empty job_list and call this method to (ok) reset the list
15:49 < zyga> spineau: I don't like how this changes all existing code, perhaps that could be avoided by computing it on demand (start with None, add internal checks where it is being accessed)
15:49 < zyga> spineau: but let me finish reading
15:49 < spineau> zyga: sure
15:49 < zyga> spineau: some imports are sorted incorrectly, that's trivial but should be fixed anyway
15:50 < spineau> zyga: ok I see them
15:50 < zyga> spineau: you seem to still have some code for readiness inhibitors, did you decide to serialize them again?
15:51 < spineau> zyga: It's an error, thanks for having catching it
15:52 < zyga> spineau: apart from that it looks ok
15:52 < spineau> zyga: I don't understand your comment about: __class__ + __module__ == implementation detail + exploit source
15:52 < zyga> spineau: fix those and ping me after the meeting, I'll get it locally and look at the details
15:53 < zyga> spineau: don't expose implementation details in that serialization format, it would allow anyone to call arbitrary code
15:53 < zyga> spineau: it's semi-fine but really just annoying in practice, what if we fix a typo in a class name? Use constant identifiers
15:54 < zyga> spineau: dict_to_object and back should just have a mapping of identifiers to classes
15:54 < zyga> spineau: you can probably even create a small class that has those two methods and keeps the mapping