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

AttributeError: 'xxxx' object has no attribute '__code__' #191

Closed
epenet opened this issue May 26, 2021 · 4 comments · Fixed by #190
Closed

AttributeError: 'xxxx' object has no attribute '__code__' #191

epenet opened this issue May 26, 2021 · 4 comments · Fixed by #190

Comments

@epenet
Copy link
Contributor

epenet commented May 26, 2021

Describe the bug
When using https://github.com/pallets/click, the @click.group() function decorator return a class instance (click.core.<Group xyz>) which does not have associated code. This causes Typeguard to fail with AttributeError: 'Group' object has no attribute '__code__'.

To Reproduce
Failure can be seen on https://github.com/hacf-fr/renault-api/runs/2664117342.
I'm working on a smaller reproducible example

Expected behavior
Should pass

Additional context
pallets/click#1927

@epenet
Copy link
Contributor Author

epenet commented May 26, 2021

I think the issue is that the @click.group() decorator effectively replaces a function (xxx) with a class instance and an associated callback (<Group xxx>)

Is it an issue with the pytest plugin? with the import hook?
I tried to add @typeguard_ignore but it didn't change a thing.

Note: I use the pytest plugin with pytest --typeguard-packages=renault_api

@cjolowicz
Copy link
Contributor

cjolowicz commented Jun 1, 2021

@agronholm I can confirm that #190 fixes this.

Here's a failing test to demonstrate the issue:

# tests/test_typeguard.py
class TestTypeChecker:
    ...
    def test_callable(self):
        class command:
            # we need an __annotations__ attribute to trigger the code path
            whatever: float

            def __init__(self, function: Callable[[int], int]):
                self.function = function

            def __call__(self, arg: int) -> None:
                self.function(arg)

        @typechecked
        @command
        def function(arg: int) -> None:
            pass

        function(1)

@epenet Feel free to include this in your PR.

Some additional info:

  • This bug is triggered by click >= 8.0 because click.Command now has an __annotations__ attribute. Before, typeguard would just bail out with a warning because this attribute was missing from the callable.
  • Typeguard's import hook inspects the AST to find function definitions that should be instrumented. So it considers a function decorated by @click.command fair game. typechecked will then use inspect.unwrap to get to a function with a __code__ attribute. However, click does not set the __wrapped__ attribute on click.Command instances, so unwrapping leads nowhere.
  • Using functools.wraps for click's decorators makes little sense, because the returned click.Command instance is used in ways other than just calling it, e.g. in click.testing.CliRunner, which would therefore break under Typeguard. The type signature of the callable is also quite different from that of the wrapped function, so this would produce a false positive in Typeguard even when it's just used as a callable.

@cjolowicz
Copy link
Contributor

Here's a temporary workaround for the issue:

try:
    import typeguard  # type: ignore[import]
except ImportError:
    pass
else:  # pragma: no cover
    from typing import no_type_check

    @no_type_check
    def _patched_typechecked(*args, **kwargs):
        if args and isinstance(args[0], click.Command):
            return args[0]
        return typeguard_typechecked(*args, **kwargs)

    typeguard_typechecked = typeguard.typechecked
    typeguard.typechecked = _patched_typechecked

Not exactly beautiful... I literally found no better way to do this 😞

@epenet
Copy link
Contributor Author

epenet commented Jun 2, 2021

Thanks @cjolowicz I've updated the PR

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

Successfully merging a pull request may close this issue.

2 participants