-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Infer typevar specializations for Callable types
#21551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-12-07 20:02:21.310268039 +0000
+++ new-output.txt 2025-12-07 20:02:25.031281607 +0000
@@ -150,7 +150,8 @@
callables_protocol.py:169:7: error[invalid-assignment] Object of type `def cb8_bad1(x: int) -> Any` is not assignable to `Proto8`
callables_protocol.py:186:5: error[invalid-assignment] Object of type `Literal["str"]` is not assignable to attribute `other_attribute` of type `int`
callables_protocol.py:187:5: error[unresolved-attribute] Unresolved attribute `xxx` on type `Proto9[P@decorator1, R@decorator1]`.
-callables_protocol.py:197:7: error[unresolved-attribute] Object of type `Proto9[(x: int), Unknown] | Proto9[Divergent, Unknown]` has no attribute `other_attribute2`
+callables_protocol.py:197:7: error[unresolved-attribute] Object of type `Proto9[(x: int), str] | Proto9[(x: Never), Unknown]` has no attribute `other_attribute2`
+callables_protocol.py:199:10: error[invalid-argument-type] Argument to bound method `__call__` is incorrect: Expected `Never`, found `Literal[3]`
callables_protocol.py:238:8: error[invalid-assignment] Object of type `def cb11_bad1(x: int, y: str, /) -> Any` is not assignable to `Proto11`
callables_protocol.py:260:8: error[invalid-assignment] Object of type `def cb12_bad1(*args: Any, *, kwarg0: Any) -> None` is not assignable to `Proto12`
callables_protocol.py:284:27: error[invalid-assignment] Object of type `def cb13_no_default(path: str) -> str` is not assignable to `Proto13_Default`
@@ -236,37 +237,42 @@
constructors_call_type.py:59:9: error[too-many-positional-arguments] Too many positional arguments to bound method `__init__`: expected 1, got 2
constructors_call_type.py:81:5: error[missing-argument] No argument provided for required parameter `y` of function `__new__`
constructors_call_type.py:82:12: error[invalid-argument-type] Argument to function `__new__` is incorrect: Expected `str`, found `Literal[2]`
-constructors_callable.py:36:13: info[revealed-type] Revealed type: `(x: int) -> Unknown`
-constructors_callable.py:37:1: error[type-assertion-failure] Type `Class1` does not match asserted type `Unknown`
+constructors_callable.py:36:13: info[revealed-type] Revealed type: `(x: int) -> Class1`
constructors_callable.py:38:1: error[missing-argument] No argument provided for required parameter `x`
constructors_callable.py:39:1: error[missing-argument] No argument provided for required parameter `x`
constructors_callable.py:39:4: error[unknown-argument] Argument `y` does not match any known parameter
-constructors_callable.py:49:13: info[revealed-type] Revealed type: `() -> Unknown`
-constructors_callable.py:50:1: error[type-assertion-failure] Type `Class2` does not match asserted type `Unknown`
+constructors_callable.py:49:13: info[revealed-type] Revealed type: `() -> Class2`
constructors_callable.py:51:4: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 1
constructors_callable.py:57:42: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `Self@__new__`
-constructors_callable.py:63:13: info[revealed-type] Revealed type: `(...) -> Unknown`
-constructors_callable.py:64:1: error[type-assertion-failure] Type `Class3` does not match asserted type `Unknown`
+constructors_callable.py:62:23: error[invalid-argument-type] Argument to function `accepts_callable` is incorrect: Expected `() -> Class3`, found `<class 'Class3'>`
+constructors_callable.py:63:13: info[revealed-type] Revealed type: `() -> Class3`
+constructors_callable.py:64:16: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 1
+constructors_callable.py:66:4: error[unknown-argument] Argument `y` does not match any known parameter
+constructors_callable.py:67:4: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 2
constructors_callable.py:73:33: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `int`
-constructors_callable.py:77:13: info[revealed-type] Revealed type: `(x: int) -> Unknown`
-constructors_callable.py:78:1: error[type-assertion-failure] Type `int` does not match asserted type `Unknown`
+constructors_callable.py:77:13: info[revealed-type] Revealed type: `(x: int) -> int`
constructors_callable.py:79:1: error[missing-argument] No argument provided for required parameter `x`
constructors_callable.py:80:1: error[missing-argument] No argument provided for required parameter `x`
constructors_callable.py:80:4: error[unknown-argument] Argument `y` does not match any known parameter
-constructors_callable.py:97:13: info[revealed-type] Revealed type: `(...) -> Unknown`
-constructors_callable.py:100:5: error[type-assertion-failure] Type `Never` does not match asserted type `Unknown`
-constructors_callable.py:105:5: error[type-assertion-failure] Type `Never` does not match asserted type `Unknown`
-constructors_callable.py:125:13: info[revealed-type] Revealed type: `() -> Unknown`
-constructors_callable.py:126:1: error[type-assertion-failure] Type `Class6Proxy` does not match asserted type `Unknown`
+constructors_callable.py:97:13: info[revealed-type] Revealed type: `() -> Never`
+constructors_callable.py:105:20: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 1
+constructors_callable.py:105:23: error[unknown-argument] Argument `x` does not match any known parameter
+constructors_callable.py:125:13: info[revealed-type] Revealed type: `() -> Class6Proxy`
constructors_callable.py:127:4: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 1
-constructors_callable.py:142:13: info[revealed-type] Revealed type: `(...) -> Unknown`
-constructors_callable.py:162:5: info[revealed-type] Revealed type: `Overload[(x: int) -> Unknown, (x: str) -> Unknown]`
-constructors_callable.py:164:1: error[type-assertion-failure] Type `Class7[int]` does not match asserted type `Unknown`
-constructors_callable.py:165:1: error[type-assertion-failure] Type `Class7[str]` does not match asserted type `Unknown`
-constructors_callable.py:182:13: info[revealed-type] Revealed type: `(x: list[Unknown], y: list[Unknown]) -> Unknown`
-constructors_callable.py:183:1: error[type-assertion-failure] Type `Class8[str]` does not match asserted type `Unknown`
-constructors_callable.py:193:13: info[revealed-type] Revealed type: `(x: list[T@__init__], y: list[T@__init__]) -> Unknown`
-constructors_callable.py:194:1: error[type-assertion-failure] Type `Class9` does not match asserted type `Unknown`
+constructors_callable.py:141:27: error[invalid-argument-type] Argument to function `accepts_callable` is incorrect: Expected `() -> Class6Any`, found `<class 'Class6Any'>`
+constructors_callable.py:142:13: info[revealed-type] Revealed type: `() -> Class6Any`
+constructors_callable.py:143:1: error[type-assertion-failure] Type `Any` does not match asserted type `Class6Any`
+constructors_callable.py:144:8: error[too-many-positional-arguments] Too many positional arguments: expected 0, got 1
+constructors_callable.py:162:5: info[revealed-type] Revealed type: `Overload[(x: int) -> Class7[int] | Class7[str], (x: str) -> Class7[int] | Class7[str]]`
+constructors_callable.py:164:1: error[type-assertion-failure] Type `Class7[int]` does not match asserted type `Class7[int] | Class7[str]`
+constructors_callable.py:165:1: error[type-assertion-failure] Type `Class7[str]` does not match asserted type `Class7[int] | Class7[str]`
+constructors_callable.py:182:13: info[revealed-type] Revealed type: `(x: list[T@Class8], y: list[T@Class8]) -> Class8[T@Class8]`
+constructors_callable.py:183:1: error[type-assertion-failure] Type `Class8[str]` does not match asserted type `Class8[T@Class8]`
+constructors_callable.py:183:16: error[invalid-argument-type] Argument is incorrect: Expected `list[T@Class8]`, found `list[Unknown | str]`
+constructors_callable.py:183:22: error[invalid-argument-type] Argument is incorrect: Expected `list[T@Class8]`, found `list[Unknown | str]`
+constructors_callable.py:184:4: error[invalid-argument-type] Argument is incorrect: Expected `list[T@Class8]`, found `list[Unknown | int]`
+constructors_callable.py:184:9: error[invalid-argument-type] Argument is incorrect: Expected `list[T@Class8]`, found `list[Unknown | str]`
+constructors_callable.py:193:13: info[revealed-type] Revealed type: `(x: list[T@__init__], y: list[T@__init__]) -> Class9`
constructors_callable.py:194:16: error[invalid-argument-type] Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown | str]`
constructors_callable.py:194:22: error[invalid-argument-type] Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown | str]`
constructors_callable.py:195:4: error[invalid-argument-type] Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown | int]`
@@ -305,6 +311,7 @@
dataclasses_transform_converter.py:25:6: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `T@model_field`
dataclasses_transform_converter.py:48:31: error[invalid-argument-type] Argument to function `model_field` is incorrect: Expected `(Unknown, /) -> Unknown`, found `def bad_converter1() -> int`
dataclasses_transform_converter.py:49:31: error[invalid-argument-type] Argument to function `model_field` is incorrect: Expected `(Unknown, /) -> Unknown`, found `def bad_converter2(*, x: int) -> int`
+dataclasses_transform_converter.py:104:30: error[invalid-assignment] Object of type `dataclasses.Field[dict[str, str] | dict[_KT@dict, _VT@dict]]` is not assignable to `dict[str, str]`
dataclasses_transform_converter.py:107:8: error[invalid-argument-type] Argument is incorrect: Expected `int`, found `Literal["f1"]`
dataclasses_transform_converter.py:107:14: error[invalid-argument-type] Argument is incorrect: Expected `int`, found `Literal["f2"]`
dataclasses_transform_converter.py:107:20: error[invalid-argument-type] Argument is incorrect: Expected `ConverterClass`, found `Literal[b"f3"]`
@@ -407,7 +414,7 @@
enums_member_values.py:96:1: error[type-assertion-failure] Type `int` does not match asserted type `Unknown`
enums_members.py:82:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `Unknown | ((x) -> Unknown)`
enums_members.py:82:37: error[invalid-type-form] Type arguments for `Literal` must be `None`, a literal value (int, bool, str, or bytes), or an enum member
-enums_members.py:83:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `Unknown | ((x: int) -> Unknown)`
+enums_members.py:83:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `Unknown | ((x: int) -> int)`
enums_members.py:83:37: error[invalid-type-form] Type arguments for `Literal` must be `None`, a literal value (int, bool, str, or bytes), or an enum member
enums_members.py:84:1: error[type-assertion-failure] Type `Unknown` does not match asserted type `property`
enums_members.py:84:35: error[invalid-type-form] Type arguments for `Literal` must be `None`, a literal value (int, bool, str, or bytes), or an enum member
@@ -1030,4 +1037,4 @@
typeddicts_usage.py:28:17: error[missing-typed-dict-key] Missing required key 'name' in TypedDict `Movie` constructor
typeddicts_usage.py:28:18: error[invalid-key] Unknown key "title" for TypedDict `Movie`: Unknown key "title"
typeddicts_usage.py:40:24: error[invalid-type-form] The special form `typing.TypedDict` is not allowed in type expressions
-Found 1032 diagnostics
+Found 1039 diagnostics
|
|
965a9f8 to
dbecc68
Compare
Callable return typesCallable types
7995e43 to
f89ec1a
Compare
| return fn(value) | ||
|
|
||
| def identity(x: T) -> T: | ||
| def identity(x: T, /) -> T: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting nuance: Callable[[A], B] creates a signature containing positional-only parameters, so we have to make the signature here positional-only as well to make identity pass the assignability check for it to be a valid argument to the fn parameter.
(That said, I'm not convinced that's...correct? Are we mixing up the ordering of the assignability check operands somewhere?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this should work, it does when there aren't any type variables involved (https://play.ty.dev/1663d70f-4daa-492f-84d7-c35c9009fbba):
from typing import Callable
def f(c: Callable[[int], int]): ...
def c(a: int) -> int: return 1
f(c)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I looked into this a bit more and what I'm seeing is that:
In infer_map_impl, the formal signature is Callable[[A], B] and the actual signature is the identity function and you invoke formal_signature.when_constraint_set_assignable_to(..., actual_signature, ...) which means we're checking when is Callable[[A], B] assignable to identity function which leads to checking when is the positional-only parameter (A) is assignable to positional-or-keyword parameter (a: int) which is never. This is because you cannot substitute a callable that takes a positional-or-keyword parameter with a callable that takes a positional-only parameter (https://play.ty.dev/7b31e7a0-19c8-4d2b-8bf6-507764372c0d).
This can be tested using Protocol to define a callable that takes a positional-or-keyword parameter:
from typing import Protocol
class PositionalOrKeyword(Protocol):
def __call__(self, a: int) -> None: ...
def positional_only(a: int, /) -> None: ...
def positional_or_keyword(a: int) -> None: ...
def test(c: PositionalOrKeyword) -> None:
c(a=1)Calling test using positional_only would fail at runtime.
| # TODO: this should be `Unknown | int` | ||
| reveal_type(invoke(head, [1, 2, 3])) # revealed: Unknown |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This TODO is not also removed because we end up inferring this constraint set when comparing head to Callable[[A], B]:
(B@invoke ≤ T@head) ∧ (list[T@head] ≤ A@invoke)
We then try to remove T@head from the constraint set by calculating
∃T@head ⋅ (B@invoke ≤ T@head) ∧ (list[T@head] ≤ A@invoke)
We should be able to pick T@head = B@invoke and simplify that to
(B@invoke = *) ∧ (list[B@invoke] ≤ A@invoke)
which I think would then be enough to propagate through the return type to discharge this TODO. I think this would require adding more derived facts to the sequent map.
|
|
||
| x12: Y[Y[Literal[1]]] = [[1]] | ||
| reveal_type(x12) # revealed: list[Y[Literal[1]]] | ||
| reveal_type(x12) # revealed: list[list[Literal[1]]] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is because we're now using specialize_recursive instead of specialize_partial.
| let when = formal_signature.when_constraint_set_assignable_to( | ||
| self.db, | ||
| actual_signature, | ||
| self.inferable, | ||
| ); | ||
| when.for_each_path(self.db, |path| { | ||
| for constraint in path.positive_constraints() { | ||
| let typevar = constraint.typevar(self.db); | ||
| let lower = constraint.lower(self.db); | ||
| let upper = constraint.upper(self.db); | ||
| if !upper.is_object() { | ||
| self.add_type_mapping(typevar, upper, polarity, &mut f); | ||
| } | ||
| if let Type::TypeVar(lower_bound_typevar) = lower { | ||
| self.add_type_mapping( | ||
| lower_bound_typevar, | ||
| Type::TypeVar(typevar), | ||
| polarity, | ||
| &mut f, | ||
| ); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the meat of the change. We use the new ConstraintAssignability type relation here to calculate a constraint set describing when the two callables are assignable. That will recurse into any typevars in the callables, and find whatever constraint set allows them to unify.
We then use this new for_each_path method to find each way that constraint set can be satisfied, and add new typevar bindings to the old solver for whatever we find.
| result.intersect( | ||
| db, | ||
| ConstraintSet::from( | ||
| relation.is_assignability() || relation.is_constraint_set_assignability(), | ||
| ), | ||
| ); | ||
| return result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to combine this with result to hang on to any typevar mapping our constraint set has recorded for the return type comparison above. That's need to support something like
Callable[[int], int] ≤ Callable[..., T]
and have it return int ≤ T.
| // A typevar satisfies a relation when...it satisfies the relation. Yes that's a | ||
| // tautology! We're moving the caller's subtyping/assignability requirement into a | ||
| // constraint set. If the typevar has an upper bound or constraints, then the relation | ||
| // only has to hold when the typevar has a valid specialization (i.e., one that | ||
| // satisfies the upper bound/constraints). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what #20093 is trying to add to replace assignability across the board. In the meantime, I've added a new TypeRelation that lets us opt into the new behavior only in certain places.
3868294 to
2c62674
Compare
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-argument-type |
657 | 44 | 36 |
non-subscriptable |
345 | 0 | 0 |
invalid-return-type |
89 | 0 | 1 |
no-matching-overload |
81 | 5 | 0 |
unresolved-attribute |
71 | 3 | 4 |
type-assertion-failure |
0 | 23 | 41 |
invalid-assignment |
31 | 0 | 9 |
possibly-missing-attribute |
35 | 0 | 4 |
unsupported-operator |
8 | 0 | 16 |
unused-ignore-comment |
1 | 21 | 0 |
not-iterable |
18 | 0 | 0 |
unknown-argument |
14 | 0 | 0 |
too-many-positional-arguments |
11 | 0 | 0 |
possibly-unresolved-reference |
0 | 4 | 0 |
unsupported-base |
2 | 1 | 0 |
call-non-callable |
1 | 0 | 0 |
invalid-await |
1 | 0 | 0 |
invalid-context-manager |
1 | 0 | 0 |
invalid-raise |
1 | 0 | 0 |
redundant-cast |
1 | 0 | 0 |
| Total | 1,368 | 101 | 111 |
* origin/main: (67 commits) Move `Token`, `TokenKind` and `Tokens` to `ruff-python-ast` (#21760) [ty] Don't confuse multiple occurrences of `typing.Self` when binding bound methods (#21754) Use our org-wide Renovate preset (#21759) Delete `my-script.py` (#21751) [ty] Move `all_members`, and related types/routines, out of `ide_support.rs` (#21695) [ty] Fix find-references for import aliases (#21736) [ty] add tests for workspaces (#21741) [ty] Stop testing the (brittle) constraint set display implementation (#21743) [ty] Use generator over list comprehension to avoid cast (#21748) [ty] Add a diagnostic for prohibited `NamedTuple` attribute overrides (#21717) [ty] Fix subtyping with `type[T]` and unions (#21740) Use `npm ci --ignore-scripts` everywhere (#21742) [`flake8-simplify`] Fix truthiness assumption for non-iterable arguments in tuple/list/set calls (`SIM222`, `SIM223`) (#21479) [`flake8-use-pathlib`] Mark fixes unsafe for return type changes (`PTH104`, `PTH105`, `PTH109`, `PTH115`) (#21440) [ty] Fix auto-import code action to handle pre-existing import Enable PEP 740 attestations when publishing to PyPI (#21735) [ty] Fix find references for type defined in stub (#21732) Use OIDC instead of codspeed token (#21719) [ty] Exclude `typing_extensions` from completions unless it's really available [ty] Fix false positives for `class F(Generic[*Ts]): ...` (#21723) ...
|
Took a look at the conformance suite. Everything looks good except that it seems like we've lost our ability to recognize the |
Yeah this showed up in the |
Codex helped pinpoint that the issue was this call involving group_indices: GroupIndices = tuple(
list(itertools.starmap(slice, pairwise(sbins))) + [slice(sbins[-1], None)]
)It turns out that That was building up a very large BDD, that was taking a long time to iterate through. But since each of those overload pairs is independent, we can just do the path walk on each one separately. That gets rid of the combinatorial explosion. So I'm waiting for CI to confirm, but I think the timeouts have been solved now too. |
|
Still in the middle of reviewing the ecosystem results, but this one stands out as interesting: It looks like we're now correctly catching a runtime |
…le` (#21798) When converting a class (whether specialized or not) into a `Callable` type, we should carry through any generic context that the constructor has. This includes both the generic context of the class itself (if it's generic) and of the constructor methods (if they are separately generic). To help test this, this also updates the `generic_context` extension function to work on `Callable` types and unions; and adds a new `into_callable` extension function that works just like `CallableTypeOf`, but on value forms instead of type forms. Pulled this out of #21551 for separate review.
* origin/main: (41 commits) [ty] Carry generic context through when converting class into `Callable` (#21798) [ty] Add more tests for renamings (#21810) [ty] Minor improvements to `assert_type` diagnostics (#21811) [ty] Add some attribute/method renaming test cases (#21809) Update mkdocs-material to 9.7.0 (Insiders now free) (#21797) Remove unused whitespaces in test cases (#21806) [ty] fix panic when instantiating a type variable with invalid constraints (#21663) [ty] fix build failure caused by conflicts between #21683 and #21800 (#21802) [ty] do nothing with `store_expression_type` if `inner_expression_inference_state` is `Get` (#21718) [ty] increase the limit on the number of elements in a non-recursively defined literal union (#21683) [ty] normalize typevar bounds/constraints in cycles (#21800) [ty] Update completion eval to include modules [ty] Add modules to auto-import [ty] Add support for module-only import requests [ty] Refactor auto-import symbol info [ty] Clarify the use of `SymbolKind` in auto-import [ty] Redact ranking of completions from e2e LSP tests [ty] Tweaks tests to use clearer language [ty] Update evaluation results [ty] Make auto-import ignore symbols in modules starting with a `_` ...
## Summary Closes: astral-sh/ty#157 This PR adds support for the following capabilities involving a `ParamSpec` type variable: - Representing `P.args` and `P.kwargs` in the type system - Matching against a callable containing `P` to create a type mapping - Specializing `P` against the stored parameters The value of a `ParamSpec` type variable is being represented using `CallableType` with a `CallableTypeKind::ParamSpecValue` variant. This `CallableTypeKind` is expanded from the existing `is_function_like` boolean flag. An `enum` is used as these variants are mutually exclusive. For context, an initial iteration made an attempt to expand the `Specialization` to use `TypeOrParameters` enum that represents that a type variable can specialize into either a `Type` or `Parameters` but that increased the complexity of the code as all downstream usages would need to handle both the variants appropriately. Additionally, we'd have also need to establish an invariant that a regular type variable always maps to a `Type` while a paramspec type variable always maps to a `Parameters`. I've intentionally left out checking and raising diagnostics when the `ParamSpec` type variable and it's components are not being used correctly to avoid scope increase and it can easily be done as a follow-up. This would also include the scoping rules which I don't think a regular type variable implements either. ## Test Plan Add new mdtest cases and update existing test cases. Ran this branch on pyx, no new diagnostics. ### Ecosystem analysis There's a case where in an annotated assignment like: ```py type CustomType[P] = Callable[...] def value[**P](...): ... def another[**P](...): target: CustomType[P] = value ``` The type of `value` is a callable and it has a paramspec that's bound to `value`, `CustomType` is a type alias that's a callable and `P` that's used in it's specialization is bound to `another`. Now, ty infers the type of `target` same as `value` and does not use the declared type `CustomType[P]`. [This is the assignment](https://github.com/mikeshardmind/async-utils/blob/0980b9d9ab2bc7a24777684a884f4ea96cbbe5f9/src/async_utils/gen_transform.py#L108) that I'm referring to which then leads to error in downstream usage. Pyright and mypy does seem to use the declared type. There are multiple diagnostics in `dd-trace-py` that requires support for `cls`. I'm seeing `Divergent` type for an example like which ~~I'm not sure why, I'll look into it tomorrow~~ is because of a cycle as mentioned in astral-sh/ty#1729 (comment): ```py from typing import Callable def decorator[**P](c: Callable[P, int]) -> Callable[P, str]: ... @decorator def func(a: int) -> int: ... # ((a: int) -> str) | ((a: Divergent) -> str) reveal_type(func) ``` I ~~need to look into why are the parameters not being specialized through multiple decorators in the following code~~ think this is also because of the cycle mentioned in astral-sh/ty#1729 (comment) and the fact that we don't support `staticmethod` properly: ```py from contextlib import contextmanager class Foo: @staticmethod @contextmanager def method(x: int): yield foo = Foo() # ty: Revealed type: `() -> _GeneratorContextManager[Unknown, None, None]` [revealed-type] reveal_type(foo.method) ``` There's some issue related to `Protocol` that are generic over a `ParamSpec` in `starlette` which might be related to astral-sh/ty#1635 but I'm not sure. Here's a minimal example to reproduce: <details><summary>Code snippet:</summary> <p> ```py from collections.abc import Awaitable, Callable, MutableMapping from typing import Any, Callable, ParamSpec, Protocol P = ParamSpec("P") Scope = MutableMapping[str, Any] Message = MutableMapping[str, Any] Receive = Callable[[], Awaitable[Message]] Send = Callable[[Message], Awaitable[None]] ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]] _Scope = Any _Receive = Callable[[], Awaitable[Any]] _Send = Callable[[Any], Awaitable[None]] # Since `starlette.types.ASGIApp` type differs from `ASGIApplication` from `asgiref` # we need to define a more permissive version of ASGIApp that doesn't cause type errors. _ASGIApp = Callable[[_Scope, _Receive, _Send], Awaitable[None]] class _MiddlewareFactory(Protocol[P]): def __call__( self, app: _ASGIApp, *args: P.args, **kwargs: P.kwargs ) -> _ASGIApp: ... class Middleware: def __init__( self, factory: _MiddlewareFactory[P], *args: P.args, **kwargs: P.kwargs ) -> None: self.factory = factory self.args = args self.kwargs = kwargs class ServerErrorMiddleware: def __init__( self, app: ASGIApp, value: int | None = None, flag: bool = False, ) -> None: self.app = app self.value = value self.flag = flag async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: ... # ty: Argument to bound method `__init__` is incorrect: Expected `_MiddlewareFactory[(...)]`, found `<class 'ServerErrorMiddleware'>` [invalid-argument-type] Middleware(ServerErrorMiddleware, value=500, flag=True) ``` </p> </details> ### Conformance analysis > ```diff > -constructors_callable.py:36:13: info[revealed-type] Revealed type: `(...) -> Unknown` > +constructors_callable.py:36:13: info[revealed-type] Revealed type: `(x: int) -> Unknown` > ``` Requires return type inference i.e., #21551 > ```diff > +constructors_callable.py:194:16: error[invalid-argument-type] Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown | str]` > +constructors_callable.py:194:22: error[invalid-argument-type] Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown | str]` > +constructors_callable.py:195:4: error[invalid-argument-type] Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown | int]` > +constructors_callable.py:195:9: error[invalid-argument-type] Argument is incorrect: Expected `list[T@__init__]`, found `list[Unknown | str]` > ``` I might need to look into why this is happening... > ```diff > +generics_defaults.py:79:1: error[type-assertion-failure] Type `type[Class_ParamSpec[(str, int, /)]]` does not match asserted type `<class 'Class_ParamSpec'>` > ``` which is on the following code ```py DefaultP = ParamSpec("DefaultP", default=[str, int]) class Class_ParamSpec(Generic[DefaultP]): ... assert_type(Class_ParamSpec, type[Class_ParamSpec[str, int]]) ``` It's occurring because there's no equivalence relationship defined between `ClassLiteral` and `KnownInstanceType::TypeGenericAlias` which is what these types are. Everything else looks good to me!
* origin/main: [ty] Allow `tuple[Any, ...]` to assign to `tuple[int, *tuple[int, ...]]` (#21803) [ty] Support renaming import aliases (#21792) [ty] Add redeclaration LSP tests (#21812) [ty] more detailed description of "Size limit on unions of literals" in mdtest (#21804) [ty] Complete support for `ParamSpec` (#21445) [ty] Update benchmark dependencies (#21815)
This reverts commit 94aca37.
0138dc3 to
f29200c
Compare
This is a first stab at solving astral-sh/ty#500, at least in part, with the old solver. We add a new
TypeRelationthat lets us opt into using constraint sets to describe when a typevar is assignability to some type, and then use that to calculate a constraint set that describes when two callable types are assignable. If the callable types contain typevars, that constraint set will describe their valid specializations. We can then walk through all of the ways the constraint set can be satisfied, and record a type mapping in the old solver for each one.