Search code examples
pythonmypy

How to fix mypy error when using click's pass_context


I am using click to build a command line application. I am using mypy for type checking.

However, passing a context to a function using @pass_context works as expected, but mypy fails with the error:

error: Argument 1 to "print_or_exit" has incompatible type "str"; expected "Context"

and I don't get why. Below is the MWE to reproduce this mypy error:

import click
from typing import Optional

@click.pass_context
def print_or_exit(ctx: click.Context, some_txt: Optional[str] = "") -> None:
    if ctx.params.get("exit_", False):
        exit(1)
    print(some_txt)

@click.command(context_settings=dict(help_option_names=["-h", "--help"]))
@click.option("--exit","-e", "exit_", is_flag=True, help="exit")
@click.pass_context
def main(ctx: click.Context, exit_: bool) -> None:
    print_or_exit("bla")


if __name__ == "__main__":
    main()

Running the script using the argument -e, then the script exists, without printing to the terminal; when omitting -e, the script prints to the terminal, therefore everything works as expected.

So, why does mypy fail?


Solution

  • I've looked to the sources of click, decorators.py

    F = t.TypeVar("F", bound=t.Callable[..., t.Any])
    FC = t.TypeVar("FC", t.Callable[..., t.Any], Command)
    
    
    def pass_context(f: F) -> F:
        """Marks a callback as wanting to receive the current context
        object as first argument.
        """
    
        def new_func(*args, **kwargs):  # type: ignore
            return f(get_current_context(), *args, **kwargs)
    
        return update_wrapper(t.cast(F, new_func), f)
    

    So, function pass_context return same type (-> F) , that receives in arguments (f: F). So, mypy expects, that you pass two arguments in print_or_exit.

    I think the best solution would pass ctx explicitly wherever you can. The benefit of this -- you can easily mock ctx in your test for print_or_exit function. So, I suggest this code:

    import click
    from typing import Optional
    
    def print_or_exit(ctx: click.Context, some_txt: Optional[str] = "") -> None:
        if ctx.params.get("exit_", False):
            exit(1)
        print(some_txt)
    
    @click.command(context_settings=dict(help_option_names=["-h", "--help"]))
    @click.option("--exit","-e", "exit_", is_flag=True, help="exit")
    @click.pass_context
    def main(ctx: click.Context, exit_: bool) -> None:
        print_or_exit(ctx, "bla")
    
    
    if __name__ == "__main__":
        main()
    

    It works as expected and passes mypy