OLD | NEW |
| (Empty) |
1 # Copyright 2013 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 | |
5 import functools | |
6 import collections | |
7 import contextlib | |
8 import json | |
9 | |
10 from recipe_engine import recipe_api | |
11 from recipe_engine import util as recipe_util | |
12 from recipe_engine import config_types | |
13 | |
14 | |
15 class JsonOutputPlaceholder(recipe_util.Placeholder): | |
16 """JsonOutputPlaceholder is meant to be a placeholder object which, when added | |
17 to a step's cmd list, will be replaced by annotated_run with the path to a | |
18 temporary file (e.g. /tmp/tmp4lp1qM) which will exist only for the duration of | |
19 the step. If the script requires a flag (e.g. --output-json /path/to/file), | |
20 you must supply that flag yourself in the cmd list. | |
21 | |
22 This placeholder can be optionally added when you use the Steps.step() | |
23 method in this module. | |
24 | |
25 FIXME | |
26 After the termination of the step, this file is expected to contain a valid | |
27 JSON document, which will be set as the json.output for that step in the | |
28 step_history OrderedDict passed to your recipe generator. | |
29 """ | |
30 def __init__(self, api, add_json_log): | |
31 self.raw = api.m.raw_io.output('.json') | |
32 self.add_json_log = add_json_log | |
33 super(JsonOutputPlaceholder, self).__init__() | |
34 | |
35 @property | |
36 def backing_file(self): | |
37 return self.raw.backing_file | |
38 | |
39 def render(self, test): | |
40 return self.raw.render(test) | |
41 | |
42 def result(self, presentation, test): | |
43 raw_data = self.raw.result(presentation, test) | |
44 | |
45 valid = False | |
46 ret = None | |
47 try: | |
48 ret = json.loads(raw_data, object_pairs_hook=collections.OrderedDict) | |
49 valid = True | |
50 # TypeError is raised when raw_data is None, which can happen if the json | |
51 # file was not created. We then correctly handle this as invalid result. | |
52 except (ValueError, TypeError): # pragma: no cover | |
53 pass | |
54 | |
55 if self.add_json_log: | |
56 key = self.name + ('' if valid else ' (invalid)') | |
57 with contextlib.closing(recipe_util.StringListIO()) as listio: | |
58 json.dump(ret, listio, indent=2, sort_keys=True) | |
59 presentation.logs[key] = listio.lines | |
60 | |
61 return ret | |
62 | |
63 | |
64 class JsonApi(recipe_api.RecipeApi): | |
65 def __init__(self, **kwargs): | |
66 super(JsonApi, self).__init__(**kwargs) | |
67 self.loads = json.loads | |
68 @functools.wraps(json.dumps) | |
69 def dumps(*args, **kwargs): | |
70 kwargs['sort_keys'] = True | |
71 kwargs.setdefault('default', config_types.json_fixup) | |
72 return json.dumps(*args, **kwargs) | |
73 self.dumps = dumps | |
74 | |
75 def is_serializable(self, obj): | |
76 """Returns True if the object is JSON-serializable.""" | |
77 try: | |
78 self.dumps(obj) | |
79 return True | |
80 except Exception: | |
81 return False | |
82 | |
83 @recipe_util.returns_placeholder | |
84 def input(self, data): | |
85 """A placeholder which will expand to a file path containing <data>.""" | |
86 return self.m.raw_io.input(self.dumps(data), '.json') | |
87 | |
88 @recipe_util.returns_placeholder | |
89 def output(self, add_json_log=True): | |
90 """A placeholder which will expand to '/tmp/file'.""" | |
91 return JsonOutputPlaceholder(self, add_json_log) | |
92 | |
93 # TODO(you): This method should be in the `file` recipe_module | |
94 def read(self, name, path, **kwargs): | |
95 """Returns a step that reads a JSON file.""" | |
96 return self.m.python.inline( | |
97 name, | |
98 """ | |
99 import shutil | |
100 import sys | |
101 shutil.copy(sys.argv[1], sys.argv[2]) | |
102 """, | |
103 args=[path, self.output()], | |
104 add_python_log=False, | |
105 **kwargs | |
106 ) | |
107 | |
108 def property_args(self): | |
109 """Return --build-properties and --factory-properties arguments. LEGACY! | |
110 | |
111 Since properties is the merge of build_properties and factory_properties, | |
112 pass the merged dict as both arguments. | |
113 | |
114 It's vastly preferable to have your recipe only pass the bare minimum | |
115 of arguments to steps. Passing property objects obscures the data that | |
116 the script actually consumes from the property object. | |
117 """ | |
118 prop_str = self.dumps(dict(self.m.properties.legacy())) | |
119 return [ | |
120 '--factory-properties', prop_str, | |
121 '--build-properties', prop_str | |
122 ] | |
OLD | NEW |