Skip to content

Commit

Permalink
Replace inspect.getargspec with inspect.signature
Browse files Browse the repository at this point in the history
FINALLY
  • Loading branch information
bitprophet committed Dec 19, 2022
1 parent b2fcb98 commit 87f9504
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 39 deletions.
66 changes: 28 additions & 38 deletions invoke/tasks.py
Expand Up @@ -17,10 +17,6 @@
from itertools import izip_longest as zip_longest


#: Sentinel object representing a truly blank value (vs ``None``).
NO_DEFAULT = object()


class Task(object):
"""
Core object representing an executable task & its argument specification.
Expand Down Expand Up @@ -134,45 +130,40 @@ def called(self):

def argspec(self, body):
"""
Returns two-tuple:
* First item is list of arg names, in order defined.
Returns a modified `inspect.Signature` based on that of ``body``.
* I.e. we *cannot* simply use a dict's ``keys()`` method here.
* Second item is dict mapping arg names to default values or
`.NO_DEFAULT` (an 'empty' value distinct from None, since None
is a valid value on its own).
:returns:
an `inspect.Signature` matching that of ``body``, but with the
initial context argument removed.
:raises TypeError:
if the task lacks an initial positional `.Context` argument.
.. versionadded:: 1.0
.. versionchanged:: 2.0
Changed from returning a two-tuple of ``(arg_names, spec_dict)`` to
returning an `inspect.Signature`.
"""
# Handle callable-but-not-function objects
# TODO: __call__ exhibits the 'self' arg; do we manually nix 1st result
# in argspec, or is there a way to get the "really callable" spec?
func = body if isinstance(body, types.FunctionType) else body.__call__
spec = inspect.getargspec(func)
arg_names = spec.args[:]
matched_args = [reversed(x) for x in [spec.args, spec.defaults or []]]
spec_dict = dict(zip_longest(*matched_args, fillvalue=NO_DEFAULT))
# Pop context argument
try:
context_arg = arg_names.pop(0)
except IndexError:
# Rebuild signature with first arg dropped, or die usefully(ish trying
sig = inspect.signature(func)
params = list(sig.parameters.values())
# TODO: this ought to also check if an extant 1st param _was_ a Context
# arg, and yell similarly if not.
if not len(params):
# TODO: see TODO under __call__, this should be same type
raise TypeError("Tasks must have an initial Context argument!")
del spec_dict[context_arg]
return arg_names, spec_dict
return sig.replace(parameters=params[1:])

def fill_implicit_positionals(self, positional):
args, spec_dict = self.argspec(self.body)
# If positionals is None, everything lacking a default
# value will be automatically considered positional.
if positional is None:
positional = []
for name in args: # Go in defined order, not dict "order"
default = spec_dict[name]
if default is NO_DEFAULT:
positional.append(name)
positional = [
x.name
for x in self.argspec(self.body).parameters.values()
if x.default is inspect.Signature.empty
]
return positional

def arg_opts(self, name, default, taken_names):
Expand Down Expand Up @@ -206,7 +197,7 @@ def arg_opts(self, name, default, taken_names):
break
opts["names"] = names
# Handle default value & kind if possible
if default not in (None, NO_DEFAULT):
if default not in (None, inspect.Signature.empty):
# TODO: allow setting 'kind' explicitly.
# NOTE: skip setting 'kind' if optional is True + type(default) is
# bool; that results in a nonsensical Argument which gives the
Expand Down Expand Up @@ -235,18 +226,17 @@ def get_arguments(self, ignore_unknown_help=None):
Added the ``ignore_unknown_help`` kwarg.
"""
# Core argspec
arg_names, spec_dict = self.argspec(self.body)
# Obtain list of args + their default values (if any) in
# declaration/definition order (i.e. based on getargspec())
tuples = [(x, spec_dict[x]) for x in arg_names]
sig = self.argspec(self.body)
# Prime the list of all already-taken names (mostly for help in
# choosing auto shortflags)
taken_names = {x[0] for x in tuples}
taken_names = set(sig.parameters.keys())
# Build arg list (arg_opts will take care of setting up shortnames,
# etc)
args = []
for name, default in tuples:
new_arg = Argument(**self.arg_opts(name, default, taken_names))
for arg in sig.parameters.values():
new_arg = Argument(
**self.arg_opts(arg.name, arg.default, taken_names)
)
args.append(new_arg)
# Update taken_names list with new argument's full name list
# (which may include new shortflags) so subsequent Argument
Expand Down
8 changes: 8 additions & 0 deletions sites/www/changelog.rst
Expand Up @@ -2,6 +2,14 @@
Changelog
=========

- :suppport:`-` `Task.argspec <invoke.tasks.Task.argspec>` has changed its
return value; it now returns an `inspect.Signature` derived from that of the
task's body callable.

.. warning::
This change is backwards incompatible if you were using this method
directly.

- :release:`1.7.3 <2022-09-30>`
- :support:`- backported` Fix a non-fatal bug in our setup.py
``long_description`` generation causing 1.7.0-1.7.2 to have malformed
Expand Down
2 changes: 1 addition & 1 deletion tests/collection.py
Expand Up @@ -305,7 +305,7 @@ def prefers_task_name_attr_over_function_name(self):
def raises_ValueError_if_no_name_found(self):
# Can't use a lambda here as they are technically real functions.
class Callable(object):
def __call__(self):
def __call__(self, ctx):
pass

with raises(ValueError):
Expand Down

0 comments on commit 87f9504

Please sign in to comment.