mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-26 18:06:43 +00:00
[ty] Use annotated parameters as type context (#20635)
Some checks are pending
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary
Use the type annotation of function parameters as bidirectional type
context when inferring the argument expression. For example, the
following example now type-checks:
```py
class TD(TypedDict):
x: int
def f(_: TD): ...
f({ "x": 1 })
```
Part of https://github.com/astral-sh/ty/issues/168.
This commit is contained in:
parent
b83ac5e234
commit
2ce3aba458
13 changed files with 519 additions and 95 deletions
|
|
@ -1662,3 +1662,67 @@ def _(arg: tuple[A | B, Any]):
|
|||
reveal_type(f(arg)) # revealed: Unknown
|
||||
reveal_type(f(*(arg,))) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Bidirectional Type Inference
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Type inference accounts for parameter type annotations across all overloads.
|
||||
|
||||
```py
|
||||
from typing import TypedDict, overload
|
||||
|
||||
class T(TypedDict):
|
||||
x: int
|
||||
|
||||
@overload
|
||||
def f(a: list[T], b: int) -> int: ...
|
||||
@overload
|
||||
def f(a: list[dict[str, int]], b: str) -> str: ...
|
||||
def f(a: list[dict[str, int]] | list[T], b: int | str) -> int | str:
|
||||
return 1
|
||||
|
||||
def int_or_str() -> int | str:
|
||||
return 1
|
||||
|
||||
x = f([{"x": 1}], int_or_str())
|
||||
reveal_type(x) # revealed: int | str
|
||||
|
||||
# TODO: error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
# we currently incorrectly consider `list[dict[str, int]]` a subtype of `list[T]`
|
||||
f([{"y": 1}], int_or_str())
|
||||
```
|
||||
|
||||
Non-matching overloads do not produce diagnostics:
|
||||
|
||||
```py
|
||||
from typing import TypedDict, overload
|
||||
|
||||
class T(TypedDict):
|
||||
x: int
|
||||
|
||||
@overload
|
||||
def f(a: T, b: int) -> int: ...
|
||||
@overload
|
||||
def f(a: dict[str, int], b: str) -> str: ...
|
||||
def f(a: T | dict[str, int], b: int | str) -> int | str:
|
||||
return 1
|
||||
|
||||
x = f({"y": 1}, "a")
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import SupportsRound, overload
|
||||
|
||||
@overload
|
||||
def takes_str_or_float(x: str): ...
|
||||
@overload
|
||||
def takes_str_or_float(x: float): ...
|
||||
def takes_str_or_float(x: float | str): ...
|
||||
|
||||
takes_str_or_float(round(1.0))
|
||||
```
|
||||
|
|
|
|||
|
|
@ -251,3 +251,59 @@ from ty_extensions import Intersection, Not
|
|||
def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]):
|
||||
reveal_type(x) # revealed: Any & ~int
|
||||
```
|
||||
|
||||
## Bidirectional Type Inference
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Type inference accounts for parameter type annotations across all signatures in a union.
|
||||
|
||||
```py
|
||||
from typing import TypedDict, overload
|
||||
|
||||
class T(TypedDict):
|
||||
x: int
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def f(x: T) -> int:
|
||||
return 1
|
||||
else:
|
||||
def f(x: dict[str, int]) -> int:
|
||||
return 1
|
||||
x = f({"x": 1})
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# TODO: error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int]`"
|
||||
# we currently consider `TypedDict` instances to be subtypes of `dict`
|
||||
f({"y": 1})
|
||||
```
|
||||
|
||||
Diagnostics unrelated to the type-context are only reported once:
|
||||
|
||||
```py
|
||||
def f[T](x: T) -> list[T]:
|
||||
return [x]
|
||||
|
||||
def a(x: list[bool], y: list[bool]): ...
|
||||
def b(x: list[int], y: list[int]): ...
|
||||
def c(x: list[int], y: list[int]): ...
|
||||
def _(x: int):
|
||||
if x == 0:
|
||||
y = a
|
||||
elif x == 1:
|
||||
y = b
|
||||
else:
|
||||
y = c
|
||||
|
||||
if x == 0:
|
||||
z = True
|
||||
|
||||
y(f(True), [True])
|
||||
|
||||
# error: [possibly-unresolved-reference] "Name `z` used when possibly not defined"
|
||||
y(f(True), [z])
|
||||
```
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from typing_extensions import assert_type
|
|||
def _(x: int):
|
||||
assert_type(x, int) # fine
|
||||
assert_type(x, str) # error: [type-assertion-failure]
|
||||
assert_type(assert_type(x, int), int)
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_type.m
|
|||
3 | def _(x: int):
|
||||
4 | assert_type(x, int) # fine
|
||||
5 | assert_type(x, str) # error: [type-assertion-failure]
|
||||
6 | assert_type(assert_type(x, int), int)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
|
@ -31,6 +32,7 @@ error[type-assertion-failure]: Argument does not have asserted type `str`
|
|||
| ^^^^^^^^^^^^-^^^^^^
|
||||
| |
|
||||
| Inferred type of argument is `int`
|
||||
6 | assert_type(assert_type(x, int), int)
|
||||
|
|
||||
info: `str` and `int` are not equivalent types
|
||||
info: rule `type-assertion-failure` is enabled by default
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ Person(name="Alice")
|
|||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||
Person({"name": "Alice"})
|
||||
|
||||
# TODO: this should be an error, similar to the above
|
||||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||
accepts_person({"name": "Alice"})
|
||||
# TODO: this should be an error, similar to the above
|
||||
house.owner = {"name": "Alice"}
|
||||
|
|
@ -171,7 +171,7 @@ Person(name=None, age=30)
|
|||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
Person({"name": None, "age": 30})
|
||||
|
||||
# TODO: this should be an error, similar to the above
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
accepts_person({"name": None, "age": 30})
|
||||
# TODO: this should be an error, similar to the above
|
||||
house.owner = {"name": None, "age": 30}
|
||||
|
|
@ -190,7 +190,7 @@ Person(name="Alice", age=30, extra=True)
|
|||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||
Person({"name": "Alice", "age": 30, "extra": True})
|
||||
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||
accepts_person({"name": "Alice", "age": 30, "extra": True})
|
||||
# TODO: this should be an error
|
||||
house.owner = {"name": "Alice", "age": 30, "extra": True}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue