Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[red-knot] Understand typing.Callable #16493

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open

Conversation

dhruvmanila
Copy link
Member

@dhruvmanila dhruvmanila commented Mar 4, 2025

Summary

Part of #15382

This PR implements a general callable type that wraps around a Signature and it uses that new type to represent typing.Callable.

It also implements Display support for Callable. The format is as:

([<arg name>][: <arg type>][ = <default type>], ...) -> <return type>

The / and * separators are added at the correct boundary for positional-only and keyword-only parameters. Now, as typing.Callable only has positional-only parameters, the rendered signature would be:

Callable[[int, str], None]
# (int, str, /) -> None

The / separator represents that all the arguments are positional-only.

The relationship methods that check assignability, subtype relationship, etc. are not yet implemented and will be done so as a follow-up.

Test Plan

Add test cases for display support for Signature and various mdtest for typing.Callable.

@dhruvmanila dhruvmanila added the red-knot Multi-file analysis & type inference label Mar 4, 2025
@dhruvmanila dhruvmanila force-pushed the dhruv/callable-type branch from e71d87b to dbae500 Compare March 5, 2025 08:44
@dhruvmanila dhruvmanila force-pushed the dhruv/callable-type branch from dbae500 to 05a7a5b Compare March 5, 2025 10:35
Copy link
Contributor

github-actions bot commented Mar 5, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool! Great work.

Comment on lines 33 to 34
# TODO: This should be `() -> Unknown` which requires understanding that `int` is not a param spec variable
reveal_type(c) # revealed: (*args: @Todo(todo signature *args), **kwargs: @Todo(todo signature **kwargs)) -> str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually think inferring Callable[int, str] (or Callable[42, str] below, or any similar malformation) as equivalent to Callable[..., str] (and of course also emitting an error) is probably better, since it will minimize cascading false positives. If you meant to say Callable[[int], str] but you accidentally said Callable[int, str], ideally we would not emit a false positive on every subsequent attempt to call that callable with one argument.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense. Just want to confirm that we still do want to keep the return type intact i.e., use Callable[..., str] and not Callable[..., Unknown] which is what I've pushed but happy to change it back to using Unknown.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer Callable[..., Unknown] here for the same reasons I stated in #16493 (comment) — it just seems like a simpler policy that involves less guesswork and that will be much easier to explain to users

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I'll also use Callable[..., Unknown] for type forms with more than 2 arguments for similar reasons and the fact that we're at least sure that it's a callable object.

Or, when it's a literal type:

```py
# error: [invalid-type-form] "The first argument to `typing.Callable` must be either a list of types, parameter specification, `typing.Concatenate`, or `...`"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "ParamSpec" is probably clearer than "parameter specification", since it's a term that can be easily searched in the typing spec / docs -- the latter could be understand to have a broader meaning

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I did a quick search on typing and Python docs and it seems that "parameter specification" would not give users much useful information. I'll change it to use typing.ParamSpec

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context, I mainly used this because ParamSpec itself cannot be used directly as type expression similar to TypeVar so suggesting ParamSpec could be slightly confused by whether to directly use ParamSpec v/s a variable that represents the parameter specification.

@@ -92,6 +101,23 @@ impl<'db> Parameters<'db> {
])
}

/// Return parameters that accepts any arguments i.e., `(*args: Any, **kwargs: Any)`
pub(crate) fn any() -> Self {
// TODO: Should this be represented using `(*Any, **Any)` instead?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yeah, maybe we should ideally omit the names here? Not sure how much fallout that change would have

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we've decided to use (...) -> Unknown I don't think this is relevant so I've avoided this.

@@ -2664,6 +2699,10 @@ impl<'db> Type<'db> {
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
KnownClass::WrapperDescriptorType.to_class_literal(db)
}
Type::Callable(CallableType::General(_)) => {
// TODO
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a small explanation of what needs to be done here or remove the comment if it's redundant to the todo_type description

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll expand the comment. I avoided here as it would be similar to todo_type but I still grep for TODO comments 😅

@dhruvmanila
Copy link
Member Author

dhruvmanila commented Mar 6, 2025

Post review changes

  • Use (...) -> Unknown consistently
  • Display ... for gradual form. This is done using an additional flag on Parameters to indicate the gradual form.
  • Use Any for gradual form while Unknown for invalid forms
  • Avoid special casing for param spec variable
  • Don't error on bare Callable, infer it as (...) -> Unknown
  • Remove various todos, add a couple more
  • Improve display handling of Signature

Regarding the relation implementation, I think I'd prefer to add it in a follow-up PR to keep the changes separate and easier to review.

@dhruvmanila dhruvmanila requested a review from carljm March 6, 2025 15:57
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
red-knot Multi-file analysis & type inference
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants