Merge lp:~sergiusens/snapcraft/yaml_init into lp:~snappy-dev/snapcraft/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
Reviewer Review Type Date Requested Status
Leo Arias (community) Approve
Review via email: mp+270325@code.launchpad.net

Commit message

Simplifying snapcraft.yaml.Config.__init__

Description of the change

The complexity is no longer 22 but 5 for __init__

To post a comment you must log in.
Revision history for this message
Leo Arias (elopio) wrote :

lgtm. Tests pass here.
I love that now not all the exceptions are sys exit. And thanks for using _ for variable names.

review: Approve
lp:~sergiusens/snapcraft/yaml_init updated
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)

Subscribers

People subscribed via source and target branches