Merge lp:~sergiusens/snapcraft/yaml_init into lp:~snappy-dev/snapcraft/core
- yaml_init
- Merge into core
Proposed by
Sergio Schvezov
Status: | Merged |
---|---|
Approved by: | Sergio Schvezov |
Approved revision: | 166 |
Merged at revision: | 155 |
Proposed branch: | lp:~sergiusens/snapcraft/yaml_init |
Merge into: | lp:~snappy-dev/snapcraft/core |
Prerequisite: | lp:~sergiusens/snapcraft/ubuntu-core |
Diff against target: |
444 lines (+132/-109) 3 files modified
snapcraft/cmds.py (+20/-3) snapcraft/tests/test_yaml.py (+20/-38) snapcraft/yaml.py (+92/-68) |
To merge this branch: | bzr merge lp:~sergiusens/snapcraft/yaml_init |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Leo Arias (community) | Approve | ||
Review via email: mp+270325@code.launchpad.net |
Commit message
Simplifying snapcraft.
Description of the change
The complexity is no longer 22 but 5 for __init__
To post a comment you must log in.
- 164. By Sergio Schvezov
-
Merged ubuntu-core into yaml_init.
- 165. By Sergio Schvezov
-
Merged ubuntu-core into yaml_init.
- 166. By Sergio Schvezov
-
Merged ubuntu-core into yaml_init.
Revision history for this message
Michael Vogt (mvo) wrote : | # |
I did not do a full review (unless you want one) but what I see looks very good indeed.
Revision history for this message
Sergio Schvezov (sergiusens) wrote : | # |
On Wed, Sep 9, 2015 at 10:49 AM, Michael Vogt <email address hidden>
wrote:
> I did not do a full review (unless you want one) but what I see looks very
> good indeed.
>
Thanks, an approve (aka full review) would be nice.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'snapcraft/cmds.py' |
2 | --- snapcraft/cmds.py 2015-08-31 17:42:31 +0000 |
3 | +++ snapcraft/cmds.py 2015-09-08 12:03:45 +0000 |
4 | @@ -52,7 +52,7 @@ |
5 | |
6 | |
7 | def shell(args): |
8 | - config = snapcraft.yaml.Config() |
9 | + config = _load_config() |
10 | common.env = config.stage_env() |
11 | userCommand = args.userCommand |
12 | if not userCommand: |
13 | @@ -67,7 +67,7 @@ |
14 | if not os.path.exists(os.path.join(common.get_snapdir(), "meta")): |
15 | arches = [snapcraft.common.get_arch(), ] |
16 | |
17 | - config = snapcraft.yaml.Config() |
18 | + config = _load_config() |
19 | |
20 | # FIXME this should be done in a more contained manner |
21 | common.env = config.snap_env() |
22 | @@ -243,7 +243,7 @@ |
23 | forceCommand = cmds[0] |
24 | cmds = common.COMMAND_ORDER[0:common.COMMAND_ORDER.index(cmds[0]) + 1] |
25 | |
26 | - config = snapcraft.yaml.Config() |
27 | + config = _load_config() |
28 | |
29 | # Install local packages that we need |
30 | if config.build_tools: |
31 | @@ -282,3 +282,20 @@ |
32 | def _check_call(args, **kwargs): |
33 | logger.info('Running: %s', ' '.join(shlex.quote(arg) for arg in args)) |
34 | return subprocess.check_call(args, **kwargs) |
35 | + |
36 | + |
37 | +def _load_config(): |
38 | + try: |
39 | + return snapcraft.yaml.Config() |
40 | + except snapcraft.yaml.SnapcraftYamlFileError as e: |
41 | + logger.error( |
42 | + 'Could not find {}. Are you sure you are in the right directory?\n' |
43 | + 'To start a new project, use \'snapcraft init\''.format(e.file)) |
44 | + sys.exit(1) |
45 | + except snapcraft.yaml.SnapcraftSchemaError as e: |
46 | + msg = "Issues while validating snapcraft.yaml: {}".format(e.message) |
47 | + logger.error(msg) |
48 | + sys.exit(1) |
49 | + except snapcraft.yaml.SnapcraftLogicError as e: |
50 | + logger.error('Issue detected while analyzing snapcraft.yaml: {}'.format(e.message)) |
51 | + sys.exit(1) |
52 | |
53 | === modified file 'snapcraft/tests/test_yaml.py' |
54 | --- snapcraft/tests/test_yaml.py 2015-09-08 12:03:45 +0000 |
55 | +++ snapcraft/tests/test_yaml.py 2015-09-08 12:03:45 +0000 |
56 | @@ -14,7 +14,6 @@ |
57 | # You should have received a copy of the GNU General Public License |
58 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
59 | |
60 | -import jsonschema |
61 | import logging |
62 | import os |
63 | import tempfile |
64 | @@ -71,14 +70,10 @@ |
65 | self.useFixture(fake_logger) |
66 | |
67 | # no snapcraft.yaml |
68 | - with self.assertRaises(SystemExit) as raised: |
69 | + with self.assertRaises(snapcraft.yaml.SnapcraftYamlFileError) as raised: |
70 | snapcraft.yaml.Config() |
71 | |
72 | - self.assertEqual(raised.exception.code, 1, 'Wrong exit code returned.') |
73 | - self.assertEqual( |
74 | - "Could not find snapcraft.yaml. Are you sure you're in the right directory?\n" |
75 | - "To start a new project, use 'snapcraft init'\n", |
76 | - fake_logger.output) |
77 | + self.assertEqual(raised.exception.file, 'snapcraft.yaml') |
78 | |
79 | def test_config_loop(self): |
80 | fake_logger = fixtures.FakeLogger(level=logging.ERROR) |
81 | @@ -99,11 +94,10 @@ |
82 | plugin: go |
83 | after: [p1] |
84 | """) |
85 | - with self.assertRaises(SystemExit) as raised: |
86 | + with self.assertRaises(snapcraft.yaml.SnapcraftLogicError) as raised: |
87 | snapcraft.yaml.Config() |
88 | |
89 | - self.assertEqual(raised.exception.code, 1, 'Wrong exit code returned.') |
90 | - self.assertEqual('Circular dependency chain!\n', fake_logger.output) |
91 | + self.assertEqual(raised.exception.message, 'circular dependency chain found in parts definition') |
92 | |
93 | @unittest.mock.patch('snapcraft.yaml.Config.load_plugin') |
94 | def test_invalid_yaml_missing_name(self, mock_loadPlugin): |
95 | @@ -122,13 +116,10 @@ |
96 | plugin: go |
97 | stage-packages: [fswebcam] |
98 | """) |
99 | - with self.assertRaises(SystemExit) as raised: |
100 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
101 | snapcraft.yaml.Config() |
102 | |
103 | - self.assertEqual(raised.exception.code, 1, 'Wrong exit code returned.') |
104 | - self.assertEqual( |
105 | - 'Issues while validating snapcraft.yaml: \'name\' is a required property\n', |
106 | - fake_logger.output) |
107 | + self.assertEqual(raised.exception.message, '\'name\' is a required property') |
108 | |
109 | @unittest.mock.patch('snapcraft.yaml.Config.load_plugin') |
110 | def test_invalid_yaml_invalid_name_as_number(self, mock_loadPlugin): |
111 | @@ -147,13 +138,10 @@ |
112 | plugin: go |
113 | stage-packages: [fswebcam] |
114 | """) |
115 | - with self.assertRaises(SystemExit) as raised: |
116 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
117 | snapcraft.yaml.Config() |
118 | |
119 | - self.assertEqual(raised.exception.code, 1, 'Wrong exit code returned.') |
120 | - self.assertEqual( |
121 | - 'Issues while validating snapcraft.yaml: 1 is not of type \'string\'\n', |
122 | - fake_logger.output) |
123 | + self.assertEqual(raised.exception.message, '1 is not of type \'string\'') |
124 | |
125 | @unittest.mock.patch('snapcraft.yaml.Config.load_plugin') |
126 | def test_invalid_yaml_invalid_name_chars(self, mock_loadPlugin): |
127 | @@ -172,13 +160,10 @@ |
128 | plugin: go |
129 | stage-packages: [fswebcam] |
130 | """) |
131 | - with self.assertRaises(SystemExit) as raised: |
132 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
133 | snapcraft.yaml.Config() |
134 | |
135 | - self.assertEqual(raised.exception.code, 1, 'Wrong exit code returned.') |
136 | - self.assertEqual( |
137 | - 'Issues while validating snapcraft.yaml: \'myapp@me_1.0\' does not match \'^[a-z0-9][a-z0-9+-]*$\'\n', |
138 | - fake_logger.output) |
139 | + self.assertEqual(raised.exception.message, '\'myapp@me_1.0\' does not match \'^[a-z0-9][a-z0-9+-]*$\'') |
140 | |
141 | @unittest.mock.patch('snapcraft.yaml.Config.load_plugin') |
142 | def test_invalid_yaml_missing_description(self, mock_loadPlugin): |
143 | @@ -196,13 +181,10 @@ |
144 | plugin: go |
145 | stage-packages: [fswebcam] |
146 | """) |
147 | - with self.assertRaises(SystemExit) as raised: |
148 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
149 | snapcraft.yaml.Config() |
150 | |
151 | - self.assertEqual(raised.exception.code, 1, 'Wrong exit code returned.') |
152 | - self.assertEqual( |
153 | - 'Issues while validating snapcraft.yaml: \'description\' is a required property\n', |
154 | - fake_logger.output) |
155 | + self.assertEqual(raised.exception.message, '\'description\' is a required property') |
156 | |
157 | |
158 | class TestValidation(TestCase): |
159 | @@ -236,7 +218,7 @@ |
160 | with self.subTest(key=key): |
161 | del data[key] |
162 | |
163 | - with self.assertRaises(jsonschema.ValidationError) as raised: |
164 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
165 | snapcraft.yaml._validate_snapcraft_yaml(data) |
166 | |
167 | expected_message = '\'{}\' is a required property'.format(key) |
168 | @@ -254,7 +236,7 @@ |
169 | with self.subTest(key=name): |
170 | data['name'] = name |
171 | |
172 | - with self.assertRaises(jsonschema.ValidationError) as raised: |
173 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
174 | snapcraft.yaml._validate_snapcraft_yaml(data) |
175 | |
176 | expected_message = '\'{}\' does not match \'^[a-z0-9][a-z0-9+-]*$\''.format(name) |
177 | @@ -262,7 +244,7 @@ |
178 | |
179 | def test_summary_too_long(self): |
180 | self.data['summary'] = 'a' * 80 |
181 | - with self.assertRaises(jsonschema.ValidationError) as raised: |
182 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
183 | snapcraft.yaml._validate_snapcraft_yaml(self.data) |
184 | |
185 | expected_message = '\'{}\' is too long'.format(self.data['summary']) |
186 | @@ -289,7 +271,7 @@ |
187 | with self.subTest(key=t): |
188 | data['type'] = t |
189 | |
190 | - with self.assertRaises(jsonschema.ValidationError) as raised: |
191 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
192 | snapcraft.yaml._validate_snapcraft_yaml(data) |
193 | |
194 | expected_message = '\'{}\' is not one of [\'app\', \'framework\']'.format(t) |
195 | @@ -320,7 +302,7 @@ |
196 | } |
197 | ] |
198 | |
199 | - with self.assertRaises(jsonschema.ValidationError) as raised: |
200 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
201 | snapcraft.yaml._validate_snapcraft_yaml(self.data) |
202 | |
203 | expected_message = '\'name\' is a required property' |
204 | @@ -331,18 +313,18 @@ |
205 | mock_the_open.side_effect = FileNotFoundError() |
206 | |
207 | with unittest.mock.patch('snapcraft.yaml.open', mock_the_open, create=True): |
208 | - with self.assertRaises(snapcraft.yaml.SchemaNotFoundError) as raised: |
209 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
210 | snapcraft.yaml._validate_snapcraft_yaml(self.data) |
211 | |
212 | expected_path = os.path.join(snapcraft.common.get_schemadir(), 'snapcraft.yaml') |
213 | mock_the_open.assert_called_once_with(expected_path) |
214 | - expected_message = 'Schema is missing, could not validate snapcraft.yaml, check installation' |
215 | + expected_message = 'snapcraft validation file is missing from installation path' |
216 | self.assertEqual(raised.exception.message, expected_message) |
217 | |
218 | def test_icon_missing(self): |
219 | self.mock_path_exists.return_value = False |
220 | |
221 | - with self.assertRaises(jsonschema.ValidationError) as raised: |
222 | + with self.assertRaises(snapcraft.yaml.SnapcraftSchemaError) as raised: |
223 | snapcraft.yaml._validate_snapcraft_yaml(self.data) |
224 | |
225 | expected_message = '\'my-icon.png\' is not a \'icon-path\'' |
226 | |
227 | === modified file 'snapcraft/yaml.py' |
228 | --- snapcraft/yaml.py 2015-08-27 22:04:07 +0000 |
229 | +++ snapcraft/yaml.py 2015-09-08 12:03:45 +0000 |
230 | @@ -15,7 +15,6 @@ |
231 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
232 | |
233 | import logging |
234 | -import sys |
235 | |
236 | import yaml |
237 | import jsonschema |
238 | @@ -34,23 +33,34 @@ |
239 | return os.path.exists(instance) |
240 | |
241 | |
242 | -class SchemaNotFoundError(Exception): |
243 | - |
244 | - def __init__(self, message): |
245 | - self.message = message |
246 | - |
247 | - |
248 | -def _validate_snapcraft_yaml(snapcraft_yaml): |
249 | - schema_file = os.path.abspath(os.path.join(common.get_schemadir(), 'snapcraft.yaml')) |
250 | - |
251 | - try: |
252 | - with open(schema_file) as fp: |
253 | - schema = yaml.load(fp) |
254 | - except FileNotFoundError: |
255 | - raise SchemaNotFoundError('Schema is missing, could not validate snapcraft.yaml, check installation') |
256 | - |
257 | - format_check = jsonschema.FormatChecker() |
258 | - jsonschema.validate(snapcraft_yaml, schema, format_checker=format_check) |
259 | +class SnapcraftYamlFileError(Exception): |
260 | + |
261 | + @property |
262 | + def file(self): |
263 | + return self._file |
264 | + |
265 | + def __init__(self, yaml_file): |
266 | + self._file = yaml_file |
267 | + |
268 | + |
269 | +class SnapcraftLogicError(Exception): |
270 | + |
271 | + @property |
272 | + def message(self): |
273 | + return self._message |
274 | + |
275 | + def __init__(self, message): |
276 | + self._message = message |
277 | + |
278 | + |
279 | +class SnapcraftSchemaError(Exception): |
280 | + |
281 | + @property |
282 | + def message(self): |
283 | + return self._message |
284 | + |
285 | + def __init__(self, message): |
286 | + self._message = message |
287 | |
288 | |
289 | class Config: |
290 | @@ -58,25 +68,10 @@ |
291 | def __init__(self): |
292 | self.build_tools = [] |
293 | self.all_parts = [] |
294 | - afterRequests = {} |
295 | - |
296 | - try: |
297 | - with open("snapcraft.yaml", 'r') as fp: |
298 | - self.data = yaml.load(fp) |
299 | - except FileNotFoundError: |
300 | - logger.error("Could not find snapcraft.yaml. Are you sure you're in the right directory?\nTo start a new project, use 'snapcraft init'") |
301 | - sys.exit(1) |
302 | - |
303 | - # Make sure the loaded snapcraft yaml follows the schema |
304 | - try: |
305 | - _validate_snapcraft_yaml(self.data) |
306 | - except SchemaNotFoundError as e: |
307 | - logger.error(e.message) |
308 | - sys.exit(1) |
309 | - except jsonschema.ValidationError as e: |
310 | - msg = "Issues while validating snapcraft.yaml: {}".format(e.message) |
311 | - logger.error(msg) |
312 | - sys.exit(1) |
313 | + after_requests = {} |
314 | + |
315 | + self.data = _snapcraft_yaml_load() |
316 | + _validate_snapcraft_yaml(self.data) |
317 | |
318 | self.build_tools = self.data.get('build-tools', []) |
319 | |
320 | @@ -88,45 +83,52 @@ |
321 | del properties["plugin"] |
322 | |
323 | if "after" in properties: |
324 | - afterRequests[part_name] = properties["after"] |
325 | + after_requests[part_name] = properties["after"] |
326 | del properties["after"] |
327 | |
328 | # TODO: support 'filter' or 'blacklist' field to filter what gets put in snap/ |
329 | |
330 | self.load_plugin(part_name, plugin_name, properties) |
331 | |
332 | - # Grab all required dependencies, if not already specified |
333 | - newParts = self.all_parts.copy() |
334 | - while newParts: |
335 | - part = newParts.pop(0) |
336 | + self._load_missing_part_plugins() |
337 | + self._compute_part_dependencies(after_requests) |
338 | + self.all_parts = self._sort_parts() |
339 | + |
340 | + def _load_missing_part_plugins(self): |
341 | + new_parts = self.all_parts.copy() |
342 | + while new_parts: |
343 | + part = new_parts.pop(0) |
344 | requires = part.config.get('requires', []) |
345 | - for requiredPart in requires: |
346 | - alreadyPresent = False |
347 | + for required_part in requires: |
348 | + present = False |
349 | for p in self.all_parts: |
350 | - if requiredPart in p.names(): |
351 | - alreadyPresent = True |
352 | + if required_part in p.names(): |
353 | + present = True |
354 | break |
355 | - if not alreadyPresent: |
356 | - newParts.append(self.load_plugin(requiredPart, requiredPart, {})) |
357 | - |
358 | - # Gather lists of dependencies |
359 | + if not present: |
360 | + new_parts.append(self.load_plugin(required_part, required_part, {})) |
361 | + |
362 | + def _compute_part_dependencies(self, after_requests): |
363 | + '''Gather the lists of dependencies and adds to all_parts.''' |
364 | + |
365 | for part in self.all_parts: |
366 | - depNames = part.config.get('requires', []) + afterRequests.get(part.names()[0], []) |
367 | - for dep in depNames: |
368 | - foundIt = False |
369 | + dep_names = part.config.get('requires', []) + after_requests.get(part.names()[0], []) |
370 | + for dep in dep_names: |
371 | + found = False |
372 | for i in range(len(self.all_parts)): |
373 | if dep in self.all_parts[i].names(): |
374 | part.deps.append(self.all_parts[i]) |
375 | - foundIt = True |
376 | + found = True |
377 | break |
378 | - if not foundIt: |
379 | - logger.error("Could not find part name %s", dep) |
380 | - sys.exit(1) |
381 | - |
382 | - # Now sort them (this is super inefficient, but easy-ish to follow) |
383 | - sortedParts = [] |
384 | + if not found: |
385 | + raise SnapcraftLogicError('part name missing {}'.format(dep)) |
386 | + |
387 | + def _sort_parts(self): |
388 | + '''Performs an inneficient but easy to follow sorting of parts.''' |
389 | + sorted_parts = [] |
390 | + |
391 | while self.all_parts: |
392 | - topPart = None |
393 | + top_part = None |
394 | for part in self.all_parts: |
395 | mentioned = False |
396 | for other in self.all_parts: |
397 | @@ -134,14 +136,14 @@ |
398 | mentioned = True |
399 | break |
400 | if not mentioned: |
401 | - topPart = part |
402 | + top_part = part |
403 | break |
404 | - if not topPart: |
405 | - logger.error("Circular dependency chain!") |
406 | - sys.exit(1) |
407 | - sortedParts = [topPart] + sortedParts |
408 | - self.all_parts.remove(topPart) |
409 | - self.all_parts = sortedParts |
410 | + if not top_part: |
411 | + raise SnapcraftLogicError('circular dependency chain found in parts definition') |
412 | + sorted_parts = [top_part] + sorted_parts |
413 | + self.all_parts.remove(top_part) |
414 | + |
415 | + return sorted_parts |
416 | |
417 | def load_plugin(self, part_name, plugin_name, properties, load_code=True): |
418 | part = snapcraft.plugin.load_plugin(part_name, plugin_name, properties, load_code=load_code) |
419 | @@ -196,3 +198,25 @@ |
420 | env += part.env(root) |
421 | |
422 | return env |
423 | + |
424 | + |
425 | +def _validate_snapcraft_yaml(snapcraft_yaml): |
426 | + schema_file = os.path.abspath(os.path.join(common.get_schemadir(), 'snapcraft.yaml')) |
427 | + |
428 | + try: |
429 | + with open(schema_file) as fp: |
430 | + schema = yaml.load(fp) |
431 | + format_check = jsonschema.FormatChecker() |
432 | + jsonschema.validate(snapcraft_yaml, schema, format_checker=format_check) |
433 | + except FileNotFoundError: |
434 | + raise SnapcraftSchemaError('snapcraft validation file is missing from installation path') |
435 | + except jsonschema.ValidationError as e: |
436 | + raise SnapcraftSchemaError(e.message) |
437 | + |
438 | + |
439 | +def _snapcraft_yaml_load(yaml_file='snapcraft.yaml'): |
440 | + try: |
441 | + with open(yaml_file) as fp: |
442 | + return yaml.load(fp) |
443 | + except FileNotFoundError: |
444 | + raise SnapcraftYamlFileError(yaml_file) |
lgtm. Tests pass here.
I love that now not all the exceptions are sys exit. And thanks for using _ for variable names.