Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(285)

Side by Side Diff: recipes.py

Issue 2845783002: [recipes.py] Move fetch, lint and bundle parsers to separate modules. (Closed)
Patch Set: rebase Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« recipe_engine/bundle.py ('K') | « recipe_engine/lint_test.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2015 The LUCI Authors. All rights reserved. 2 # Copyright 2015 The LUCI Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0 3 # Use of this source code is governed under the Apache License, Version 2.0
4 # that can be found in the LICENSE file. 4 # that can be found in the LICENSE file.
5 5
6 """Tool to interact with recipe repositories. 6 """Tool to interact with recipe repositories.
7 7
8 This tool operates on the nearest ancestor directory containing an 8 This tool operates on the nearest ancestor directory containing an
9 infra/config/recipes.cfg. 9 infra/config/recipes.cfg.
10 """ 10 """
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
52 universe_view = loader.UniverseView(universe, package_deps.root_package) 52 universe_view = loader.UniverseView(universe, package_deps.root_package)
53 53
54 # Prevent flakiness caused by stale pyc files. 54 # Prevent flakiness caused by stale pyc files.
55 package.cleanup_pyc(package_deps.root_package.recipes_dir) 55 package.cleanup_pyc(package_deps.root_package.recipes_dir)
56 56
57 return test.main( 57 return test.main(
58 universe_view, raw_args=args.args, 58 universe_view, raw_args=args.args,
59 engine_flags=args.operational_args.engine_flags) 59 engine_flags=args.operational_args.engine_flags)
60 60
61 61
62 def lint(config_file, package_deps, args):
63 from recipe_engine import lint_test
64 from recipe_engine import loader
65
66 universe = loader.RecipeUniverse(package_deps, config_file)
67 universe_view = loader.UniverseView(universe, package_deps.root_package)
68
69 lint_test.main(universe_view, args.whitelist or [])
70
71
72 def bundle(config_file, package_deps, args):
73 from recipe_engine import bundle
74 from recipe_engine import loader
75
76 universe = loader.RecipeUniverse(package_deps, config_file)
77
78 bundle.main(package_deps.root_package, universe, args.destination)
79
80
81 def handle_recipe_return(recipe_result, result_filename, stream_engine, 62 def handle_recipe_return(recipe_result, result_filename, stream_engine,
82 engine_flags): 63 engine_flags):
83 if engine_flags and engine_flags.use_result_proto: 64 if engine_flags and engine_flags.use_result_proto:
84 return new_handle_recipe_return( 65 return new_handle_recipe_return(
85 recipe_result, result_filename, stream_engine) 66 recipe_result, result_filename, stream_engine)
86 67
87 if 'recipe_result' in recipe_result.result: 68 if 'recipe_result' in recipe_result.result:
88 result_string = json.dumps( 69 result_string = json.dumps(
89 recipe_result.result['recipe_result'], indent=2) 70 recipe_result.result['recipe_result'], indent=2)
90 if result_filename: 71 if result_filename:
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
162 143
163 def get_properties_from_operational_args(op_args): 144 def get_properties_from_operational_args(op_args):
164 if not op_args.properties.property: 145 if not op_args.properties.property:
165 return None 146 return None
166 return _op_properties_to_dict(op_args.properties.property) 147 return _op_properties_to_dict(op_args.properties.property)
167 148
168 op_args = args.operational_args 149 op_args = args.operational_args
169 op_properties = get_properties_from_operational_args(op_args) 150 op_properties = get_properties_from_operational_args(op_args)
170 if args.properties and op_properties: 151 if args.properties and op_properties:
171 raise ValueError( 152 raise ValueError(
172 "Got operational args properties as well as CLI properties.") 153 'Got operational args properties as well as CLI properties.')
173 154
174 properties = op_properties 155 properties = op_properties
175 if not properties: 156 if not properties:
176 properties = args.properties 157 properties = args.properties
177 158
178 properties['recipe'] = args.recipe 159 properties['recipe'] = args.recipe
179 160
180 properties = recipe_util.strip_unicode(properties) 161 properties = recipe_util.strip_unicode(properties)
181 162
182 os.environ['PYTHONUNBUFFERED'] = '1' 163 os.environ['PYTHONUNBUFFERED'] = '1'
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after
252 print >> sys.stderr, '--verbose-json passed without --output-json' 233 print >> sys.stderr, '--verbose-json passed without --output-json'
253 return 1 234 return 1
254 235
255 return autoroll.main(args, repo_root, config_file) 236 return autoroll.main(args, repo_root, config_file)
256 237
257 238
258 class ProjectOverrideAction(argparse.Action): 239 class ProjectOverrideAction(argparse.Action):
259 def __call__(self, parser, namespace, values, option_string=None): 240 def __call__(self, parser, namespace, values, option_string=None):
260 p = values.split('=', 2) 241 p = values.split('=', 2)
261 if len(p) != 2: 242 if len(p) != 2:
262 raise ValueError("Override must have the form: repo=path") 243 raise ValueError('Override must have the form: repo=path')
263 project_id, path = p 244 project_id, path = p
264 245
265 v = getattr(namespace, self.dest, None) 246 v = getattr(namespace, self.dest, None)
266 if v is None: 247 if v is None:
267 v = {} 248 v = {}
268 setattr(namespace, self.dest, v) 249 setattr(namespace, self.dest, v)
269 250
270 if v.get(project_id): 251 if v.get(project_id):
271 raise ValueError("An override is already defined for [%s] (%s)" % ( 252 raise ValueError('An override is already defined for [%s] (%s)' % (
272 project_id, v[project_id])) 253 project_id, v[project_id]))
273 path = os.path.abspath(os.path.expanduser(path)) 254 path = os.path.abspath(os.path.expanduser(path))
274 if not os.path.isdir(path): 255 if not os.path.isdir(path):
275 raise ValueError("Override path [%s] is not a directory" % (path,)) 256 raise ValueError('Override path [%s] is not a directory' % (path,))
276 v[project_id] = path 257 v[project_id] = path
277 258
278 259
279 def depgraph(config_file, package_deps, args): 260 def depgraph(config_file, package_deps, args):
280 from recipe_engine import depgraph 261 from recipe_engine import depgraph
281 from recipe_engine import loader 262 from recipe_engine import loader
282 263
283 universe = loader.RecipeUniverse(package_deps, config_file) 264 universe = loader.RecipeUniverse(package_deps, config_file)
284 265
285 depgraph.main(universe, package_deps.root_package, 266 depgraph.main(universe, package_deps.root_package,
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
320 'list': lambda prop: [_op_property_value(v) for v in prop.list.property], 301 'list': lambda prop: [_op_property_value(v) for v in prop.list.property],
321 } 302 }
322 303
323 def _op_property_value(prop): 304 def _op_property_value(prop):
324 """Returns the Python-converted value of an arguments_pb2.Property. 305 """Returns the Python-converted value of an arguments_pb2.Property.
325 306
326 Args: 307 Args:
327 prop (arguments_pb2.Property): property to convert. 308 prop (arguments_pb2.Property): property to convert.
328 Returns: The converted value. 309 Returns: The converted value.
329 Raises: 310 Raises:
330 ValueError: If "prop" is incomplete or invalid. 311 ValueError: If 'prop' is incomplete or invalid.
331 """ 312 """
332 typ = prop.WhichOneof('value') 313 typ = prop.WhichOneof('value')
333 conv = _OP_PROPERTY_CONV.get(typ) 314 conv = _OP_PROPERTY_CONV.get(typ)
334 if not conv: 315 if not conv:
335 raise ValueError('Unknown property field [%s]' % (typ,)) 316 raise ValueError('Unknown property field [%s]' % (typ,))
336 return conv(prop) 317 return conv(prop)
337 318
338 319
339 def _op_properties_to_dict(pmap): 320 def _op_properties_to_dict(pmap):
340 """Creates a properties dictionary from an arguments_pb2.PropertyMap entry. 321 """Creates a properties dictionary from an arguments_pb2.PropertyMap entry.
341 322
342 Args: 323 Args:
343 pmap (arguments_pb2.PropertyMap): Map to convert to dictionary form. 324 pmap (arguments_pb2.PropertyMap): Map to convert to dictionary form.
344 Returns (dict): A dictionary derived from the properties in "pmap". 325 Returns (dict): A dictionary derived from the properties in 'pmap'.
345 """ 326 """
346 return dict((k, _op_property_value(pmap[k])) for k in pmap) 327 return dict((k, _op_property_value(pmap[k])) for k in pmap)
347 328
348 329
349 def add_common_args(parser): 330 def add_common_args(parser):
350 from recipe_engine import package_io 331 from recipe_engine import package_io
351 332
352 def package_type(value): 333 def package_type(value):
353 if not os.path.isfile(value): 334 if not os.path.isfile(value):
354 raise argparse.ArgumentTypeError( 335 raise argparse.ArgumentTypeError(
(...skipping 15 matching lines...) Expand all
370 '--verbose', '-v', action='count', 351 '--verbose', '-v', action='count',
371 help='Increase logging verboisty') 352 help='Increase logging verboisty')
372 # TODO(phajdan.jr): Figure out if we need --no-fetch; remove if not. 353 # TODO(phajdan.jr): Figure out if we need --no-fetch; remove if not.
373 parser.add_argument( 354 parser.add_argument(
374 '--no-fetch', action='store_true', 355 '--no-fetch', action='store_true',
375 help='Disable automatic fetching') 356 help='Disable automatic fetching')
376 parser.add_argument('-O', '--project-override', metavar='ID=PATH', 357 parser.add_argument('-O', '--project-override', metavar='ID=PATH',
377 action=ProjectOverrideAction, 358 action=ProjectOverrideAction,
378 help='Override a project repository path with a local one.') 359 help='Override a project repository path with a local one.')
379 parser.add_argument( 360 parser.add_argument(
380 # Use "None" as default so that we can recognize when none of the 361 # Use 'None' as default so that we can recognize when none of the
381 # bootstrap options were passed. 362 # bootstrap options were passed.
382 '--use-bootstrap', action='store_true', default=None, 363 '--use-bootstrap', action='store_true', default=None,
383 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv' 364 help='Use bootstrap/bootstrap.py to create a isolated python virtualenv'
384 ' with required python dependencies.') 365 ' with required python dependencies.')
385 parser.add_argument( 366 parser.add_argument(
386 '--disable-bootstrap', action='store_false', dest='use_bootstrap', 367 '--disable-bootstrap', action='store_false', dest='use_bootstrap',
387 help='Disables bootstrap (see --use-bootstrap)') 368 help='Disables bootstrap (see --use-bootstrap)')
388 369
389 def operational_args_type(value): 370 def operational_args_type(value):
390 with open(value) as fd: 371 with open(value) as fd:
391 return jsonpb.ParseDict(json.load(fd), arguments_pb2.Arguments()) 372 return jsonpb.ParseDict(json.load(fd), arguments_pb2.Arguments())
392 373
393 parser.set_defaults(operational_args=arguments_pb2.Arguments()) 374 parser.set_defaults(operational_args=arguments_pb2.Arguments())
394 375
395 parser.add_argument( 376 parser.add_argument(
396 '--operational-args-path', 377 '--operational-args-path',
397 dest='operational_args', 378 dest='operational_args',
398 type=operational_args_type, 379 type=operational_args_type,
399 help='The path to an operational Arguments file. If provided, this file ' 380 help='The path to an operational Arguments file. If provided, this file '
400 'must contain a JSONPB-encoded Arguments protobuf message, and will ' 381 'must contain a JSONPB-encoded Arguments protobuf message, and will '
401 'be integrated into the runtime parameters.') 382 'be integrated into the runtime parameters.')
402 383
384 def post_process_args(parser, args):
385 if args.command == 'remote':
386 # TODO(iannucci): this is a hack; remote doesn't behave like ANY other
387 # commands. A way to solve this will be to allow --package to take
388 # a remote repo and then simply remove the remote subcommand entirely.
389 return
403 390
404 def post_process_common_args(parser, args): 391 if not args.package:
405 if args.command == "remote": 392 parser.error('%s requires --package' % args.command)
406 # TODO(iannucci): this is a hack; remote doesn't behave like ANY other
407 # commands. A way to solve this will be to allow --package to take a remote
408 # repo and then simply remove the remote subcommand entirely.
409 return
410 393
411 if not args.package: 394 return post_process_args
412 parser.error('%s requires --package' % args.command)
413 395
414 396
415 def main(): 397 def main():
416 parser = argparse.ArgumentParser( 398 parser = argparse.ArgumentParser(
417 description='Interact with the recipe system.') 399 description='Interact with the recipe system.')
418 400
419 add_common_args(parser) 401 common_postprocess_func = add_common_args(parser)
402
403 from recipe_engine import fetch, lint_test, bundle
404 to_add = [fetch, lint_test, bundle]
420 405
421 subp = parser.add_subparsers() 406 subp = parser.add_subparsers()
422 407 for module in to_add:
423 fetch_p = subp.add_parser( 408 module.add_subparser(subp)
424 'fetch',
425 description='Fetch and update dependencies.')
426 fetch_p.set_defaults(command='fetch')
427 409
428 test_p = subp.add_parser( 410 test_p = subp.add_parser(
429 'test', 411 'test',
430 description='Generate or check expectations by simulation') 412 description='Generate or check expectations by simulation')
431 test_p.set_defaults(command='test') 413 test_p.set_defaults(command='test')
432 test_p.add_argument('args', nargs=argparse.REMAINDER) 414 test_p.add_argument('args', nargs=argparse.REMAINDER)
433 415
434 lint_p = subp.add_parser(
435 'lint',
436 description='Check recipes for stylistic and hygenic issues')
437 lint_p.set_defaults(command='lint')
438
439 lint_p.add_argument(
440 '--whitelist', '-w', action='append',
441 help='A regexp matching module names to add to the default whitelist. '
442 'Use multiple times to add multiple patterns,')
443
444 bundle_p = subp.add_parser(
445 'bundle',
446 description=(
447 'Create a hermetically runnable recipe bundle. This captures the result'
448 ' of all network operations the recipe_engine might normally do to'
449 ' bootstrap itself.'))
450 bundle_p.set_defaults(command='bundle')
451 bundle_p.add_argument(
452 '--destination', default='./bundle',
453 type=os.path.abspath,
454 help='The directory of where to put the bundle (default: %(default)r).')
455 416
456 def properties_file_type(filename): 417 def properties_file_type(filename):
457 with (sys.stdin if filename == '-' else open(filename)) as f: 418 with (sys.stdin if filename == '-' else open(filename)) as f:
458 obj = json.load(f) 419 obj = json.load(f)
459 if not isinstance(obj, dict): 420 if not isinstance(obj, dict):
460 raise argparse.ArgumentTypeError( 421 raise argparse.ArgumentTypeError(
461 "must contain a JSON object, i.e. `{}`.") 422 'must contain a JSON object, i.e. `{}`.')
462 return obj 423 return obj
463 424
464 def parse_prop(prop): 425 def parse_prop(prop):
465 key, val = prop.split('=', 1) 426 key, val = prop.split('=', 1)
466 try: 427 try:
467 val = json.loads(val) 428 val = json.loads(val)
468 except (ValueError, SyntaxError): 429 except (ValueError, SyntaxError):
469 pass # If a value couldn't be evaluated, keep the string version 430 pass # If a value couldn't be evaluated, keep the string version
470 return {key: val} 431 return {key: val}
471 432
472 def properties_type(value): 433 def properties_type(value):
473 obj = json.loads(value) 434 obj = json.loads(value)
474 if not isinstance(obj, dict): 435 if not isinstance(obj, dict):
475 raise argparse.ArgumentTypeError("must contain a JSON object, i.e. `{}`.") 436 raise argparse.ArgumentTypeError('must contain a JSON object, i.e. `{}`.')
476 return obj 437 return obj
477 438
478 run_p = subp.add_parser( 439 run_p = subp.add_parser(
479 'run', 440 'run',
480 description='Run a recipe locally') 441 description='Run a recipe locally')
481 run_p.set_defaults(command='run', properties={}) 442 run_p.set_defaults(command='run', properties={})
482 443
483 run_p.add_argument( 444 run_p.add_argument(
484 '--workdir', 445 '--workdir',
485 type=os.path.abspath, 446 type=os.path.abspath,
(...skipping 30 matching lines...) Expand all
516 'recipe', 477 'recipe',
517 help='The recipe to execute') 478 help='The recipe to execute')
518 run_p.add_argument( 479 run_p.add_argument(
519 'props', 480 'props',
520 nargs=argparse.REMAINDER, 481 nargs=argparse.REMAINDER,
521 type=parse_prop, 482 type=parse_prop,
522 help='A list of property pairs; e.g. mastername=chromium.linux ' 483 help='A list of property pairs; e.g. mastername=chromium.linux '
523 'issue=12345. The property value will be decoded as JSON, but if ' 484 'issue=12345. The property value will be decoded as JSON, but if '
524 'this decoding fails the value will be interpreted as a string.') 485 'this decoding fails the value will be interpreted as a string.')
525 486
487
526 remote_p = subp.add_parser( 488 remote_p = subp.add_parser(
527 'remote', 489 'remote',
528 description='Invoke a recipe command from specified repo and revision') 490 description='Invoke a recipe command from specified repo and revision')
529 remote_p.set_defaults(command='remote') 491 remote_p.set_defaults(command='remote')
530 remote_p.add_argument( 492 remote_p.add_argument(
531 '--repository', required=True, 493 '--repository', required=True,
532 help='URL of a git repository to fetch') 494 help='URL of a git repository to fetch')
533 remote_p.add_argument( 495 remote_p.add_argument(
534 '--revision', 496 '--revision',
535 help=( 497 help=(
(...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after
597 'doc', 559 'doc',
598 description='List all known modules reachable from the current package, ' 560 description='List all known modules reachable from the current package, '
599 'with their documentation') 561 'with their documentation')
600 doc_p.add_argument('recipe', nargs='?', 562 doc_p.add_argument('recipe', nargs='?',
601 help='Restrict documentation to this recipe') 563 help='Restrict documentation to this recipe')
602 doc_p.add_argument('--kind', default='jsonpb', choices=doc_kinds, 564 doc_p.add_argument('--kind', default='jsonpb', choices=doc_kinds,
603 help='Output this kind of documentation') 565 help='Output this kind of documentation')
604 doc_p.set_defaults(command='doc') 566 doc_p.set_defaults(command='doc')
605 567
606 args = parser.parse_args() 568 args = parser.parse_args()
607 post_process_common_args(parser, args) 569 common_postprocess_func(parser, args)
570 if hasattr(args, 'postprocess_func'):
dnj 2017/04/27 16:33:53 ew ... maybe ABC / classes are actually the way to
571 args.postprocess_func(parser, args)
608 572
609 # TODO(iannucci): We should always do logging.basicConfig() (probably with 573 # TODO(iannucci): We should always do logging.basicConfig() (probably with
610 # logging.WARNING), even if no verbose is passed. However we need to be 574 # logging.WARNING), even if no verbose is passed. However we need to be
611 # careful as this could cause issues with spurious/unexpected output. I think 575 # careful as this could cause issues with spurious/unexpected output. I think
612 # it's risky enough to do in a different CL. 576 # it's risky enough to do in a different CL.
613 577
614 if args.verbose > 0: 578 if args.verbose > 0:
615 logging.basicConfig() 579 logging.basicConfig()
616 logging.getLogger().setLevel(logging.INFO) 580 logging.getLogger().setLevel(logging.INFO)
617 if args.verbose > 1: 581 if args.verbose > 1:
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after
711 # to automatically find a consistent state, rather than bailing out. 675 # to automatically find a consistent state, rather than bailing out.
712 # Especially that only some subcommands refer to package_deps. 676 # Especially that only some subcommands refer to package_deps.
713 package_deps = package.PackageDeps.create( 677 package_deps = package.PackageDeps.create(
714 repo_root, config_file, allow_fetch=not args.no_fetch, 678 repo_root, config_file, allow_fetch=not args.no_fetch,
715 deps_path=args.deps_path, overrides=args.project_override) 679 deps_path=args.deps_path, overrides=args.project_override)
716 except subprocess.CalledProcessError: 680 except subprocess.CalledProcessError:
717 # A git checkout failed somewhere. Return 2, which is the sign that this is 681 # A git checkout failed somewhere. Return 2, which is the sign that this is
718 # an infra failure, rather than a test failure. 682 # an infra failure, rather than a test failure.
719 return 2 683 return 2
720 684
721 if args.command == 'fetch': 685 if hasattr(args, 'func'):
722 # We already did everything in the create() call above. 686 return args.func(package_deps, args)
723 assert not args.no_fetch, 'Fetch? No-fetch? Make up your mind!' 687
724 return 0 688 if args.command == 'test':
725 elif args.command == 'test':
726 return test(config_file, package_deps, args) 689 return test(config_file, package_deps, args)
727 elif args.command == 'bundle':
728 return bundle(config_file, package_deps, args)
729 elif args.command == 'lint':
730 return lint(config_file, package_deps, args)
731 elif args.command == 'run': 690 elif args.command == 'run':
732 return run(config_file, package_deps, args) 691 return run(config_file, package_deps, args)
733 elif args.command == 'autoroll': 692 elif args.command == 'autoroll':
734 return autoroll(repo_root, config_file, args) 693 return autoroll(repo_root, config_file, args)
735 elif args.command == 'depgraph': 694 elif args.command == 'depgraph':
736 return depgraph(config_file, package_deps, args) 695 return depgraph(config_file, package_deps, args)
737 elif args.command == 'refs': 696 elif args.command == 'refs':
738 return refs(config_file, package_deps, args) 697 return refs(config_file, package_deps, args)
739 elif args.command == 'doc': 698 elif args.command == 'doc':
740 return doc(config_file, package_deps, args) 699 return doc(config_file, package_deps, args)
(...skipping 29 matching lines...) Expand all
770 729
771 if not isinstance(ret, int): 730 if not isinstance(ret, int):
772 if ret is None: 731 if ret is None:
773 ret = 0 732 ret = 0
774 else: 733 else:
775 print >> sys.stderr, ret 734 print >> sys.stderr, ret
776 ret = 1 735 ret = 1
777 sys.stdout.flush() 736 sys.stdout.flush()
778 sys.stderr.flush() 737 sys.stderr.flush()
779 os._exit(ret) 738 os._exit(ret)
OLDNEW
« recipe_engine/bundle.py ('K') | « recipe_engine/lint_test.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698