-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[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
Conversation
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-12-16 16:22:20.701518109 +0000
+++ new-output.txt 2025-12-16 16:22:24.208547350 +0000
@@ -150,7 +150,7 @@
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]` has no attribute `other_attribute2`
+callables_protocol.py:197:7: error[unresolved-attribute] Object of type `Proto9[(x: int), str]` has no attribute `other_attribute2`
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 +236,38 @@
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:63:13: info[revealed-type] Revealed type: `(...) -> Class3`
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: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 `() -> Any | Class6Any`, found `<class 'Class6Any'>`
+constructors_callable.py:142:13: info[revealed-type] Revealed type: `() -> Any | Class6Any`
+constructors_callable.py:143:1: error[type-assertion-failure] Type `Any` does not match asserted type `Any | 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]`
@@ -304,6 +305,10 @@
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:102:42: error[invalid-argument-type] Argument to function `model_field` is incorrect: Expected `(str | bytes, /) -> ConverterClass`, found `<class 'ConverterClass'>`
+dataclasses_transform_converter.py:103:31: error[invalid-argument-type] Argument to function `model_field` is incorrect: Expected `(str | list[str], /) -> int`, found `Overload[(s: str) -> int, (s: list[str]) -> int]`
+dataclasses_transform_converter.py:104:30: error[invalid-assignment] Object of type `dict[str, str] | dict[bytes, bytes]` is not assignable to `dict[str, str]`
+dataclasses_transform_converter.py:104:42: error[invalid-argument-type] Argument to function `model_field` is incorrect: Expected `(Iterable[list[str]] | Iterable[list[bytes]], /) -> dict[str, str] | dict[bytes, bytes]`, found `<class 'dict'>`
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"]`
@@ -333,7 +338,8 @@
dataclasses_transform_converter.py:121:29: error[invalid-argument-type] Argument is incorrect: Expected `ConverterClass`, found `Literal["f6"]`
dataclasses_transform_converter.py:121:35: error[invalid-argument-type] Argument is incorrect: Expected `int`, found `Literal["1"]`
dataclasses_transform_converter.py:121:40: error[invalid-argument-type] Argument is incorrect: Expected `dict[str, str]`, found `tuple[tuple[Literal["a"], Literal["1"]], tuple[Literal["b"], Literal["2"]]]`
-dataclasses_transform_converter.py:130:31: error[invalid-argument-type] Argument to function `model_field` is incorrect: Expected `(Literal[1], /) -> Unknown`, found `def converter_simple(s: str) -> int`
+dataclasses_transform_converter.py:130:31: error[invalid-argument-type] Argument to function `model_field` is incorrect: Expected `(str | Literal[1], /) -> int`, found `def converter_simple(s: str) -> int`
+dataclasses_transform_converter.py:133:31: error[invalid-argument-type] Argument to function `model_field` is incorrect: Expected `(str | int, /) -> int`, found `def converter_simple(s: str) -> int`
dataclasses_transform_field.py:49:43: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `(type[T@create_model], /) -> type[T@create_model]`
dataclasses_transform_field.py:64:16: error[unknown-argument] Argument `id` does not match any known parameter
dataclasses_transform_field.py:75:1: error[missing-argument] No argument provided for required parameter `name`
@@ -357,6 +363,7 @@
dataclasses_usage.py:51:28: error[invalid-argument-type] Argument is incorrect: Expected `int | float`, found `Literal["price"]`
dataclasses_usage.py:52:36: error[too-many-positional-arguments] Too many positional arguments: expected 3, got 4
dataclasses_usage.py:83:13: error[too-many-positional-arguments] Too many positional arguments: expected 1, got 2
+dataclasses_usage.py:88:14: error[invalid-assignment] Object of type `dataclasses.Field[str | int]` is not assignable to `int`
dataclasses_usage.py:127:8: error[too-many-positional-arguments] Too many positional arguments: expected 1, got 2
dataclasses_usage.py:130:1: error[missing-argument] No argument provided for required parameter `y` of bound method `__init__`
dataclasses_usage.py:179:6: error[too-many-positional-arguments] Too many positional arguments to bound method `__init__`: expected 1, got 2
@@ -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
@@ -1026,4 +1033,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 1028 diagnostics
+Found 1035 diagnostics
|
|
965a9f8 to
dbecc68
Compare
Callable return typesCallable types
7995e43 to
f89ec1a
Compare
crates/ty_python_semantic/resources/mdtest/generics/legacy/functions.md
Outdated
Show resolved
Hide resolved
| # 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 |
262 | 110 | 143 |
type-assertion-failure |
4 | 56 | 51 |
invalid-return-type |
88 | 0 | 16 |
invalid-assignment |
75 | 0 | 28 |
invalid-await |
101 | 0 | 1 |
possibly-missing-attribute |
63 | 6 | 27 |
unresolved-attribute |
62 | 2 | 28 |
unsupported-operator |
56 | 0 | 19 |
missing-argument |
0 | 59 | 0 |
no-matching-overload |
36 | 8 | 0 |
unused-ignore-comment |
0 | 27 | 0 |
not-iterable |
21 | 0 | 0 |
non-subscriptable |
7 | 0 | 13 |
unknown-argument |
14 | 1 | 0 |
too-many-positional-arguments |
11 | 1 | 0 |
call-non-callable |
9 | 0 | 0 |
invalid-context-manager |
1 | 0 | 2 |
invalid-raise |
0 | 0 | 1 |
redundant-cast |
1 | 0 | 0 |
| Total | 811 | 270 | 329 |
* 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 |
|
Yeah, I was wrong, homeassistant does look like a bug. Other type checkers are fine with it. Minimal repro: from typing import Generic, TypeVar, Mapping, Any, Callable, Never
T = TypeVar("T", bound=Mapping[str, Any], default=Mapping[str, Any])
class Event(Generic[T]):
pass
class EventType(Generic[T]):
pass
STOP: EventType[Mapping[str, Never]] = EventType()
def listen(et: EventType[T], cb: Callable[[Event[T]], None]):
pass
def mycb(event: Event) -> None:
pass
def _():
listen(STOP, mycb)We have a false positive on the last line, we solve Do we still take top materializations of gradual upper bounds? That seems like it might be the problem here. |
Are you thinking to get that PR landed first and then rebase this and revisit its ecosystem report? Or would you prefer to just land this with its remaining issues and tackle them all as follow-ups? |
Given the time, another option would be to combine this PR and the generic protocol one and try to land it all together. I'm down to one last mdtest file with test regressions to fix. |
|
We can try that. Do you have a lot of other fixes in the protocol PR that might help this PR, too? If so, combining might be most expedient. The downside might be if the combined PR is harder to review ecosystem impact and land, because its so big? If you want, I could also pull out that commit into its own PR and add tests for it, while you keep working on the protocol PR? |
Sure, that works! I've got it started on the |
For this homeassistant error, I think it's the same as this TODO: https://github.com/astral-sh/ruff/pull/21551/changes#diff-d08dc0c815ce12a46b39702c611395d769e53bc49746c3a0c8533ed8af014863R814-R815 (That same comment appears as a TODO in some other mdtests, too.) The issue is that we're creating a constraint set of the form |
|
Hmm, not sure about that, because the issue repros even without But removing the default and changing the upper bound from typing import Generic, TypeVar, Mapping, Any, Callable, Never
T = TypeVar("T", bound=Mapping[str, object])
class Event(Generic[T]):
pass
class EventType(Generic[T]):
pass
STOP: EventType[Mapping[str, int]] = EventType()
def listen(et: EventType[T], cb: Callable[[Event[T]], None]):
pass
def mycb(event: Event) -> None:
pass
def _():
listen(STOP, mycb)Other typecheckers are still happy with this; we emit an error on the bottom line yet. Maybe this is a variant of the same problem as the Expression one, and will also be fixed by the explicit-constraints change? If not I agree we can look into it as follow-on. |
Ah that's a good catch. If we're inferring the bounds, then yes I think the explicit-constraints change will help. |
* main: [ty] Document `TY_CONFIG_FILE` (#22001) [ty] Cache `KnownClass::to_class_literal` (#22000) [ty] Fix benchmark assertion (#22003) Add uv and ty to the Ruff README (#21996) [ty] Infer precise types for `isinstance(…)` calls involving typevars (#21999) [ty] Use `FxHashMap` in `Signature::has_relation_to` (#21997) [ty] Avoid enforcing standalone expression for tests in f-strings (#21967) [ty] Use `title` for configuration code fences in ty reference documentation (#21992)
|
Any idea where a 101 exit code comes from? Does that happen when we panic? When I run ty locally on spack (both dev and profiling profiles), I get a successful type check. |
|
Just in terms of numbers, ecosystem diff is definitely improved here! We were adding almost 1200 new diagnostics before, now its under 800. |
|
Where do you see the 101 return code? Primer and ecosystem analyzer jobs both completed successfully... EDIT: oh never mind, I see it once I link to the full ecosystem analyzer output... but spack checked fine on the mypy-primer job. Running ecosystem analyzer again to see if it persists. |
|
Spack error is gone on the second run; I suspect an infra issue. (I guess I didn't check the logs for the ecosystem analyzer run to see if there was anything useful there.) Spot-checked the first Expression failure -- looks like a combo of generic protocol inference, plus us using narrower inferred types. Not an issue for this PR. I'll look at a couple more quick? |
Sounds good, thanks! |
|
Second one also looks like generic protocols. |
|
In bokeh I see an example that boils down to this: from typing import Callable, TypeVar
T = TypeVar("T")
def infer(func: Callable[[T | str], T]) -> T:
return func("foo")
def convert_str_seq(value: list[str] | str) -> list[str]:
if isinstance(value, str):
return [value]
return value
reveal_type(infer(convert_str_seq)) # expected `list[str]` but we say `list[str] | str`Basically we don't unpack the union and match it item-by-item, so the But pyrefly does the same as we do here -- I think this one is fine to look at as follow-up. |
carljm
left a comment
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.
Ship it!
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.