Skip to content

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

@danieleades

Description

@danieleades

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions