Skip to content

Commit 85abdcd

Browse files
authored
Argparse "Overlay" Module (#12873)
* Implemented a wrapper module around the standard argparse package, exposing the same interface with some extras on top, including a new builder type with expressive DSL for constructing complex argument parsers. * Fixed imports in build_swift/argparse/__init__.py to make flake8 happy. * More re-formmating to meet the exacting standards of the python_lint script. * Added doc-strings to all the modules in the build_swift argparse overlay. * Implemented a new BoolType for the argparse module which handles boolean-like values and replaces the hard-coded boolean values in the _ToggleAction class. * Fixed the mess of imports in the tests sub-package to favor relative imports, so now the unit-tests will actually run as expected. The README has also been updated with a better command for executing the unit-test suite. * Updated the add_positional method on the ArgumentParser builder class to only take a single action or default to the store action. * Cleaned up the set_defaults method. * Added validation test to run the build_swift unit-tests. * Updated validation-test for the build_swift unit-test suite to use %utils. * Fixed hard-coded default values in the expected_options module used for generating argument parser tests. * Updated the comment in the Python validation test to run the build_swift unit-tests.
1 parent 44d8720 commit 85abdcd

File tree

14 files changed

+1622
-16
lines changed

14 files changed

+1622
-16
lines changed

utils/build_swift/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ the Swift build-script.
88
You may run the unit test suite using the command:
99

1010
```sh
11-
$ python -m unittest discover -s utils/build_swift
11+
$ python -m unittest discover -s utils/build_swift/ -t utils/
1212
```
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# This source file is part of the Swift.org open source project
2+
#
3+
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See https://swift.org/LICENSE.txt for license information
7+
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
9+
10+
"""
11+
Wrapper module around the standard argparse that extends the default
12+
functionality with support for multi-destination actions, an expressive DSL for
13+
constructing parsers and more argument types. This module exposes a strict
14+
super-set of the argparse API and is meant to be used as a drop-in replacement.
15+
"""
16+
17+
18+
from argparse import (ArgumentDefaultsHelpFormatter, ArgumentError,
19+
ArgumentTypeError, FileType, HelpFormatter,
20+
Namespace, RawDescriptionHelpFormatter,
21+
RawTextHelpFormatter)
22+
from argparse import ONE_OR_MORE, OPTIONAL, SUPPRESS, ZERO_OR_MORE
23+
24+
from .actions import Action, Nargs
25+
from .parser import ArgumentParser
26+
from .types import (BoolType, ClangVersionType, PathType, RegexType,
27+
SwiftVersionType)
28+
29+
30+
__all__ = [
31+
'Action',
32+
'ArgumentDefaultsHelpFormatter',
33+
'ArgumentError',
34+
'ArgumentParser',
35+
'ArgumentTypeError',
36+
'HelpFormatter',
37+
'Namespace',
38+
'Nargs',
39+
'RawDescriptionHelpFormatter',
40+
'RawTextHelpFormatter',
41+
42+
'BoolType',
43+
'FileType',
44+
'PathType',
45+
'RegexType',
46+
'ClangVersionType',
47+
'SwiftVersionType',
48+
49+
'SUPPRESS',
50+
'OPTIONAL',
51+
'ZERO_OR_MORE',
52+
'ONE_OR_MORE',
53+
]

utils/build_swift/argparse/actions.py

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# This source file is part of the Swift.org open source project
2+
#
3+
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
4+
# Licensed under Apache License v2.0 with Runtime Library Exception
5+
#
6+
# See https://swift.org/LICENSE.txt for license information
7+
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
8+
9+
10+
"""
11+
Hierarchy of action classes which support multiple destinations, similar to the
12+
default actions provided by the standard argparse.
13+
"""
14+
15+
16+
import argparse
17+
import copy
18+
19+
from .types import BoolType, PathType
20+
21+
22+
__all__ = [
23+
'Action',
24+
'Nargs',
25+
26+
'AppendAction',
27+
'CustomCallAction',
28+
'StoreAction',
29+
'StoreIntAction',
30+
'StoreTrueAction',
31+
'StoreFalseAction',
32+
'StorePathAction',
33+
'ToggleTrueAction',
34+
'ToggleFalseAction',
35+
'UnsupportedAction',
36+
]
37+
38+
39+
# -----------------------------------------------------------------------------
40+
41+
class Nargs(object):
42+
"""Container class holding valid values for the number of arguments
43+
actions can accept. Other possible values include integer literals.
44+
"""
45+
46+
ZERO = 0
47+
SINGLE = None
48+
OPTIONAL = argparse.OPTIONAL
49+
ZERO_OR_MORE = argparse.ZERO_OR_MORE
50+
ONE_OR_MORE = argparse.ONE_OR_MORE
51+
52+
53+
# -----------------------------------------------------------------------------
54+
55+
class Action(argparse.Action):
56+
"""Slightly modified version of the Action class from argparse which has
57+
support for multiple destinations available rather than just a single one.
58+
"""
59+
60+
def __init__(self,
61+
option_strings,
62+
dests=None,
63+
nargs=Nargs.SINGLE,
64+
const=None,
65+
default=None,
66+
type=None,
67+
choices=None,
68+
required=False,
69+
help=None,
70+
metavar=None,
71+
dest=None): # exists for compatibility purposes
72+
73+
if dests is None and dest is None:
74+
raise TypeError('action must supply either dests or dest')
75+
76+
dests = dests or dest
77+
if dests == argparse.SUPPRESS:
78+
dests = []
79+
elif isinstance(dests, str):
80+
dests = [dests]
81+
82+
super(Action, self).__init__(
83+
option_strings=option_strings,
84+
dest=argparse.SUPPRESS,
85+
nargs=nargs,
86+
const=const,
87+
default=default,
88+
type=type,
89+
choices=choices,
90+
required=required,
91+
help=help,
92+
metavar=metavar)
93+
94+
self.dests = dests
95+
96+
def _get_kwargs(self):
97+
"""Unofficial method used for pretty-printing out actions.
98+
"""
99+
100+
names = [
101+
'option_strings',
102+
'dests',
103+
'nargs',
104+
'const',
105+
'default',
106+
'type',
107+
'choices',
108+
'required',
109+
'help',
110+
'metavar',
111+
]
112+
113+
return [(name, getattr(self, name)) for name in names]
114+
115+
def __call__(self, parser, namespace, values, option_string=None):
116+
raise NotImplementedError('__call__ not defined')
117+
118+
119+
# -----------------------------------------------------------------------------
120+
121+
class AppendAction(Action):
122+
"""Action that appends
123+
"""
124+
125+
def __init__(self, option_strings, join=None, **kwargs):
126+
127+
kwargs['nargs'] = Nargs.SINGLE
128+
kwargs.setdefault('default', [])
129+
130+
super(AppendAction, self).__init__(
131+
option_strings=option_strings,
132+
**kwargs)
133+
134+
def __call__(self, parser, namespace, values, option_string=None):
135+
if isinstance(values, str):
136+
values = [values]
137+
138+
for dest in self.dests:
139+
if getattr(namespace, dest) is None:
140+
setattr(namespace, dest, [])
141+
142+
items = copy.copy(getattr(namespace, dest))
143+
items += values
144+
145+
setattr(namespace, dest, items)
146+
147+
148+
# -----------------------------------------------------------------------------
149+
150+
class CustomCallAction(Action):
151+
"""Action that allows for instances to implement custom call functionality.
152+
The call_func must follow the same calling convention as implementing the
153+
__call__ method for an Action.
154+
"""
155+
156+
def __init__(self, option_strings, call_func, **kwargs):
157+
158+
if not callable(call_func):
159+
raise TypeError('call_func must be callable')
160+
161+
super(CustomCallAction, self).__init__(
162+
option_strings=option_strings,
163+
**kwargs)
164+
165+
self.call_func = call_func
166+
167+
def __call__(self, parser, namespace, values, option_string=None):
168+
self.call_func(self, parser, namespace, values, option_string)
169+
170+
171+
# -----------------------------------------------------------------------------
172+
173+
class StoreAction(Action):
174+
"""Action that stores a string.
175+
"""
176+
177+
def __init__(self, option_strings, **kwargs):
178+
179+
if 'choices' in kwargs:
180+
kwargs['nargs'] = Nargs.OPTIONAL
181+
if 'const' in kwargs:
182+
kwargs['nargs'] = Nargs.ZERO
183+
184+
super(StoreAction, self).__init__(
185+
option_strings=option_strings,
186+
**kwargs)
187+
188+
def __call__(self, parser, namespace, values, option_string=None):
189+
for dest in self.dests:
190+
if self.nargs == Nargs.ZERO and self.const is not None:
191+
values = self.const
192+
193+
setattr(namespace, dest, values)
194+
195+
196+
class StoreIntAction(StoreAction):
197+
"""Action that stores an int.
198+
"""
199+
200+
def __init__(self, option_strings, **kwargs):
201+
kwargs['nargs'] = Nargs.SINGLE
202+
kwargs['type'] = int
203+
kwargs.setdefault('metavar', 'N')
204+
205+
super(StoreAction, self).__init__(
206+
option_strings=option_strings,
207+
**kwargs)
208+
209+
210+
class StoreTrueAction(StoreAction):
211+
"""Action that stores True when called and False by default.
212+
"""
213+
214+
def __init__(self, option_strings, **kwargs):
215+
kwargs['nargs'] = Nargs.ZERO
216+
kwargs['const'] = True
217+
kwargs['default'] = False
218+
219+
super(StoreTrueAction, self).__init__(
220+
option_strings=option_strings,
221+
**kwargs)
222+
223+
224+
class StoreFalseAction(StoreAction):
225+
"""Action that stores False when called and True by default.
226+
"""
227+
228+
def __init__(self, option_strings, **kwargs):
229+
kwargs['nargs'] = Nargs.ZERO
230+
kwargs['const'] = False
231+
kwargs['default'] = True
232+
233+
super(StoreFalseAction, self).__init__(
234+
option_strings=option_strings,
235+
**kwargs)
236+
237+
238+
class StorePathAction(StoreAction):
239+
"""Action that stores a path, which it will attempt to expand.
240+
"""
241+
242+
def __init__(self, option_strings, **kwargs):
243+
kwargs['nargs'] = Nargs.SINGLE
244+
kwargs['type'] = PathType()
245+
246+
super(StorePathAction, self).__init__(
247+
option_strings=option_strings,
248+
**kwargs)
249+
250+
251+
# -----------------------------------------------------------------------------
252+
253+
class _ToggleAction(Action):
254+
"""Action that can be toggled on or off, either implicitly when called or
255+
explicitly when an optional boolean value is parsed.
256+
"""
257+
258+
def __init__(self, option_strings, on_value, off_value, **kwargs):
259+
kwargs['nargs'] = Nargs.OPTIONAL
260+
kwargs['type'] = BoolType()
261+
kwargs.setdefault('default', off_value)
262+
kwargs.setdefault('metavar', 'BOOL')
263+
264+
super(_ToggleAction, self).__init__(
265+
option_strings=option_strings,
266+
**kwargs)
267+
268+
self.on_value = on_value
269+
self.off_value = off_value
270+
271+
def __call__(self, parser, namespace, values, option_string=None):
272+
if values is None:
273+
values = self.on_value
274+
elif values is True:
275+
values = self.on_value
276+
elif values is False:
277+
values = self.off_value
278+
else:
279+
raise argparse.ArgumentTypeError(
280+
'{} is not a boolean value'.format(values))
281+
282+
for dest in self.dests:
283+
setattr(namespace, dest, values)
284+
285+
286+
class ToggleTrueAction(_ToggleAction):
287+
"""Action that toggles True when called or False otherwise, with the
288+
option to explicitly override the value.
289+
"""
290+
291+
def __init__(self, option_strings, **kwargs):
292+
293+
super(ToggleTrueAction, self).__init__(
294+
option_strings=option_strings,
295+
on_value=True,
296+
off_value=False,
297+
**kwargs)
298+
299+
300+
class ToggleFalseAction(_ToggleAction):
301+
"""Action that toggles False when called or True otherwise, with the
302+
option to explicitly override the value.
303+
"""
304+
305+
def __init__(self, option_strings, **kwargs):
306+
307+
super(ToggleFalseAction, self).__init__(
308+
option_strings=option_strings,
309+
on_value=False,
310+
off_value=True,
311+
**kwargs)
312+
313+
314+
# -----------------------------------------------------------------------------
315+
316+
class UnsupportedAction(Action):
317+
"""Action that denotes an unsupported argument or opiton and subsequently
318+
raises an ArgumentError.
319+
"""
320+
321+
def __init__(self, option_strings, message=None, **kwargs):
322+
kwargs['nargs'] = Nargs.ZERO
323+
kwargs['default'] = argparse.SUPPRESS
324+
325+
super(UnsupportedAction, self).__init__(
326+
option_strings=option_strings,
327+
dests=argparse.SUPPRESS,
328+
**kwargs)
329+
330+
self.message = message
331+
332+
def __call__(self, parser, namespace, values, option_string=None):
333+
if self.message is not None:
334+
parser.error(self.message)
335+
336+
arg = option_string or str(values)
337+
parser.error('unsupported argument: {}'.format(arg))

0 commit comments

Comments
 (0)