Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using python3 keyword-only arguments or annotated arguments causes a ValueError from inspect #357

Closed
KuangEleven opened this issue Jun 4, 2016 · 39 comments · Fixed by vovanz/invoke#1 or strongholdprojects/invoke#1
Assignees

Comments

@KuangEleven
Copy link

KuangEleven commented Jun 4, 2016

Defining a task that uses the new Python3 keyword-only or annotation argument syntax and invoking causes a ValueError: Function has keyword-only arguments or annotations, use getfullargspec() API which can support them and halts.

Per the docstring for Python3 inspect, getfullargspec() could be used, but that function is itself deprecated in favor of inspect.signature() Unfortunately, neither of those functions exists in Python 2.6, so some level of cascading will be necessary.

For example, neither of these can be invoked:

from invoke import task

@task
def func(ctx, *args, keyonlyarg=42):
    print(keyonlyarg)

@task
def func2(ctx, x:int=3):
    print(x)
rsxm added a commit to rsxm/invoke that referenced this issue Aug 20, 2016
Use inspect.signature instead of inspect.getargspect when
using Python 3 to allow function annotations to be used in tasks.

Also adds TaskContextError and fix missing context in Collection test.

Fixes pyinvoke#357
rsxm added a commit to rsxm/invoke that referenced this issue Mar 10, 2017
Use inspect.signature instead of inspect.getargspect when
using Python 3 to allow function annotations to be used in tasks.

Also fix missing context in Collection test.

Fixes pyinvoke#357
rsxm added a commit to rsxm/invoke that referenced this issue Mar 10, 2017
Use inspect.signature instead of inspect.getargspect when
using Python 3 to allow function annotations to be used in tasks.

Also fix missing context in Collection test.

Fixes pyinvoke#357

Make Travis CI flake8 exit with 0

Revert adding custom exception
rsxm added a commit to rsxm/invoke that referenced this issue Jun 6, 2017
Use inspect.signature instead of inspect.getargspect when
using Python 3 to allow function annotations to be used in tasks.

Also fix missing context in Collection test.

Fixes pyinvoke#357

Make Travis CI flake8 exit with 0

Revert adding custom exception
@FranklinChen
Copy link

Any word on this? I use Python 3 with mypy for types and just realized that I'm blocked on using invoke right now.

@bitprophet
Copy link
Member

Thanks for the bump, I'll take a look at #373 soon which seems to be the main patch around this.

@FranklinChen If you can check out that PR's branch and confirm it works against latest master & solves your particular flavor of the issue, that'd be a big help!

@FranklinChen
Copy link

@bitprophet Yes, I am actually using the fork at #373 and it works for me.

@nathanielford
Copy link

Hi! I'm still seeing this with version 1.1.1.

nford@Nathaniels-MacBook-Pro:~/repos/deploy-apps $ invoke --list -v
Traceback (most recent call last):
  File "/usr/local/bin/invoke", line 11, in <module>
    sys.exit(program.run())
  File "/usr/local/lib/python3.6/site-packages/invoke/program.py", line 324, in run
    self.parse_collection()
  File "/usr/local/lib/python3.6/site-packages/invoke/program.py", line 408, in parse_collection
    self.load_collection()
  File "/usr/local/lib/python3.6/site-packages/invoke/program.py", line 599, in load_collection
    module, parent = loader.load(coll_name)
  File "/usr/local/lib/python3.6/site-packages/invoke/loader.py", line 76, in load
    module = imp.load_module(name, fd, path, desc)
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/imp.py", line 245, in load_module
    return load_package(name, filename)
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/imp.py", line 217, in load_package
    return _load(spec)
  File "<frozen importlib._bootstrap>", line 684, in _load
  File "<frozen importlib._bootstrap>", line 665, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/nmford/repos/deploy-apps/tasks/__init__.py", line 2, in <module>
    from tasks import install
  File "/Users/nmford/repos/deploy-apps/tasks/install.py", line 26, in <module>
    def domino(context, app: str):
  File "/usr/local/lib/python3.6/site-packages/invoke/tasks.py", line 313, in task
    return klass(args[0], **kwargs)
  File "/usr/local/lib/python3.6/site-packages/invoke/tasks.py", line 77, in __init__
    self.positional = self.fill_implicit_positionals(positional)
  File "/usr/local/lib/python3.6/site-packages/invoke/tasks.py", line 168, in fill_implicit_positionals
    args, spec_dict = self.argspec(self.body)
  File "/usr/local/lib/python3.6/site-packages/invoke/tasks.py", line 154, in argspec
    spec = inspect.getargspec(func)
  File "/usr/local/Cellar/python/3.6.5/Frameworks/Python.framework/Versions/3.6/lib/python3.6/inspect.py", line 1075, in getargspec
    raise ValueError("Function has keyword-only parameters or annotations"
ValueError: Function has keyword-only parameters or annotations, use getfullargspec() API which can support them

I am not sure what might need to be done to diagnose or resolve it from my end. Is anyone else still running into this?

@SantjagoCorkez
Copy link

@nathanielford Just run into this too. As it says in the exception itself, replaced inspect.getargspec at invoke/tasks.py with inspect.getfullargspec manually. Works for me.

@nathanielford
Copy link

@SantjagoCorkez I may not be understanding you correctly, but I don't think it'll work for us to manually fork the invoke library code. If I get a chance, though, I'll see if I can put a PR together to submit here, unless the maintainers already have one ready? I'm a little unclear on where they believe the state of the bug to be, given the merged PR.

@SantjagoCorkez
Copy link

@nathanielford Well, the bug is more than 2 years old at the moment. Developers still didn't apply a patch that is proposed by the exception message itself. So, they don't care. You're on your own.

@JesseTG
Copy link

JesseTG commented Nov 15, 2018

@bitprophet Hello? How's it going?

toejough added a commit to toejough/invoke that referenced this issue Jan 6, 2019
As mentioned in pyinvoke#357 - using `getfullargspec` as recommended in the error itself.
@toejough
Copy link

toejough commented Jan 6, 2019

@bitprophet - submitted a patch ^

Seems to be failing on 2.7 and pypy - I can work on adding some logic to fix that tomorrow.

@toejough
Copy link

toejough commented Jan 7, 2019

passing now.

@eruvanos
Copy link

Hi,
still facing this issue, is there any update?

@shatil
Copy link

shatil commented Aug 4, 2019

@italomaia and I encountered this using Fabric 2 with Python 3 (3.7.3 and 3.7.4 on Linux and macOS). Type hints in a Fabric task triggers ValueError complaining about getargspec: fabric/fabric#1997

Several seemingly-easy solutions presented themselves over the years (since 2016?!) in PRs mentioning this issue. To recap:

  1. Simplest is to use inspect.getfullargspec for Python 3: Use inspect.signature to enable Python 3 only features when defining tasks #373
    diff --git a/invoke/tasks.py b/invoke/tasks.py
    index ed838c31..2ed82a2e 100644
    --- a/invoke/tasks.py
    +++ b/invoke/tasks.py
    @@ -11,8 +11,10 @@ from .util import six
    
    if six.PY3:
        from itertools import zip_longest
    +    getargspec = inspect.getfullargspec
    else:
        from itertools import izip_longest as zip_longest
    +    getargspec = inspect.getargspec
    
    from .context import Context
    from .parser import Argument, translate_underscores
    @@ -151,7 +153,7 @@ class Task(object):
            # 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)
    +       spec = 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))
  2. Use invoke's vendored module called decorator, which already did the heavy lifting: Use inspect.signature to enable Python 3 only features when defining tasks #373 (comment)
    diff --git a/invoke/tasks.py b/invoke/tasks.py
    index ed838c31..eaf0b622 100644
    --- a/invoke/tasks.py
    +++ b/invoke/tasks.py
    @@ -4,7 +4,7 @@ generate new tasks.
    """
    
    from copy import deepcopy
    -import inspect
    +from .vendor import decorator
    import types
    
    from .util import six
    @@ -151,7 +151,7 @@ class Task(object):
            # 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)
    +       spec = decorator.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))
  3. Or, since clearly decorator has given this more thought, which was the original holdup for Use inspect.signature to enable Python 3 only features when defining tasks #373, just copy that block into invoke/task.py.

@lsorber
Copy link

lsorber commented Sep 8, 2020

@eruvanos We experimented with Typer as well, but missed the ability to run simple commands like invoke docs. With Typer, the equivalent would presumably look something like python -m tasks docs, which is much more verbose. Did you find a solution for that?

@D3f0
Copy link

D3f0 commented Sep 9, 2020

Typer is awesome, it leverages the type system and reduces the decorators you'd use with click. Although I agree with @lsorber about the simplicity Inovke provides, specially if your tasks can only depend on Python 3. In my experience, having something close to Inovke in Click/Typer would require to use https://github.com/amoffat/sh, and for some tasks can be tricky, for example, calling pytest with the flags you're using in CI.
For context, I found myself writing too either bash scripts or Makefile, or a combination of both for tasks like testing, building docs, building wheels, etc. Makefile is everywhere, but I'm OK with the trade off of having to install invoke in other developer machine.

Deimos added a commit to spectria/tildes that referenced this issue Oct 7, 2020
First invoke task: uses pip-compile to update the versions of all the
pip packages in requirements.txt and requirements-dev.txt. It also
post-processes the output file and removes any comments that have a "-r"
reference in them, since those currently cause Salt to break (and are
kind of redundant anyway).

Unfortunately, as part of writing this I discovered that invoke can't
handle type annotations in the definitions of its task functions, so I
had to exclude tasks.py from being checked by mypy. That makes me a
little nervous about whether invoke is still being maintained. Relevant
issue (over 4 years old): pyinvoke/invoke#357
@hangingman
Copy link

If I declare the variable 'c' as Context, I wish set the type. But, this problem prevent me to type-hinting...
I can't understand why this is not fixed.

@dralshehri
Copy link

dralshehri commented Apr 29, 2021

As a workaround for code completion to work in PyCharm,

I have to add type comments:

from invoke import context, task


@task
def hello(ctx, name):
    c = ctx  # type: context.Context
    c.run(f"echo 'Hello {name}'")

or sometimes I specify the type in docstrings:

from invoke import context, task


@task
def hello(c, name):
   """
   My docstring here.

   :type c: context.Context
   """
   c.run(f"echo 'Hello {name}'")

@coilysiren
Copy link

Heya, it looks like this still isn't fixed? Any idea what the blockers are? It's been years

@ghost
Copy link

ghost commented Sep 14, 2021

Python 2 doesn't support type hinting, So removing all the type hinting code will fix the issue.

@nathanielford
Copy link

@karan-webkul Python 2 is at EOL, and while everyone using this package already is avoiding using type hints because it does not support it, this issue is around the fact that we'd like to both use the package and type hints, like other modern Python 3 apps.

@davidalo
Copy link

@zelo 's workaround works pretty well, any update on this to avoid this workaround?

@emersonmx
Copy link

emersonmx commented Oct 2, 2021

I do something similar to what @dralshehri showed, but for the function.

from invoke import Context, task


@task
def run(c):
    # type: (Context) -> None
    c.run("python src/main.py")

@bitprophet
Copy link
Member

I'll revisit this after dropping Python 2, which is one of the next things on the roadmap. Should make it much easier to apply/fix overall.

@sbienkow-ninja
Copy link

I ran into this as well, and am currently using the following workaround:

from invoke import Context, task

def _mytask(ctx: Context, arg: str = 'foo') -> None:
    ctx.run('ls %s' % arg)

@task
def mytask(ctx, arg):
    _mytask(ctx, arg)

This gives me both invoke tasks and type-safety. But it is really cumbersome as the args need to be touched in 3 areas if they change. Can't wait to get this fix released wink crossed_fingers

Once this is released I can drop the delegator and just move the @task decorator to the real function.

@exhuma I decided to do something like this, just automatically. I created a wrapper around @task, which simply creates wrapper function with modified signature, where the annotations are removed. Since there's plan to add proper support for those tasks in invoke at some point, it should be easy to just replace it with from invoke import task instead and have it working.

import inspect
import invoke
import makefun
from typing import Optional


def task(*taskargs, **taskkwargs):
    def decorator(fun):
        """
        Define invoke.task, while removing annotations from the visible signature in order to work around invoke bug pyinvoke/invoke/357
        """

        # Copy over `fun` signature to `wrapper`
        # without annotations to work around https://github.com/pyinvoke/invoke/issues/357
        func_sig = inspect.Signature(
            [
                p.replace(annotation=inspect.Parameter.empty)
                for p in inspect.signature(fun).parameters.values()
            ]
        )

        @invoke.task(*taskargs, **taskkwargs)
        @makefun.wraps(fun, new_sig=func_sig)
        def wrapper(ctx, *args, **kwargs):
            return fun(ctx, *args, **kwargs)

        return wrapper

    if not taskkwargs and len(taskargs) == 1 and callable(taskargs[0]):
        # The decorator was used without any arguments
        fun = taskargs[0]
        taskargs = []
        return decorator(fun)
    return decorator


@task
def hello(ctx, name: str, surname: Optional[str] = None) -> None:
    """Greets you by your name and (optionally) surname"""
    if surname is None:
        print(f"Hello {name}!")
    else:
        print(f"Hello {name} {surname}!")

@task(default=True)
def hi(ctx) -> None:
    """Says Hi you :)"""
    print("Hi you")

Usage:

$ inv -l
Available tasks:

  hello   Greets you by your name and (optionally) surname
  hi      Says Hi you :)

Default task: hi

$ inv
Hi you

$ inv --help hello
Usage: inv[oke] [--core-opts] hello [--options] [other tasks here ...]

Docstring:
  Greets you by your name and (optionally) surname

Options:
  -n STRING, --name=STRING
  -s STRING, --surname=STRING
  
$ inv hello John
Hello John!

$ inv hello John --surname=Doe
Hello John Doe!

@Jma353
Copy link

Jma353 commented Feb 11, 2022

@bitprophet - have you folks started working on this support? I'd be interested in contributing this support if not!

@neozenith
Copy link

@bitprophet - I would also like to contribute. I have previously worked on vcrpy as a maintainer for a year and my new role is seeing me need invoke a lot and I would like to put the time and energy into breathing new life into keeping invoke relevant. I couldn't see a CONTRIBUTING.md to get a gist of who to contact about becoming a maintainer.

In particular I am coming from a few years over in Typescript where projects like lerna and npm workspaces coupled with the fact npm has scripts built into their package.json I feel this is lacking in the Python ecosystem.

poetry is about to launch v1.2.+ which allows plugins and I think combining poetry and invoke is a really awesome combination especially for multi-project mono-repos.

Especially as a consultant I can provide packages of prebaked tasks similar to the invocations project for our clients to get their dev workflows started.

If there is a gitter channel hit me up or DM me on LinkedIn or twitter.

@neozenith
Copy link

For those playing along at home I am now working on issue and PR triage to help catch up on the backlog that has accumulated.

https://bitprophet.org/projects/#roadmap
The big tasks that are being worked on are:

  • Migrating CI to Circle CI
  • Dropping Python2 support (that test matrix was getting wild)

Also invoke is part of the fabric and paramiko ecosystem and they need equal amounts of love too.

But I am doing my best over the coming weeks and months to help close the items that will have the greatest impact for our limited cognitive bandwidth and volunteer time.

@nnnewb
Copy link

nnnewb commented Sep 19, 2022

another workaround here.

monkey patch #373

import inspect
import six
import types
from invoke.tasks import Task, NO_DEFAULT

if six.PY3:
    from itertools import zip_longest
else:
    from itertools import izip_longest as zip_longest


def patched_argspec(self, body):
    """
    Returns two-tuple:
    * First item is list of arg names, in order defined.
        * 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).
    """
    # 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__
    if six.PY3:
        sig = inspect.signature(func)
        arg_names = [k for k, v in sig.parameters.items()]
        spec_dict = {}
        for k, v in sig.parameters.items():
            value = v.default if not v.default == sig.empty else NO_DEFAULT
            spec_dict.update({k: value})
    else:
        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:
        # 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


Task.argspec = types.MethodType(patched_argspec, Task)

save above code to hotfix.py and import it when you need.

import hotfix
from invoke import task,Context

@task
def hello(c: Context):
    c.run('echo Hello world!')

@adamcunnington-mlg
Copy link

This might also work:

F = TypeVar('F', bound=Callable)

def task(f: F) -> F:
    f.__annotations__ = {}
    return invoke.task(f)

Haven't tested it against anything but my current environment, so I don't know if this will break MyPy or other type checkers, etc.

  • @cahna how is this fix actually used? I tried invoke.task = task but that doesn't work because it's a decorator

@bitprophet
Copy link
Member

2.0.0 will be out soon & is now using inspect.signature!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet