Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add support for argument defaults #37

Open
wants to merge 1 commit into from

3 participants

@Met48

Argument descriptions will be recognized as any line that starts with an argument.
It will be ignored if there is not a [default: ] section in the description. Multi-line descriptions are supported.

This commit fixes issue #36. It also disables option parsing within any usage: section.

@Met48 Met48 Add support for argument defaults, fix issue #36.
Argument defaults will match on any non-usage line
that starts with  <.*>  or  [A-Z]+

Supports multi-line descriptions.
a737b65
@keleshev
Owner

Sorry, I was busy the whole week. I will review your code now.

I wonder if it is possible to implement the same in less code... I'm very afraid of docopt growing.

@Met48

The code volume could be decreased, although there's a few reasons for the additional code:

  1. It has to ignore anything in a usage: section. This took ~15 lines.
  2. It now supports tab characters for indentation and description separation. I omitted this from the commit message by mistake, but it is there and underwent local testing (no test cases added). This adds another ~12 lines since partition could no longer be used
  3. It moves the common parse code outside of the parse method. This actually saves a few lines in both parse methods but increases the apparent diff size

EDIT: Edited comment a bit for clarity.

@ghost Unknown referenced this pull request
Closed

Add support for enumerations #45

@keleshev
Owner

Support for this is upcoming in 0.6.0, and is now in a branch:

https://github.com/docopt/docopt/tree/args-defaults

Sorry @Met48 for not including your code—I want docopt to be as small as possible (in terms of LOC), so I came up with my own solution, which is a bit shorter.

Also branch supports only <angular-bracket> style for arguments—I'm thinking of keeping it the only style (maybe). If not, I will add support for default arguments for UPPER-CASE style as well.

@jric

+1 -- Iet me know if I can help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 6, 2012
  1. @Met48

    Add support for argument defaults, fix issue #36.

    Met48 authored
    Argument defaults will match on any non-usage line
    that starts with  <.*>  or  [A-Z]+
    
    Supports multi-line descriptions.
This page is out of date. Refresh to see the latest.
View
103 docopt.py
@@ -112,6 +112,24 @@ def __init__(self, name, value=None):
self.name = name
self.value = value
+ @classmethod
+ def parse(class_, terms, description='', default=None):
+ if default is None:
+ return []
+ args = []
+
+ for i, s in enumerate(terms):
+ if (s.startswith('<') and s.endswith('>')) or s.isupper():
+ args.append(class_(s, default))
+ elif s == '...':
+ continue
+ elif i != 0:
+ raise DocoptLanguageError("Invalid argument name \"%s\"" % s)
+ else:
+ return []
+
+ return args
+
def match(self, left, collected=None):
collected = [] if collected is None else collected
args = [l for l in left if type(l) is Argument]
@@ -162,11 +180,9 @@ def __init__(self, short=None, long=None, argcount=0, value=False):
self.value = None if value == False and argcount else value # HACK
@classmethod
- def parse(class_, option_description):
+ def parse(class_, terms, description='', default=None):
short, long, argcount, value = None, None, 0, False
- options, _, description = option_description.strip().partition(' ')
- options = options.replace(',', ' ').replace('=', ' ')
- for s in options.split():
+ for s in terms:
if s.startswith('--'):
long = s
elif s.startswith('-'):
@@ -174,9 +190,8 @@ def parse(class_, option_description):
else:
argcount = 1
if argcount:
- matched = re.findall('\[default: (.*)\]', description, flags=re.I)
- value = matched[0] if matched else None
- return class_(short, long, argcount, value)
+ value = default
+ return [class_(short, long, argcount, value)]
def match(self, left, collected=None):
collected = [] if collected is None else collected
@@ -417,8 +432,75 @@ def parse_args(source, options):
return parsed
-def parse_doc_options(doc):
- return [Option.parse('-' + s) for s in re.split('^ *-|\n *-', doc)[1:]]
+def parse_doc_descriptors(doc):
+ options = []
+ arguments = []
+
+ sections = re.split(r'(?mi)(^\s*[a-z]+:|\n\s*(?=\n))', doc)
+
+ #Group headers with content
+ sections = [''] + sections
+ sections = zip(sections[0::2], sections[1::2])
+
+ for header, content in sections:
+ if header.strip().lower() == 'usage:':
+ #Ignore text within a usage section
+ continue
+ else:
+ #Split by option descriptors
+ option_descriptors = re.split(r'(?m)^\s*-', content)
+ option_descriptors = (option_descriptors[0:1]
+ + ['-' + x for x in option_descriptors[1:]])
+
+ #Split each option descriptor by argument descriptors
+ descriptors = []
+ for item in option_descriptors:
+ lines = item.split('\n')
+ descriptor = ['']
+ for line in lines:
+ line = line.lstrip()
+ arg = line.replace(',', ' ').split()
+ if not arg:
+ continue
+ else:
+ arg = arg[0]
+ if arg.startswith('<') and arg.endswith('>') or arg.isupper():
+ descriptor.append('')
+ descriptor[-1] += '\n' + line
+ descriptors.append(descriptor)
+
+ #Flatten list of lists
+ descriptors = [item for sublist in descriptors for item in sublist]
+
+ #Parse each descriptor
+ for descriptor in descriptors:
+ #Partition descriptor
+ terms, description = '', ''
+ parts = re.split(r' |\t', descriptor, 1)
+
+ #Set terms
+ terms = parts[0].strip()
+ terms = terms.replace(',', ' ').replace('=', ' ')
+ terms = terms.split()
+
+ if not terms:
+ continue
+
+ #Set description
+ if len(parts) > 1:
+ description = parts[1]
+
+ #Find any default arguments
+ default = re.findall('(?i)\[default: (.*)\]', description)
+ default = default[0] if default else None
+
+ #Call correct parser
+ if terms[0].startswith('-'):
+ options += Option.parse(terms, description, default)
+ else:
+ arguments += Argument.parse(terms, description, default)
+
+ return options, arguments
def printable_usage(doc):
@@ -452,7 +534,7 @@ def __repr__(self):
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)
+ pot_options, def_arguments = parse_doc_descriptors(doc)
formal_pattern = parse_pattern(formal_usage(usage), options=pot_options)
argv = parse_args(argv, options=pot_options)
extras(help, version, argv, doc)
@@ -461,6 +543,7 @@ def docopt(doc, argv=sys.argv[1:], help=True, version=None):
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]]
+ pot_arguments += def_arguments
return Dict((a.name, a.value) for a in
(pot_options + options + pot_arguments + arguments))
raise DocoptExit()
View
37 language_agnostic_test/language_agnostic_tester.py
@@ -587,6 +587,43 @@
$ prog -op
{"-o": true, "-p": true, "-r": false}
+
+r"""Dictionary
+
+Usage:
+ prog [options] <word> [<language>]
+
+Arguments:
+ <word> The word to lookup
+ <language> The language to use [default: English]
+
+Options:
+ -h --help Help
+ --verbose
+
+"""
+$ prog PROGRAMMING
+{"--help": false,
+ "--verbose": false,
+ "<word>": "PROGRAMMING",
+ "<language>": "English"}
+
+$ prog BONJOUR FRENCH
+{"--help": false,
+ "--verbose": false,
+ "<word>": "BONJOUR",
+ "<language>": "FRENCH"}
+
+r"""usage: prog [PATH]
+
+PATH Directory to use [default: ./]
+
+"""
+$ prog
+{"PATH": "./"}
+
+$ prog "Some Folder"
+{"PATH": "Some Folder"}
'''
import sys, json
from subprocess import Popen, PIPE, STDOUT
View
87 test_docopt.py
@@ -3,7 +3,7 @@
Option, Argument, Command,
Required, Optional, Either, OneOrMore, AnyOptions,
parse_args, parse_pattern,
- parse_doc_options, printable_usage, formal_usage
+ parse_doc_descriptors, printable_usage, formal_usage
)
from pytest import raises
@@ -14,34 +14,63 @@ def test_pattern_flat():
[Argument('N'), Option('-a'), Argument('M')]
+#Convenience function for option / argument defaults
+def descriptor_parse(s, argument=False):
+ return parse_doc_descriptors(s)[argument]
+
+
def test_option():
- assert Option.parse('-h') == Option('-h', None)
- assert Option.parse('--help') == Option(None, '--help')
- assert Option.parse('-h --help') == Option('-h', '--help')
- assert Option.parse('-h, --help') == Option('-h', '--help')
-
- assert Option.parse('-h TOPIC') == Option('-h', None, 1)
- assert Option.parse('--help TOPIC') == Option(None, '--help', 1)
- assert Option.parse('-h TOPIC --help TOPIC') == Option('-h', '--help', 1)
- assert Option.parse('-h TOPIC, --help TOPIC') == Option('-h', '--help', 1)
- assert Option.parse('-h TOPIC, --help=TOPIC') == Option('-h', '--help', 1)
-
- assert Option.parse('-h Description...') == Option('-h', None)
- assert Option.parse('-h --help Description...') == Option('-h', '--help')
- assert Option.parse('-h TOPIC Description...') == Option('-h', None, 1)
-
- assert Option.parse(' -h') == Option('-h', None)
-
- assert Option.parse('-h TOPIC Descripton... [default: 2]') == \
- Option('-h', None, 1, '2')
- assert Option.parse('-h TOPIC Descripton... [default: topic-1]') == \
- Option('-h', None, 1, 'topic-1')
- assert Option.parse('--help=TOPIC ... [default: 3.14]') == \
- Option(None, '--help', 1, '3.14')
- assert Option.parse('-h, --help=DIR ... [default: ./]') == \
- Option('-h', '--help', 1, "./")
- assert Option.parse('-h TOPIC Descripton... [dEfAuLt: 2]') == \
- Option('-h', None, 1, '2')
+
+ assert descriptor_parse('-h') == [Option('-h', None)]
+ assert descriptor_parse('--help') == [Option(None, '--help')]
+ assert descriptor_parse('-h --help') == [Option('-h', '--help')]
+ assert descriptor_parse('-h, --help') == [Option('-h', '--help')]
+
+ assert descriptor_parse('-h TOPIC') == [Option('-h', None, 1)]
+ assert descriptor_parse('--help TOPIC') == [Option(None, '--help', 1)]
+ assert descriptor_parse('-h TOPIC --help TOPIC') == [Option('-h', '--help', 1)]
+ assert descriptor_parse('-h TOPIC, --help TOPIC') == [Option('-h', '--help', 1)]
+ assert descriptor_parse('-h TOPIC, --help=TOPIC') == [Option('-h', '--help', 1)]
+
+ assert descriptor_parse('-h Description...') == [Option('-h', None)]
+ assert descriptor_parse('-h --help Description...') == [Option('-h', '--help')]
+ assert descriptor_parse('-h TOPIC Description...') == [Option('-h', None, 1)]
+
+ assert descriptor_parse(' -h') == [Option('-h', None)]
+
+ assert descriptor_parse('-h TOPIC Descripton... [default: 2]') == \
+ [Option('-h', None, 1, '2')]
+ assert descriptor_parse('-h TOPIC Descripton... [default: topic-1]') == \
+ [Option('-h', None, 1, 'topic-1')]
+ assert descriptor_parse('--help=TOPIC ... [default: 3.14]') == \
+ [Option(None, '--help', 1, '3.14')]
+ assert descriptor_parse('-h, --help=DIR ... [default: ./]') == \
+ [Option('-h', '--help', 1, "./")]
+ assert descriptor_parse('-h TOPIC Descripton... [dEfAuLt: 2]') == \
+ [Option('-h', None, 1, '2')]
+
+
+def test_argument_defaults():
+ assert descriptor_parse('<arg>', True) == []
+ assert descriptor_parse('<arg> Description ', True) == []
+ assert descriptor_parse('<arg> Desc... [dEfAuLt: Hello World]', True) == \
+ [Argument('<arg>', 'Hello World')]
+ assert descriptor_parse('<arg> <arg2>', True) == []
+ assert descriptor_parse('<arg> <arg2> [default: 5]', True) == \
+ [Argument('<arg>', '5'), Argument('<arg2>', '5')]
+ assert descriptor_parse('<arg>, <arg2> [default: 5]', True) == \
+ [Argument('<arg>', '5'), Argument('<arg2>', '5')]
+ assert descriptor_parse('SPEED Desc1\nDesc2[default: 5]', True) == \
+ [Argument('SPEED', '5')]
+
+
+def test_multiple_defaults():
+ assert parse_doc_descriptors('-h TOPIC\n<speed> [default: 5]') == \
+ ([Option('-h', None, 1)], [Argument('<speed>', '5')])
+ assert parse_doc_descriptors('ARG1 Desc\n--speed=<kn>\nARG2 Desc') == \
+ ([Option(None, '--speed', 1)], [])
+ assert parse_doc_descriptors('PATH [default: ./]\n--verbose') == \
+ ([Option(None, '--verbose')], [Argument('PATH', './')])
def test_option_name():
@@ -75,7 +104,7 @@ def test_parse_doc_options():
doc = '''-h, --help Print help message.
-o FILE Output file.
--verbose Verbose mode.'''
- assert parse_doc_options(doc) == [Option('-h', '--help'),
+ assert descriptor_parse(doc) == [Option('-h', '--help'),
Option('-o', None, 1),
Option(None, '--verbose')]
Something went wrong with that request. Please try again.