Merge lp:~sylvain-pineau/checkbox/json_load_classmethods_bis into lp:checkbox

Proposed by Sylvain Pineau
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
Reviewer Review Type Date Requested Status
Zygmunt Krynicki (community) Approve
Sylvain Pineau (community) Needs Resubmitting
Review via email: mp+146126@code.launchpad.net

Description of the change

Prerequisite to resume a full plainbox session

This MR adds classmethods to re create instances from JSON objects.

To post a comment you must log in.
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

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.
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_job_state_map() is odd
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

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"

Revision history for this message
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.

Revision history for this message
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_program': None, '_origin': <Origin filename:'/home/sylvain/checkbox-repo/plainbox_localstorage_JSON/jobs/resource.txt.in' line_start:1 line_end:4>}
#################
 {'_data': {'name': 'cpuinfo', 'plugin': 'resource'}, '_resource_program': None, '_origin': None}

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_persistance_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

Revision history for this message
Sylvain Pineau (sylvain-pineau) wrote :

Restore the job identity check in the result setter of JobState.

Should be Ok to land now.

review: Needs Resubmitting
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

+1

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/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

Subscribers

People subscribed via source and target branches