1from typing import Any, Iterable, Optional, Tuple
2
3# TODO: dynamic type for kind
4# T = TypeVar('T')
5
6
7class Argument:
8 """
9 A command-line argument/flag.
10
11 :param name:
12 Syntactic sugar for ``names=[<name>]``. Giving both ``name`` and
13 ``names`` is invalid.
14 :param names:
15 List of valid identifiers for this argument. For example, a "help"
16 argument may be defined with a name list of ``['-h', '--help']``.
17 :param kind:
18 Type factory & parser hint. E.g. ``int`` will turn the default text
19 value parsed, into a Python integer; and ``bool`` will tell the
20 parser not to expect an actual value but to treat the argument as a
21 toggle/flag.
22 :param default:
23 Default value made available to the parser if no value is given on the
24 command line.
25 :param help:
26 Help text, intended for use with ``--help``.
27 :param positional:
28 Whether or not this argument's value may be given positionally. When
29 ``False`` (default) arguments must be explicitly named.
30 :param optional:
31 Whether or not this (non-``bool``) argument requires a value.
32 :param incrementable:
33 Whether or not this (``int``) argument is to be incremented instead of
34 overwritten/assigned to.
35 :param attr_name:
36 A Python identifier/attribute friendly name, typically filled in with
37 the underscored version when ``name``/``names`` contain dashes.
38
39 .. versionadded:: 1.0
40 """
41
42 def __init__(
43 self,
44 name: Optional[str] = None,
45 names: Iterable[str] = (),
46 kind: Any = str,
47 default: Optional[Any] = None,
48 help: Optional[str] = None,
49 positional: bool = False,
50 optional: bool = False,
51 incrementable: bool = False,
52 attr_name: Optional[str] = None,
53 ) -> None:
54 if name and names:
55 raise TypeError(
56 "Cannot give both 'name' and 'names' arguments! Pick one."
57 )
58 if not (name or names):
59 raise TypeError("An Argument must have at least one name.")
60 if names:
61 self.names = tuple(names)
62 elif name and not names:
63 self.names = (name,)
64 self.kind = kind
65 initial_value: Optional[Any] = None
66 # Special case: list-type args start out as empty list, not None.
67 if kind is list:
68 initial_value = []
69 # Another: incrementable args start out as their default value.
70 if incrementable:
71 initial_value = default
72 self.raw_value = self._value = initial_value
73 self.default = default
74 self.help = help
75 self.positional = positional
76 self.optional = optional
77 self.incrementable = incrementable
78 self.attr_name = attr_name
79
80 def __repr__(self) -> str:
81 nicks = ""
82 if self.nicknames:
83 nicks = " ({})".format(", ".join(self.nicknames))
84 flags = ""
85 if self.positional or self.optional:
86 flags = " "
87 if self.positional:
88 flags += "*"
89 if self.optional:
90 flags += "?"
91 # TODO: store this default value somewhere other than signature of
92 # Argument.__init__?
93 kind = ""
94 if self.kind != str:
95 kind = " [{}]".format(self.kind.__name__)
96 return "<{}: {}{}{}{}>".format(
97 self.__class__.__name__, self.name, nicks, kind, flags
98 )
99
100 @property
101 def name(self) -> Optional[str]:
102 """
103 The canonical attribute-friendly name for this argument.
104
105 Will be ``attr_name`` (if given to constructor) or the first name in
106 ``names`` otherwise.
107
108 .. versionadded:: 1.0
109 """
110 return self.attr_name or self.names[0]
111
112 @property
113 def nicknames(self) -> Tuple[str, ...]:
114 return self.names[1:]
115
116 @property
117 def takes_value(self) -> bool:
118 if self.kind is bool:
119 return False
120 if self.incrementable:
121 return False
122 return True
123
124 @property
125 def value(self) -> Any:
126 # TODO: should probably be optional instead
127 return self._value if self._value is not None else self.default
128
129 @value.setter
130 def value(self, arg: str) -> None:
131 self.set_value(arg, cast=True)
132
133 def set_value(self, value: Any, cast: bool = True) -> None:
134 """
135 Actual explicit value-setting API call.
136
137 Sets ``self.raw_value`` to ``value`` directly.
138
139 Sets ``self.value`` to ``self.kind(value)``, unless:
140
141 - ``cast=False``, in which case the raw value is also used.
142 - ``self.kind==list``, in which case the value is appended to
143 ``self.value`` instead of cast & overwritten.
144 - ``self.incrementable==True``, in which case the value is ignored and
145 the current (assumed int) value is simply incremented.
146
147 .. versionadded:: 1.0
148 """
149 self.raw_value = value
150 # Default to do-nothing/identity function
151 func = lambda x: x
152 # If cast, set to self.kind, which should be str/int/etc
153 if cast:
154 func = self.kind
155 # If self.kind is a list, append instead of using cast func.
156 if self.kind is list:
157 func = lambda x: self.value + [x]
158 # If incrementable, just increment.
159 if self.incrementable:
160 # TODO: explode nicely if self.value was not an int to start
161 # with
162 func = lambda x: self.value + 1
163 self._value = func(value)
164
165 @property
166 def got_value(self) -> bool:
167 """
168 Returns whether the argument was ever given a (non-default) value.
169
170 For most argument kinds, this simply checks whether the internally
171 stored value is non-``None``; for others, such as ``list`` kinds,
172 different checks may be used.
173
174 .. versionadded:: 1.3
175 """
176 if self.kind is list:
177 return bool(self._value)
178 return self._value is not None