From 812492ddd2f8c172676bfff72a756271f7472b4e Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 29 Apr 2015 20:51:04 +0000 Subject: morphlib: Add utility module for parsing argv into lists of systems The `morph anchor`, `morph build-morphology` and a potential `morph diff` command would all benefit from having a unified way to parse the argv for the systems it must operate on, especially in the case of the potential `morph diff`, which needs to be able to handle being given two sets of systems. `morph anchor` may make use of it now by passing the list of systems to the Source resolver, but `morph build-morphology` would have to iterate over the systems and graph each independently. Change-Id: I91ab4764ffca3aa16f144f89f68f37cc21b9f643 --- morphlib/__init__.py | 1 + morphlib/cmdline_parse_utils.py | 141 ++++++++++++++++++++++++++++++++++++++++ without-test-modules | 1 + 3 files changed, 143 insertions(+) create mode 100644 morphlib/cmdline_parse_utils.py diff --git a/morphlib/__init__.py b/morphlib/__init__.py index 90cc3d80..94b42580 100644 --- a/morphlib/__init__.py +++ b/morphlib/__init__.py @@ -59,6 +59,7 @@ import buildsystem import builder import cachedrepo import cachekeycomputer +import cmdline_parse_utils import extensions import extractedtarball import fsutils diff --git a/morphlib/cmdline_parse_utils.py b/morphlib/cmdline_parse_utils.py new file mode 100644 index 00000000..f995d016 --- /dev/null +++ b/morphlib/cmdline_parse_utils.py @@ -0,0 +1,141 @@ +# -*- coding: utf-8 -*- +# Copyright © 2015 Codethink Limited +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . + + +from cliapp import AppException as _AppException + + +from .util import word_join_list as _word_join_list + + +def _split(iterable, split_token): + sequence = [] + for token in iterable: + if token == split_token: + yield tuple(sequence) + sequence = [] + else: + sequence.append(token) + if sequence: + yield tuple(sequence) + + +definition_list_synopsis = 'REPO REF [PATH]...' + + +def definition_lists_synopsis(sep='-', at_least=0, at_most=None): + one = definition_list_synopsis + res = '{rest}' + + # If we may have none, then we have to mark the whole expression as + # optional, as the trailing separator may be omitted. Otherwise we could + # just list as many `REPO REF [PATH]... -` sequences as necessary. + if at_least == 0: + res = res.format(rest='[{rest}]') + res = res.format(rest=('{one}{{rest}} [{sep}]'.format(one=one, sep=sep))) + + # Insert extra mandatory entries + for i in xrange(at_least - 1): + res = res.format(rest=' {sep} {one}{{rest}}'.format(sep=sep, one=one)) + + # Add a variadic many if we have no maximum + if at_most is None: + res = res.format( + rest=' [{sep} {one}]...{{rest}}'.format(sep=sep, one=one)) + # Otherwise add as many optional entries as needed to reach the maximum + else: + for i in xrange(at_most - 1 - at_least): + res = res.format( + rest=' [{sep} {one}]{{rest}}'.format(sep=sep, one=one)) + + # Terminate synopsis string interpolations + res = res.format(rest='') + + return res + + +class SystemsSpecsParseWrongNumber(_AppException): + def __init__(self, specs, definitions_names): + self.specs = specs + self.definitions_names = definitions_names + msg = 'From expected definition specs {expected};'.format( + expected=_word_join_list(map(repr, definitions_names))) + if len(specs) < len(definitions_names): + missing_spec_names = definitions_names[len(specs):] + super(SystemsSpecsParseWrongNumber, self).__init__( + '{msg} missing {missing}'.format( + msg=msg, + missing=_word_join_list(map(repr, missing_spec_names)))) + else: + super(SystemsSpecsParseWrongNumber, self).__init__( + '{msg} {extra} extra specs given'.format( + msg=msg, + extra=len(specs) - len(definitions_names))) + + +class SystemsSpecsParseWrongFormat(_AppException): + def __init__(self, names, malformed_definition_lists): + self.names = names + self.malformed_definition_lists = malformed_definition_lists + errors = [] + for spec, name, i in malformed_definition_lists: + pre = 'Spec {i} named {name!r}'.format(i=i, name=name) + if not spec: + errors.append('{} is empty, want {}'.format( + pre, definition_list_synopsis)) + elif len(spec) == 1: + errors.append('{} missing REF, want {}'.format( + pre, definition_list_synopsis)) + super(SystemsSpecsParseWrongFormat, self).__init__( + 'From expected definition specs {expected}:\n\t{errors}'.format( + expected=_word_join_list(map(repr, names)), + errors='\n\t'.join(errors))) + + +def parse_definition_lists(args, names, sep='-'): + '''Parse definition lists, raising Exceptions for invalid input. + + Raises a SystemsSpecsParseWrongNumber if the number of definition lists is + not the same as the number of names. + + Raises a SystemsSpecsParseWrongFormat if any of the definition lists is too + short to be valid. + + parse_definition_lists(args=['.', 'HEAD', 'foo.morph', '-', + '../def2', 'HEAD', '-'], + names=('from', 'to')) + --> ('.', 'HEAD', ('foo.morph',)), ('../def2', 'HEAD', ()) + ''' + # NOTE: To extend this to support arbitrary length definition lists, such + # as a list of systems to build, the best approach may be to allow it + # to be passed an infinite generator, used for reporting names, skip + # the size check if it's not an instance of Sized, and adapt the code + # to handle being given an infinite list, by using izip and islice so + # the length of the args sequence is used instead. + specs = tuple(_split(args, sep)) + if len(specs) != len(names): + raise SystemsSpecsParseWrongNumber(specs, names) + + malformed_definition_lists = [] + specinfo = enumerate(zip(names, specs), start=1) + for i, (definition_list_name, definitions_spec) in specinfo: + if len(definitions_spec) < 2: + malformed_definition_lists.append( + (definitions_spec, definition_list_name, i)) + if malformed_definition_lists: + raise SystemsSpecsParseWrongFormat(names, + malformed_definition_lists) + + return ((spec[0], spec[1], spec[2:]) for spec in specs) diff --git a/without-test-modules b/without-test-modules index fb8bca10..1ad9503c 100644 --- a/without-test-modules +++ b/without-test-modules @@ -24,6 +24,7 @@ morphlib/plugins/expand_repo_plugin.py morphlib/plugins/deploy_plugin.py morphlib/plugins/__init__.py morphlib/writeexts.py +morphlib/cmdline_parse_utils.py morphlib/plugins/list_artifacts_plugin.py morphlib/plugins/trovectl_plugin.py morphlib/plugins/get_chunk_details_plugin.py -- cgit v1.2.1