pallets / click

Python composable command line interface toolkit
https://click.palletsprojects.com
BSD 3-Clause "New" or "Revised" License
15.82k stars 1.4k forks source link

Make `click.Context` generic for `obj` #2493

Open Viicos opened 1 year ago

Viicos commented 1 year ago

To add some type safety to the obj attribute of the Context class, I was thinking maybe it could be made generic with respect to obj attribute. It could then be used this way (with a TypedDict but it could be any user defined object):

class ContextObj(TypedDict):
    attr: int

def subcommand(
    ctx: click.Context[ContextObj],
    ...
) -> None:
    reveal_type(ctx.obj["attr"])  # Revealed type is "int"

That might be a bit cumbersome, so I'd understand if this is rejected (but I'm open to alternatives). Otherwise, I will be happy to implement this

thehale commented 11 months ago

I would welcome a generic as described by the OP. However, I would discourage allowing "any user defined object" since the click docs clearly indicate that ctx.obj is a dict type. As such, I wouldn't want the ability for ctx.obj to be, for example, a list.


Alternatively, the typing could allow any subclass of click.Context. That way code like the following could be used to define custom context schemas.

import click
from typing import TypedDict

class MyContextObj(TypedDict):
  foo: int
  bar: str

class MyContext(click.Context)
  obj: MyContextObj

@click.command()
@click.pass_context
def my_command(ctx: MyContext):
  ctx.obj # Type checkers can tell that this has keys `foo: int` and `bar: str`

Unfortunately, this code currently throws the following typing error when checked by mypy:

Argument 1 to "pass_context" has incompatible type "Callable[[MyContext, Iterable[str]], Any]"; expected "Callable[[Context, Iterable[str]], Any]"
Viicos commented 11 months ago

Pleasantly surprised to see some people are also looking for this. I'll look into this this week and will come with a PR that will hopefully be accepted.

Viicos commented 10 months ago

Unfortunately, this code currently throws the following typing error when checked by mypy:

This can be fixed by using a TypeVar in the pass_context signature, however I don't think this is a great idea, as this isn't type safe: at runtime, ctx is still an instance of Context.

the click docs clearly indicate that ctx.obj is a dict type.

I couldn't find anything stating this. Do you know in which section of the docs this is described? The type hint for obj is Any, so I think you can allow any object.

thehale commented 10 months ago

the click docs clearly indicate that ctx.obj is a dict type.

I couldn't find anything stating this. Do you know in which section of the docs this is described? The type hint for obj is Any, so I think you can allow any object.

You appear to be correct. I was looking at the code example for Nested Handling and Contexts which includes an assertion that ctx.obj is a dict. That said, upon closer look it appears that one could assert that ctx.obj is indeed any type.