Compare commits
24 Commits
jack/loop-
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
110ae7c7c2 | ||
|
|
7926fb1dc1 | ||
|
|
7a10c640c4 | ||
|
|
48c3b5efe6 | ||
|
|
c440f4bc90 | ||
|
|
f4da2502e5 | ||
|
|
fa9474f3a8 | ||
|
|
fee0888f76 | ||
|
|
ac66d4c74c | ||
|
|
f0c0c7770c | ||
|
|
849e267268 | ||
|
|
c04fc6b965 | ||
|
|
feb4df1449 | ||
|
|
d9b22cf276 | ||
|
|
fafd6e20dc | ||
|
|
01b1937065 | ||
|
|
930f13da9f | ||
|
|
cc1ddc5d72 | ||
|
|
bc4798f2ee | ||
|
|
4866f6e2ea | ||
|
|
6b4a0b874c | ||
|
|
360f1e9e84 | ||
|
|
b3b1b7ba40 | ||
|
|
f39370a6f5 |
@@ -151,7 +151,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
600,
|
||||
650,
|
||||
);
|
||||
|
||||
static PANDAS: Benchmark = Benchmark::new(
|
||||
|
||||
@@ -129,3 +129,96 @@ async def f():
|
||||
|
||||
reveal_type(f()) # revealed: CoroutineType[Any, Any, Unknown]
|
||||
```
|
||||
|
||||
## Awaiting intersection types (3.13+)
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
Intersection types can be awaited when their elements are awaitable. This is important for patterns
|
||||
like `inspect.isawaitable()` which narrow types to intersections with `Awaitable`.
|
||||
|
||||
```py
|
||||
import inspect
|
||||
|
||||
def get_unknown():
|
||||
pass
|
||||
|
||||
async def test():
|
||||
x = get_unknown()
|
||||
if inspect.isawaitable(x):
|
||||
reveal_type(x) # revealed: Unknown & Awaitable[object]
|
||||
y = await x
|
||||
reveal_type(y) # revealed: Unknown
|
||||
```
|
||||
|
||||
The return type of awaiting an intersection is the intersection of the return types of awaiting each
|
||||
element:
|
||||
|
||||
```py
|
||||
from typing import Coroutine
|
||||
from ty_extensions import Intersection
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
async def test(x: Intersection[Coroutine[object, object, A], Coroutine[object, object, B]]):
|
||||
y = await x
|
||||
reveal_type(y) # revealed: A & B
|
||||
```
|
||||
|
||||
If some intersection elements are not awaitable, we skip them and use the return types from the
|
||||
awaitable elements:
|
||||
|
||||
```py
|
||||
from typing import Coroutine
|
||||
from ty_extensions import Intersection
|
||||
|
||||
class NotAwaitable:
|
||||
pass
|
||||
|
||||
async def test(x: Intersection[Coroutine[object, object, str], NotAwaitable]):
|
||||
y = await x
|
||||
reveal_type(y) # revealed: str
|
||||
```
|
||||
|
||||
If all intersection elements fail to be awaitable, the await is invalid:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
|
||||
class NotAwaitable1:
|
||||
pass
|
||||
|
||||
class NotAwaitable2:
|
||||
pass
|
||||
|
||||
async def test(x: Intersection[NotAwaitable1, NotAwaitable2]):
|
||||
# error: [invalid-await]
|
||||
await x
|
||||
```
|
||||
|
||||
## Awaiting intersection types (Python 3.12 or lower)
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
The return type of awaiting an intersection is the intersection of the return types of awaiting each
|
||||
element:
|
||||
|
||||
```py
|
||||
from typing import Coroutine
|
||||
from ty_extensions import Intersection
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
async def test(x: Intersection[Coroutine[object, object, A], Coroutine[object, object, B]]):
|
||||
y = await x
|
||||
# TODO: should be `A & B`, but suffers from https://github.com/astral-sh/ty/issues/2426
|
||||
reveal_type(y) # revealed: A
|
||||
```
|
||||
|
||||
@@ -834,3 +834,34 @@ def _(flag: bool):
|
||||
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int] & dict[Unknown | str, Unknown | int]`"
|
||||
f({"y": 1})
|
||||
```
|
||||
|
||||
## Union of intersections with failing bindings
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
When calling a union where one element is an intersection of callables, and all bindings in that
|
||||
intersection fail, we should report errors with both union and intersection context.
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
from typing import Callable
|
||||
|
||||
class IntCaller:
|
||||
def __call__(self, x: int) -> int:
|
||||
return x
|
||||
|
||||
class StrCaller:
|
||||
def __call__(self, x: str) -> str:
|
||||
return x
|
||||
|
||||
class BytesCaller:
|
||||
def __call__(self, x: bytes) -> bytes:
|
||||
return x
|
||||
|
||||
def test(f: Intersection[IntCaller, StrCaller] | BytesCaller):
|
||||
# Call with None - should fail for IntCaller, StrCaller, and BytesCaller
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
f(None)
|
||||
```
|
||||
|
||||
@@ -946,6 +946,194 @@ def mixed(
|
||||
reveal_type(i4) # revealed: Any
|
||||
```
|
||||
|
||||
## Calling intersection types
|
||||
|
||||
When calling an intersection type, we try to call each positive element with the given arguments.
|
||||
Elements where the call fails (wrong arguments, not callable, etc.) are discarded. The return type
|
||||
is the intersection of return types from the elements where the call succeeded.
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
from typing import Callable
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
def _(
|
||||
x: Intersection[type[Foo], Callable[[], str]],
|
||||
) -> None:
|
||||
# Both `type[Foo]` and `Callable[[], str]` are callable with no arguments.
|
||||
# `type[Foo]()` returns `Foo`, `Callable[[], str]()` returns `str`.
|
||||
# The return type is the intersection of `Foo` and `str`.
|
||||
reveal_type(x()) # revealed: Foo & str
|
||||
```
|
||||
|
||||
If one element accepts the call but another rejects it (e.g., due to incompatible arguments), the
|
||||
call still succeeds using only the element that accepts:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
from typing import Callable
|
||||
|
||||
class Bar:
|
||||
pass
|
||||
|
||||
def _(
|
||||
x: Intersection[type[Bar], Callable[[int], str]],
|
||||
) -> None:
|
||||
# `type[Bar]()` accepts no arguments and returns `Bar`.
|
||||
# `Callable[[int], str]` requires an int argument, so it fails for this call.
|
||||
# We discard the failing element and use only `type[Bar]`.
|
||||
reveal_type(x()) # revealed: Bar
|
||||
```
|
||||
|
||||
If all elements are callable but all reject the specific call (e.g., incompatible arguments), we
|
||||
show errors for each failing element:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
from typing import Callable
|
||||
|
||||
def _(
|
||||
x: Intersection[Callable[[int], str], Callable[[str], int]],
|
||||
) -> None:
|
||||
# Both callables reject a `float` argument:
|
||||
# - `Callable[[int], str]` expects `int`
|
||||
# - `Callable[[str], int]` expects `str`
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
x(1.0)
|
||||
```
|
||||
|
||||
When intersection elements fail with different error types, we use a priority hierarchy to determine
|
||||
which errors to show. More specific errors (like `invalid-argument-type`) take precedence over less
|
||||
specific ones (like `call-top-callable` or `call-non-callable`).
|
||||
|
||||
A specific argument error takes priority over a top-callable error:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection, Top
|
||||
from typing import Callable
|
||||
|
||||
def _(
|
||||
x: Intersection[Callable[[int], str], Top[Callable[..., object]]],
|
||||
) -> None:
|
||||
# `Callable[[int], str]` fails with invalid-argument-type (expects int, got str)
|
||||
# `Top[Callable[..., object]]` would fail with call-top-callable
|
||||
# We only show the more specific invalid-argument-type error
|
||||
# error: [invalid-argument-type]
|
||||
x("hello")
|
||||
```
|
||||
|
||||
A specific argument error takes priority over a not-callable error:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
from typing import Callable
|
||||
|
||||
class NotCallable: ...
|
||||
|
||||
def _(
|
||||
x: Intersection[Callable[[int], str], NotCallable],
|
||||
) -> None:
|
||||
# `Callable[[int], str]` fails with invalid-argument-type (expects int, got str)
|
||||
# `NotCallable` would fail with call-non-callable
|
||||
# We only show the more specific invalid-argument-type error
|
||||
# error: [invalid-argument-type]
|
||||
x("hello")
|
||||
```
|
||||
|
||||
A top-callable error takes priority over a not-callable error:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection, Top
|
||||
from typing import Callable
|
||||
|
||||
class NotCallable: ...
|
||||
|
||||
def _(
|
||||
x: Intersection[Top[Callable[..., object]], NotCallable],
|
||||
) -> None:
|
||||
# `Top[Callable[..., object]]` fails with call-top-callable
|
||||
# `NotCallable` would fail with call-non-callable
|
||||
# We only show the call-top-callable error (it's more specific)
|
||||
# error: [call-top-callable]
|
||||
x()
|
||||
```
|
||||
|
||||
If no positive element is callable, the intersection is not callable:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(x: Intersection[A, B]) -> None:
|
||||
# error: [call-non-callable] "Object of type `A & B` is not callable"
|
||||
reveal_type(x()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unions containing intersections
|
||||
|
||||
When a union contains intersection elements (e.g., from `callable()` narrowing), the type checker
|
||||
properly handles each union element. If an intersection element succeeds, it contributes to the
|
||||
result. If all elements within an intersection fail, the priority hierarchy is used for diagnostics:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection, Top
|
||||
from typing import Callable
|
||||
|
||||
def _(
|
||||
i: Intersection[Callable[[int], str], Top[Callable[..., object]]],
|
||||
c: Callable[[str], int],
|
||||
flag: bool,
|
||||
) -> None:
|
||||
# Create a union of an intersection and a regular callable:
|
||||
# (Callable[[int], str] & Top[...]) | Callable[[str], int]
|
||||
if flag:
|
||||
f = i
|
||||
else:
|
||||
f = c
|
||||
|
||||
# When called with a string argument:
|
||||
# - The intersection element: Callable[[int], str] fails (wrong type),
|
||||
# Top[...] would fail with call-top-callable. Due to priority hierarchy,
|
||||
# only the invalid-argument-type error is shown for the intersection.
|
||||
# - The Callable[[str], int] element succeeds.
|
||||
# The return type includes both elements' return types:
|
||||
# - intersection: str (from Callable[[int], str])
|
||||
# - regular: int (from Callable[[str], int])
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("hello")) # revealed: str | int
|
||||
```
|
||||
|
||||
When all union elements fail (including intersection elements), errors are reported for each:
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection, Top
|
||||
from typing import Callable
|
||||
|
||||
def _(
|
||||
i: Intersection[Callable[[int], str], Top[Callable[..., object]]],
|
||||
c: Callable[[str], int],
|
||||
flag: bool,
|
||||
) -> None:
|
||||
if flag:
|
||||
f = i
|
||||
else:
|
||||
f = c
|
||||
|
||||
# When called with no arguments:
|
||||
# - The intersection element: Callable[[int], str] fails (missing argument),
|
||||
# Top[...] would fail with call-top-callable. Due to priority hierarchy,
|
||||
# only the missing-argument error is shown.
|
||||
# - The Callable[[str], int] also fails (missing argument).
|
||||
# error: [missing-argument]
|
||||
# error: [missing-argument]
|
||||
f()
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
```py
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
assertion_line: 623
|
||||
expression: snapshot
|
||||
---
|
||||
|
||||
---
|
||||
mdtest name: union.md - Unions in calls - Union of intersections with failing bindings
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/call/union.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from ty_extensions import Intersection
|
||||
2 | from typing import Callable
|
||||
3 |
|
||||
4 | class IntCaller:
|
||||
5 | def __call__(self, x: int) -> int:
|
||||
6 | return x
|
||||
7 |
|
||||
8 | class StrCaller:
|
||||
9 | def __call__(self, x: str) -> str:
|
||||
10 | return x
|
||||
11 |
|
||||
12 | class BytesCaller:
|
||||
13 | def __call__(self, x: bytes) -> bytes:
|
||||
14 | return x
|
||||
15 |
|
||||
16 | def test(f: Intersection[IntCaller, StrCaller] | BytesCaller):
|
||||
17 | # Call with None - should fail for IntCaller, StrCaller, and BytesCaller
|
||||
18 | # error: [invalid-argument-type]
|
||||
19 | # error: [invalid-argument-type]
|
||||
20 | # error: [invalid-argument-type]
|
||||
21 | f(None)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to bound method `__call__` is incorrect
|
||||
--> src/mdtest_snippet.py:21:7
|
||||
|
|
||||
19 | # error: [invalid-argument-type]
|
||||
20 | # error: [invalid-argument-type]
|
||||
21 | f(None)
|
||||
| ^^^^ Expected `int`, found `None`
|
||||
|
|
||||
info: Method defined here
|
||||
--> src/mdtest_snippet.py:5:9
|
||||
|
|
||||
4 | class IntCaller:
|
||||
5 | def __call__(self, x: int) -> int:
|
||||
| ^^^^^^^^ ------ Parameter declared here
|
||||
6 | return x
|
||||
|
|
||||
info: Intersection element `IntCaller` is incompatible with this call site
|
||||
info: Attempted to call intersection type `IntCaller & StrCaller`
|
||||
info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller`
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to bound method `__call__` is incorrect
|
||||
--> src/mdtest_snippet.py:21:7
|
||||
|
|
||||
19 | # error: [invalid-argument-type]
|
||||
20 | # error: [invalid-argument-type]
|
||||
21 | f(None)
|
||||
| ^^^^ Expected `str`, found `None`
|
||||
|
|
||||
info: Method defined here
|
||||
--> src/mdtest_snippet.py:9:9
|
||||
|
|
||||
8 | class StrCaller:
|
||||
9 | def __call__(self, x: str) -> str:
|
||||
| ^^^^^^^^ ------ Parameter declared here
|
||||
10 | return x
|
||||
|
|
||||
info: Intersection element `StrCaller` is incompatible with this call site
|
||||
info: Attempted to call intersection type `IntCaller & StrCaller`
|
||||
info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller`
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-argument-type]: Argument to bound method `__call__` is incorrect
|
||||
--> src/mdtest_snippet.py:21:7
|
||||
|
|
||||
19 | # error: [invalid-argument-type]
|
||||
20 | # error: [invalid-argument-type]
|
||||
21 | f(None)
|
||||
| ^^^^ Expected `bytes`, found `None`
|
||||
|
|
||||
info: Method defined here
|
||||
--> src/mdtest_snippet.py:13:9
|
||||
|
|
||||
12 | class BytesCaller:
|
||||
13 | def __call__(self, x: bytes) -> bytes:
|
||||
| ^^^^^^^^ -------- Parameter declared here
|
||||
14 | return x
|
||||
|
|
||||
info: Union variant `BytesCaller` is incompatible with this call site
|
||||
info: Attempted to call union type `(IntCaller & StrCaller) | BytesCaller`
|
||||
info: rule `invalid-argument-type` is enabled by default
|
||||
|
||||
```
|
||||
@@ -4517,8 +4517,17 @@ impl<'db> Type<'db> {
|
||||
.map(|element| element.bindings(db)),
|
||||
),
|
||||
|
||||
Type::Intersection(_) => {
|
||||
Binding::single(self, Signature::todo("Type::Intersection.call")).into()
|
||||
Type::Intersection(intersection) => {
|
||||
// For intersections, we try to call each positive element.
|
||||
// Elements where the call fails are discarded.
|
||||
// The return type is the intersection of return types from successful calls.
|
||||
Bindings::from_intersection(
|
||||
db,
|
||||
self,
|
||||
intersection
|
||||
.positive_elements_or_object(db)
|
||||
.map(|element| element.bindings(db)),
|
||||
)
|
||||
}
|
||||
|
||||
Type::DataclassDecorator(_) => {
|
||||
@@ -4630,6 +4639,36 @@ impl<'db> Type<'db> {
|
||||
tcx: TypeContext<'db>,
|
||||
policy: MemberLookupPolicy,
|
||||
) -> Result<Bindings<'db>, CallDunderError<'db>> {
|
||||
// For intersection types, call the dunder on each element separately and combine
|
||||
// the results. This avoids intersecting bound methods (which often collapses to Never)
|
||||
// and instead intersects the return types. TODO we might be able to remove this after
|
||||
// fixing https://github.com/astral-sh/ty/issues/2428.
|
||||
if let Type::Intersection(intersection) = self {
|
||||
let mut successful_bindings = Vec::new();
|
||||
let mut last_error = None;
|
||||
|
||||
for element in intersection.positive(db) {
|
||||
match element.try_call_dunder_with_policy(
|
||||
db,
|
||||
name,
|
||||
&mut argument_types.clone(),
|
||||
tcx,
|
||||
policy,
|
||||
) {
|
||||
Ok(bindings) => successful_bindings.push(bindings),
|
||||
Err(err) => last_error = Some(err),
|
||||
}
|
||||
}
|
||||
|
||||
if successful_bindings.is_empty() {
|
||||
// TODO we are only showing one of the errors here; should we aggregate them
|
||||
// somehow or show all of them?
|
||||
return Err(last_error.unwrap_or(CallDunderError::MethodNotAvailable));
|
||||
}
|
||||
|
||||
return Ok(Bindings::from_intersection(db, self, successful_bindings));
|
||||
}
|
||||
|
||||
// Implicit calls to dunder methods never access instance members, so we pass
|
||||
// `NO_INSTANCE_FALLBACK` here in addition to other policies:
|
||||
match self
|
||||
@@ -5123,6 +5162,17 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
Type::Union(union) => union.try_map(db, |ty| ty.generator_return_type(db)),
|
||||
Type::Intersection(intersection) => {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
let mut any_success = false;
|
||||
for ty in intersection.positive(db) {
|
||||
if let Some(return_ty) = ty.generator_return_type(db) {
|
||||
builder = builder.add_positive(return_ty);
|
||||
any_success = true;
|
||||
}
|
||||
}
|
||||
any_success.then(|| builder.build())
|
||||
}
|
||||
ty @ (Type::Dynamic(_) | Type::Never) => Some(ty),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3055,7 +3055,7 @@ impl<'db> StaticClassLiteral<'db> {
|
||||
// the `__set__` method can be called. We build a union of all possible options
|
||||
// to account for possible overloads.
|
||||
let mut value_types = UnionBuilder::new(db);
|
||||
for binding in &dunder_set.bindings(db) {
|
||||
for binding in dunder_set.bindings(db).iter() {
|
||||
for overload in binding {
|
||||
if let Some(value_param) =
|
||||
overload.signature.parameters().get_positional(2)
|
||||
|
||||
@@ -517,11 +517,11 @@ pub fn call_signature_details<'db>(
|
||||
|
||||
// Extract signature details from all callable bindings
|
||||
bindings
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.iter()
|
||||
.flat_map(IntoIterator::into_iter)
|
||||
.map(|binding| {
|
||||
let argument_to_parameter_mapping = binding.argument_matches().to_vec();
|
||||
let signature = binding.signature;
|
||||
let signature = binding.signature.clone();
|
||||
let display_details = signature.display(model.db()).to_string_parts();
|
||||
let parameter_label_offsets = display_details.parameter_ranges;
|
||||
let parameter_names = display_details.parameter_names;
|
||||
@@ -591,7 +591,7 @@ pub fn call_type_simplified_by_overloads(
|
||||
.check_types(db, &args, TypeContext::default(), &[])
|
||||
// Only use the Ok
|
||||
.iter()
|
||||
.flatten()
|
||||
.flat_map(super::call::bind::Bindings::iter)
|
||||
.flat_map(|binding| {
|
||||
binding.matching_overloads().map(|(_, overload)| {
|
||||
overload
|
||||
@@ -625,8 +625,8 @@ pub fn definitions_for_bin_op<'db>(
|
||||
let callable_type = promote_literals_for_self(model.db(), bindings.callable_type());
|
||||
|
||||
let definitions: Vec<_> = bindings
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.iter()
|
||||
.flat_map(IntoIterator::into_iter)
|
||||
.filter_map(|binding| {
|
||||
Some(ResolvedDefinition::Definition(
|
||||
binding.signature.definition?,
|
||||
@@ -683,8 +683,8 @@ pub fn definitions_for_unary_op<'db>(
|
||||
let callable_type = promote_literals_for_self(model.db(), bindings.callable_type());
|
||||
|
||||
let definitions = bindings
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.iter()
|
||||
.flat_map(IntoIterator::into_iter)
|
||||
.filter_map(|binding| {
|
||||
Some(ResolvedDefinition::Definition(
|
||||
binding.signature.definition?,
|
||||
|
||||
@@ -9477,7 +9477,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
}
|
||||
};
|
||||
|
||||
for binding in &mut bindings {
|
||||
for binding in bindings.iter_mut() {
|
||||
let binding_type = binding.callable_type;
|
||||
for (_, overload) in binding.matching_overloads_mut() {
|
||||
match binding_type {
|
||||
|
||||
Reference in New Issue
Block a user