tomerfiliba / plumbum

Plumbum: Shell Combinators
https://plumbum.readthedocs.io
MIT License
2.81k stars 183 forks source link

`plumbum.cli` doesn't play nicely with decorators and `__future__.annotations` #675

Open danieleades opened 7 months ago

danieleades commented 7 months ago

the copier project is using a decorator to wrap error handling around some plumbum methods, like so-

see https://github.com/copier-org/copier/blob/5e98d21f35767de527c64da371045f4f018fa619/copier/cli.py

from decorator import decorator

@decorator
def handle_exceptions(method, *args, **kwargs):
    """Handle keyboard interruption while running a method."""
    try:
        try:
            return method(*args, **kwargs)
        except KeyboardInterrupt:
            raise UserMessageError("Execution stopped by user")
    except UserMessageError as error:
        print(colors.red | "\n".join(error.args), file=sys.stderr)
        return 1
    except UnsafeTemplateError as error:
        print(colors.red | "\n".join(error.args), file=sys.stderr)
        # DOCS https://github.com/copier-org/copier/issues/1328#issuecomment-1723214165
        return 0b100

this decorator is then applied to plumbum.cli subcommand methods.

This works fine- the @decorator decorator ensures the wrapped method's signature doesn't change, so plumbum.cli's introspection still works. There are two downsides

  1. @decorator is untyped
  2. @decorator breaks if you add from __future__ import annotations to the file

I tried a different approach using a native decorator:

def _handle_exceptions(method: Callable[P, int]) -> Callable[P, int]:
    @functools.wraps(method)
    def inner(*args: P.args, **kwargs: P.kwargs) -> int:
        try:
            try:
                return method(*args, **kwargs)
            except KeyboardInterrupt:
                raise UserMessageError("Execution stopped by user")
        except UserMessageError as error:
            print(colors.red | "\n".join(error.args), file=sys.stderr)
            return 1
        except UnsafeTemplateError as error:
            print(colors.red | "\n".join(error.args), file=sys.stderr)
            # DOCS https://github.com/copier-org/copier/issues/1328#issuecomment-1723214165
            return 0b100

    return inner

but this fails, presumably because plumbum.cli is relying on some element of the method signature that functools.wraps fails to forward.

I tried instead-

# ...
inner.__signature = inspect.signature(method)

return inner

that works, unless you add from __future__ import annotations, because that import changes the behaviour of inspect.signature (see https://bugs.python.org/issue43355)

i'm guessing that's the exact issue that was causing me problems at the beginning of this journey...

How should I proceed?

danieleades commented 7 months ago

ultimately i found a way around by avoiding decorators entirely, but it would be nice if there was some further consideration given to the interaction between plumbum.cli, decorators, and from __future__ import annotations.

see https://github.com/copier-org/copier/pull/1513