summaryrefslogtreecommitdiffstats
path: root/port_for/docopt.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 05:38:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 05:38:14 +0000
commit9e256557e44c09aed7da1420bbd066d434a10951 (patch)
tree7c040c7b4dc2e6205d6003fabcda874acd551025 /port_for/docopt.py
parentInitial commit. (diff)
downloadport-for-9e256557e44c09aed7da1420bbd066d434a10951.tar.xz
port-for-9e256557e44c09aed7da1420bbd066d434a10951.zip
Adding upstream version 0.7.1.upstream/0.7.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'port_for/docopt.py')
-rw-r--r--port_for/docopt.py471
1 files changed, 471 insertions, 0 deletions
diff --git a/port_for/docopt.py b/port_for/docopt.py
new file mode 100644
index 0000000..eff4e2f
--- /dev/null
+++ b/port_for/docopt.py
@@ -0,0 +1,471 @@
+from copy import copy
+import sys
+import re
+
+
+class DocoptLanguageError(Exception):
+ """Error in construction of usage-message by developer."""
+
+
+class DocoptExit(SystemExit):
+ """Exit in case user invoked program with incorrect arguments."""
+
+ usage = ""
+
+ def __init__(self, message=""):
+ SystemExit.__init__(self, (message + "\n" + self.usage).strip())
+
+
+class Pattern(object):
+ def __init__(self, *children):
+ self.children = list(children)
+
+ def __eq__(self, other):
+ return repr(self) == repr(other)
+
+ def __hash__(self):
+ return hash(repr(self))
+
+ def __repr__(self):
+ return "%s(%s)" % (
+ self.__class__.__name__,
+ ", ".join(repr(a) for a in self.children),
+ )
+
+ @property
+ def flat(self):
+ if not hasattr(self, "children"):
+ return [self]
+ return sum([c.flat for c in self.children], [])
+
+ def fix(self):
+ self.fix_identities()
+ self.fix_list_arguments()
+ return self
+
+ def fix_identities(self, uniq=None):
+ """Make pattern-tree tips point to same object if they are equal."""
+ if not hasattr(self, "children"):
+ return self
+ uniq = list(set(self.flat)) if uniq is None else uniq
+ for i, c in enumerate(self.children):
+ if not hasattr(c, "children"):
+ assert c in uniq
+ self.children[i] = uniq[uniq.index(c)]
+ else:
+ c.fix_identities(uniq)
+
+ def fix_list_arguments(self):
+ """Find arguments that should accumulate values and fix them."""
+ either = [list(c.children) for c in self.either.children]
+ for case in either:
+ case = [c for c in case if case.count(c) > 1]
+ for a in [e for e in case if type(e) == Argument]:
+ a.value = []
+ return self
+
+ @property
+ def either(self):
+ """Transform pattern into an equivalent, with only top-level Either."""
+ # Currently the pattern will not be equivalent, but more "narrow",
+ # although good enough to reason about list arguments.
+ if not hasattr(self, "children"):
+ return Either(Required(self))
+ else:
+ ret = []
+ groups = [[self]]
+ while groups:
+ children = groups.pop(0)
+ types = [type(c) for c in children]
+ if Either in types:
+ either = [c for c in children if type(c) is Either][0]
+ children.pop(children.index(either))
+ for c in either.children:
+ groups.append([c] + children)
+ elif Required in types:
+ required = [c for c in children if type(c) is Required][0]
+ children.pop(children.index(required))
+ groups.append(list(required.children) + children)
+ elif Optional in types:
+ optional = [c for c in children if type(c) is Optional][0]
+ children.pop(children.index(optional))
+ groups.append(list(optional.children) + children)
+ elif OneOrMore in types:
+ oneormore = [c for c in children if type(c) is OneOrMore][0]
+ children.pop(children.index(oneormore))
+ groups.append(list(oneormore.children) * 2 + children)
+ else:
+ ret.append(children)
+ return Either(*[Required(*e) for e in ret])
+
+
+class Argument(Pattern):
+ def __init__(self, name, value=None):
+ self.name = name
+ self.value = value
+
+ def match(self, left, collected=None):
+ collected = [] if collected is None else collected
+ args = [arg_left for arg_left in left if type(arg_left) is Argument]
+ if not len(args):
+ return False, left, collected
+ left.remove(args[0])
+ if type(self.value) is not list:
+ return True, left, collected + [Argument(self.name, args[0].value)]
+ same_name = [
+ a for a in collected if type(a) is Argument and a.name == self.name
+ ]
+ if len(same_name):
+ same_name[0].value += [args[0].value]
+ return True, left, collected
+ else:
+ return (
+ True,
+ left,
+ collected + [Argument(self.name, [args[0].value])],
+ )
+
+ def __repr__(self):
+ return "Argument(%r, %r)" % (self.name, self.value)
+
+
+class Command(Pattern):
+ def __init__(self, name, value=False):
+ self.name = name
+ self.value = value
+
+ def match(self, left, collected=None):
+ collected = [] if collected is None else collected
+ args = [arg_left for arg_left in left if type(arg_left) is Argument]
+ if not len(args) or args[0].value != self.name:
+ return False, left, collected
+ left.remove(args[0])
+ return True, left, collected + [Command(self.name, True)]
+
+ def __repr__(self):
+ return "Command(%r, %r)" % (self.name, self.value)
+
+
+class Option(Pattern):
+ def __init__(self, short=None, long=None, argcount=0, value=False):
+ assert argcount in (0, 1)
+ self.short, self.long = short, long
+ self.argcount, self.value = argcount, value
+ self.value = None if not value and argcount else value # HACK
+
+ @classmethod
+ def parse(class_, option_description):
+ short, long, argcount, value = None, None, 0, False
+ options, _, description = option_description.strip().partition(" ")
+ options = options.replace(",", " ").replace("=", " ")
+ for s in options.split():
+ if s.startswith("--"):
+ long = s
+ elif s.startswith("-"):
+ short = s
+ else:
+ argcount = 1
+ if argcount:
+ matched = re.findall(r"\[default: (.*)\]", description, flags=re.I)
+ value = matched[0] if matched else None
+ return class_(short, long, argcount, value)
+
+ def match(self, left, collected=None):
+ collected = [] if collected is None else collected
+ left_ = []
+ for arg_left in left:
+ # if this is so greedy, how to handle OneOrMore then?
+ if not (
+ type(arg_left) is Option
+ and (self.short, self.long) == (arg_left.short, arg_left.long)
+ ):
+ left_.append(arg_left)
+ return (left != left_), left_, collected
+
+ @property
+ def name(self):
+ return self.long or self.short
+
+ def __repr__(self):
+ return "Option(%r, %r, %r, %r)" % (
+ self.short,
+ self.long,
+ self.argcount,
+ self.value,
+ )
+
+
+class AnyOptions(Pattern):
+ def match(self, left, collected=None):
+ collected = [] if collected is None else collected
+ left_ = [opt_left for opt_left in left if not type(opt_left) == Option]
+ return (left != left_), left_, collected
+
+
+class Required(Pattern):
+ def match(self, left, collected=None):
+ collected = [] if collected is None else collected
+ copied_left = copy(left)
+ c = copy(collected)
+ for p in self.children:
+ matched, copied_left, c = p.match(copied_left, c)
+ if not matched:
+ return False, left, collected
+ return True, copied_left, c
+
+
+class Optional(Pattern):
+ def match(self, left, collected=None):
+ collected = [] if collected is None else collected
+ left = copy(left)
+ for p in self.children:
+ m, left, collected = p.match(left, collected)
+ return True, left, collected
+
+
+class OneOrMore(Pattern):
+ def match(self, left, collected=None):
+ assert len(self.children) == 1
+ collected = [] if collected is None else collected
+ pattern_left = copy(left)
+ c = copy(collected)
+ l_ = None
+ matched = True
+ times = 0
+ while matched:
+ # could it be that something didn't match but
+ # changed pattern_left or c?
+ matched, pattern_left, c = self.children[0].match(pattern_left, c)
+ times += 1 if matched else 0
+ if l_ == pattern_left:
+ break
+ l_ = copy(pattern_left)
+ if times >= 1:
+ return True, pattern_left, c
+ return False, left, collected
+
+
+class Either(Pattern):
+ def match(self, left, collected=None):
+ collected = [] if collected is None else collected
+ outcomes = []
+ for p in self.children:
+ matched, _, _ = outcome = p.match(copy(left), copy(collected))
+ if matched:
+ outcomes.append(outcome)
+ if outcomes:
+ return min(outcomes, key=lambda outcome: len(outcome[1]))
+ return False, left, collected
+
+
+class TokenStream(list):
+ def __init__(self, source, error):
+ self += source.split() if type(source) is str else source
+ self.error = error
+
+ def move(self):
+ return self.pop(0) if len(self) else None
+
+ def current(self):
+ return self[0] if len(self) else None
+
+
+def parse_long(tokens, options):
+ raw, eq, value = tokens.move().partition("=")
+ value = None if eq == value == "" else value
+ opt = [o for o in options if o.long and o.long.startswith(raw)]
+ if len(opt) < 1:
+ if tokens.error is DocoptExit:
+ raise tokens.error("%s is not recognized" % raw)
+ else:
+ o = Option(None, raw, (1 if eq == "=" else 0))
+ options.append(o)
+ return [o]
+ if len(opt) > 1:
+ raise tokens.error(
+ "%s is not a unique prefix: %s?"
+ % (raw, ", ".join("%s" % o.long for o in opt))
+ )
+ opt = copy(opt[0])
+ if opt.argcount == 1:
+ if value is None:
+ if tokens.current() is None:
+ raise tokens.error("%s requires argument" % opt.name)
+ value = tokens.move()
+ elif value is not None:
+ raise tokens.error("%s must not have an argument" % opt.name)
+ opt.value = value or True
+ return [opt]
+
+
+def parse_shorts(tokens, options):
+ raw = tokens.move()[1:]
+ parsed = []
+ while raw != "":
+ opt = [
+ o
+ for o in options
+ if o.short and o.short.lstrip("-").startswith(raw[0])
+ ]
+ if len(opt) > 1:
+ raise tokens.error(
+ "-%s is specified ambiguously %d times" % (raw[0], len(opt))
+ )
+ if len(opt) < 1:
+ if tokens.error is DocoptExit:
+ raise tokens.error("-%s is not recognized" % raw[0])
+ else:
+ o = Option("-" + raw[0], None)
+ options.append(o)
+ parsed.append(o)
+ raw = raw[1:]
+ continue
+ opt = copy(opt[0])
+ raw = raw[1:]
+ if opt.argcount == 0:
+ value = True
+ else:
+ if raw == "":
+ if tokens.current() is None:
+ raise tokens.error("-%s requires argument" % opt.short[0])
+ raw = tokens.move()
+ value, raw = raw, ""
+ opt.value = value
+ parsed.append(opt)
+ return parsed
+
+
+def parse_pattern(source, options):
+ tokens = TokenStream(
+ re.sub(r"([\[\]\(\)\|]|\.\.\.)", r" \1 ", source), DocoptLanguageError
+ )
+ result = parse_expr(tokens, options)
+ if tokens.current() is not None:
+ raise tokens.error("unexpected ending: %r" % " ".join(tokens))
+ return Required(*result)
+
+
+def parse_expr(tokens, options):
+ """expr ::= seq ( '|' seq )* ;"""
+ seq = parse_seq(tokens, options)
+ if tokens.current() != "|":
+ return seq
+ result = [Required(*seq)] if len(seq) > 1 else seq
+ while tokens.current() == "|":
+ tokens.move()
+ seq = parse_seq(tokens, options)
+ result += [Required(*seq)] if len(seq) > 1 else seq
+ return [Either(*result)] if len(result) > 1 else result
+
+
+def parse_seq(tokens, options):
+ """seq ::= ( atom [ '...' ] )* ;"""
+ result = []
+ while tokens.current() not in [None, "]", ")", "|"]:
+ atom = parse_atom(tokens, options)
+ if tokens.current() == "...":
+ atom = [OneOrMore(*atom)]
+ tokens.move()
+ result += atom
+ return result
+
+
+def parse_atom(tokens, options):
+ """atom ::= '(' expr ')' | '[' expr ']' | 'options'
+ | long | shorts | argument | command ;
+ """
+ token = tokens.current()
+ result = []
+ if token == "(":
+ tokens.move()
+ result = [Required(*parse_expr(tokens, options))]
+ if tokens.move() != ")":
+ raise tokens.error("Unmatched '('")
+ return result
+ elif token == "[":
+ tokens.move()
+ result = [Optional(*parse_expr(tokens, options))]
+ if tokens.move() != "]":
+ raise tokens.error("Unmatched '['")
+ return result
+ elif token == "options":
+ tokens.move()
+ return [AnyOptions()]
+ elif token.startswith("--") and token != "--":
+ return parse_long(tokens, options)
+ elif token.startswith("-") and token not in ("-", "--"):
+ return parse_shorts(tokens, options)
+ elif token.startswith("<") and token.endswith(">") or token.isupper():
+ return [Argument(tokens.move())]
+ else:
+ return [Command(tokens.move())]
+
+
+def parse_args(source, options):
+ tokens = TokenStream(source, DocoptExit)
+ options = copy(options)
+ parsed = []
+ while tokens.current() is not None:
+ if tokens.current() == "--":
+ return parsed + [Argument(None, v) for v in tokens]
+ elif tokens.current().startswith("--"):
+ parsed += parse_long(tokens, options)
+ elif tokens.current().startswith("-") and tokens.current() != "-":
+ parsed += parse_shorts(tokens, options)
+ else:
+ parsed.append(Argument(None, tokens.move()))
+ return parsed
+
+
+def parse_doc_options(doc):
+ return [Option.parse("-" + s) for s in re.split("^ *-|\n *-", doc)[1:]]
+
+
+def printable_usage(doc):
+ usage_split = re.split(r"([Uu][Ss][Aa][Gg][Ee]:)", doc)
+ if len(usage_split) < 3:
+ raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
+ if len(usage_split) > 3:
+ raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
+ return re.split(r"\n\s*\n", "".join(usage_split[1:]))[0].strip()
+
+
+def formal_usage(printable_usage):
+ pu = printable_usage.split()[1:] # split and drop "usage:"
+ return " ".join("|" if s == pu[0] else s for s in pu[1:])
+
+
+def extras(help, version, options, doc):
+ if help and any((o.name in ("-h", "--help")) and o.value for o in options):
+ print(doc.strip())
+ exit()
+ if version and any(o.name == "--version" and o.value for o in options):
+ print(version)
+ exit()
+
+
+class Dict(dict):
+ """Dictionary with custom repr bbehaviour."""
+
+ def __repr__(self):
+ """Dictionary representation for docopt."""
+ return "{%s}" % ",\n ".join("%r: %r" % i for i in sorted(self.items()))
+
+
+def docopt(doc, argv=sys.argv[1:], help=True, version=None):
+ DocoptExit.usage = docopt.usage = usage = printable_usage(doc)
+ pot_options = parse_doc_options(doc)
+ formal_pattern = parse_pattern(formal_usage(usage), options=pot_options)
+ argv = parse_args(argv, options=pot_options)
+ extras(help, version, argv, doc)
+ matched, left, arguments = formal_pattern.fix().match(argv)
+ if matched and left == []: # better message if left?
+ options = [o for o in argv if type(o) is Option]
+ pot_arguments = [
+ a for a in formal_pattern.flat if type(a) in [Argument, Command]
+ ]
+ return Dict(
+ (a.name, a.value)
+ for a in (pot_options + options + pot_arguments + arguments)
+ )
+ raise DocoptExit()