summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@codethink.co.uk>2015-04-29 20:51:04 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2015-05-11 13:07:46 +0000
commit812492ddd2f8c172676bfff72a756271f7472b4e (patch)
treef8f9cc7243ae985e497e0bd438d6b60a34eb0b4b
parentd22a4a27b77e229207d43d8040f2168ae30e64f5 (diff)
downloadmorph-812492ddd2f8c172676bfff72a756271f7472b4e.tar.gz
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
-rw-r--r--morphlib/__init__.py1
-rw-r--r--morphlib/cmdline_parse_utils.py141
-rw-r--r--without-test-modules1
3 files changed, 143 insertions, 0 deletions
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 <http://www.gnu.org/licenses/>.
+
+
+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