click icon indicating copy to clipboard operation
click copied to clipboard

`decorators.pyi` interface file?

Open dwreeves opened this issue 4 months ago • 3 comments

Background

In rich-click 1.9.0.dev1, I added a decorators.pyi (link) that enables typed args and-- critically for the developer experience-- tab completion for IDEs.

So in rich-click, @click.option() (and other decorators) still take in arbitrary **kwargs, but as far as type-checking and the overall developer experience in a modern IDE is concerned, the experience is vastly improved:

image

context_settings=... is an especially opaque and benefits tremendously from a TypedDict:

image

The ask

I've found this to be an incredibly useful change that vastly improves the developer experience, but I also feel like it should be something in base Click? It feels odd to add this additional functionality that's not in base click and is orthogonal to the purpose of rich-click, and yet it still feels like a necessity to have for modern development.

I'm posting this here because I am wondering if you are interested in a contribution to this effect. I don't see any open issues relating to this. I would be willing to contribute this, especially since I've done a lot of this work already.

Additional considerations

  • I think it is both a rare use case, and a violation of the Liskov substitution principle, to create a subclass of Command, Option, etc. that does not accept all the parent class's args/kwargs. Additionally, if a user does this, then worst case scenario is they can just ignore their IDE's auto-completion, and the static type-checker also shouldn't get upset by it either.
  • context_settings takes in an arbitrary dict, but in rich-click's decorators.pyi, we use a TypedDict. I don't think it's common for developers to subclass Context, but it's certainly possible, and Context will often take in additional kwargs. Thus, if not handled properly, a TypedDict could cause a static type checker to fail when it shouldn't. Additionally, PEP 728 is a long way's away.
    • That said, there is a simple solution: @overload the command and group decorators' context_settings to take in both a TypedDict and a dict[str, Any]; by placing the TypedDict as the first overload, PyCharm autocompletes correctly (this is untested with VSCode). as long as cls=Command or cls=None then the TypedDict is appropriate; otherwise for any arbitrary subclass of Command then an arbitrary dict[str, Any] is the appropriate type.
  • core.py would also strongly benefit from its own core.pyi because of the Group.command and Group.group decorators.

dwreeves avatar Aug 21 '25 03:08 dwreeves

I don't want to add pyi files, from experience in Werkzeug they are too hard to maintain and keep in sync.

I do want to stop using kwargs and start typing them all out even though it will be verbose. I don't necessarily want to do that right this instant though, as there are other things going on with signatures right now that need to settle.

davidism avatar Aug 21 '25 03:08 davidism

For subclassing and type dicts, ultimately we should be using generics that specify their component classes. But that's another thing that will have to happen a little later than now. And it's really complicated, I've tried to do it a few times in Werkzeug and Flask and realized how pervasive it becomes.

davidism avatar Aug 21 '25 04:08 davidism

I'm pretty sure there have been requests to the typing spec for a general way to copy signatures, but they haven't gone forward. That would be a good place to work to fully solve this problem.

davidism avatar Aug 21 '25 04:08 davidism