11
11
12
12
13
13
__all__ = ['docopt' ]
14
- __version__ = '0.6.1 '
14
+ __version__ = '0.6.2 '
15
15
16
16
17
17
class DocoptLanguageError (Exception ):
@@ -47,18 +47,18 @@ def fix_identities(self, uniq=None):
47
47
if not hasattr (self , 'children' ):
48
48
return self
49
49
uniq = list (set (self .flat ())) if uniq is None else uniq
50
- for i , child in enumerate (self .children ):
51
- if not hasattr (child , 'children' ):
52
- assert child in uniq
53
- self .children [i ] = uniq [uniq .index (child )]
50
+ for i , c in enumerate (self .children ):
51
+ if not hasattr (c , 'children' ):
52
+ assert c in uniq
53
+ self .children [i ] = uniq [uniq .index (c )]
54
54
else :
55
- child .fix_identities (uniq )
55
+ c .fix_identities (uniq )
56
56
57
57
def fix_repeating_arguments (self ):
58
58
"""Fix elements that should accumulate/increment values."""
59
- either = [list (child .children ) for child in transform ( self ) .children ]
59
+ either = [list (c .children ) for c in self . either .children ]
60
60
for case in either :
61
- for e in [child for child in case if case .count (child ) > 1 ]:
61
+ for e in [c for c in case if case .count (c ) > 1 ]:
62
62
if type (e ) is Argument or type (e ) is Option and e .argcount :
63
63
if e .value is None :
64
64
e .value = []
@@ -68,40 +68,47 @@ def fix_repeating_arguments(self):
68
68
e .value = 0
69
69
return self
70
70
71
-
72
- def transform (pattern ):
73
- """Expand pattern into an (almost) equivalent one, but with single Either.
74
-
75
- Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d)
76
- Quirks: [-a] => (-a), (-a...) => (-a -a)
77
-
78
- """
79
- result = []
80
- groups = [[pattern ]]
81
- while groups :
82
- children = groups .pop (0 )
83
- parents = [Required , Optional , OptionsShortcut , Either , OneOrMore ]
84
- if any (t in map (type , children ) for t in parents ):
85
- child = [c for c in children if type (c ) in parents ][0 ]
86
- children .remove (child )
87
- if type (child ) is Either :
88
- for c in child .children :
71
+ @property
72
+ def either (self ):
73
+ """Transform pattern into an equivalent, with only top-level Either."""
74
+ # Currently the pattern will not be equivalent, but more "narrow",
75
+ # although good enough to reason about list arguments.
76
+ ret = []
77
+ groups = [[self ]]
78
+ while groups :
79
+ children = groups .pop (0 )
80
+ types = [type (c ) for c in children ]
81
+ if Either in types :
82
+ either = [c for c in children if type (c ) is Either ][0 ]
83
+ children .pop (children .index (either ))
84
+ for c in either .children :
89
85
groups .append ([c ] + children )
90
- elif type (child ) is OneOrMore :
91
- groups .append (child .children * 2 + children )
86
+ elif Required in types :
87
+ required = [c for c in children if type (c ) is Required ][0 ]
88
+ children .pop (children .index (required ))
89
+ groups .append (list (required .children ) + children )
90
+ elif Optional in types :
91
+ optional = [c for c in children if type (c ) is Optional ][0 ]
92
+ children .pop (children .index (optional ))
93
+ groups .append (list (optional .children ) + children )
94
+ elif AnyOptions in types :
95
+ optional = [c for c in children if type (c ) is AnyOptions ][0 ]
96
+ children .pop (children .index (optional ))
97
+ groups .append (list (optional .children ) + children )
98
+ elif OneOrMore in types :
99
+ oneormore = [c for c in children if type (c ) is OneOrMore ][0 ]
100
+ children .pop (children .index (oneormore ))
101
+ groups .append (list (oneormore .children ) * 2 + children )
92
102
else :
93
- groups .append (child .children + children )
94
- else :
95
- result .append (children )
96
- return Either (* [Required (* e ) for e in result ])
103
+ ret .append (children )
104
+ return Either (* [Required (* e ) for e in ret ])
97
105
98
106
99
- class LeafPattern (Pattern ):
100
-
101
- """Leaf/terminal node of a pattern tree."""
107
+ class ChildPattern (Pattern ):
102
108
103
109
def __init__ (self , name , value = None ):
104
- self .name , self .value = name , value
110
+ self .name = name
111
+ self .value = value
105
112
106
113
def __repr__ (self ):
107
114
return '%s(%r, %r)' % (self .__class__ .__name__ , self .name , self .value )
@@ -130,9 +137,7 @@ def match(self, left, collected=None):
130
137
return True , left_ , collected + [match ]
131
138
132
139
133
- class BranchPattern (Pattern ):
134
-
135
- """Branch/inner node of a pattern tree."""
140
+ class ParentPattern (Pattern ):
136
141
137
142
def __init__ (self , * children ):
138
143
self .children = list (children )
@@ -144,15 +149,15 @@ def __repr__(self):
144
149
def flat (self , * types ):
145
150
if type (self ) in types :
146
151
return [self ]
147
- return sum ([child .flat (* types ) for child in self .children ], [])
152
+ return sum ([c .flat (* types ) for c in self .children ], [])
148
153
149
154
150
- class Argument (LeafPattern ):
155
+ class Argument (ChildPattern ):
151
156
152
157
def single_match (self , left ):
153
- for n , pattern in enumerate (left ):
154
- if type (pattern ) is Argument :
155
- return n , Argument (self .name , pattern .value )
158
+ for n , p in enumerate (left ):
159
+ if type (p ) is Argument :
160
+ return n , Argument (self .name , p .value )
156
161
return None , None
157
162
158
163
@classmethod
@@ -165,23 +170,25 @@ def parse(class_, source):
165
170
class Command (Argument ):
166
171
167
172
def __init__ (self , name , value = False ):
168
- self .name , self .value = name , value
173
+ self .name = name
174
+ self .value = value
169
175
170
176
def single_match (self , left ):
171
- for n , pattern in enumerate (left ):
172
- if type (pattern ) is Argument :
173
- if pattern .value == self .name :
177
+ for n , p in enumerate (left ):
178
+ if type (p ) is Argument :
179
+ if p .value == self .name :
174
180
return n , Command (self .name , True )
175
181
else :
176
182
break
177
183
return None , None
178
184
179
185
180
- class Option (LeafPattern ):
186
+ class Option (ChildPattern ):
181
187
182
188
def __init__ (self , short = None , long = None , argcount = 0 , value = False ):
183
189
assert argcount in (0 , 1 )
184
- self .short , self .long , self .argcount = short , long , argcount
190
+ self .short , self .long = short , long
191
+ self .argcount , self .value = argcount , value
185
192
self .value = None if value is False and argcount else value
186
193
187
194
@classmethod
@@ -202,9 +209,9 @@ def parse(class_, option_description):
202
209
return class_ (short , long , argcount , value )
203
210
204
211
def single_match (self , left ):
205
- for n , pattern in enumerate (left ):
206
- if self .name == pattern .name :
207
- return n , pattern
212
+ for n , p in enumerate (left ):
213
+ if self .name == p .name :
214
+ return n , p
208
215
return None , None
209
216
210
217
@property
@@ -216,34 +223,34 @@ def __repr__(self):
216
223
self .argcount , self .value )
217
224
218
225
219
- class Required (BranchPattern ):
226
+ class Required (ParentPattern ):
220
227
221
228
def match (self , left , collected = None ):
222
229
collected = [] if collected is None else collected
223
230
l = left
224
231
c = collected
225
- for pattern in self .children :
226
- matched , l , c = pattern .match (l , c )
232
+ for p in self .children :
233
+ matched , l , c = p .match (l , c )
227
234
if not matched :
228
235
return False , left , collected
229
236
return True , l , c
230
237
231
238
232
- class Optional (BranchPattern ):
239
+ class Optional (ParentPattern ):
233
240
234
241
def match (self , left , collected = None ):
235
242
collected = [] if collected is None else collected
236
- for pattern in self .children :
237
- m , left , collected = pattern .match (left , collected )
243
+ for p in self .children :
244
+ m , left , collected = p .match (left , collected )
238
245
return True , left , collected
239
246
240
247
241
- class OptionsShortcut (Optional ):
248
+ class AnyOptions (Optional ):
242
249
243
250
"""Marker/placeholder for [options] shortcut."""
244
251
245
252
246
- class OneOrMore (BranchPattern ):
253
+ class OneOrMore (ParentPattern ):
247
254
248
255
def match (self , left , collected = None ):
249
256
assert len (self .children ) == 1
@@ -265,32 +272,26 @@ def match(self, left, collected=None):
265
272
return False , left , collected
266
273
267
274
268
- class Either (BranchPattern ):
275
+ class Either (ParentPattern ):
269
276
270
277
def match (self , left , collected = None ):
271
278
collected = [] if collected is None else collected
272
279
outcomes = []
273
- for pattern in self .children :
274
- matched , _ , _ = outcome = pattern .match (left , collected )
280
+ for p in self .children :
281
+ matched , _ , _ = outcome = p .match (left , collected )
275
282
if matched :
276
283
outcomes .append (outcome )
277
284
if outcomes :
278
285
return min (outcomes , key = lambda outcome : len (outcome [1 ]))
279
286
return False , left , collected
280
287
281
288
282
- class Tokens (list ):
289
+ class TokenStream (list ):
283
290
284
- def __init__ (self , source , error = DocoptExit ):
291
+ def __init__ (self , source , error ):
285
292
self += source .split () if hasattr (source , 'split' ) else source
286
293
self .error = error
287
294
288
- @staticmethod
289
- def from_pattern (source ):
290
- source = re .sub (r'([\[\]\(\)\|]|\.\.\.)' , r' \1 ' , source )
291
- source = [s for s in re .split ('\s+|(\S*<.*?>)' , source ) if s ]
292
- return Tokens (source , error = DocoptLanguageError )
293
-
294
295
def move (self ):
295
296
return self .pop (0 ) if len (self ) else None
296
297
@@ -323,7 +324,7 @@ def parse_long(tokens, options):
323
324
raise tokens .error ('%s must not have an argument' % o .long )
324
325
else :
325
326
if value is None :
326
- if tokens .current () in [ None , '--' ] :
327
+ if tokens .current () is None :
327
328
raise tokens .error ('%s requires argument' % o .long )
328
329
value = tokens .move ()
329
330
if tokens .error is DocoptExit :
@@ -354,7 +355,7 @@ def parse_shorts(tokens, options):
354
355
value = None
355
356
if o .argcount != 0 :
356
357
if left == '' :
357
- if tokens .current () in [ None , '--' ] :
358
+ if tokens .current () is None :
358
359
raise tokens .error ('%s requires argument' % short )
359
360
value = tokens .move ()
360
361
else :
@@ -367,7 +368,8 @@ def parse_shorts(tokens, options):
367
368
368
369
369
370
def parse_pattern (source , options ):
370
- tokens = Tokens .from_pattern (source )
371
+ tokens = TokenStream (re .sub (r'([\[\]\(\)\|]|\.\.\.)' , r' \1 ' , source ),
372
+ DocoptLanguageError )
371
373
result = parse_expr (tokens , options )
372
374
if tokens .current () is not None :
373
375
raise tokens .error ('unexpected ending: %r' % ' ' .join (tokens ))
@@ -414,7 +416,7 @@ def parse_atom(tokens, options):
414
416
return [result ]
415
417
elif token == 'options' :
416
418
tokens .move ()
417
- return [OptionsShortcut ()]
419
+ return [AnyOptions ()]
418
420
elif token .startswith ('--' ) and token != '--' :
419
421
return parse_long (tokens , options )
420
422
elif token .startswith ('-' ) and token not in ('-' , '--' ):
@@ -450,26 +452,27 @@ def parse_argv(tokens, options, options_first=False):
450
452
451
453
452
454
def parse_defaults (doc ):
453
- defaults = []
454
- for s in parse_section ('options:' , doc ):
455
- # FIXME corner case "bla: options: --foo"
456
- _ , _ , s = s .partition (':' ) # get rid of "options:"
457
- split = re .split ('\n [ \t ]*(-\S+?)' , '\n ' + s )[1 :]
458
- split = [s1 + s2 for s1 , s2 in zip (split [::2 ], split [1 ::2 ])]
459
- options = [Option .parse (s ) for s in split if s .startswith ('-' )]
460
- defaults += options
461
- return defaults
462
-
463
-
464
- def parse_section (name , source ):
465
- pattern = re .compile ('^([^\n ]*' + name + '[^\n ]*\n ?(?:[ \t ].*?(?:\n |$))*)' ,
466
- re .IGNORECASE | re .MULTILINE )
467
- return [s .strip () for s in pattern .findall (source )]
468
-
469
-
470
- def formal_usage (section ):
471
- _ , _ , section = section .partition (':' ) # drop "usage:"
472
- pu = section .split ()
455
+ # in python < 2.7 you can't pass flags=re.MULTILINE
456
+ split = re .split ('\n *(<\S+?>|-\S+?)' , doc )[1 :]
457
+ split = [s1 + s2 for s1 , s2 in zip (split [::2 ], split [1 ::2 ])]
458
+ options = [Option .parse (s ) for s in split if s .startswith ('-' )]
459
+ #arguments = [Argument.parse(s) for s in split if s.startswith('<')]
460
+ #return options, arguments
461
+ return options
462
+
463
+
464
+ def printable_usage (doc ):
465
+ # in python < 2.7 you can't pass flags=re.IGNORECASE
466
+ usage_split = re .split (r'([Uu][Ss][Aa][Gg][Ee]:)' , doc )
467
+ if len (usage_split ) < 3 :
468
+ raise DocoptLanguageError ('"usage:" (case-insensitive) not found.' )
469
+ if len (usage_split ) > 3 :
470
+ raise DocoptLanguageError ('More than one "usage:" (case-insensitive).' )
471
+ return re .split (r'\n\s*\n' , '' .join (usage_split [1 :]))[0 ].strip ()
472
+
473
+
474
+ def formal_usage (printable_usage ):
475
+ pu = printable_usage .split ()[1 :] # split and drop "usage:"
473
476
return '( ' + ' ' .join (') | (' if s == pu [0 ] else s for s in pu [1 :]) + ' )'
474
477
475
478
@@ -509,7 +512,7 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False):
509
512
If passed, the object will be printed if --version is in
510
513
`argv`.
511
514
options_first : bool (default: False)
512
- Set to True to require options precede positional arguments,
515
+ Set to True to require options preceed positional arguments,
513
516
i.e. to forbid options and positional arguments intermix.
514
517
515
518
Returns
@@ -523,15 +526,15 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False):
523
526
-------
524
527
>>> from docopt import docopt
525
528
>>> doc = '''
526
- ... Usage:
527
- ... my_program tcp <host> <port> [--timeout=<seconds>]
528
- ... my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
529
- ... my_program (-h | --help | --version)
530
- ...
531
- ... Options:
532
- ... -h, --help Show this screen and exit.
533
- ... --baud=<n> Baudrate [default: 9600]
534
- ... '''
529
+ Usage:
530
+ my_program tcp <host> <port> [--timeout=<seconds>]
531
+ my_program serial <port> [--baud=<n>] [--timeout=<seconds>]
532
+ my_program (-h | --help | --version)
533
+
534
+ Options:
535
+ -h, --help Show this screen and exit.
536
+ --baud=<n> Baudrate [default: 9600]
537
+ '''
535
538
>>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30']
536
539
>>> docopt(doc, argv)
537
540
{'--baud': '9600',
@@ -550,32 +553,27 @@ def docopt(doc, argv=None, help=True, version=None, options_first=False):
550
553
at https://github.com/docopt/docopt#readme
551
554
552
555
"""
553
- argv = sys .argv [1 :] if argv is None else argv
554
-
555
- usage_sections = parse_section ('usage:' , doc )
556
- if len (usage_sections ) == 0 :
557
- raise DocoptLanguageError ('"usage:" (case-insensitive) not found.' )
558
- if len (usage_sections ) > 1 :
559
- raise DocoptLanguageError ('More than one "usage:" (case-insensitive).' )
560
- DocoptExit .usage = usage_sections [0 ]
561
-
556
+ if argv is None :
557
+ argv = sys .argv [1 :]
558
+ DocoptExit .usage = printable_usage (doc )
562
559
options = parse_defaults (doc )
563
560
pattern = parse_pattern (formal_usage (DocoptExit .usage ), options )
564
561
# [default] syntax for argument is disabled
565
562
#for a in pattern.flat(Argument):
566
563
# same_name = [d for d in arguments if d.name == a.name]
567
564
# if same_name:
568
565
# a.value = same_name[0].value
569
- argv = parse_argv (Tokens (argv ), list (options ), options_first )
566
+ argv = parse_argv (TokenStream (argv , DocoptExit ), list (options ),
567
+ options_first )
570
568
pattern_options = set (pattern .flat (Option ))
571
- for options_shortcut in pattern .flat (OptionsShortcut ):
569
+ for ao in pattern .flat (AnyOptions ):
572
570
doc_options = parse_defaults (doc )
573
- options_shortcut .children = list (set (doc_options ) - pattern_options )
571
+ ao .children = list (set (doc_options ) - pattern_options )
574
572
#if any_options:
575
- # options_shortcut .children += [Option(o.short, o.long, o.argcount)
573
+ # ao .children += [Option(o.short, o.long, o.argcount)
576
574
# for o in argv if type(o) is Option]
577
575
extras (help , version , argv , doc )
578
576
matched , left , collected = pattern .fix ().match (argv )
579
577
if matched and left == []: # better error message if left?
580
578
return Dict ((a .name , a .value ) for a in (pattern .flat () + collected ))
581
- raise DocoptExit ()
579
+ raise DocoptExit ()
0 commit comments