update docopt to 0.6.2

This commit is contained in:
Radovan Bast 2016-04-18 22:08:51 +02:00
parent 95042c02ba
commit 7e6aeca792

View File

@ -11,7 +11,7 @@ import re
__all__ = ['docopt'] __all__ = ['docopt']
__version__ = '0.6.1' __version__ = '0.6.2'
class DocoptLanguageError(Exception): class DocoptLanguageError(Exception):
@ -47,18 +47,18 @@ class Pattern(object):
if not hasattr(self, 'children'): if not hasattr(self, 'children'):
return self return self
uniq = list(set(self.flat())) if uniq is None else uniq uniq = list(set(self.flat())) if uniq is None else uniq
for i, child in enumerate(self.children): for i, c in enumerate(self.children):
if not hasattr(child, 'children'): if not hasattr(c, 'children'):
assert child in uniq assert c in uniq
self.children[i] = uniq[uniq.index(child)] self.children[i] = uniq[uniq.index(c)]
else: else:
child.fix_identities(uniq) c.fix_identities(uniq)
def fix_repeating_arguments(self): def fix_repeating_arguments(self):
"""Fix elements that should accumulate/increment values.""" """Fix elements that should accumulate/increment values."""
either = [list(child.children) for child in transform(self).children] either = [list(c.children) for c in self.either.children]
for case in either: for case in either:
for e in [child for child in case if case.count(child) > 1]: for e in [c for c in case if case.count(c) > 1]:
if type(e) is Argument or type(e) is Option and e.argcount: if type(e) is Argument or type(e) is Option and e.argcount:
if e.value is None: if e.value is None:
e.value = [] e.value = []
@ -68,40 +68,47 @@ class Pattern(object):
e.value = 0 e.value = 0
return self return self
@property
def transform(pattern): def either(self):
"""Expand pattern into an (almost) equivalent one, but with single Either. """Transform pattern into an equivalent, with only top-level Either."""
# Currently the pattern will not be equivalent, but more "narrow",
Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d) # although good enough to reason about list arguments.
Quirks: [-a] => (-a), (-a...) => (-a -a) ret = []
groups = [[self]]
"""
result = []
groups = [[pattern]]
while groups: while groups:
children = groups.pop(0) children = groups.pop(0)
parents = [Required, Optional, OptionsShortcut, Either, OneOrMore] types = [type(c) for c in children]
if any(t in map(type, children) for t in parents): if Either in types:
child = [c for c in children if type(c) in parents][0] either = [c for c in children if type(c) is Either][0]
children.remove(child) children.pop(children.index(either))
if type(child) is Either: for c in either.children:
for c in child.children:
groups.append([c] + children) groups.append([c] + children)
elif type(child) is OneOrMore: elif Required in types:
groups.append(child.children * 2 + children) 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 AnyOptions in types:
optional = [c for c in children if type(c) is AnyOptions][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: else:
groups.append(child.children + children) ret.append(children)
else: return Either(*[Required(*e) for e in ret])
result.append(children)
return Either(*[Required(*e) for e in result])
class LeafPattern(Pattern): class ChildPattern(Pattern):
"""Leaf/terminal node of a pattern tree."""
def __init__(self, name, value=None): def __init__(self, name, value=None):
self.name, self.value = name, value self.name = name
self.value = value
def __repr__(self): def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value) return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value)
@ -130,9 +137,7 @@ class LeafPattern(Pattern):
return True, left_, collected + [match] return True, left_, collected + [match]
class BranchPattern(Pattern): class ParentPattern(Pattern):
"""Branch/inner node of a pattern tree."""
def __init__(self, *children): def __init__(self, *children):
self.children = list(children) self.children = list(children)
@ -144,15 +149,15 @@ class BranchPattern(Pattern):
def flat(self, *types): def flat(self, *types):
if type(self) in types: if type(self) in types:
return [self] return [self]
return sum([child.flat(*types) for child in self.children], []) return sum([c.flat(*types) for c in self.children], [])
class Argument(LeafPattern): class Argument(ChildPattern):
def single_match(self, left): def single_match(self, left):
for n, pattern in enumerate(left): for n, p in enumerate(left):
if type(pattern) is Argument: if type(p) is Argument:
return n, Argument(self.name, pattern.value) return n, Argument(self.name, p.value)
return None, None return None, None
@classmethod @classmethod
@ -165,23 +170,25 @@ class Argument(LeafPattern):
class Command(Argument): class Command(Argument):
def __init__(self, name, value=False): def __init__(self, name, value=False):
self.name, self.value = name, value self.name = name
self.value = value
def single_match(self, left): def single_match(self, left):
for n, pattern in enumerate(left): for n, p in enumerate(left):
if type(pattern) is Argument: if type(p) is Argument:
if pattern.value == self.name: if p.value == self.name:
return n, Command(self.name, True) return n, Command(self.name, True)
else: else:
break break
return None, None return None, None
class Option(LeafPattern): class Option(ChildPattern):
def __init__(self, short=None, long=None, argcount=0, value=False): def __init__(self, short=None, long=None, argcount=0, value=False):
assert argcount in (0, 1) assert argcount in (0, 1)
self.short, self.long, self.argcount = short, long, argcount self.short, self.long = short, long
self.argcount, self.value = argcount, value
self.value = None if value is False and argcount else value self.value = None if value is False and argcount else value
@classmethod @classmethod
@ -202,9 +209,9 @@ class Option(LeafPattern):
return class_(short, long, argcount, value) return class_(short, long, argcount, value)
def single_match(self, left): def single_match(self, left):
for n, pattern in enumerate(left): for n, p in enumerate(left):
if self.name == pattern.name: if self.name == p.name:
return n, pattern return n, p
return None, None return None, None
@property @property
@ -216,34 +223,34 @@ class Option(LeafPattern):
self.argcount, self.value) self.argcount, self.value)
class Required(BranchPattern): class Required(ParentPattern):
def match(self, left, collected=None): def match(self, left, collected=None):
collected = [] if collected is None else collected collected = [] if collected is None else collected
l = left l = left
c = collected c = collected
for pattern in self.children: for p in self.children:
matched, l, c = pattern.match(l, c) matched, l, c = p.match(l, c)
if not matched: if not matched:
return False, left, collected return False, left, collected
return True, l, c return True, l, c
class Optional(BranchPattern): class Optional(ParentPattern):
def match(self, left, collected=None): def match(self, left, collected=None):
collected = [] if collected is None else collected collected = [] if collected is None else collected
for pattern in self.children: for p in self.children:
m, left, collected = pattern.match(left, collected) m, left, collected = p.match(left, collected)
return True, left, collected return True, left, collected
class OptionsShortcut(Optional): class AnyOptions(Optional):
"""Marker/placeholder for [options] shortcut.""" """Marker/placeholder for [options] shortcut."""
class OneOrMore(BranchPattern): class OneOrMore(ParentPattern):
def match(self, left, collected=None): def match(self, left, collected=None):
assert len(self.children) == 1 assert len(self.children) == 1
@ -265,13 +272,13 @@ class OneOrMore(BranchPattern):
return False, left, collected return False, left, collected
class Either(BranchPattern): class Either(ParentPattern):
def match(self, left, collected=None): def match(self, left, collected=None):
collected = [] if collected is None else collected collected = [] if collected is None else collected
outcomes = [] outcomes = []
for pattern in self.children: for p in self.children:
matched, _, _ = outcome = pattern.match(left, collected) matched, _, _ = outcome = p.match(left, collected)
if matched: if matched:
outcomes.append(outcome) outcomes.append(outcome)
if outcomes: if outcomes:
@ -279,18 +286,12 @@ class Either(BranchPattern):
return False, left, collected return False, left, collected
class Tokens(list): class TokenStream(list):
def __init__(self, source, error=DocoptExit): def __init__(self, source, error):
self += source.split() if hasattr(source, 'split') else source self += source.split() if hasattr(source, 'split') else source
self.error = error self.error = error
@staticmethod
def from_pattern(source):
source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source)
source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s]
return Tokens(source, error=DocoptLanguageError)
def move(self): def move(self):
return self.pop(0) if len(self) else None return self.pop(0) if len(self) else None
@ -323,7 +324,7 @@ def parse_long(tokens, options):
raise tokens.error('%s must not have an argument' % o.long) raise tokens.error('%s must not have an argument' % o.long)
else: else:
if value is None: if value is None:
if tokens.current() in [None, '--']: if tokens.current() is None:
raise tokens.error('%s requires argument' % o.long) raise tokens.error('%s requires argument' % o.long)
value = tokens.move() value = tokens.move()
if tokens.error is DocoptExit: if tokens.error is DocoptExit:
@ -354,7 +355,7 @@ def parse_shorts(tokens, options):
value = None value = None
if o.argcount != 0: if o.argcount != 0:
if left == '': if left == '':
if tokens.current() in [None, '--']: if tokens.current() is None:
raise tokens.error('%s requires argument' % short) raise tokens.error('%s requires argument' % short)
value = tokens.move() value = tokens.move()
else: else:
@ -367,7 +368,8 @@ def parse_shorts(tokens, options):
def parse_pattern(source, options): def parse_pattern(source, options):
tokens = Tokens.from_pattern(source) tokens = TokenStream(re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source),
DocoptLanguageError)
result = parse_expr(tokens, options) result = parse_expr(tokens, options)
if tokens.current() is not None: if tokens.current() is not None:
raise tokens.error('unexpected ending: %r' % ' '.join(tokens)) raise tokens.error('unexpected ending: %r' % ' '.join(tokens))
@ -414,7 +416,7 @@ def parse_atom(tokens, options):
return [result] return [result]
elif token == 'options': elif token == 'options':
tokens.move() tokens.move()
return [OptionsShortcut()] return [AnyOptions()]
elif token.startswith('--') and token != '--': elif token.startswith('--') and token != '--':
return parse_long(tokens, options) return parse_long(tokens, options)
elif token.startswith('-') and token not in ('-', '--'): elif token.startswith('-') and token not in ('-', '--'):
@ -450,26 +452,27 @@ def parse_argv(tokens, options, options_first=False):
def parse_defaults(doc): def parse_defaults(doc):
defaults = [] # in python < 2.7 you can't pass flags=re.MULTILINE
for s in parse_section('options:', doc): split = re.split('\n *(<\S+?>|-\S+?)', doc)[1:]
# FIXME corner case "bla: options: --foo"
_, _, s = s.partition(':') # get rid of "options:"
split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:]
split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])] split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])]
options = [Option.parse(s) for s in split if s.startswith('-')] options = [Option.parse(s) for s in split if s.startswith('-')]
defaults += options #arguments = [Argument.parse(s) for s in split if s.startswith('<')]
return defaults #return options, arguments
return options
def parse_section(name, source): def printable_usage(doc):
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)', # in python < 2.7 you can't pass flags=re.IGNORECASE
re.IGNORECASE | re.MULTILINE) usage_split = re.split(r'([Uu][Ss][Aa][Gg][Ee]:)', doc)
return [s.strip() for s in pattern.findall(source)] 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(section): def formal_usage(printable_usage):
_, _, section = section.partition(':') # drop "usage:" pu = printable_usage.split()[1:] # split and drop "usage:"
pu = section.split()
return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )' return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )'
@ -509,7 +512,7 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False):
If passed, the object will be printed if --version is in If passed, the object will be printed if --version is in
`argv`. `argv`.
options_first : bool (default: False) options_first : bool (default: False)
Set to True to require options precede positional arguments, Set to True to require options preceed positional arguments,
i.e. to forbid options and positional arguments intermix. i.e. to forbid options and positional arguments intermix.
Returns Returns
@ -523,15 +526,15 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False):
------- -------
>>> from docopt import docopt >>> from docopt import docopt
>>> doc = ''' >>> doc = '''
... Usage: Usage:
... my_program tcp <host> <port> [--timeout=<seconds>] my_program tcp <host> <port> [--timeout=<seconds>]
... my_program serial <port> [--baud=<n>] [--timeout=<seconds>] my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
... my_program (-h | --help | --version) my_program (-h | --help | --version)
...
... Options: Options:
... -h, --help Show this screen and exit. -h, --help Show this screen and exit.
... --baud=<n> Baudrate [default: 9600] --baud=<n> Baudrate [default: 9600]
... ''' '''
>>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30'] >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
>>> docopt(doc, argv) >>> docopt(doc, argv)
{'--baud': '9600', {'--baud': '9600',
@ -550,15 +553,9 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False):
at https://github.com/docopt/docopt#readme at https://github.com/docopt/docopt#readme
""" """
argv = sys.argv[1:] if argv is None else argv if argv is None:
argv = sys.argv[1:]
usage_sections = parse_section('usage:', doc) DocoptExit.usage = printable_usage(doc)
if len(usage_sections) == 0:
raise DocoptLanguageError('"usage:" (case-insensitive) not found.')
if len(usage_sections) > 1:
raise DocoptLanguageError('More than one "usage:" (case-insensitive).')
DocoptExit.usage = usage_sections[0]
options = parse_defaults(doc) options = parse_defaults(doc)
pattern = parse_pattern(formal_usage(DocoptExit.usage), options) pattern = parse_pattern(formal_usage(DocoptExit.usage), options)
# [default] syntax for argument is disabled # [default] syntax for argument is disabled
@ -566,13 +563,14 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False):
# same_name = [d for d in arguments if d.name == a.name] # same_name = [d for d in arguments if d.name == a.name]
# if same_name: # if same_name:
# a.value = same_name[0].value # a.value = same_name[0].value
argv = parse_argv(Tokens(argv), list(options), options_first) argv = parse_argv(TokenStream(argv, DocoptExit), list(options),
options_first)
pattern_options = set(pattern.flat(Option)) pattern_options = set(pattern.flat(Option))
for options_shortcut in pattern.flat(OptionsShortcut): for ao in pattern.flat(AnyOptions):
doc_options = parse_defaults(doc) doc_options = parse_defaults(doc)
options_shortcut.children = list(set(doc_options) - pattern_options) ao.children = list(set(doc_options) - pattern_options)
#if any_options: #if any_options:
# options_shortcut.children += [Option(o.short, o.long, o.argcount) # ao.children += [Option(o.short, o.long, o.argcount)
# for o in argv if type(o) is Option] # for o in argv if type(o) is Option]
extras(help, version, argv, doc) extras(help, version, argv, doc)
matched, left, collected = pattern.fix().match(argv) matched, left, collected = pattern.fix().match(argv)