|
17 | 17 | from itertools import izip_longest as zip_longest
|
18 | 18 |
|
19 | 19 |
|
20 |
| -#: Sentinel object representing a truly blank value (vs ``None``). |
21 |
| -NO_DEFAULT = object() |
22 |
| - |
23 |
| - |
24 | 20 | class Task(object):
|
25 | 21 | """
|
26 | 22 | Core object representing an executable task & its argument specification.
|
@@ -134,45 +130,40 @@ def called(self):
|
134 | 130 |
|
135 | 131 | def argspec(self, body):
|
136 | 132 | """
|
137 |
| - Returns two-tuple: |
138 |
| -
|
139 |
| - * First item is list of arg names, in order defined. |
| 133 | + Returns a modified `inspect.Signature` based on that of ``body``. |
140 | 134 |
|
141 |
| - * I.e. we *cannot* simply use a dict's ``keys()`` method here. |
142 |
| -
|
143 |
| - * Second item is dict mapping arg names to default values or |
144 |
| - `.NO_DEFAULT` (an 'empty' value distinct from None, since None |
145 |
| - is a valid value on its own). |
| 135 | + :returns: |
| 136 | + an `inspect.Signature` matching that of ``body``, but with the |
| 137 | + initial context argument removed. |
| 138 | + :raises TypeError: |
| 139 | + if the task lacks an initial positional `.Context` argument. |
146 | 140 |
|
147 | 141 | .. versionadded:: 1.0
|
| 142 | + .. versionchanged:: 2.0 |
| 143 | + Changed from returning a two-tuple of ``(arg_names, spec_dict)`` to |
| 144 | + returning an `inspect.Signature`. |
148 | 145 | """
|
149 | 146 | # Handle callable-but-not-function objects
|
150 |
| - # TODO: __call__ exhibits the 'self' arg; do we manually nix 1st result |
151 |
| - # in argspec, or is there a way to get the "really callable" spec? |
152 | 147 | func = body if isinstance(body, types.FunctionType) else body.__call__
|
153 |
| - spec = inspect.getargspec(func) |
154 |
| - arg_names = spec.args[:] |
155 |
| - matched_args = [reversed(x) for x in [spec.args, spec.defaults or []]] |
156 |
| - spec_dict = dict(zip_longest(*matched_args, fillvalue=NO_DEFAULT)) |
157 |
| - # Pop context argument |
158 |
| - try: |
159 |
| - context_arg = arg_names.pop(0) |
160 |
| - except IndexError: |
| 148 | + # Rebuild signature with first arg dropped, or die usefully(ish trying |
| 149 | + sig = inspect.signature(func) |
| 150 | + params = list(sig.parameters.values()) |
| 151 | + # TODO: this ought to also check if an extant 1st param _was_ a Context |
| 152 | + # arg, and yell similarly if not. |
| 153 | + if not len(params): |
161 | 154 | # TODO: see TODO under __call__, this should be same type
|
162 | 155 | raise TypeError("Tasks must have an initial Context argument!")
|
163 |
| - del spec_dict[context_arg] |
164 |
| - return arg_names, spec_dict |
| 156 | + return sig.replace(parameters=params[1:]) |
165 | 157 |
|
166 | 158 | def fill_implicit_positionals(self, positional):
|
167 |
| - args, spec_dict = self.argspec(self.body) |
168 | 159 | # If positionals is None, everything lacking a default
|
169 | 160 | # value will be automatically considered positional.
|
170 | 161 | if positional is None:
|
171 |
| - positional = [] |
172 |
| - for name in args: # Go in defined order, not dict "order" |
173 |
| - default = spec_dict[name] |
174 |
| - if default is NO_DEFAULT: |
175 |
| - positional.append(name) |
| 162 | + positional = [ |
| 163 | + x.name |
| 164 | + for x in self.argspec(self.body).parameters.values() |
| 165 | + if x.default is inspect.Signature.empty |
| 166 | + ] |
176 | 167 | return positional
|
177 | 168 |
|
178 | 169 | def arg_opts(self, name, default, taken_names):
|
@@ -206,7 +197,7 @@ def arg_opts(self, name, default, taken_names):
|
206 | 197 | break
|
207 | 198 | opts["names"] = names
|
208 | 199 | # Handle default value & kind if possible
|
209 |
| - if default not in (None, NO_DEFAULT): |
| 200 | + if default not in (None, inspect.Signature.empty): |
210 | 201 | # TODO: allow setting 'kind' explicitly.
|
211 | 202 | # NOTE: skip setting 'kind' if optional is True + type(default) is
|
212 | 203 | # bool; that results in a nonsensical Argument which gives the
|
@@ -235,18 +226,17 @@ def get_arguments(self, ignore_unknown_help=None):
|
235 | 226 | Added the ``ignore_unknown_help`` kwarg.
|
236 | 227 | """
|
237 | 228 | # Core argspec
|
238 |
| - arg_names, spec_dict = self.argspec(self.body) |
239 |
| - # Obtain list of args + their default values (if any) in |
240 |
| - # declaration/definition order (i.e. based on getargspec()) |
241 |
| - tuples = [(x, spec_dict[x]) for x in arg_names] |
| 229 | + sig = self.argspec(self.body) |
242 | 230 | # Prime the list of all already-taken names (mostly for help in
|
243 | 231 | # choosing auto shortflags)
|
244 |
| - taken_names = {x[0] for x in tuples} |
| 232 | + taken_names = set(sig.parameters.keys()) |
245 | 233 | # Build arg list (arg_opts will take care of setting up shortnames,
|
246 | 234 | # etc)
|
247 | 235 | args = []
|
248 |
| - for name, default in tuples: |
249 |
| - new_arg = Argument(**self.arg_opts(name, default, taken_names)) |
| 236 | + for arg in sig.parameters.values(): |
| 237 | + new_arg = Argument( |
| 238 | + **self.arg_opts(arg.name, arg.default, taken_names) |
| 239 | + ) |
250 | 240 | args.append(new_arg)
|
251 | 241 | # Update taken_names list with new argument's full name list
|
252 | 242 | # (which may include new shortflags) so subsequent Argument
|
|
0 commit comments