mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-08 20:58:32 +00:00
Rename Red Knot (#17820)
This commit is contained in:
parent
e6a798b962
commit
b51c4f82ea
1564 changed files with 1598 additions and 1578 deletions
4
crates/ty_python_semantic/resources/README.md
Normal file
4
crates/ty_python_semantic/resources/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
Markdown files within the `mdtest/` subdirectory are tests of type inference and type checking;
|
||||
executed by the `tests/mdtest.rs` integration test.
|
||||
|
||||
See `crates/ty_test/README.md` for documentation of this test format.
|
|
@ -0,0 +1 @@
|
|||
wrap = 100
|
|
@ -0,0 +1,92 @@
|
|||
# `Annotated`
|
||||
|
||||
`Annotated` attaches arbitrary metadata to a given type.
|
||||
|
||||
## Usages
|
||||
|
||||
`Annotated[T, ...]` is equivalent to `T`: All metadata arguments are simply ignored.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
def _(x: Annotated[int, "foo"]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[int, lambda: 0 + 1 * 2 // 3, _(4)]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[int, "arbitrary", "metadata", "elements", "are", "fine"]):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
def _(x: Annotated[tuple[str, int], bytes]):
|
||||
reveal_type(x) # revealed: tuple[str, int]
|
||||
```
|
||||
|
||||
## Parameterization
|
||||
|
||||
It is invalid to parameterize `Annotated` with less than two arguments.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
|
||||
def _(x: Annotated):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
X = Annotated
|
||||
else:
|
||||
X = bool
|
||||
|
||||
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
|
||||
def f(y: X):
|
||||
reveal_type(y) # revealed: Unknown | bool
|
||||
|
||||
# error: [invalid-type-form] "`typing.Annotated` requires at least two arguments when used in a type expression"
|
||||
def _(x: Annotated | bool):
|
||||
reveal_type(x) # revealed: Unknown | bool
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[()]):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[int]):
|
||||
# `Annotated[T]` is invalid and will raise an error at runtime,
|
||||
# but we treat it the same as `T` to provide better diagnostics later on.
|
||||
# The subscription itself is still reported, regardless.
|
||||
# Same for the `(int,)` form below.
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# error: [invalid-type-form]
|
||||
def _(x: Annotated[(int,)]):
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
### Correctly parameterized
|
||||
|
||||
Inheriting from `Annotated[T, ...]` is equivalent to inheriting from `T` itself.
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
class C(Annotated[int, "foo"]): ...
|
||||
|
||||
# TODO: Should be `tuple[Literal[C], Literal[int], Literal[object]]`
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], @Todo(Inference of subscript on special form), Literal[object]]
|
||||
```
|
||||
|
||||
### Not parameterized
|
||||
|
||||
```py
|
||||
from typing_extensions import Annotated
|
||||
|
||||
# At runtime, this is an error.
|
||||
# error: [invalid-base]
|
||||
class C(Annotated): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
```
|
150
crates/ty_python_semantic/resources/mdtest/annotations/any.md
Normal file
150
crates/ty_python_semantic/resources/mdtest/annotations/any.md
Normal file
|
@ -0,0 +1,150 @@
|
|||
# Any
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Any` is a way to name the Any type.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
x: Any = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
```
|
||||
|
||||
## Aliased to a different name
|
||||
|
||||
If you alias `typing.Any` to another name, we still recognize that as a spelling of the Any type.
|
||||
|
||||
```py
|
||||
from typing import Any as RenamedAny
|
||||
|
||||
x: RenamedAny = 1
|
||||
x = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
```
|
||||
|
||||
## Shadowed class
|
||||
|
||||
If you define your own class named `Any`, using that in a type expression refers to your class, and
|
||||
isn't a spelling of the Any type.
|
||||
|
||||
```py
|
||||
class Any: ...
|
||||
|
||||
x: Any
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: Any
|
||||
|
||||
# This verifies that we're not accidentally seeing typing.Any, since str is assignable
|
||||
# to that but not to our locally defined class.
|
||||
y: Any = "not an Any" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Subclasses of `Any`
|
||||
|
||||
The spec allows you to define subclasses of `Any`.
|
||||
|
||||
`SubclassOfAny` has an unknown superclass, which might be `int`. The assignment to `x` should not be
|
||||
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
|
||||
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class SubclassOfAny(Any): ...
|
||||
|
||||
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]]
|
||||
|
||||
x: SubclassOfAny = 1 # error: [invalid-assignment]
|
||||
y: int = SubclassOfAny()
|
||||
```
|
||||
|
||||
`SubclassOfAny` should not be assignable to a final class though, because `SubclassOfAny` could not
|
||||
possibly be a subclass of `FinalClass`:
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class FinalClass: ...
|
||||
|
||||
f: FinalClass = SubclassOfAny() # error: [invalid-assignment]
|
||||
|
||||
@final
|
||||
class OtherFinalClass: ...
|
||||
|
||||
f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
A subclass of `Any` can also be assigned to arbitrary `Callable` and `Protocol` types:
|
||||
|
||||
```py
|
||||
from typing import Callable, Any, Protocol
|
||||
|
||||
def takes_callable1(f: Callable):
|
||||
f()
|
||||
|
||||
takes_callable1(SubclassOfAny())
|
||||
|
||||
def takes_callable2(f: Callable[[int], None]):
|
||||
f(1)
|
||||
|
||||
takes_callable2(SubclassOfAny())
|
||||
|
||||
class CallbackProtocol(Protocol):
|
||||
def __call__(self, x: int, /) -> None: ...
|
||||
|
||||
def takes_callback_proto(f: CallbackProtocol):
|
||||
f(1)
|
||||
|
||||
takes_callback_proto(SubclassOfAny())
|
||||
|
||||
class OtherProtocol(Protocol):
|
||||
x: int
|
||||
@property
|
||||
def foo(self) -> bytes: ...
|
||||
@foo.setter
|
||||
def foo(self, x: str) -> None: ...
|
||||
|
||||
def takes_other_protocol(f: OtherProtocol): ...
|
||||
|
||||
takes_other_protocol(SubclassOfAny())
|
||||
```
|
||||
|
||||
A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed:
|
||||
|
||||
```py
|
||||
from typing import Any, Literal
|
||||
|
||||
class MockAny(Any):
|
||||
pass
|
||||
|
||||
x: Literal[1] = MockAny() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
A use case where subclasses of `Any` come up is in mocking libraries, where the mock object should
|
||||
be assignable to (almost) any type:
|
||||
|
||||
```py
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
x: int = MagicMock()
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
`Any` cannot be parameterized:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
# error: [invalid-type-form] "Type `typing.Any` expected no type parameter"
|
||||
def f(x: Any[int]):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,306 @@
|
|||
# Callable
|
||||
|
||||
References:
|
||||
|
||||
- <https://typing.python.org/en/latest/spec/callables.html#callable>
|
||||
|
||||
Note that `typing.Callable` is deprecated at runtime, in favour of `collections.abc.Callable` (see:
|
||||
<https://docs.python.org/3/library/typing.html#deprecated-aliases>). However, removal of
|
||||
`typing.Callable` is not currently planned, and the canonical location of the stub for the symbol in
|
||||
typeshed is still `typing.pyi`.
|
||||
|
||||
## Invalid forms
|
||||
|
||||
The `Callable` special form requires _exactly_ two arguments where the first argument is either a
|
||||
parameter type list, parameter specification, `typing.Concatenate`, or `...` and the second argument
|
||||
is the return type. Here, we explore various invalid forms.
|
||||
|
||||
### Empty
|
||||
|
||||
A bare `Callable` without any type arguments:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### Invalid parameter type argument
|
||||
|
||||
When it's not a list:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
def _(c: Callable[int, str]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
Or, when it's a literal type:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
def _(c: Callable[42, str]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
Or, when one of the parameter type is invalid in the list:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||
# error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
|
||||
def _(c: Callable[[int, 42, str, False], None]):
|
||||
# revealed: (int, Unknown, str, Unknown, /) -> None
|
||||
reveal_type(c)
|
||||
```
|
||||
|
||||
### Missing return type
|
||||
|
||||
Using a parameter list:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
def _(c: Callable[[int, str]]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
Or, an ellipsis:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
def _(c: Callable[...]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
Or something else that's invalid in a type expression generally:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
{1, 2} # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
]
|
||||
):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### More than two arguments
|
||||
|
||||
We can't reliably infer the callable type if there are more then 2 arguments because we don't know
|
||||
which argument corresponds to either the parameters or the return type.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
def _(c: Callable[[int], str, str]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### List as the second argument
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# fmt: off
|
||||
|
||||
def _(c: Callable[
|
||||
int, # error: [invalid-type-form] "The first argument to `Callable` must be either a list of types, ParamSpec, Concatenate, or `...`"
|
||||
[str] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
]
|
||||
):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
### List as both arguments
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
def _(c: Callable[[int], [str]]):
|
||||
reveal_type(c) # revealed: (int, /) -> Unknown
|
||||
```
|
||||
|
||||
### Three list arguments
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
# fmt: off
|
||||
|
||||
|
||||
def _(c: Callable[ # error: [invalid-type-form] "Special form `typing.Callable` expected exactly two arguments (parameter types and return type)"
|
||||
[int],
|
||||
[str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
[bytes] # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
]
|
||||
):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
## Simple
|
||||
|
||||
A simple `Callable` with multiple parameters and a return type:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[int, str], int]):
|
||||
reveal_type(c) # revealed: (int, str, /) -> int
|
||||
```
|
||||
|
||||
## Union
|
||||
|
||||
```py
|
||||
from typing import Callable, Union
|
||||
|
||||
def _(
|
||||
c: Callable[[Union[int, str]], int] | None,
|
||||
d: None | Callable[[Union[int, str]], int],
|
||||
e: None | Callable[[Union[int, str]], int] | int,
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) | None
|
||||
reveal_type(d) # revealed: None | ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: None | ((int | str, /) -> int) | int
|
||||
```
|
||||
|
||||
## Intersection
|
||||
|
||||
```py
|
||||
from typing import Callable, Union
|
||||
from ty_extensions import Intersection, Not
|
||||
|
||||
def _(
|
||||
c: Intersection[Callable[[Union[int, str]], int], int],
|
||||
d: Intersection[int, Callable[[Union[int, str]], int]],
|
||||
e: Intersection[int, Callable[[Union[int, str]], int], str],
|
||||
f: Intersection[Not[Callable[[int, str], Intersection[int, str]]]],
|
||||
):
|
||||
reveal_type(c) # revealed: ((int | str, /) -> int) & int
|
||||
reveal_type(d) # revealed: int & ((int | str, /) -> int)
|
||||
reveal_type(e) # revealed: int & ((int | str, /) -> int) & str
|
||||
reveal_type(f) # revealed: ~((int, str, /) -> int & str)
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
A nested `Callable` as one of the parameter types:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[Callable[[int], str]], int]):
|
||||
reveal_type(c) # revealed: ((int, /) -> str, /) -> int
|
||||
```
|
||||
|
||||
And, as the return type:
|
||||
|
||||
```py
|
||||
def _(c: Callable[[int, str], Callable[[int], int]]):
|
||||
reveal_type(c) # revealed: (int, str, /) -> (int, /) -> int
|
||||
```
|
||||
|
||||
## Gradual form
|
||||
|
||||
The `Callable` special form supports the use of `...` in place of the list of parameter types. This
|
||||
is a [gradual form] indicating that the type is consistent with any input signature:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def gradual_form(c: Callable[..., str]):
|
||||
reveal_type(c) # revealed: (...) -> str
|
||||
```
|
||||
|
||||
## Using `typing.Concatenate`
|
||||
|
||||
Using `Concatenate` as the first argument to `Callable`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable, Concatenate
|
||||
|
||||
def _(c: Callable[Concatenate[int, str, ...], int]):
|
||||
# TODO: Should reveal the correct signature
|
||||
reveal_type(c) # revealed: (...) -> int
|
||||
```
|
||||
|
||||
And, as one of the parameter types:
|
||||
|
||||
```py
|
||||
def _(c: Callable[[Concatenate[int, str, ...], int], int]):
|
||||
# TODO: Should reveal the correct signature
|
||||
reveal_type(c) # revealed: (...) -> int
|
||||
```
|
||||
|
||||
## Using `typing.ParamSpec`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Using a `ParamSpec` in a `Callable` annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable
|
||||
|
||||
# TODO: Not an error; remove once `ParamSpec` is supported
|
||||
# error: [invalid-type-form]
|
||||
def _[**P1](c: Callable[P1, int]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
And, using the legacy syntax:
|
||||
|
||||
```py
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
P2 = ParamSpec("P2")
|
||||
|
||||
# TODO: Not an error; remove once `ParamSpec` is supported
|
||||
# error: [invalid-type-form]
|
||||
def _(c: Callable[P2, int]):
|
||||
reveal_type(c) # revealed: (...) -> Unknown
|
||||
```
|
||||
|
||||
## Using `typing.Unpack`
|
||||
|
||||
Using the unpack operator (`*`):
|
||||
|
||||
```py
|
||||
from typing_extensions import Callable, TypeVarTuple
|
||||
|
||||
Ts = TypeVarTuple("Ts")
|
||||
|
||||
def _(c: Callable[[int, *Ts], int]):
|
||||
# TODO: Should reveal the correct signature
|
||||
reveal_type(c) # revealed: (...) -> int
|
||||
```
|
||||
|
||||
And, using the legacy syntax using `Unpack`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Unpack
|
||||
|
||||
def _(c: Callable[[int, Unpack[Ts]], int]):
|
||||
# TODO: Should reveal the correct signature
|
||||
reveal_type(c) # revealed: (...) -> int
|
||||
```
|
||||
|
||||
## Member lookup
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[int], int]):
|
||||
reveal_type(c.__init__) # revealed: def __init__(self) -> None
|
||||
reveal_type(c.__class__) # revealed: type
|
||||
reveal_type(c.__call__) # revealed: (int, /) -> int
|
||||
```
|
||||
|
||||
[gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form
|
|
@ -0,0 +1,179 @@
|
|||
# Deferred annotations
|
||||
|
||||
## Deferred annotations in stubs always resolve
|
||||
|
||||
`mod.pyi`:
|
||||
|
||||
```pyi
|
||||
def get_foo() -> Foo: ...
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import get_foo
|
||||
|
||||
reveal_type(get_foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred annotations in regular code fail
|
||||
|
||||
In (regular) source files, annotations are *not* deferred. This also tests that imports from
|
||||
`__future__` that are not `annotations` are ignored.
|
||||
|
||||
```py
|
||||
from __future__ import with_statement as annotations
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def get_foo() -> Foo: ...
|
||||
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(get_foo()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Deferred annotations in regular code with `__future__.annotations`
|
||||
|
||||
If `__future__.annotations` is imported, annotations *are* deferred.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
def get_foo() -> Foo:
|
||||
return Foo()
|
||||
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(get_foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## Deferred self-reference annotations in a class definition
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Foo:
|
||||
this: Foo
|
||||
# error: [unresolved-reference]
|
||||
_ = Foo()
|
||||
# error: [unresolved-reference]
|
||||
[Foo for _ in range(1)]
|
||||
a = int
|
||||
|
||||
def f(self, x: Foo):
|
||||
reveal_type(x) # revealed: Foo
|
||||
|
||||
def g(self) -> Foo:
|
||||
_: Foo = self
|
||||
return self
|
||||
|
||||
class Bar:
|
||||
foo: Foo
|
||||
b = int
|
||||
|
||||
def f(self, x: Foo):
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Bar:
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def h[T: Bar](self):
|
||||
pass
|
||||
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
return Bar()
|
||||
type Baz = Foo
|
||||
```
|
||||
|
||||
## Non-deferred self-reference annotations in a class definition
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
# error: [unresolved-reference]
|
||||
this: Foo
|
||||
ok: "Foo"
|
||||
# error: [unresolved-reference]
|
||||
_ = Foo()
|
||||
# error: [unresolved-reference]
|
||||
[Foo for _ in range(1)]
|
||||
a = int
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def f(self, x: Foo):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Foo:
|
||||
_: Foo = self
|
||||
return self
|
||||
|
||||
class Bar:
|
||||
# error: [unresolved-reference]
|
||||
foo: Foo
|
||||
b = int
|
||||
|
||||
# error: [unresolved-reference]
|
||||
def f(self, x: Foo):
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def g(self) -> Bar:
|
||||
return self
|
||||
# error: [unresolved-reference]
|
||||
def h[T: Bar](self):
|
||||
pass
|
||||
|
||||
class Baz[T: Foo]:
|
||||
pass
|
||||
|
||||
# error: [unresolved-reference]
|
||||
type S = a
|
||||
type T = b
|
||||
|
||||
def h[T: Bar]():
|
||||
# error: [unresolved-reference]
|
||||
return Bar()
|
||||
type Qux = Foo
|
||||
|
||||
def _():
|
||||
class C:
|
||||
# error: [unresolved-reference]
|
||||
def f(self) -> C:
|
||||
return self
|
||||
```
|
||||
|
||||
## Base class references
|
||||
|
||||
### Not deferred by __future__.annotations
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A(B): # error: [unresolved-reference]
|
||||
pass
|
||||
|
||||
class B:
|
||||
pass
|
||||
```
|
||||
|
||||
### Deferred in stub files
|
||||
|
||||
```pyi
|
||||
class A(B): ...
|
||||
class B: ...
|
||||
```
|
|
@ -0,0 +1,90 @@
|
|||
# Special cases for int/float/complex in annotations
|
||||
|
||||
In order to support common use cases, an annotation of `float` actually means `int | float`, and an
|
||||
annotation of `complex` actually means `int | float | complex`. See
|
||||
[the specification](https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex)
|
||||
|
||||
## float
|
||||
|
||||
An annotation of `float` means `int | float`, so `int` is assignable to it:
|
||||
|
||||
```py
|
||||
def takes_float(x: float):
|
||||
pass
|
||||
|
||||
def passes_int_to_float(x: int):
|
||||
# no error!
|
||||
takes_float(x)
|
||||
```
|
||||
|
||||
It also applies to variable annotations:
|
||||
|
||||
```py
|
||||
def assigns_int_to_float(x: int):
|
||||
# no error!
|
||||
y: float = x
|
||||
```
|
||||
|
||||
It doesn't work the other way around:
|
||||
|
||||
```py
|
||||
def takes_int(x: int):
|
||||
pass
|
||||
|
||||
def passes_float_to_int(x: float):
|
||||
# error: [invalid-argument-type]
|
||||
takes_int(x)
|
||||
|
||||
def assigns_float_to_int(x: float):
|
||||
# error: [invalid-assignment]
|
||||
y: int = x
|
||||
```
|
||||
|
||||
Unlike other type checkers, we choose not to obfuscate this special case by displaying `int | float`
|
||||
as just `float`; we display the actual type:
|
||||
|
||||
```py
|
||||
def f(x: float):
|
||||
reveal_type(x) # revealed: int | float
|
||||
```
|
||||
|
||||
## complex
|
||||
|
||||
An annotation of `complex` means `int | float | complex`, so `int` and `float` are both assignable
|
||||
to it (but not the other way around):
|
||||
|
||||
```py
|
||||
def takes_complex(x: complex):
|
||||
pass
|
||||
|
||||
def passes_to_complex(x: float, y: int):
|
||||
# no errors!
|
||||
takes_complex(x)
|
||||
takes_complex(y)
|
||||
|
||||
def assigns_to_complex(x: float, y: int):
|
||||
# no errors!
|
||||
a: complex = x
|
||||
b: complex = y
|
||||
|
||||
def takes_int(x: int):
|
||||
pass
|
||||
|
||||
def takes_float(x: float):
|
||||
pass
|
||||
|
||||
def passes_complex(x: complex):
|
||||
# error: [invalid-argument-type]
|
||||
takes_int(x)
|
||||
# error: [invalid-argument-type]
|
||||
takes_float(x)
|
||||
|
||||
def assigns_complex(x: complex):
|
||||
# error: [invalid-assignment]
|
||||
y: int = x
|
||||
# error: [invalid-assignment]
|
||||
z: float = x
|
||||
|
||||
def f(x: complex):
|
||||
reveal_type(x) # revealed: int | float | complex
|
||||
```
|
|
@ -0,0 +1,115 @@
|
|||
# Tests for invalid types in type expressions
|
||||
|
||||
## Invalid types are rejected
|
||||
|
||||
Many types are illegal in the context of a type expression:
|
||||
|
||||
```py
|
||||
import typing
|
||||
from ty_extensions import AlwaysTruthy, AlwaysFalsy
|
||||
from typing_extensions import Literal, Never
|
||||
|
||||
class A: ...
|
||||
|
||||
def _(
|
||||
a: type[int],
|
||||
b: AlwaysTruthy,
|
||||
c: AlwaysFalsy,
|
||||
d: Literal[True],
|
||||
e: Literal["bar"],
|
||||
f: Literal[b"foo"],
|
||||
g: tuple[int, str],
|
||||
h: Never,
|
||||
i: int,
|
||||
j: A,
|
||||
):
|
||||
def foo(): ...
|
||||
def invalid(
|
||||
a_: a, # error: [invalid-type-form] "Variable of type `type[int]` is not allowed in a type expression"
|
||||
b_: b, # error: [invalid-type-form]
|
||||
c_: c, # error: [invalid-type-form]
|
||||
d_: d, # error: [invalid-type-form]
|
||||
e_: e, # error: [invalid-type-form]
|
||||
f_: f, # error: [invalid-type-form]
|
||||
g_: g, # error: [invalid-type-form]
|
||||
h_: h, # error: [invalid-type-form]
|
||||
i_: typing, # error: [invalid-type-form]
|
||||
j_: foo, # error: [invalid-type-form]
|
||||
k_: i, # error: [invalid-type-form] "Variable of type `int` is not allowed in a type expression"
|
||||
l_: j, # error: [invalid-type-form] "Variable of type `A` is not allowed in a type expression"
|
||||
):
|
||||
reveal_type(a_) # revealed: Unknown
|
||||
reveal_type(b_) # revealed: Unknown
|
||||
reveal_type(c_) # revealed: Unknown
|
||||
reveal_type(d_) # revealed: Unknown
|
||||
reveal_type(e_) # revealed: Unknown
|
||||
reveal_type(f_) # revealed: Unknown
|
||||
reveal_type(g_) # revealed: Unknown
|
||||
reveal_type(h_) # revealed: Unknown
|
||||
reveal_type(i_) # revealed: Unknown
|
||||
reveal_type(j_) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Invalid AST nodes
|
||||
|
||||
```py
|
||||
def bar() -> None:
|
||||
return None
|
||||
|
||||
async def outer(): # avoid unrelated syntax errors on yield, yield from, and await
|
||||
def _(
|
||||
a: 1, # error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||
b: 2.3, # error: [invalid-type-form] "Float literals are not allowed in type expressions"
|
||||
c: 4j, # error: [invalid-type-form] "Complex literals are not allowed in type expressions"
|
||||
d: True, # error: [invalid-type-form] "Boolean literals are not allowed in this context in a type expression"
|
||||
e: int | b"foo", # error: [invalid-type-form] "Bytes literals are not allowed in this context in a type expression"
|
||||
f: 1 and 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
|
||||
g: 1 or 2, # error: [invalid-type-form] "Boolean operations are not allowed in type expressions"
|
||||
h: (foo := 1), # error: [invalid-type-form] "Named expressions are not allowed in type expressions"
|
||||
i: not 1, # error: [invalid-type-form] "Unary operations are not allowed in type expressions"
|
||||
j: lambda: 1, # error: [invalid-type-form] "`lambda` expressions are not allowed in type expressions"
|
||||
k: 1 if True else 2, # error: [invalid-type-form] "`if` expressions are not allowed in type expressions"
|
||||
l: await 1, # error: [invalid-type-form] "`await` expressions are not allowed in type expressions"
|
||||
m: (yield 1), # error: [invalid-type-form] "`yield` expressions are not allowed in type expressions"
|
||||
n: (yield from [1]), # error: [invalid-type-form] "`yield from` expressions are not allowed in type expressions"
|
||||
o: 1 < 2, # error: [invalid-type-form] "Comparison expressions are not allowed in type expressions"
|
||||
p: bar(), # error: [invalid-type-form] "Function calls are not allowed in type expressions"
|
||||
q: int | f"foo", # error: [invalid-type-form] "F-strings are not allowed in type expressions"
|
||||
r: [1, 2, 3][1:2], # error: [invalid-type-form] "Slices are not allowed in type expressions"
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
reveal_type(e) # revealed: int | Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(g) # revealed: Unknown
|
||||
reveal_type(h) # revealed: Unknown
|
||||
reveal_type(i) # revealed: Unknown
|
||||
reveal_type(j) # revealed: Unknown
|
||||
reveal_type(k) # revealed: Unknown
|
||||
reveal_type(p) # revealed: Unknown
|
||||
reveal_type(q) # revealed: int | Unknown
|
||||
reveal_type(r) # revealed: @Todo(unknown type subscript)
|
||||
```
|
||||
|
||||
## Invalid Collection based AST nodes
|
||||
|
||||
```py
|
||||
def _(
|
||||
a: {1: 2}, # error: [invalid-type-form] "Dict literals are not allowed in type expressions"
|
||||
b: {1, 2}, # error: [invalid-type-form] "Set literals are not allowed in type expressions"
|
||||
c: {k: v for k, v in [(1, 2)]}, # error: [invalid-type-form] "Dict comprehensions are not allowed in type expressions"
|
||||
d: [k for k in [1, 2]], # error: [invalid-type-form] "List comprehensions are not allowed in type expressions"
|
||||
e: {k for k in [1, 2]}, # error: [invalid-type-form] "Set comprehensions are not allowed in type expressions"
|
||||
f: (k for k in [1, 2]), # error: [invalid-type-form] "Generator expressions are not allowed in type expressions"
|
||||
g: [int, str], # error: [invalid-type-form] "List literals are not allowed in this context in a type expression"
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,162 @@
|
|||
# Literal
|
||||
|
||||
<https://typing.python.org/en/latest/spec/literal.html#literals>
|
||||
|
||||
## Parameterization
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from enum import Enum
|
||||
|
||||
mode: Literal["w", "r"]
|
||||
a1: Literal[26]
|
||||
a2: Literal[0x1A]
|
||||
a3: Literal[-4]
|
||||
a4: Literal["hello world"]
|
||||
a5: Literal[b"hello world"]
|
||||
a6: Literal[True]
|
||||
a7: Literal[None]
|
||||
a8: Literal[Literal[1]]
|
||||
|
||||
class Color(Enum):
|
||||
RED = 0
|
||||
GREEN = 1
|
||||
BLUE = 2
|
||||
|
||||
b1: Literal[Color.RED]
|
||||
|
||||
def f():
|
||||
reveal_type(mode) # revealed: Literal["w", "r"]
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
reveal_type(a2) # revealed: Literal[26]
|
||||
reveal_type(a3) # revealed: Literal[-4]
|
||||
reveal_type(a4) # revealed: Literal["hello world"]
|
||||
reveal_type(a5) # revealed: Literal[b"hello world"]
|
||||
reveal_type(a6) # revealed: Literal[True]
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: @Todo(Attribute access on enum classes)
|
||||
|
||||
# error: [invalid-type-form]
|
||||
invalid1: Literal[3 + 4]
|
||||
# error: [invalid-type-form]
|
||||
invalid2: Literal[4 + 3j]
|
||||
# error: [invalid-type-form]
|
||||
invalid3: Literal[(3, 4)]
|
||||
|
||||
hello = "hello"
|
||||
invalid4: Literal[
|
||||
1 + 2, # error: [invalid-type-form]
|
||||
"foo",
|
||||
hello, # error: [invalid-type-form]
|
||||
(1, 2, 3), # error: [invalid-type-form]
|
||||
]
|
||||
```
|
||||
|
||||
## Shortening unions of literals
|
||||
|
||||
When a Literal is parameterized with more than one value, it’s treated as exactly to equivalent to
|
||||
the union of those types.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def x(
|
||||
a1: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None],
|
||||
a2: Literal["w"] | Literal["r"],
|
||||
a3: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]],
|
||||
a4: Literal[True] | Literal[1, 2] | Literal["foo"],
|
||||
):
|
||||
reveal_type(a1) # revealed: Literal[1, 2, 3, 5, "foo"] | None
|
||||
reveal_type(a2) # revealed: Literal["w", "r"]
|
||||
reveal_type(a3) # revealed: Literal["w", "r", "w+"]
|
||||
reveal_type(a4) # revealed: Literal[True, 1, 2, "foo"]
|
||||
```
|
||||
|
||||
## Display of heterogeneous unions of literals
|
||||
|
||||
```py
|
||||
from typing import Literal, Union
|
||||
|
||||
def foo(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
def bar(s: str) -> str:
|
||||
return s
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def union_example(
|
||||
x: Union[
|
||||
# unknown type
|
||||
# error: [unresolved-reference]
|
||||
y,
|
||||
Literal[-1],
|
||||
Literal["A"],
|
||||
Literal[b"A"],
|
||||
Literal[b"\x00"],
|
||||
Literal[b"\x07"],
|
||||
Literal[0],
|
||||
Literal[1],
|
||||
Literal["B"],
|
||||
Literal["foo"],
|
||||
Literal["bar"],
|
||||
Literal["B"],
|
||||
Literal[True],
|
||||
None,
|
||||
],
|
||||
):
|
||||
reveal_type(x) # revealed: Unknown | Literal[-1, 0, 1, "A", "B", "foo", "bar", b"A", b"\x00", b"\x07", True] | None
|
||||
```
|
||||
|
||||
## Detecting Literal outside typing and typing_extensions
|
||||
|
||||
Only Literal that is defined in typing and typing_extension modules is detected as the special
|
||||
Literal.
|
||||
|
||||
`other.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import _SpecialForm
|
||||
|
||||
Literal: _SpecialForm
|
||||
```
|
||||
|
||||
```py
|
||||
from other import Literal
|
||||
|
||||
# TODO: can we add a subdiagnostic here saying something like:
|
||||
#
|
||||
# `other.Literal` and `typing.Literal` have similar names, but are different symbols and don't have the same semantics
|
||||
#
|
||||
# ?
|
||||
#
|
||||
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: @Todo(unknown type subscript)
|
||||
```
|
||||
|
||||
## Detecting typing_extensions.Literal
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
# error: [invalid-type-form] "`typing.Literal` requires at least one argument when used in a type expression"
|
||||
def _(x: Literal):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,152 @@
|
|||
# `LiteralString`
|
||||
|
||||
`LiteralString` represents a string that is either defined directly within the source code or is
|
||||
made up of such components.
|
||||
|
||||
Parts of the testcases defined here were adapted from [the specification's examples][1].
|
||||
|
||||
## Usages
|
||||
|
||||
### Valid places
|
||||
|
||||
It can be used anywhere a type is accepted:
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
x: LiteralString
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Within `Literal`
|
||||
|
||||
`LiteralString` cannot be used within `Literal`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
bad_union: Literal["hello", LiteralString] # error: [invalid-type-form]
|
||||
bad_nesting: Literal[LiteralString] # error: [invalid-type-form]
|
||||
```
|
||||
|
||||
### Parameterized
|
||||
|
||||
`LiteralString` cannot be parameterized.
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
# error: [invalid-type-form]
|
||||
a: LiteralString[str]
|
||||
|
||||
# error: [invalid-type-form]
|
||||
# error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
b: LiteralString["foo"]
|
||||
```
|
||||
|
||||
### As a base class
|
||||
|
||||
Subclassing `LiteralString` leads to a runtime error.
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
class C(LiteralString): ... # error: [invalid-base]
|
||||
```
|
||||
|
||||
## Inference
|
||||
|
||||
### Common operations
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
foo: LiteralString = "foo"
|
||||
reveal_type(foo) # revealed: Literal["foo"]
|
||||
|
||||
bar: LiteralString = "bar"
|
||||
reveal_type(foo + bar) # revealed: Literal["foobar"]
|
||||
|
||||
baz: LiteralString = "baz"
|
||||
baz += foo
|
||||
reveal_type(baz) # revealed: Literal["bazfoo"]
|
||||
|
||||
qux = (foo, bar)
|
||||
reveal_type(qux) # revealed: tuple[Literal["foo"], Literal["bar"]]
|
||||
|
||||
reveal_type(foo.join(qux)) # revealed: LiteralString
|
||||
|
||||
template: LiteralString = "{}, {}"
|
||||
reveal_type(template) # revealed: Literal["{}, {}"]
|
||||
reveal_type(template.format(foo, bar)) # revealed: LiteralString
|
||||
```
|
||||
|
||||
### Assignability
|
||||
|
||||
`Literal[""]` is assignable to `LiteralString`, and `LiteralString` is assignable to `str`, but not
|
||||
vice versa.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
|
||||
def _(flag: bool):
|
||||
foo_1: Literal["foo"] = "foo"
|
||||
bar_1: LiteralString = foo_1 # fine
|
||||
|
||||
foo_2 = "foo" if flag else "bar"
|
||||
reveal_type(foo_2) # revealed: Literal["foo", "bar"]
|
||||
bar_2: LiteralString = foo_2 # fine
|
||||
|
||||
foo_3: LiteralString = "foo" * 1_000_000_000
|
||||
bar_3: str = foo_2 # fine
|
||||
|
||||
baz_1: str = repr(object())
|
||||
qux_1: LiteralString = baz_1 # error: [invalid-assignment]
|
||||
|
||||
baz_2: LiteralString = "baz" * 1_000_000_000
|
||||
qux_2: Literal["qux"] = baz_2 # error: [invalid-assignment]
|
||||
|
||||
baz_3 = "foo" if flag else 1
|
||||
reveal_type(baz_3) # revealed: Literal["foo", 1]
|
||||
qux_3: LiteralString = baz_3 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
### Narrowing
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
lorem: LiteralString = "lorem" * 1_000_000_000
|
||||
|
||||
reveal_type(lorem) # revealed: LiteralString
|
||||
|
||||
if lorem == "ipsum":
|
||||
reveal_type(lorem) # revealed: Literal["ipsum"]
|
||||
|
||||
reveal_type(lorem) # revealed: LiteralString
|
||||
|
||||
if "" < lorem == "ipsum":
|
||||
reveal_type(lorem) # revealed: Literal["ipsum"]
|
||||
```
|
||||
|
||||
## `typing.LiteralString`
|
||||
|
||||
`typing.LiteralString` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import LiteralString
|
||||
|
||||
x: LiteralString = "foo"
|
||||
|
||||
def f():
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
```
|
||||
|
||||
[1]: https://typing.python.org/en/latest/spec/literal.html#literalstring
|
|
@ -0,0 +1,75 @@
|
|||
# NoReturn & Never
|
||||
|
||||
`NoReturn` is used to annotate the return type for functions that never return. `Never` is the
|
||||
bottom type, representing the empty set of Python objects. These two annotations can be used
|
||||
interchangeably.
|
||||
|
||||
## Function Return Type Annotation
|
||||
|
||||
```py
|
||||
from typing import NoReturn
|
||||
|
||||
def stop() -> NoReturn:
|
||||
raise RuntimeError("no way")
|
||||
|
||||
# revealed: Never
|
||||
reveal_type(stop())
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing_extensions import NoReturn, Never, Any
|
||||
|
||||
# error: [invalid-type-form] "Type `typing.Never` expected no type parameter"
|
||||
x: Never[int]
|
||||
a1: NoReturn
|
||||
a2: Never
|
||||
b1: Any
|
||||
b2: int
|
||||
|
||||
def f():
|
||||
# revealed: Never
|
||||
reveal_type(a1)
|
||||
# revealed: Never
|
||||
reveal_type(a2)
|
||||
|
||||
# Never is assignable to all types.
|
||||
v1: int = a1
|
||||
v2: str = a1
|
||||
# Other types are not assignable to Never except for Never (and Any).
|
||||
v3: Never = b1
|
||||
v4: Never = a2
|
||||
v5: Any = b2
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Never`"
|
||||
v6: Never = 1
|
||||
```
|
||||
|
||||
## `typing.Never`
|
||||
|
||||
`typing.Never` is only available in Python 3.11 and later.
|
||||
|
||||
### Python 3.11
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
|
||||
reveal_type(Never) # revealed: typing.Never
|
||||
```
|
||||
|
||||
### Python 3.10
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
from typing import Never
|
||||
```
|
|
@ -0,0 +1,24 @@
|
|||
# NewType
|
||||
|
||||
Currently, ty doesn't support `typing.NewType` in type annotations.
|
||||
|
||||
## Valid forms
|
||||
|
||||
```py
|
||||
from typing_extensions import NewType
|
||||
from types import GenericAlias
|
||||
|
||||
X = GenericAlias(type, ())
|
||||
A = NewType("A", int)
|
||||
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
|
||||
# to be compatible with `type`
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `type`, found `NewType`"
|
||||
B = GenericAlias(A, ())
|
||||
|
||||
def _(
|
||||
a: A,
|
||||
b: B,
|
||||
):
|
||||
reveal_type(a) # revealed: @Todo(Support for `typing.NewType` instances in type expressions)
|
||||
reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
|
||||
```
|
|
@ -0,0 +1,57 @@
|
|||
# Optional
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Optional` is equivalent to using the type with a None in a Union.
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
a: Optional[int]
|
||||
a1: Optional[bool]
|
||||
a2: Optional[Optional[bool]]
|
||||
a3: Optional[None]
|
||||
|
||||
def f():
|
||||
# revealed: int | None
|
||||
reveal_type(a)
|
||||
# revealed: bool | None
|
||||
reveal_type(a1)
|
||||
# revealed: bool | None
|
||||
reveal_type(a2)
|
||||
# revealed: None
|
||||
reveal_type(a3)
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
a: Optional[int] = 1
|
||||
a = None
|
||||
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int | None`"
|
||||
a = ""
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import Optional
|
||||
|
||||
a: Optional[int]
|
||||
|
||||
def f():
|
||||
# revealed: int | None
|
||||
reveal_type(a)
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
```py
|
||||
from typing import Optional
|
||||
|
||||
# error: [invalid-type-form] "`typing.Optional` requires exactly one argument when used in a type expression"
|
||||
def f(x: Optional) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,23 @@
|
|||
# Starred expression annotations
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
Type annotations for `*args` can be starred expressions themselves:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVarTuple
|
||||
|
||||
Ts = TypeVarTuple("Ts")
|
||||
|
||||
def append_int(*args: *Ts) -> tuple[*Ts, int]:
|
||||
# TODO: tuple[*Ts]
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
return (*args, 1)
|
||||
|
||||
# TODO should be tuple[Literal[True], Literal["a"], int]
|
||||
reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
|
@ -0,0 +1,142 @@
|
|||
# Typing-module aliases to other stdlib classes
|
||||
|
||||
The `typing` module has various aliases to other stdlib classes. These are a legacy feature, but
|
||||
still need to be supported by a type checker.
|
||||
|
||||
## Correspondence
|
||||
|
||||
All of the following symbols can be mapped one-to-one with the actual type:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
def f(
|
||||
list_bare: typing.List,
|
||||
list_parametrized: typing.List[int],
|
||||
dict_bare: typing.Dict,
|
||||
dict_parametrized: typing.Dict[int, str],
|
||||
set_bare: typing.Set,
|
||||
set_parametrized: typing.Set[int],
|
||||
frozen_set_bare: typing.FrozenSet,
|
||||
frozen_set_parametrized: typing.FrozenSet[str],
|
||||
chain_map_bare: typing.ChainMap,
|
||||
chain_map_parametrized: typing.ChainMap[str, int],
|
||||
counter_bare: typing.Counter,
|
||||
counter_parametrized: typing.Counter[int],
|
||||
default_dict_bare: typing.DefaultDict,
|
||||
default_dict_parametrized: typing.DefaultDict[str, int],
|
||||
deque_bare: typing.Deque,
|
||||
deque_parametrized: typing.Deque[str],
|
||||
ordered_dict_bare: typing.OrderedDict,
|
||||
ordered_dict_parametrized: typing.OrderedDict[int, str],
|
||||
):
|
||||
# TODO: revealed: list[Unknown]
|
||||
reveal_type(list_bare) # revealed: list
|
||||
# TODO: revealed: list[int]
|
||||
reveal_type(list_parametrized) # revealed: list
|
||||
|
||||
reveal_type(dict_bare) # revealed: dict[Unknown, Unknown]
|
||||
# TODO: revealed: dict[int, str]
|
||||
reveal_type(dict_parametrized) # revealed: dict[Unknown, Unknown]
|
||||
|
||||
# TODO: revealed: set[Unknown]
|
||||
reveal_type(set_bare) # revealed: set
|
||||
# TODO: revealed: set[int]
|
||||
reveal_type(set_parametrized) # revealed: set
|
||||
|
||||
# TODO: revealed: frozenset[Unknown]
|
||||
reveal_type(frozen_set_bare) # revealed: frozenset
|
||||
# TODO: revealed: frozenset[str]
|
||||
reveal_type(frozen_set_parametrized) # revealed: frozenset
|
||||
|
||||
reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown]
|
||||
# TODO: revealed: ChainMap[str, int]
|
||||
reveal_type(chain_map_parametrized) # revealed: ChainMap[Unknown, Unknown]
|
||||
|
||||
reveal_type(counter_bare) # revealed: Counter[Unknown]
|
||||
# TODO: revealed: Counter[int]
|
||||
reveal_type(counter_parametrized) # revealed: Counter[Unknown]
|
||||
|
||||
reveal_type(default_dict_bare) # revealed: defaultdict[Unknown, Unknown]
|
||||
# TODO: revealed: defaultdict[str, int]
|
||||
reveal_type(default_dict_parametrized) # revealed: defaultdict[Unknown, Unknown]
|
||||
|
||||
# TODO: revealed: deque[Unknown]
|
||||
reveal_type(deque_bare) # revealed: deque
|
||||
# TODO: revealed: deque[str]
|
||||
reveal_type(deque_parametrized) # revealed: deque
|
||||
|
||||
reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown]
|
||||
# TODO: revealed: OrderedDict[int, str]
|
||||
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[Unknown, Unknown]
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
The aliases can be inherited from. Some of these are still partially or wholly TODOs.
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
####################
|
||||
### Built-ins
|
||||
####################
|
||||
|
||||
class ListSubclass(typing.List): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
reveal_type(ListSubclass.__mro__)
|
||||
|
||||
class DictSubclass(typing.Dict): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[DictSubclass], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
reveal_type(DictSubclass.__mro__)
|
||||
|
||||
class SetSubclass(typing.Set): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
reveal_type(SetSubclass.__mro__)
|
||||
|
||||
class FrozenSetSubclass(typing.FrozenSet): ...
|
||||
|
||||
# TODO: should have `Generic`, should not have `Unknown`
|
||||
# revealed: tuple[Literal[FrozenSetSubclass], Literal[frozenset], Unknown, Literal[object]]
|
||||
reveal_type(FrozenSetSubclass.__mro__)
|
||||
|
||||
####################
|
||||
### `collections`
|
||||
####################
|
||||
|
||||
class ChainMapSubclass(typing.ChainMap): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
reveal_type(ChainMapSubclass.__mro__)
|
||||
|
||||
class CounterSubclass(typing.Counter): ...
|
||||
|
||||
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[CounterSubclass], Literal[Counter[Unknown]], Literal[dict[_T, int]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], Literal[object]]
|
||||
reveal_type(CounterSubclass.__mro__)
|
||||
|
||||
class DefaultDictSubclass(typing.DefaultDict): ...
|
||||
|
||||
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
reveal_type(DefaultDictSubclass.__mro__)
|
||||
|
||||
class DequeSubclass(typing.Deque): ...
|
||||
|
||||
# TODO: generic protocols
|
||||
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
|
||||
reveal_type(DequeSubclass.__mro__)
|
||||
|
||||
class OrderedDictSubclass(typing.OrderedDict): ...
|
||||
|
||||
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
|
||||
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
|
||||
reveal_type(OrderedDictSubclass.__mro__)
|
||||
```
|
212
crates/ty_python_semantic/resources/mdtest/annotations/string.md
Normal file
212
crates/ty_python_semantic/resources/mdtest/annotations/string.md
Normal file
|
@ -0,0 +1,212 @@
|
|||
# String annotations
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def f(v: "int"):
|
||||
reveal_type(v) # revealed: int
|
||||
```
|
||||
|
||||
## Nested
|
||||
|
||||
```py
|
||||
def f(v: "'int'"):
|
||||
reveal_type(v) # revealed: int
|
||||
```
|
||||
|
||||
## Type expression
|
||||
|
||||
```py
|
||||
def f1(v: "int | str", w: "tuple[int, str]"):
|
||||
reveal_type(v) # revealed: int | str
|
||||
reveal_type(w) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Partial
|
||||
|
||||
```py
|
||||
def f(v: tuple[int, "str"]):
|
||||
reveal_type(v) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Deferred
|
||||
|
||||
```py
|
||||
def f(v: "Foo"):
|
||||
reveal_type(v) # revealed: Foo
|
||||
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
## Deferred (undefined)
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
def f(v: "Foo"):
|
||||
reveal_type(v) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Partial deferred
|
||||
|
||||
```py
|
||||
def f(v: int | "Foo"):
|
||||
reveal_type(v) # revealed: int | Foo
|
||||
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
## `typing.Literal`
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f1(v: Literal["Foo", "Bar"], w: 'Literal["Foo", "Bar"]'):
|
||||
reveal_type(v) # revealed: Literal["Foo", "Bar"]
|
||||
reveal_type(w) # revealed: Literal["Foo", "Bar"]
|
||||
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
## Various string kinds
|
||||
|
||||
```py
|
||||
def f1(
|
||||
# error: [raw-string-type-annotation] "Type expressions cannot use raw string literal"
|
||||
a: r"int",
|
||||
# error: [fstring-type-annotation] "Type expressions cannot use f-strings"
|
||||
b: f"int",
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
c: b"int",
|
||||
d: "int",
|
||||
# error: [implicit-concatenated-string-type-annotation] "Type expressions cannot span multiple string literals"
|
||||
e: "in" "t",
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
f: "\N{LATIN SMALL LETTER I}nt",
|
||||
# error: [escape-character-in-forward-annotation] "Type expressions cannot contain escape characters"
|
||||
g: "\x69nt",
|
||||
h: """int""",
|
||||
# error: [byte-string-type-annotation] "Type expressions cannot use bytes literal"
|
||||
i: "b'int'",
|
||||
):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: int
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
reveal_type(g) # revealed: Unknown
|
||||
reveal_type(h) # revealed: int
|
||||
reveal_type(i) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Various string kinds in `typing.Literal`
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f(v: Literal["a", r"b", b"c", "d" "e", "\N{LATIN SMALL LETTER F}", "\x67", """h"""]):
|
||||
reveal_type(v) # revealed: Literal["a", "b", "de", "f", "g", "h", b"c"]
|
||||
```
|
||||
|
||||
## Class variables
|
||||
|
||||
```py
|
||||
MyType = int
|
||||
|
||||
class Aliases:
|
||||
MyType = str
|
||||
|
||||
forward: "MyType" = "value"
|
||||
not_forward: MyType = "value"
|
||||
|
||||
reveal_type(Aliases.forward) # revealed: str
|
||||
reveal_type(Aliases.not_forward) # revealed: str
|
||||
```
|
||||
|
||||
## Annotated assignment
|
||||
|
||||
```py
|
||||
a: "int" = 1
|
||||
b: "'int'" = 1
|
||||
c: "Foo"
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Foo`"
|
||||
d: "Foo" = 1
|
||||
|
||||
class Foo: ...
|
||||
|
||||
c = Foo()
|
||||
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[1]
|
||||
reveal_type(c) # revealed: Foo
|
||||
reveal_type(d) # revealed: Foo
|
||||
```
|
||||
|
||||
## Parameter
|
||||
|
||||
TODO: Add tests once parameter inference is supported
|
||||
|
||||
## Invalid expressions
|
||||
|
||||
The expressions in these string annotations aren't valid expressions in this context but we
|
||||
shouldn't panic.
|
||||
|
||||
```py
|
||||
a: "1 or 2"
|
||||
b: "(x := 1)"
|
||||
c: "1 + 2"
|
||||
d: "lambda x: x"
|
||||
e: "x if True else y"
|
||||
f: "{'a': 1, 'b': 2}"
|
||||
g: "{1, 2}"
|
||||
h: "[i for i in range(5)]"
|
||||
i: "{i for i in range(5)}"
|
||||
j: "{i: i for i in range(5)}"
|
||||
k: "(i for i in range(5))"
|
||||
l: "await 1"
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
m: "yield 1"
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
n: "yield from 1"
|
||||
o: "1 < 2"
|
||||
p: "call()"
|
||||
r: "[1, 2]"
|
||||
s: "(1, 2)"
|
||||
```
|
||||
|
||||
## Multi line annotation
|
||||
|
||||
Quoted type annotations should be parsed as if surrounded by parentheses.
|
||||
|
||||
```py
|
||||
def valid(
|
||||
a1: """(
|
||||
int |
|
||||
str
|
||||
)
|
||||
""",
|
||||
a2: """
|
||||
int |
|
||||
str
|
||||
""",
|
||||
):
|
||||
reveal_type(a1) # revealed: int | str
|
||||
reveal_type(a2) # revealed: int | str
|
||||
|
||||
def invalid(
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
a1: """
|
||||
int |
|
||||
str)
|
||||
""",
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
a2: """
|
||||
int) |
|
||||
str
|
||||
""",
|
||||
# error: [invalid-syntax-in-forward-annotation]
|
||||
a3: """
|
||||
(int)) """,
|
||||
):
|
||||
pass
|
||||
```
|
|
@ -0,0 +1,88 @@
|
|||
# Union
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Union` can be used to construct union types in the same way as the `|` operator.
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a1: Union[int, bool]
|
||||
a2: Union[int, Union[bytes, str]]
|
||||
a3: Union[int, None]
|
||||
a4: Union[Union[bytes, str]]
|
||||
a5: Union[int]
|
||||
a6: Union[()]
|
||||
|
||||
def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
|
||||
# revealed: int
|
||||
reveal_type(a1)
|
||||
# revealed: int | bytes | str
|
||||
reveal_type(a2)
|
||||
# revealed: int | None
|
||||
reveal_type(a3)
|
||||
# revealed: bytes | str
|
||||
reveal_type(a4)
|
||||
# revealed: int
|
||||
reveal_type(a5)
|
||||
# revealed: Never
|
||||
reveal_type(a6)
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a = 1
|
||||
a = ""
|
||||
a1: Union[int, bool]
|
||||
a1 = 1
|
||||
a1 = True
|
||||
# error: [invalid-assignment] "Object of type `Literal[b""]` is not assignable to `int | str`"
|
||||
a = b""
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import Union
|
||||
|
||||
a: Union[int, str]
|
||||
|
||||
def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
# error: [invalid-type-form] "`typing.Union` requires at least one argument when used in a type expression"
|
||||
def f(x: Union) -> None:
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Implicit type aliases using new-style unions
|
||||
|
||||
We don't recognise these as type aliases yet, but we also don't emit false-positive diagnostics if
|
||||
you use them in type expressions:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
X = int | str
|
||||
|
||||
def f(y: X):
|
||||
reveal_type(y) # revealed: @Todo(Support for `types.UnionType` instances in type expressions)
|
||||
```
|
|
@ -0,0 +1,101 @@
|
|||
# Unsupported special forms
|
||||
|
||||
## Not yet supported
|
||||
|
||||
Several special forms are unsupported by ty currently. However, we also don't emit false-positive
|
||||
errors if you use one in an annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Self, TypeVarTuple, Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, TypeAlias, Callable, TypeVar
|
||||
|
||||
P = ParamSpec("P")
|
||||
Ts = TypeVarTuple("Ts")
|
||||
R_co = TypeVar("R_co", covariant=True)
|
||||
|
||||
Alias: TypeAlias = int
|
||||
|
||||
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
|
||||
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
|
||||
|
||||
def g() -> TypeGuard[int]: ...
|
||||
def h() -> TypeIs[int]: ...
|
||||
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
|
||||
# TODO: should understand the annotation
|
||||
reveal_type(args) # revealed: tuple
|
||||
reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)]
|
||||
return callback(42, *args, **kwargs)
|
||||
|
||||
class Foo:
|
||||
def method(self, x: Self):
|
||||
reveal_type(x) # revealed: @Todo(Support for `typing.Self`)
|
||||
```
|
||||
|
||||
## Type expressions
|
||||
|
||||
One thing that is supported is error messages for using special forms in type expressions.
|
||||
|
||||
```py
|
||||
from typing_extensions import Unpack, TypeGuard, TypeIs, Concatenate, ParamSpec, Generic
|
||||
|
||||
def _(
|
||||
a: Unpack, # error: [invalid-type-form] "`typing.Unpack` requires exactly one argument when used in a type expression"
|
||||
b: TypeGuard, # error: [invalid-type-form] "`typing.TypeGuard` requires exactly one argument when used in a type expression"
|
||||
c: TypeIs, # error: [invalid-type-form] "`typing.TypeIs` requires exactly one argument when used in a type expression"
|
||||
d: Concatenate, # error: [invalid-type-form] "`typing.Concatenate` requires at least two arguments when used in a type expression"
|
||||
e: ParamSpec,
|
||||
f: Generic, # error: [invalid-type-form] "`typing.Generic` is not allowed in type expressions"
|
||||
) -> None:
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
|
||||
def foo(a_: e) -> None:
|
||||
reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`)
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
You can't inherit from most of these. `typing.Callable` is an exception.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from typing_extensions import Self, Unpack, TypeGuard, TypeIs, Concatenate, Generic
|
||||
|
||||
class A(Self): ... # error: [invalid-base]
|
||||
class B(Unpack): ... # error: [invalid-base]
|
||||
class C(TypeGuard): ... # error: [invalid-base]
|
||||
class D(TypeIs): ... # error: [invalid-base]
|
||||
class E(Concatenate): ... # error: [invalid-base]
|
||||
class F(Callable): ...
|
||||
class G(Generic): ... # error: [invalid-base] "Cannot inherit from plain `Generic`"
|
||||
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], @Todo(Support for Callable as a base class), Literal[object]]
|
||||
```
|
||||
|
||||
## Subscriptability
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Some of these are not subscriptable:
|
||||
|
||||
```py
|
||||
from typing_extensions import Self, TypeAlias, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.TypeAlias` expected no type parameter"
|
||||
X: TypeAlias[T] = int
|
||||
|
||||
class Foo[T]:
|
||||
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"
|
||||
# error: [invalid-type-form] "Special form `typing.Self` expected no type parameter"
|
||||
def method(self: Self[int]) -> Self[int]:
|
||||
reveal_type(self) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,59 @@
|
|||
# Unsupported type qualifiers
|
||||
|
||||
## Not yet fully supported
|
||||
|
||||
Several type qualifiers are unsupported by ty currently. However, we also don't emit false-positive
|
||||
errors if you use one in an annotation:
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, Required, NotRequired, ReadOnly, TypedDict
|
||||
|
||||
X: Final = 42
|
||||
Y: Final[int] = 42
|
||||
|
||||
class Bar(TypedDict):
|
||||
x: Required[int]
|
||||
y: NotRequired[str]
|
||||
z: ReadOnly[bytes]
|
||||
```
|
||||
|
||||
## Type expressions
|
||||
|
||||
One thing that is supported is error messages for using type qualifiers in type expressions.
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
|
||||
|
||||
def _(
|
||||
a: (
|
||||
Final # error: [invalid-type-form] "Type qualifier `typing.Final` is not allowed in type expressions (only in annotation expressions)"
|
||||
| int
|
||||
),
|
||||
b: (
|
||||
ClassVar # error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
|
||||
| int
|
||||
),
|
||||
c: Required, # error: [invalid-type-form] "Type qualifier `typing.Required` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
||||
d: NotRequired, # error: [invalid-type-form] "Type qualifier `typing.NotRequired` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
||||
e: ReadOnly, # error: [invalid-type-form] "Type qualifier `typing.ReadOnly` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)"
|
||||
) -> None:
|
||||
reveal_type(a) # revealed: Unknown | int
|
||||
reveal_type(b) # revealed: Unknown | int
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
You can't inherit from a type qualifier.
|
||||
|
||||
```py
|
||||
from typing_extensions import Final, ClassVar, Required, NotRequired, ReadOnly
|
||||
|
||||
class A(Final): ... # error: [invalid-base]
|
||||
class B(ClassVar): ... # error: [invalid-base]
|
||||
class C(Required): ... # error: [invalid-base]
|
||||
class D(NotRequired): ... # error: [invalid-base]
|
||||
class E(ReadOnly): ... # error: [invalid-base]
|
||||
```
|
|
@ -0,0 +1,138 @@
|
|||
# Assignment with annotations
|
||||
|
||||
## Annotation only transparent to local inference
|
||||
|
||||
```py
|
||||
x = 1
|
||||
x: int
|
||||
y = x
|
||||
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Violates own annotation
|
||||
|
||||
```py
|
||||
x: int = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## Violates previous annotation
|
||||
|
||||
```py
|
||||
x: int
|
||||
x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `int`"
|
||||
```
|
||||
|
||||
## Tuple annotations are understood
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Unpack
|
||||
|
||||
a: tuple[()] = ()
|
||||
b: tuple[int] = (42,)
|
||||
c: tuple[str, int] = ("42", 42)
|
||||
d: tuple[tuple[str, str], tuple[int, int]] = (("foo", "foo"), (42, 42))
|
||||
e: tuple[str, ...] = ()
|
||||
f: tuple[str, *tuple[int, ...], bytes] = ("42", b"42")
|
||||
g: tuple[str, Unpack[tuple[int, ...]], bytes] = ("42", b"42")
|
||||
h: tuple[list[int], list[int]] = ([], [])
|
||||
i: tuple[str | int, str | int] = (42, 42)
|
||||
j: tuple[str | int] = (42,)
|
||||
```
|
||||
|
||||
`script.py`:
|
||||
|
||||
```py
|
||||
from module import a, b, c, d, e, f, g, h, i, j
|
||||
|
||||
reveal_type(a) # revealed: tuple[()]
|
||||
reveal_type(b) # revealed: tuple[int]
|
||||
reveal_type(c) # revealed: tuple[str, int]
|
||||
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
|
||||
# TODO: homogeneous tuples, PEP-646 tuples, generics
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(h) # revealed: tuple[@Todo(specialized non-generic class), @Todo(specialized non-generic class)]
|
||||
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
reveal_type(j) # revealed: tuple[str | int]
|
||||
```
|
||||
|
||||
## Incorrect tuple assignments are complained about
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Object of type `tuple[Literal[1], Literal[2]]` is not assignable to `tuple[()]`"
|
||||
a: tuple[()] = (1, 2)
|
||||
|
||||
# error: [invalid-assignment] "Object of type `tuple[Literal["foo"]]` is not assignable to `tuple[int]`"
|
||||
b: tuple[int] = ("foo",)
|
||||
|
||||
# error: [invalid-assignment] "Object of type `tuple[list, Literal["foo"]]` is not assignable to `tuple[str | int, str]`"
|
||||
c: tuple[str | int, str] = ([], "foo")
|
||||
```
|
||||
|
||||
## PEP-604 annotations are supported
|
||||
|
||||
```py
|
||||
def foo(v: str | int | None, w: str | str | None, x: str | str):
|
||||
reveal_type(v) # revealed: str | int | None
|
||||
reveal_type(w) # revealed: str | None
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
## Attribute expressions in type annotations are understood
|
||||
|
||||
```py
|
||||
import builtins
|
||||
|
||||
int = "foo"
|
||||
a: builtins.int = 42
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal["bar"]` is not assignable to `int`"
|
||||
b: builtins.int = "bar"
|
||||
|
||||
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = ((42, 42), 42)
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal["foo"]` is not assignable to `tuple[tuple[int, int], int]`"
|
||||
c: builtins.tuple[builtins.tuple[builtins.int, builtins.int], builtins.int] = "foo"
|
||||
```
|
||||
|
||||
## Future annotations are deferred
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
x: Foo
|
||||
|
||||
class Foo: ...
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
## Annotations in stub files are deferred
|
||||
|
||||
```pyi
|
||||
x: Foo
|
||||
|
||||
class Foo: ...
|
||||
|
||||
x = Foo()
|
||||
reveal_type(x) # revealed: Foo
|
||||
```
|
||||
|
||||
## Annotated assignments in stub files are inferred correctly
|
||||
|
||||
```pyi
|
||||
x: int = 1
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
|
@ -0,0 +1,182 @@
|
|||
# Augmented assignment
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
x = 3
|
||||
x -= 1
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
x = 1.0
|
||||
x /= 2
|
||||
reveal_type(x) # revealed: int | float
|
||||
|
||||
x = (1, 2)
|
||||
x += (3, 4)
|
||||
reveal_type(x) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
|
||||
```
|
||||
|
||||
## Dunder methods
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __isub__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
x = C()
|
||||
x -= 1
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
class C:
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 1
|
||||
|
||||
x = C()
|
||||
x += "Hello"
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Unsupported types
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __isub__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
x = C()
|
||||
# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`"
|
||||
x -= 1
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Method union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
if flag:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
else:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
f = Foo()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | int
|
||||
```
|
||||
|
||||
## Partially bound `__iadd__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
if flag:
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
f = Foo()
|
||||
|
||||
# error: [unsupported-operator] "Operator `+=` is unsupported between objects of type `Foo` and `Literal["Hello, world!"]`"
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Partially bound with `__add__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: str) -> str:
|
||||
return "Hello, world!"
|
||||
if flag:
|
||||
def __iadd__(self, other: str) -> int:
|
||||
return 42
|
||||
|
||||
f = Foo()
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int | str
|
||||
```
|
||||
|
||||
## Partially bound target union
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if flag1:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
if flag2:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
## Target union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Foo:
|
||||
def __iadd__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = 42
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: str | Literal[54]
|
||||
```
|
||||
|
||||
## Partially bound target union with `__add__`
|
||||
|
||||
```py
|
||||
def f(flag: bool, flag2: bool):
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if flag:
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
class Bar:
|
||||
def __add__(self, other: int) -> bytes:
|
||||
return b"Hello, world!"
|
||||
|
||||
def __iadd__(self, other: int) -> float:
|
||||
return 42.0
|
||||
|
||||
if flag2:
|
||||
f = Foo()
|
||||
else:
|
||||
f = Bar()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
## Implicit dunder calls on class objects
|
||||
|
||||
```py
|
||||
class Meta(type):
|
||||
def __iadd__(cls, other: int) -> str:
|
||||
return ""
|
||||
|
||||
class C(metaclass=Meta): ...
|
||||
|
||||
cls = C
|
||||
cls += 1
|
||||
|
||||
reveal_type(cls) # revealed: str
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
# Multi-target assignment
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
x = y = 1
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
|
@ -0,0 +1,20 @@
|
|||
# Unbound
|
||||
|
||||
## Unbound
|
||||
|
||||
```py
|
||||
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
foo = 1
|
||||
|
||||
# No error `unresolved-reference` diagnostic is reported for `x`. This is
|
||||
# desirable because we would get a lot of cascading errors even though there
|
||||
# is only one root cause (the unbound variable `foo`).
|
||||
|
||||
# revealed: Unknown
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
Note: in this particular example, one could argue that the most likely error would be a wrong order
|
||||
of the `x`/`foo` definitions, and so it could be desirable to infer `Literal[1]` for the type of
|
||||
`x`. On the other hand, there might be a variable `fob` a little higher up in this file, and the
|
||||
actual error might have been just a typo. Inferring `Unknown` thus seems like the safest option.
|
|
@ -0,0 +1,17 @@
|
|||
# Walrus operator
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
x = (y := 1) + 1
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Walrus self-addition
|
||||
|
||||
```py
|
||||
x = 0
|
||||
(x := x + 1)
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
1999
crates/ty_python_semantic/resources/mdtest/attributes.md
Normal file
1999
crates/ty_python_semantic/resources/mdtest/attributes.md
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,93 @@
|
|||
## Binary operations on booleans
|
||||
|
||||
## Basic Arithmetic
|
||||
|
||||
We try to be precise and all operations except for division will result in Literal type.
|
||||
|
||||
```py
|
||||
a = True
|
||||
b = False
|
||||
|
||||
reveal_type(a + a) # revealed: Literal[2]
|
||||
reveal_type(a + b) # revealed: Literal[1]
|
||||
reveal_type(b + a) # revealed: Literal[1]
|
||||
reveal_type(b + b) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a - a) # revealed: Literal[0]
|
||||
reveal_type(a - b) # revealed: Literal[1]
|
||||
reveal_type(b - a) # revealed: Literal[-1]
|
||||
reveal_type(b - b) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a * a) # revealed: Literal[1]
|
||||
reveal_type(a * b) # revealed: Literal[0]
|
||||
reveal_type(b * a) # revealed: Literal[0]
|
||||
reveal_type(b * b) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a % a) # revealed: Literal[0]
|
||||
reveal_type(b % a) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a // a) # revealed: Literal[1]
|
||||
reveal_type(b // a) # revealed: Literal[0]
|
||||
|
||||
reveal_type(a**a) # revealed: Literal[1]
|
||||
reveal_type(a**b) # revealed: Literal[1]
|
||||
reveal_type(b**a) # revealed: Literal[0]
|
||||
reveal_type(b**b) # revealed: Literal[1]
|
||||
|
||||
# Division
|
||||
reveal_type(a / a) # revealed: float
|
||||
reveal_type(b / a) # revealed: float
|
||||
b / b # error: [division-by-zero] "Cannot divide object of type `Literal[False]` by zero"
|
||||
a / b # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
|
||||
|
||||
# bitwise OR
|
||||
reveal_type(a | a) # revealed: Literal[True]
|
||||
reveal_type(a | b) # revealed: Literal[True]
|
||||
reveal_type(b | a) # revealed: Literal[True]
|
||||
reveal_type(b | b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Arithmetic with a variable
|
||||
|
||||
```py
|
||||
def _(a: bool):
|
||||
def lhs_is_int(x: int):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: int | float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def rhs_is_int(x: int):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: int | float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def lhs_is_bool(x: bool):
|
||||
reveal_type(x + a) # revealed: int
|
||||
reveal_type(x - a) # revealed: int
|
||||
reveal_type(x * a) # revealed: int
|
||||
reveal_type(x // a) # revealed: int
|
||||
reveal_type(x / a) # revealed: int | float
|
||||
reveal_type(x % a) # revealed: int
|
||||
|
||||
def rhs_is_bool(x: bool):
|
||||
reveal_type(a + x) # revealed: int
|
||||
reveal_type(a - x) # revealed: int
|
||||
reveal_type(a * x) # revealed: int
|
||||
reveal_type(a // x) # revealed: int
|
||||
reveal_type(a / x) # revealed: int | float
|
||||
reveal_type(a % x) # revealed: int
|
||||
|
||||
def both_are_bool(x: bool, y: bool):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(x - y) # revealed: int
|
||||
reveal_type(x * y) # revealed: int
|
||||
reveal_type(x // y) # revealed: int
|
||||
reveal_type(x / y) # revealed: int | float
|
||||
reveal_type(x % y) # revealed: int
|
||||
```
|
27
crates/ty_python_semantic/resources/mdtest/binary/classes.md
Normal file
27
crates/ty_python_semantic/resources/mdtest/binary/classes.md
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Binary operations on classes
|
||||
|
||||
## Union of two classes
|
||||
|
||||
Unioning two classes via the `|` operator is only available in Python 3.10 and later.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
reveal_type(A | B) # revealed: UnionType
|
||||
```
|
||||
|
||||
## Union of two classes (prior to 3.10)
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A | B) # revealed: Unknown
|
||||
```
|
379
crates/ty_python_semantic/resources/mdtest/binary/custom.md
Normal file
379
crates/ty_python_semantic/resources/mdtest/binary/custom.md
Normal file
|
@ -0,0 +1,379 @@
|
|||
# Custom binary operations
|
||||
|
||||
## Class instances
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
def __sub__(self, other) -> Literal["-"]:
|
||||
return "-"
|
||||
|
||||
def __mul__(self, other) -> Literal["*"]:
|
||||
return "*"
|
||||
|
||||
def __matmul__(self, other) -> Literal["@"]:
|
||||
return "@"
|
||||
|
||||
def __truediv__(self, other) -> Literal["/"]:
|
||||
return "/"
|
||||
|
||||
def __mod__(self, other) -> Literal["%"]:
|
||||
return "%"
|
||||
|
||||
def __pow__(self, other) -> Literal["**"]:
|
||||
return "**"
|
||||
|
||||
def __lshift__(self, other) -> Literal["<<"]:
|
||||
return "<<"
|
||||
|
||||
def __rshift__(self, other) -> Literal[">>"]:
|
||||
return ">>"
|
||||
|
||||
def __or__(self, other) -> Literal["|"]:
|
||||
return "|"
|
||||
|
||||
def __xor__(self, other) -> Literal["^"]:
|
||||
return "^"
|
||||
|
||||
def __and__(self, other) -> Literal["&"]:
|
||||
return "&"
|
||||
|
||||
def __floordiv__(self, other) -> Literal["//"]:
|
||||
return "//"
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# Yes implements all of the dunder methods.
|
||||
reveal_type(Yes() + Yes()) # revealed: Literal["+"]
|
||||
reveal_type(Yes() - Yes()) # revealed: Literal["-"]
|
||||
reveal_type(Yes() * Yes()) # revealed: Literal["*"]
|
||||
reveal_type(Yes() @ Yes()) # revealed: Literal["@"]
|
||||
reveal_type(Yes() / Yes()) # revealed: Literal["/"]
|
||||
reveal_type(Yes() % Yes()) # revealed: Literal["%"]
|
||||
reveal_type(Yes() ** Yes()) # revealed: Literal["**"]
|
||||
reveal_type(Yes() << Yes()) # revealed: Literal["<<"]
|
||||
reveal_type(Yes() >> Yes()) # revealed: Literal[">>"]
|
||||
reveal_type(Yes() | Yes()) # revealed: Literal["|"]
|
||||
reveal_type(Yes() ^ Yes()) # revealed: Literal["^"]
|
||||
reveal_type(Yes() & Yes()) # revealed: Literal["&"]
|
||||
reveal_type(Yes() // Yes()) # revealed: Literal["//"]
|
||||
|
||||
# Sub inherits Yes's implementation of the dunder methods.
|
||||
reveal_type(Sub() + Sub()) # revealed: Literal["+"]
|
||||
reveal_type(Sub() - Sub()) # revealed: Literal["-"]
|
||||
reveal_type(Sub() * Sub()) # revealed: Literal["*"]
|
||||
reveal_type(Sub() @ Sub()) # revealed: Literal["@"]
|
||||
reveal_type(Sub() / Sub()) # revealed: Literal["/"]
|
||||
reveal_type(Sub() % Sub()) # revealed: Literal["%"]
|
||||
reveal_type(Sub() ** Sub()) # revealed: Literal["**"]
|
||||
reveal_type(Sub() << Sub()) # revealed: Literal["<<"]
|
||||
reveal_type(Sub() >> Sub()) # revealed: Literal[">>"]
|
||||
reveal_type(Sub() | Sub()) # revealed: Literal["|"]
|
||||
reveal_type(Sub() ^ Sub()) # revealed: Literal["^"]
|
||||
reveal_type(Sub() & Sub()) # revealed: Literal["&"]
|
||||
reveal_type(Sub() // Sub()) # revealed: Literal["//"]
|
||||
|
||||
# No does not implement any of the dunder methods.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() + No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() - No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() * No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() @ No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() / No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() % No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() ** No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() << No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() >> No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() | No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() ^ No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() & No()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`"
|
||||
reveal_type(No() // No()) # revealed: Unknown
|
||||
|
||||
# Yes does not implement any of the reflected dunder methods.
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() + Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() - Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() * Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() @ Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() / Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() % Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() ** Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() << Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() >> Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() | Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() ^ Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() & Yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`"
|
||||
reveal_type(No() // Yes()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Subclass reflections override superclass dunders
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
def __sub__(self, other) -> Literal["-"]:
|
||||
return "-"
|
||||
|
||||
def __mul__(self, other) -> Literal["*"]:
|
||||
return "*"
|
||||
|
||||
def __matmul__(self, other) -> Literal["@"]:
|
||||
return "@"
|
||||
|
||||
def __truediv__(self, other) -> Literal["/"]:
|
||||
return "/"
|
||||
|
||||
def __mod__(self, other) -> Literal["%"]:
|
||||
return "%"
|
||||
|
||||
def __pow__(self, other) -> Literal["**"]:
|
||||
return "**"
|
||||
|
||||
def __lshift__(self, other) -> Literal["<<"]:
|
||||
return "<<"
|
||||
|
||||
def __rshift__(self, other) -> Literal[">>"]:
|
||||
return ">>"
|
||||
|
||||
def __or__(self, other) -> Literal["|"]:
|
||||
return "|"
|
||||
|
||||
def __xor__(self, other) -> Literal["^"]:
|
||||
return "^"
|
||||
|
||||
def __and__(self, other) -> Literal["&"]:
|
||||
return "&"
|
||||
|
||||
def __floordiv__(self, other) -> Literal["//"]:
|
||||
return "//"
|
||||
|
||||
class Sub(Yes):
|
||||
def __radd__(self, other) -> Literal["r+"]:
|
||||
return "r+"
|
||||
|
||||
def __rsub__(self, other) -> Literal["r-"]:
|
||||
return "r-"
|
||||
|
||||
def __rmul__(self, other) -> Literal["r*"]:
|
||||
return "r*"
|
||||
|
||||
def __rmatmul__(self, other) -> Literal["r@"]:
|
||||
return "r@"
|
||||
|
||||
def __rtruediv__(self, other) -> Literal["r/"]:
|
||||
return "r/"
|
||||
|
||||
def __rmod__(self, other) -> Literal["r%"]:
|
||||
return "r%"
|
||||
|
||||
def __rpow__(self, other) -> Literal["r**"]:
|
||||
return "r**"
|
||||
|
||||
def __rlshift__(self, other) -> Literal["r<<"]:
|
||||
return "r<<"
|
||||
|
||||
def __rrshift__(self, other) -> Literal["r>>"]:
|
||||
return "r>>"
|
||||
|
||||
def __ror__(self, other) -> Literal["r|"]:
|
||||
return "r|"
|
||||
|
||||
def __rxor__(self, other) -> Literal["r^"]:
|
||||
return "r^"
|
||||
|
||||
def __rand__(self, other) -> Literal["r&"]:
|
||||
return "r&"
|
||||
|
||||
def __rfloordiv__(self, other) -> Literal["r//"]:
|
||||
return "r//"
|
||||
|
||||
class No:
|
||||
def __radd__(self, other) -> Literal["r+"]:
|
||||
return "r+"
|
||||
|
||||
def __rsub__(self, other) -> Literal["r-"]:
|
||||
return "r-"
|
||||
|
||||
def __rmul__(self, other) -> Literal["r*"]:
|
||||
return "r*"
|
||||
|
||||
def __rmatmul__(self, other) -> Literal["r@"]:
|
||||
return "r@"
|
||||
|
||||
def __rtruediv__(self, other) -> Literal["r/"]:
|
||||
return "r/"
|
||||
|
||||
def __rmod__(self, other) -> Literal["r%"]:
|
||||
return "r%"
|
||||
|
||||
def __rpow__(self, other) -> Literal["r**"]:
|
||||
return "r**"
|
||||
|
||||
def __rlshift__(self, other) -> Literal["r<<"]:
|
||||
return "r<<"
|
||||
|
||||
def __rrshift__(self, other) -> Literal["r>>"]:
|
||||
return "r>>"
|
||||
|
||||
def __ror__(self, other) -> Literal["r|"]:
|
||||
return "r|"
|
||||
|
||||
def __rxor__(self, other) -> Literal["r^"]:
|
||||
return "r^"
|
||||
|
||||
def __rand__(self, other) -> Literal["r&"]:
|
||||
return "r&"
|
||||
|
||||
def __rfloordiv__(self, other) -> Literal["r//"]:
|
||||
return "r//"
|
||||
|
||||
# Subclass reflected dunder methods take precedence over the superclass's regular dunders.
|
||||
reveal_type(Yes() + Sub()) # revealed: Literal["r+"]
|
||||
reveal_type(Yes() - Sub()) # revealed: Literal["r-"]
|
||||
reveal_type(Yes() * Sub()) # revealed: Literal["r*"]
|
||||
reveal_type(Yes() @ Sub()) # revealed: Literal["r@"]
|
||||
reveal_type(Yes() / Sub()) # revealed: Literal["r/"]
|
||||
reveal_type(Yes() % Sub()) # revealed: Literal["r%"]
|
||||
reveal_type(Yes() ** Sub()) # revealed: Literal["r**"]
|
||||
reveal_type(Yes() << Sub()) # revealed: Literal["r<<"]
|
||||
reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"]
|
||||
reveal_type(Yes() | Sub()) # revealed: Literal["r|"]
|
||||
reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"]
|
||||
reveal_type(Yes() & Sub()) # revealed: Literal["r&"]
|
||||
reveal_type(Yes() // Sub()) # revealed: Literal["r//"]
|
||||
|
||||
# But for an unrelated class, the superclass regular dunders are used.
|
||||
reveal_type(Yes() + No()) # revealed: Literal["+"]
|
||||
reveal_type(Yes() - No()) # revealed: Literal["-"]
|
||||
reveal_type(Yes() * No()) # revealed: Literal["*"]
|
||||
reveal_type(Yes() @ No()) # revealed: Literal["@"]
|
||||
reveal_type(Yes() / No()) # revealed: Literal["/"]
|
||||
reveal_type(Yes() % No()) # revealed: Literal["%"]
|
||||
reveal_type(Yes() ** No()) # revealed: Literal["**"]
|
||||
reveal_type(Yes() << No()) # revealed: Literal["<<"]
|
||||
reveal_type(Yes() >> No()) # revealed: Literal[">>"]
|
||||
reveal_type(Yes() | No()) # revealed: Literal["|"]
|
||||
reveal_type(Yes() ^ No()) # revealed: Literal["^"]
|
||||
reveal_type(Yes() & No()) # revealed: Literal["&"]
|
||||
reveal_type(Yes() // No()) # revealed: Literal["//"]
|
||||
```
|
||||
|
||||
## Classes
|
||||
|
||||
Dunder methods defined in a class are available to instances of that class, but not to the class
|
||||
itself. (For these operators to work on the class itself, they would have to be defined on the
|
||||
class's type, i.e. `type`.)
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`"
|
||||
reveal_type(Yes + Yes) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`"
|
||||
reveal_type(Sub + Sub) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`"
|
||||
reveal_type(No + No) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Subclass
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
class Sub(Yes): ...
|
||||
class No: ...
|
||||
|
||||
def yes() -> type[Yes]:
|
||||
return Yes
|
||||
|
||||
def sub() -> type[Sub]:
|
||||
return Sub
|
||||
|
||||
def no() -> type[No]:
|
||||
return No
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`"
|
||||
reveal_type(yes() + yes()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`"
|
||||
reveal_type(sub() + sub()) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`"
|
||||
reveal_type(no() + no()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Function literals
|
||||
|
||||
```py
|
||||
def f():
|
||||
pass
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f + f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f - f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f * f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f @ f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f / f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f % f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f**f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f << f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f >> f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f | f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f ^ f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f & f) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `def f() -> Unknown` and `def f() -> Unknown`"
|
||||
reveal_type(f // f) # revealed: Unknown
|
||||
```
|
475
crates/ty_python_semantic/resources/mdtest/binary/instances.md
Normal file
475
crates/ty_python_semantic/resources/mdtest/binary/instances.md
Normal file
|
@ -0,0 +1,475 @@
|
|||
# Binary operations on instances
|
||||
|
||||
Binary operations in Python are implemented by means of magic double-underscore methods.
|
||||
|
||||
For references, see:
|
||||
|
||||
- <https://snarky.ca/unravelling-binary-arithmetic-operations-in-python/>
|
||||
- <https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types>
|
||||
|
||||
## Operations
|
||||
|
||||
We support inference for all Python's binary operators: `+`, `-`, `*`, `@`, `/`, `//`, `%`, `**`,
|
||||
`<<`, `>>`, `&`, `^`, and `|`.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __sub__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __mul__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __matmul__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __truediv__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __floordiv__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __mod__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __pow__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __lshift__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rshift__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __and__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __xor__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __or__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
|
||||
reveal_type(A() + B()) # revealed: A
|
||||
reveal_type(A() - B()) # revealed: A
|
||||
reveal_type(A() * B()) # revealed: A
|
||||
reveal_type(A() @ B()) # revealed: A
|
||||
reveal_type(A() / B()) # revealed: A
|
||||
reveal_type(A() // B()) # revealed: A
|
||||
reveal_type(A() % B()) # revealed: A
|
||||
reveal_type(A() ** B()) # revealed: A
|
||||
reveal_type(A() << B()) # revealed: A
|
||||
reveal_type(A() >> B()) # revealed: A
|
||||
reveal_type(A() & B()) # revealed: A
|
||||
reveal_type(A() ^ B()) # revealed: A
|
||||
reveal_type(A() | B()) # revealed: A
|
||||
```
|
||||
|
||||
## Reflected
|
||||
|
||||
We also support inference for reflected operations:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __radd__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rsub__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rmul__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rmatmul__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rtruediv__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rfloordiv__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rmod__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rpow__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rlshift__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rrshift__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rand__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __rxor__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __ror__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
class B: ...
|
||||
|
||||
reveal_type(B() + A()) # revealed: A
|
||||
reveal_type(B() - A()) # revealed: A
|
||||
reveal_type(B() * A()) # revealed: A
|
||||
reveal_type(B() @ A()) # revealed: A
|
||||
reveal_type(B() / A()) # revealed: A
|
||||
reveal_type(B() // A()) # revealed: A
|
||||
reveal_type(B() % A()) # revealed: A
|
||||
reveal_type(B() ** A()) # revealed: A
|
||||
reveal_type(B() << A()) # revealed: A
|
||||
reveal_type(B() >> A()) # revealed: A
|
||||
reveal_type(B() & A()) # revealed: A
|
||||
reveal_type(B() ^ A()) # revealed: A
|
||||
reveal_type(B() | A()) # revealed: A
|
||||
```
|
||||
|
||||
## Returning a different type
|
||||
|
||||
The magic methods aren't required to return the type of `self`:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> int:
|
||||
return 1
|
||||
|
||||
def __rsub__(self, other) -> int:
|
||||
return 1
|
||||
|
||||
class B: ...
|
||||
|
||||
reveal_type(A() + B()) # revealed: int
|
||||
reveal_type(B() - A()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-reflected precedence in general
|
||||
|
||||
In general, if the left-hand side defines `__add__` and the right-hand side defines `__radd__` and
|
||||
the right-hand side is not a subtype of the left-hand side, `lhs.__add__` will take precedence:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other: "B") -> int:
|
||||
return 42
|
||||
|
||||
class B:
|
||||
def __radd__(self, other: "A") -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(A() + B()) # revealed: int
|
||||
|
||||
# Edge case: C is a subtype of C, *but* if the two sides are of *equal* types,
|
||||
# the lhs *still* takes precedence
|
||||
class C:
|
||||
def __add__(self, other: "C") -> int:
|
||||
return 42
|
||||
|
||||
def __radd__(self, other: "C") -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type(C() + C()) # revealed: int
|
||||
```
|
||||
|
||||
## Reflected precedence for subtypes (in some cases)
|
||||
|
||||
If the right-hand operand is a subtype of the left-hand operand and has a different implementation
|
||||
of the reflected method, the reflected method on the right-hand operand takes precedence.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> str:
|
||||
return "foo"
|
||||
|
||||
def __radd__(self, other) -> str:
|
||||
return "foo"
|
||||
|
||||
class MyString(str): ...
|
||||
|
||||
class B(A):
|
||||
def __radd__(self, other) -> MyString:
|
||||
return MyString()
|
||||
|
||||
reveal_type(A() + B()) # revealed: MyString
|
||||
|
||||
# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__`
|
||||
class C(B): ...
|
||||
|
||||
reveal_type(A() + C()) # revealed: MyString
|
||||
```
|
||||
|
||||
## Reflected precedence 2
|
||||
|
||||
If the right-hand operand is a subtype of the left-hand operand, but does not override the reflected
|
||||
method, the left-hand operand's non-reflected method still takes precedence:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> str:
|
||||
return "foo"
|
||||
|
||||
def __radd__(self, other) -> int:
|
||||
return 42
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A() + B()) # revealed: str
|
||||
```
|
||||
|
||||
## Only reflected supported
|
||||
|
||||
For example, at runtime, `(1).__add__(1.2)` is `NotImplemented`, but `(1.2).__radd__(1) == 2.2`,
|
||||
meaning that `1 + 1.2` succeeds at runtime (producing `2.2`). The runtime tries the second one only
|
||||
if the first one returns `NotImplemented` to signal failure.
|
||||
|
||||
Typeshed and other stubs annotate dunder-method calls that would return `NotImplemented` as being
|
||||
"illegal" calls. `int.__add__` is annotated as only "accepting" `int`s, even though it
|
||||
strictly-speaking "accepts" any other object without raising an exception -- it will simply return
|
||||
`NotImplemented`, allowing the runtime to try the `__radd__` method of the right-hand operand as
|
||||
well.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __sub__(self, other: "A") -> "A":
|
||||
return A()
|
||||
|
||||
class B:
|
||||
def __rsub__(self, other: A) -> "B":
|
||||
return B()
|
||||
|
||||
reveal_type(A() - B()) # revealed: B
|
||||
```
|
||||
|
||||
## Callable instances as dunders
|
||||
|
||||
Believe it or not, this is supported at runtime:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __call__(self, other) -> int:
|
||||
return 42
|
||||
|
||||
class B:
|
||||
__add__ = A()
|
||||
|
||||
reveal_type(B() + B()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
Note that we union with `Unknown` here because `__add__` is not declared. We do infer just `int` if
|
||||
the callable is declared:
|
||||
|
||||
```py
|
||||
class B2:
|
||||
__add__: A = A()
|
||||
|
||||
reveal_type(B2() + B2()) # revealed: int
|
||||
```
|
||||
|
||||
## Integration test: numbers from typeshed
|
||||
|
||||
We get less precise results from binary operations on float/complex literals due to the special case
|
||||
for annotations of `float` or `complex`, which applies also to return annotations for typeshed
|
||||
dunder methods. Perhaps we could have a special-case on the special-case, to exclude these typeshed
|
||||
return annotations from the widening, and preserve a bit more precision here?
|
||||
|
||||
```py
|
||||
reveal_type(3j + 3.14) # revealed: int | float | complex
|
||||
reveal_type(4.2 + 42) # revealed: int | float
|
||||
reveal_type(3j + 3) # revealed: int | float | complex
|
||||
reveal_type(3.14 + 3j) # revealed: int | float | complex
|
||||
reveal_type(42 + 4.2) # revealed: int | float
|
||||
reveal_type(3 + 3j) # revealed: int | float | complex
|
||||
|
||||
def _(x: bool, y: int):
|
||||
reveal_type(x + y) # revealed: int
|
||||
reveal_type(4.2 + x) # revealed: int | float
|
||||
reveal_type(y + 4.12) # revealed: int | float
|
||||
```
|
||||
|
||||
## With literal types
|
||||
|
||||
When we have a literal type for one operand, we're able to fall back to the instance handling for
|
||||
its instance super-type.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
def __radd__(self, other) -> "A":
|
||||
return self
|
||||
|
||||
reveal_type(A() + 1) # revealed: A
|
||||
reveal_type(1 + A()) # revealed: A
|
||||
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
reveal_type("foo" + A()) # revealed: A
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
reveal_type(b"foo" + A()) # revealed: bytes
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||
reveal_type(() + A()) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
literal_string_instance = "foo" * 1_000_000_000
|
||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||
reveal_type(literal_string_instance) # revealed: LiteralString
|
||||
|
||||
reveal_type(A() + literal_string_instance) # revealed: A
|
||||
reveal_type(literal_string_instance + A()) # revealed: A
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
|
||||
`Any` and `Unknown` represent a set of possible runtime objects, wherein the bounds of the set are
|
||||
unknown. Whether the left-hand operand's dunder or the right-hand operand's reflected dunder depends
|
||||
on whether the right-hand operand is an instance of a class that is a subclass of the left-hand
|
||||
operand's class and overrides the reflected dunder. In the following example, because of the
|
||||
unknowable nature of `Any`/`Unknown`, we must consider both possibilities: `Any`/`Unknown` might
|
||||
resolve to an unknown third class that inherits from `X` and overrides `__radd__`; but it also might
|
||||
not. Thus, the correct answer here for the `reveal_type` is `int | Unknown`.
|
||||
|
||||
```py
|
||||
from does_not_exist import Foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
|
||||
class X:
|
||||
def __add__(self, other: object) -> int:
|
||||
return 42
|
||||
|
||||
class Y(Foo): ...
|
||||
|
||||
# TODO: Should be `int | Unknown`; see above discussion.
|
||||
reveal_type(X() + Y()) # revealed: int
|
||||
```
|
||||
|
||||
## Operations involving types with invalid `__bool__` methods
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
a = NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Operations on class objects
|
||||
|
||||
When operating on class objects, the corresponding dunder methods are looked up on the metaclass.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Meta(type):
|
||||
def __add__(self, other: Meta) -> int:
|
||||
return 1
|
||||
|
||||
def __lt__(self, other: Meta) -> bool:
|
||||
return True
|
||||
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return "a"
|
||||
|
||||
class A(metaclass=Meta): ...
|
||||
class B(metaclass=Meta): ...
|
||||
|
||||
reveal_type(A + B) # revealed: int
|
||||
# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A - B) # revealed: Unknown
|
||||
|
||||
reveal_type(A < B) # revealed: bool
|
||||
reveal_type(A > B) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `Literal[A]` and `Literal[B]`"
|
||||
reveal_type(A <= B) # revealed: Unknown
|
||||
|
||||
reveal_type(A[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Unsupported
|
||||
|
||||
### Dunder as instance attribute
|
||||
|
||||
The magic method must exist on the class, not just on the instance:
|
||||
|
||||
```py
|
||||
def add_impl(self, other) -> int:
|
||||
return 1
|
||||
|
||||
class A:
|
||||
def __init__(self):
|
||||
self.__add__ = add_impl
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `A` and `A`"
|
||||
# revealed: Unknown
|
||||
reveal_type(A() + A())
|
||||
```
|
||||
|
||||
### Missing dunder
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
# error: [unsupported-operator]
|
||||
# revealed: Unknown
|
||||
reveal_type(A() + A())
|
||||
```
|
||||
|
||||
### Wrong position
|
||||
|
||||
A left-hand dunder method doesn't apply for the right-hand operand, or vice versa:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __add__(self, other) -> int:
|
||||
return 1
|
||||
|
||||
class B:
|
||||
def __radd__(self, other) -> int:
|
||||
return 1
|
||||
|
||||
class C: ...
|
||||
|
||||
# error: [unsupported-operator]
|
||||
# revealed: Unknown
|
||||
reveal_type(C() + A())
|
||||
|
||||
# error: [unsupported-operator]
|
||||
# revealed: Unknown
|
||||
reveal_type(B() + C())
|
||||
```
|
||||
|
||||
### Reflected dunder is not tried between two objects of the same type
|
||||
|
||||
For the specific case where the left-hand operand is the exact same type as the right-hand operand,
|
||||
the reflected dunder of the right-hand operand is not tried; the runtime short-circuits after trying
|
||||
the unreflected dunder of the left-hand operand. For context, see
|
||||
[this mailing list discussion](https://mail.python.org/archives/list/python-dev@python.org/thread/7NZUCODEAPQFMRFXYRMGJXDSIS3WJYIV/).
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __radd__(self, other: "Foo") -> "Foo":
|
||||
return self
|
||||
|
||||
# error: [unsupported-operator]
|
||||
# revealed: Unknown
|
||||
reveal_type(Foo() + Foo())
|
||||
```
|
||||
|
||||
### Wrong type
|
||||
|
||||
TODO: check signature and error if `other` is the wrong type
|
112
crates/ty_python_semantic/resources/mdtest/binary/integers.md
Normal file
112
crates/ty_python_semantic/resources/mdtest/binary/integers.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
# Binary operations on integers
|
||||
|
||||
## Basic Arithmetic
|
||||
|
||||
```py
|
||||
reveal_type(2 + 1) # revealed: Literal[3]
|
||||
reveal_type(3 - 4) # revealed: Literal[-1]
|
||||
reveal_type(3 * -1) # revealed: Literal[-3]
|
||||
reveal_type(-3 // 3) # revealed: Literal[-1]
|
||||
reveal_type(-3 / 3) # revealed: float
|
||||
reveal_type(5 % 3) # revealed: Literal[2]
|
||||
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[2]` and `Literal["f"]`"
|
||||
reveal_type(2 + "f") # revealed: Unknown
|
||||
|
||||
def lhs(x: int):
|
||||
reveal_type(x + 1) # revealed: int
|
||||
reveal_type(x - 4) # revealed: int
|
||||
reveal_type(x * -1) # revealed: int
|
||||
reveal_type(x // 3) # revealed: int
|
||||
reveal_type(x / 3) # revealed: int | float
|
||||
reveal_type(x % 3) # revealed: int
|
||||
|
||||
def rhs(x: int):
|
||||
reveal_type(2 + x) # revealed: int
|
||||
reveal_type(3 - x) # revealed: int
|
||||
reveal_type(3 * x) # revealed: int
|
||||
reveal_type(-3 // x) # revealed: int
|
||||
reveal_type(-3 / x) # revealed: int | float
|
||||
reveal_type(5 % x) # revealed: int
|
||||
|
||||
def both(x: int):
|
||||
reveal_type(x + x) # revealed: int
|
||||
reveal_type(x - x) # revealed: int
|
||||
reveal_type(x * x) # revealed: int
|
||||
reveal_type(x // x) # revealed: int
|
||||
reveal_type(x / x) # revealed: int | float
|
||||
reveal_type(x % x) # revealed: int
|
||||
```
|
||||
|
||||
## Power
|
||||
|
||||
For power if the result fits in the int literal type it will be a Literal type. Otherwise the
|
||||
outcome is int.
|
||||
|
||||
```py
|
||||
largest_u32 = 4_294_967_295
|
||||
reveal_type(2**2) # revealed: Literal[4]
|
||||
reveal_type(1 ** (largest_u32 + 1)) # revealed: int
|
||||
reveal_type(2**largest_u32) # revealed: int
|
||||
|
||||
def variable(x: int):
|
||||
reveal_type(x**2) # revealed: int
|
||||
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
|
||||
reveal_type(2**x) # revealed: int
|
||||
# TODO: should be `Any` (overload 5 on `__pow__`), requires correct overload matching
|
||||
reveal_type(x**x) # revealed: int
|
||||
```
|
||||
|
||||
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but
|
||||
the second argument is >=0, an `int` is still returned:
|
||||
|
||||
```py
|
||||
reveal_type(1**0) # revealed: Literal[1]
|
||||
reveal_type(0**1) # revealed: Literal[0]
|
||||
reveal_type(0**0) # revealed: Literal[1]
|
||||
reveal_type((-1) ** 2) # revealed: Literal[1]
|
||||
reveal_type(2 ** (-1)) # revealed: float
|
||||
reveal_type((-1) ** (-1)) # revealed: float
|
||||
```
|
||||
|
||||
## Division by Zero
|
||||
|
||||
This error is really outside the current Python type system, because e.g. `int.__truediv__` and
|
||||
friends are not annotated to indicate that it's an error, and we don't even have a facility to
|
||||
permit such an annotation. So arguably divide-by-zero should be a lint error rather than a type
|
||||
checker error. But we choose to go ahead and error in the cases that are very likely to be an error:
|
||||
dividing something typed as `int` or `float` by something known to be `Literal[0]`.
|
||||
|
||||
This isn't _definitely_ an error, because the object typed as `int` or `float` could be an instance
|
||||
of a custom subclass which overrides division behavior to handle zero without error. But if this
|
||||
unusual case occurs, the error can be avoided by explicitly typing the dividend as that safe custom
|
||||
subclass; we only emit the error if the LHS type is exactly `int` or `float`, not if its a subclass.
|
||||
|
||||
```py
|
||||
a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero"
|
||||
reveal_type(a) # revealed: float
|
||||
|
||||
b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero"
|
||||
reveal_type(b) # revealed: int
|
||||
|
||||
c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
|
||||
reveal_type(c) # revealed: int
|
||||
|
||||
# error: "Cannot divide object of type `int` by zero"
|
||||
reveal_type(int() / 0) # revealed: int | float
|
||||
|
||||
# error: "Cannot divide object of type `Literal[1]` by zero"
|
||||
reveal_type(1 / False) # revealed: float
|
||||
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
|
||||
True / False
|
||||
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
|
||||
bool(1) / False
|
||||
|
||||
# error: "Cannot divide object of type `float` by zero"
|
||||
reveal_type(1.0 / 0) # revealed: int | float
|
||||
|
||||
class MyInt(int): ...
|
||||
|
||||
# No error for a subclass of int
|
||||
reveal_type(MyInt(3) / 0) # revealed: int | float
|
||||
```
|
22
crates/ty_python_semantic/resources/mdtest/binary/tuples.md
Normal file
22
crates/ty_python_semantic/resources/mdtest/binary/tuples.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Binary operations on tuples
|
||||
|
||||
## Concatenation for heterogeneous tuples
|
||||
|
||||
```py
|
||||
reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
|
||||
reveal_type(() + (1, 2)) # revealed: tuple[Literal[1], Literal[2]]
|
||||
reveal_type((1, 2) + ()) # revealed: tuple[Literal[1], Literal[2]]
|
||||
reveal_type(() + ()) # revealed: tuple[()]
|
||||
|
||||
def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
|
||||
reveal_type(x + y) # revealed: tuple[int, str, None, tuple[int]]
|
||||
reveal_type(y + x) # revealed: tuple[None, tuple[int], int, str]
|
||||
```
|
||||
|
||||
## Concatenation for homogeneous tuples
|
||||
|
||||
```py
|
||||
def _(x: tuple[int, ...], y: tuple[str, ...]):
|
||||
reveal_type(x + y) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(x + (1, 2)) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
59
crates/ty_python_semantic/resources/mdtest/binary/unions.md
Normal file
59
crates/ty_python_semantic/resources/mdtest/binary/unions.md
Normal file
|
@ -0,0 +1,59 @@
|
|||
# Binary operations on union types
|
||||
|
||||
Binary operations on union types are only available if they are supported for all possible
|
||||
combinations of types:
|
||||
|
||||
```py
|
||||
def f1(i: int, u: int | None):
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int` and `int | None`"
|
||||
reveal_type(i + u) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | None` and `int`"
|
||||
reveal_type(u + i) # revealed: Unknown
|
||||
```
|
||||
|
||||
`int` can be added to `int`, and `str` can be added to `str`, but expressions of type `int | str`
|
||||
cannot be added, because that would require addition of `int` and `str` or vice versa:
|
||||
|
||||
```py
|
||||
def f2(i: int, s: str, int_or_str: int | str):
|
||||
i + i
|
||||
s + s
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
|
||||
reveal_type(int_or_str + int_or_str) # revealed: Unknown
|
||||
```
|
||||
|
||||
However, if an operation is supported for all possible combinations, the result will be a union of
|
||||
the possible outcomes:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f3(two_or_three: Literal[2, 3], a_or_b: Literal["a", "b"]):
|
||||
reveal_type(two_or_three + two_or_three) # revealed: Literal[4, 5, 6]
|
||||
reveal_type(two_or_three**two_or_three) # revealed: Literal[4, 8, 9, 27]
|
||||
|
||||
reveal_type(a_or_b + a_or_b) # revealed: Literal["aa", "ab", "ba", "bb"]
|
||||
|
||||
reveal_type(two_or_three * a_or_b) # revealed: Literal["aa", "bb", "aaa", "bbb"]
|
||||
```
|
||||
|
||||
We treat a type annotation of `float` as a union of `int` and `float`, so union handling is relevant
|
||||
here:
|
||||
|
||||
```py
|
||||
def f4(x: float, y: float):
|
||||
reveal_type(x + y) # revealed: int | float
|
||||
reveal_type(x - y) # revealed: int | float
|
||||
reveal_type(x * y) # revealed: int | float
|
||||
reveal_type(x / y) # revealed: int | float
|
||||
reveal_type(x // y) # revealed: int | float
|
||||
reveal_type(x % y) # revealed: int | float
|
||||
```
|
||||
|
||||
If any of the union elements leads to a division by zero, we will report an error:
|
||||
|
||||
```py
|
||||
def f5(m: int, n: Literal[-1, 0, 1]):
|
||||
# error: [division-by-zero] "Cannot divide object of type `int` by zero"
|
||||
return m / n
|
||||
```
|
|
@ -0,0 +1,67 @@
|
|||
# Short-Circuit Evaluation
|
||||
|
||||
## Not all boolean expressions must be evaluated
|
||||
|
||||
In `or` expressions, if the left-hand side is truthy, the right-hand side is not evaluated.
|
||||
Similarly, in `and` expressions, if the left-hand side is falsy, the right-hand side is not
|
||||
evaluated.
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag or (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if flag and (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## First expression is always evaluated
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if (x := 1) or flag:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if (x := 1) and flag:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Statically known truthiness
|
||||
|
||||
```py
|
||||
if True or (x := 1):
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
if True and (x := 1):
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Later expressions can always use variables from earlier expressions
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
flag or (x := 1) or reveal_type(x) # revealed: Never
|
||||
|
||||
# error: [unresolved-reference]
|
||||
flag or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Nested expressions
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
if flag1 or ((x := 1) and flag2):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if ((y := 1) and flag1) or flag2:
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
if (flag1 and (z := 1)) or reveal_type(z): # revealed: Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(z) # revealed: Literal[1]
|
||||
```
|
|
@ -0,0 +1,357 @@
|
|||
# Boundness and declaredness: public uses
|
||||
|
||||
This document demonstrates how type-inference and diagnostics work for *public* uses of a symbol,
|
||||
that is, a use of a symbol from another scope. If a symbol has a declared type in its local scope
|
||||
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective
|
||||
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`).
|
||||
|
||||
If a symbol has no declared type, we use the union of `Unknown` with the inferred type as the public
|
||||
type. If there is no declaration, then the symbol can be reassigned to any type from another scope;
|
||||
the union with `Unknown` reflects that its type must at least be as large as the type of the
|
||||
assigned value, but could be arbitrarily larger.
|
||||
|
||||
We test the whole matrix of possible boundness and declaredness states. The current behavior is
|
||||
summarized in the following table, while the tests below demonstrate each case. Note that some of
|
||||
this behavior is questionable and might change in the future. See the TODOs in `symbol_by_id`
|
||||
(`types.rs`) and [this issue](https://github.com/astral-sh/ruff/issues/14297) for more information.
|
||||
In particular, we should raise errors in the "possibly-undeclared-and-unbound" as well as the
|
||||
"undeclared-and-possibly-unbound" cases (marked with a "?").
|
||||
|
||||
| **Public type** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | ------------ | -------------------------- | ----------------------- |
|
||||
| bound | `T_declared` | `T_declared \| T_inferred` | `Unknown \| T_inferred` |
|
||||
| possibly-unbound | `T_declared` | `T_declared \| T_inferred` | `Unknown \| T_inferred` |
|
||||
| unbound | `T_declared` | `T_declared` | `Unknown` |
|
||||
|
||||
| **Diagnostic** | declared | possibly-undeclared | undeclared |
|
||||
| ---------------- | -------- | ------------------------- | ------------------- |
|
||||
| bound | | | |
|
||||
| possibly-unbound | | `possibly-unbound-import` | ? |
|
||||
| unbound | | ? | `unresolved-import` |
|
||||
|
||||
## Declared
|
||||
|
||||
### Declared and bound
|
||||
|
||||
If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type
|
||||
(`Literal[1]`), or a conflicting inferred type (`str` vs. `Literal[2]` below):
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def any() -> Any: ...
|
||||
|
||||
a: int = 1
|
||||
b: str = 2 # error: [invalid-assignment]
|
||||
c: Any = 3
|
||||
d: int = any()
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import a, b, c, d
|
||||
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
reveal_type(c) # revealed: Any
|
||||
reveal_type(d) # revealed: int
|
||||
```
|
||||
|
||||
### Declared and possibly unbound
|
||||
|
||||
If a symbol is declared and *possibly* unbound, we trust that other module and use the declared type
|
||||
without raising an error.
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def any() -> Any: ...
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
a: int
|
||||
b: str
|
||||
c: Any
|
||||
d: int
|
||||
|
||||
if flag:
|
||||
a = 1
|
||||
b = 2 # error: [invalid-assignment]
|
||||
c = 3
|
||||
d = any()
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import a, b, c, d
|
||||
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
reveal_type(c) # revealed: Any
|
||||
reveal_type(d) # revealed: int
|
||||
```
|
||||
|
||||
### Declared and unbound
|
||||
|
||||
Similarly, if a symbol is declared but unbound, we do not raise an error. We trust that this symbol
|
||||
is available somehow and simply use the declared type.
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
a: int
|
||||
b: Any
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import a, b
|
||||
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: Any
|
||||
```
|
||||
|
||||
## Possibly undeclared
|
||||
|
||||
### Possibly undeclared and bound
|
||||
|
||||
If a symbol is possibly undeclared but definitely bound, we use the union of the declared and
|
||||
inferred types:
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def any() -> Any: ...
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
a = 1
|
||||
b = 2
|
||||
c = 3
|
||||
d = any()
|
||||
if flag():
|
||||
a: int
|
||||
b: Any
|
||||
c: str # error: [invalid-declaration]
|
||||
d: int
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import a, b, c, d
|
||||
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: Literal[2] | Any
|
||||
reveal_type(c) # revealed: Literal[3] | Unknown
|
||||
reveal_type(d) # revealed: Any | int
|
||||
|
||||
# External modifications of `a` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
a = None
|
||||
```
|
||||
|
||||
### Possibly undeclared and possibly unbound
|
||||
|
||||
If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and
|
||||
inferred types. This case is interesting because the "possibly declared" definition might not be the
|
||||
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import`
|
||||
error for both `a` and `b`:
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
if flag():
|
||||
a: Any = 1
|
||||
b = 2
|
||||
else:
|
||||
b: str
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [possibly-unbound-import]
|
||||
# error: [possibly-unbound-import]
|
||||
from mod import a, b
|
||||
|
||||
reveal_type(a) # revealed: Literal[1] | Any
|
||||
reveal_type(b) # revealed: Literal[2] | str
|
||||
|
||||
# External modifications of `b` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
b = None
|
||||
```
|
||||
|
||||
### Possibly undeclared and unbound
|
||||
|
||||
If a symbol is possibly undeclared and definitely unbound, we currently do not raise an error. This
|
||||
seems inconsistent when compared to the case just above.
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
if flag():
|
||||
a: int
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO: this should raise an error. Once we fix this, update the section description and the table
|
||||
# on top of this document.
|
||||
from mod import a
|
||||
|
||||
reveal_type(a) # revealed: int
|
||||
|
||||
# External modifications to `a` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
a = None
|
||||
```
|
||||
|
||||
## Undeclared
|
||||
|
||||
### Undeclared but bound
|
||||
|
||||
If a symbol is *undeclared*, we use the union of `Unknown` with the inferred type. Note that we
|
||||
treat this case differently from the case where a symbol is implicitly declared with `Unknown`,
|
||||
possibly due to the usage of an unknown name in the annotation:
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
# Undeclared:
|
||||
a = 1
|
||||
|
||||
# Implicitly declared with `Unknown`, due to the usage of an unknown name in the annotation:
|
||||
b: SomeUnknownName = 1 # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import a, b
|
||||
|
||||
reveal_type(a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
# All external modifications of `a` are allowed:
|
||||
a = None
|
||||
```
|
||||
|
||||
### Undeclared and possibly unbound
|
||||
|
||||
If a symbol is undeclared and *possibly* unbound, we currently do not raise an error. This seems
|
||||
inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case.
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
if flag:
|
||||
a = 1
|
||||
b: SomeUnknownName = 1 # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO: this should raise an error. Once we fix this, update the section description and the table
|
||||
# on top of this document.
|
||||
from mod import a, b
|
||||
|
||||
reveal_type(a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
# All external modifications of `a` are allowed:
|
||||
a = None
|
||||
```
|
||||
|
||||
### Undeclared and unbound
|
||||
|
||||
If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error.
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
if False:
|
||||
a: int = 1
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
from mod import a
|
||||
|
||||
reveal_type(a) # revealed: Unknown
|
||||
|
||||
# Modifications allowed in this case:
|
||||
a = None
|
||||
```
|
||||
|
||||
## In stub files
|
||||
|
||||
In stub files, we have a minor modification to the rules above: we do not union with `Unknown` for
|
||||
undeclared symbols.
|
||||
|
||||
### Undeclared and bound
|
||||
|
||||
`mod.pyi`:
|
||||
|
||||
```pyi
|
||||
MyInt = int
|
||||
|
||||
class C:
|
||||
MyStr = str
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import MyInt, C
|
||||
|
||||
reveal_type(MyInt) # revealed: Literal[int]
|
||||
reveal_type(C.MyStr) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
### Undeclared and possibly unbound
|
||||
|
||||
`mod.pyi`:
|
||||
|
||||
```pyi
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
if flag():
|
||||
MyInt = int
|
||||
|
||||
class C:
|
||||
MyStr = str
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [possibly-unbound-import]
|
||||
# error: [possibly-unbound-import]
|
||||
from mod import MyInt, C
|
||||
|
||||
reveal_type(MyInt) # revealed: Literal[int]
|
||||
reveal_type(C.MyStr) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
### Undeclared and unbound
|
||||
|
||||
`mod.pyi`:
|
||||
|
||||
```pyi
|
||||
if False:
|
||||
MyInt = int
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
from mod import MyInt
|
||||
|
||||
reveal_type(MyInt) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,43 @@
|
|||
# `typing.Callable`
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def _(c: Callable[[], int]):
|
||||
reveal_type(c()) # revealed: int
|
||||
|
||||
def _(c: Callable[[int, str], int]):
|
||||
reveal_type(c(1, "a")) # revealed: int
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["a"]`"
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
|
||||
reveal_type(c("a", 1)) # revealed: int
|
||||
```
|
||||
|
||||
The `Callable` annotation can only be used to describe positional-only parameters.
|
||||
|
||||
```py
|
||||
def _(c: Callable[[int, str], None]):
|
||||
# error: [unknown-argument] "Argument `a` does not match any known parameter"
|
||||
# error: [unknown-argument] "Argument `b` does not match any known parameter"
|
||||
# error: [missing-argument] "No arguments provided for required parameters 1, 2"
|
||||
reveal_type(c(a=1, b="b")) # revealed: None
|
||||
```
|
||||
|
||||
If the annotation uses a gradual form (`...`) for the parameter list, then it can accept any kind of
|
||||
parameter with any type.
|
||||
|
||||
```py
|
||||
def _(c: Callable[..., int]):
|
||||
reveal_type(c()) # revealed: int
|
||||
reveal_type(c(1)) # revealed: int
|
||||
reveal_type(c(1, "str", False, a=[1, 2], b=(3, 4))) # revealed: int
|
||||
```
|
||||
|
||||
An invalid `Callable` form can accept any parameters and will return `Unknown`.
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form]
|
||||
def _(c: Callable[42, str]):
|
||||
reveal_type(c()) # revealed: Unknown
|
||||
```
|
101
crates/ty_python_semantic/resources/mdtest/call/builtins.md
Normal file
101
crates/ty_python_semantic/resources/mdtest/call/builtins.md
Normal file
|
@ -0,0 +1,101 @@
|
|||
# Calling builtins
|
||||
|
||||
## `bool` with incorrect arguments
|
||||
|
||||
```py
|
||||
class NotBool:
|
||||
__bool__ = None
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2"
|
||||
bool(1, 2)
|
||||
|
||||
# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly.
|
||||
bool(NotBool())
|
||||
```
|
||||
|
||||
## Calls to `type()`
|
||||
|
||||
A single-argument call to `type()` returns an object that has the argument's meta-type. (This is
|
||||
tested more extensively in `crates/ty_python_semantic/resources/mdtest/attributes.md`, alongside the
|
||||
tests for the `__class__` attribute.)
|
||||
|
||||
```py
|
||||
reveal_type(type(1)) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
But a three-argument call to type creates a dynamic instance of the `type` class:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
reveal_type(type("Foo", (), {})) # revealed: type
|
||||
|
||||
reveal_type(type("Foo", (Base,), {"attr": 1})) # revealed: type
|
||||
```
|
||||
|
||||
Other numbers of arguments are invalid
|
||||
|
||||
```py
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", ())
|
||||
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", (), {}, weird_other_arg=42)
|
||||
```
|
||||
|
||||
The following calls are also invalid, due to incorrect argument types:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type(b"Foo", (), {})
|
||||
|
||||
# error: [no-matching-overload] "No overload of class `type` matches arguments"
|
||||
type("Foo", Base, {})
|
||||
|
||||
# TODO: this should be an error
|
||||
type("Foo", (1, 2), {})
|
||||
|
||||
# TODO: this should be an error
|
||||
type("Foo", (Base,), {b"attr": 1})
|
||||
```
|
||||
|
||||
## Calls to `str()`
|
||||
|
||||
### Valid calls
|
||||
|
||||
```py
|
||||
str()
|
||||
str("")
|
||||
str(b"")
|
||||
str(1)
|
||||
str(object=1)
|
||||
|
||||
str(b"M\xc3\xbcsli", "utf-8")
|
||||
str(b"M\xc3\xbcsli", "utf-8", "replace")
|
||||
|
||||
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16")
|
||||
str(b"M\x00\xfc\x00s\x00l\x00i\x00", encoding="utf-16", errors="ignore")
|
||||
|
||||
str(bytearray.fromhex("4d c3 bc 73 6c 69"), "utf-8")
|
||||
str(bytearray(), "utf-8")
|
||||
|
||||
str(encoding="utf-8", object=b"M\xc3\xbcsli")
|
||||
str(b"", errors="replace")
|
||||
str(encoding="utf-8")
|
||||
str(errors="replace")
|
||||
```
|
||||
|
||||
### Invalid calls
|
||||
|
||||
```py
|
||||
str(1, 2) # error: [no-matching-overload]
|
||||
str(o=1) # error: [no-matching-overload]
|
||||
|
||||
# First argument is not a bytes-like object:
|
||||
str("Müsli", "utf-8") # error: [no-matching-overload]
|
||||
|
||||
# Second argument is not a valid encoding:
|
||||
str(b"M\xc3\xbcsli", b"utf-8") # error: [no-matching-overload]
|
||||
```
|
|
@ -0,0 +1,127 @@
|
|||
# Callable instance
|
||||
|
||||
## Dunder call
|
||||
|
||||
```py
|
||||
class Multiplier:
|
||||
def __init__(self, factor: int):
|
||||
self.factor = factor
|
||||
|
||||
def __call__(self, number: int) -> int:
|
||||
return number * self.factor
|
||||
|
||||
a = Multiplier(2)(3)
|
||||
reveal_type(a) # revealed: int
|
||||
|
||||
class Unit: ...
|
||||
|
||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly unbound `__call__` method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class PossiblyNotCallable:
|
||||
if flag:
|
||||
def __call__(self) -> int:
|
||||
return 1
|
||||
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound callable
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int:
|
||||
return 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-callable `__call__`
|
||||
|
||||
```py
|
||||
class NonCallable:
|
||||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly non-callable `__call__`
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class NonCallable:
|
||||
if flag:
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int:
|
||||
return 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Call binding errors
|
||||
|
||||
### Wrong argument type
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __call__(self, x: int) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(c("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Wrong argument type on `self`
|
||||
|
||||
```py
|
||||
class C:
|
||||
# TODO this definition should also be an error; `C` must be assignable to type of `self`
|
||||
def __call__(self: int) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
|
||||
# error: 13 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `C`"
|
||||
reveal_type(c()) # revealed: int
|
||||
```
|
||||
|
||||
## Union over callables
|
||||
|
||||
### Possibly unbound `__call__`
|
||||
|
||||
```py
|
||||
def outer(cond1: bool):
|
||||
class Test:
|
||||
if cond1:
|
||||
def __call__(self): ...
|
||||
|
||||
class Other:
|
||||
def __call__(self): ...
|
||||
|
||||
def inner(cond2: bool):
|
||||
if cond2:
|
||||
a = Test()
|
||||
else:
|
||||
a = Other()
|
||||
|
||||
# error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)"
|
||||
a()
|
||||
```
|
427
crates/ty_python_semantic/resources/mdtest/call/constructor.md
Normal file
427
crates/ty_python_semantic/resources/mdtest/call/constructor.md
Normal file
|
@ -0,0 +1,427 @@
|
|||
# Constructor
|
||||
|
||||
When classes are instantiated, Python calls the metaclass's `__call__` method. The metaclass of most
|
||||
Python classes is the class `builtins.type`.
|
||||
|
||||
`type.__call__` calls the `__new__` method of the class, which is responsible for creating the
|
||||
instance. `__init__` is then called on the constructed instance with the same arguments that were
|
||||
passed to `__new__`.
|
||||
|
||||
Both `__new__` and `__init__` are looked up using the descriptor protocol, i.e., `__get__` is called
|
||||
if these attributes are descriptors. `__new__` is always treated as a static method, i.e., `cls` is
|
||||
passed as the first argument. `__init__` has no special handling; it is fetched as a bound method
|
||||
and called just like any other dunder method.
|
||||
|
||||
`type.__call__` does other things too, but this is not yet handled by us.
|
||||
|
||||
Since every class has `object` in it's MRO, the default implementations are `object.__new__` and
|
||||
`object.__init__`. They have some special behavior, namely:
|
||||
|
||||
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for
|
||||
`object`), no arguments are accepted and `TypeError` is raised if any are passed.
|
||||
- If `__new__` is defined but `__init__` is not, `object.__init__` will allow arbitrary arguments!
|
||||
|
||||
As of today there are a number of behaviors that we do not support:
|
||||
|
||||
- `__new__` is assumed to return an instance of the class on which it is called
|
||||
- User defined `__call__` on metaclass is ignored
|
||||
|
||||
## Creating an instance of the `object` class itself
|
||||
|
||||
Test the behavior of the `object` class itself. As implementation has to ignore `object` own methods
|
||||
as defined in typeshed due to behavior not expressible in typeshed (see above how `__init__` behaves
|
||||
differently depending on whether `__new__` is defined or not), we have to test the behavior of
|
||||
`object` itself.
|
||||
|
||||
```py
|
||||
reveal_type(object()) # revealed: object
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to class `object`: expected 0, got 1"
|
||||
reveal_type(object(1)) # revealed: object
|
||||
```
|
||||
|
||||
## No init or new
|
||||
|
||||
```py
|
||||
class Foo: ...
|
||||
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__new__` present on the class itself
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __new__(cls, x: int) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__new__` present on a superclass
|
||||
|
||||
If the `__new__` method is defined on a superclass, we can still infer the signature of the
|
||||
constructor from it.
|
||||
|
||||
```py
|
||||
from typing_extensions import Self
|
||||
|
||||
class Base:
|
||||
def __new__(cls, x: int) -> Self: ...
|
||||
|
||||
class Foo(Base): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## Conditional `__new__`
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Foo:
|
||||
if flag:
|
||||
def __new__(cls, x: int): ...
|
||||
else:
|
||||
def __new__(cls, x: int, y: int = 1): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["1"]`"
|
||||
reveal_type(Foo("1")) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## A descriptor in place of `__new__`
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
def __call__(self, cls, x: int) -> "Foo":
|
||||
obj = object.__new__(cls)
|
||||
obj.x = x
|
||||
return obj
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance, owner) -> SomeCallable:
|
||||
return SomeCallable()
|
||||
|
||||
class Foo:
|
||||
__new__: Descriptor = Descriptor()
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## A callable instance in place of `__new__`
|
||||
|
||||
### Bound
|
||||
|
||||
```py
|
||||
class Callable:
|
||||
def __call__(self, cls, x: int) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
class Foo:
|
||||
__new__ = Callable()
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
### Possibly Unbound
|
||||
|
||||
#### Possibly unbound `__new__` method
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Foo:
|
||||
if flag:
|
||||
def __new__(cls):
|
||||
return object.__new__(cls)
|
||||
|
||||
# error: [call-possibly-unbound-method]
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
|
||||
# error: [call-possibly-unbound-method]
|
||||
# error: [too-many-positional-arguments]
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
```
|
||||
|
||||
#### Possibly unbound `__call__` on `__new__` callable
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Callable:
|
||||
if flag:
|
||||
def __call__(self, cls, x: int) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
class Foo:
|
||||
__new__ = Callable()
|
||||
|
||||
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
|
||||
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__init__` present on the class itself
|
||||
|
||||
If the class has an `__init__` method, we can infer the signature of the constructor from it.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __init__(self, x: int): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__init__` present on a superclass
|
||||
|
||||
If the `__init__` method is defined on a superclass, we can still infer the signature of the
|
||||
constructor from it.
|
||||
|
||||
```py
|
||||
class Base:
|
||||
def __init__(self, x: int): ...
|
||||
|
||||
class Foo(Base): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## Conditional `__init__`
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Foo:
|
||||
if flag:
|
||||
def __init__(self, x: int): ...
|
||||
else:
|
||||
def __init__(self, x: int, y: int = 1): ...
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["1"]`"
|
||||
reveal_type(Foo("1")) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
## A descriptor in place of `__init__`
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
# TODO: at runtime `__init__` is checked to return `None` and
|
||||
# a `TypeError` is raised if it doesn't. However, apparently
|
||||
# this is not true when the descriptor is used as `__init__`.
|
||||
# However, we may still want to check this.
|
||||
def __call__(self, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance, owner) -> SomeCallable:
|
||||
return SomeCallable()
|
||||
|
||||
class Foo:
|
||||
__init__: Descriptor = Descriptor()
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## A callable instance in place of `__init__`
|
||||
|
||||
### Bound
|
||||
|
||||
```py
|
||||
class Callable:
|
||||
def __call__(self, x: int) -> None:
|
||||
pass
|
||||
|
||||
class Foo:
|
||||
__init__ = Callable()
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
### Possibly Unbound
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class Callable:
|
||||
if flag:
|
||||
def __call__(self, x: int) -> None:
|
||||
pass
|
||||
|
||||
class Foo:
|
||||
__init__ = Callable()
|
||||
|
||||
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
|
||||
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
```
|
||||
|
||||
## `__new__` and `__init__` both present
|
||||
|
||||
### Identical signatures
|
||||
|
||||
A common case is to have `__new__` and `__init__` with identical signatures (except for the first
|
||||
argument). We report errors for both `__new__` and `__init__` if the arguments are incorrect.
|
||||
|
||||
At runtime `__new__` is called first and will fail without executing `__init__` if the arguments are
|
||||
incorrect. However, we decided that it is better to report errors for both methods, since after
|
||||
fixing the `__new__` method, the user may forget to fix the `__init__` method.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __new__(cls, x: int) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: int): ...
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
```
|
||||
|
||||
### Compatible signatures
|
||||
|
||||
But they can also be compatible, but not identical. We should correctly report errors only for the
|
||||
mthod that would fail.
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def __new__(cls, *args, **kwargs):
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: int) -> None:
|
||||
self.x = x
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
reveal_type(Foo(1)) # revealed: Foo
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||
```
|
||||
|
||||
### Incompatible signatures
|
||||
|
||||
```py
|
||||
import abc
|
||||
|
||||
class Foo:
|
||||
def __new__(cls) -> "Foo":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x):
|
||||
self.x = 42
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo()) # revealed: Foo
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 0, got 1"
|
||||
reveal_type(Foo(42)) # revealed: Foo
|
||||
|
||||
class Foo2:
|
||||
def __new__(cls, x) -> "Foo2":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
reveal_type(Foo2()) # revealed: Foo2
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
|
||||
reveal_type(Foo2(42)) # revealed: Foo2
|
||||
|
||||
class Foo3(metaclass=abc.ABCMeta):
|
||||
def __new__(cls) -> "Foo3":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x):
|
||||
self.x = 42
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(Foo3()) # revealed: Foo3
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 0, got 1"
|
||||
reveal_type(Foo3(42)) # revealed: Foo3
|
||||
|
||||
class Foo4(metaclass=abc.ABCMeta):
|
||||
def __new__(cls, x) -> "Foo4":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||
reveal_type(Foo4()) # revealed: Foo4
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
|
||||
reveal_type(Foo4(42)) # revealed: Foo4
|
||||
```
|
||||
|
||||
### Lookup of `__new__`
|
||||
|
||||
The `__new__` method is always invoked on the class itself, never on the metaclass. This is
|
||||
different from how other dunder methods like `__lt__` are implicitly called (always on the
|
||||
meta-type, never on the type itself).
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
class Meta(type):
|
||||
def __new__(mcls, name, bases, namespace, /, **kwargs):
|
||||
return super().__new__(mcls, name, bases, namespace)
|
||||
|
||||
def __lt__(cls, other) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class C(metaclass=Meta): ...
|
||||
|
||||
# No error is raised here, since we don't implicitly call `Meta.__new__`
|
||||
reveal_type(C()) # revealed: C
|
||||
|
||||
# Meta.__lt__ is implicitly called here:
|
||||
reveal_type(C < C) # revealed: Literal[True]
|
||||
```
|
241
crates/ty_python_semantic/resources/mdtest/call/dunder.md
Normal file
241
crates/ty_python_semantic/resources/mdtest/call/dunder.md
Normal file
|
@ -0,0 +1,241 @@
|
|||
# Dunder calls
|
||||
|
||||
## Introduction
|
||||
|
||||
This test suite explains and documents how dunder methods are looked up and called. Throughout the
|
||||
document, we use `__getitem__` as an example, but the same principles apply to other dunder methods.
|
||||
|
||||
Dunder methods are implicitly called when using certain syntax. For example, the index operator
|
||||
`obj[key]` calls the `__getitem__` method under the hood. Exactly *how* a dunder method is looked up
|
||||
and called works slightly different from regular methods. Dunder methods are not looked up on `obj`
|
||||
directly, but rather on `type(obj)`. But in many ways, they still *act* as if they were called on
|
||||
`obj` directly. If the `__getitem__` member of `type(obj)` is a descriptor, it is called with `obj`
|
||||
as the `instance` argument to `__get__`. A desugared version of `obj[key]` is roughly equivalent to
|
||||
`getitem_desugared(obj, key)` as defined below:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def find_name_in_mro(typ: type, name: str) -> Any:
|
||||
# See implementation in https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance
|
||||
pass
|
||||
|
||||
def getitem_desugared(obj: object, key: object) -> object:
|
||||
getitem_callable = find_name_in_mro(type(obj), "__getitem__")
|
||||
if hasattr(getitem_callable, "__get__"):
|
||||
getitem_callable = getitem_callable.__get__(obj, type(obj))
|
||||
|
||||
return getitem_callable(key)
|
||||
```
|
||||
|
||||
In the following tests, we demonstrate that we implement this behavior correctly.
|
||||
|
||||
## Operating on class objects
|
||||
|
||||
If we invoke a dunder method on a class, it is looked up on the *meta* class, since any class is an
|
||||
instance of its metaclass:
|
||||
|
||||
```py
|
||||
class Meta(type):
|
||||
def __getitem__(cls, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class DunderOnMetaclass(metaclass=Meta):
|
||||
pass
|
||||
|
||||
reveal_type(DunderOnMetaclass[0]) # revealed: str
|
||||
```
|
||||
|
||||
If the dunder method is only present on the class itself, it will not be called:
|
||||
|
||||
```py
|
||||
class ClassWithNormalDunder:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
# error: [non-subscriptable]
|
||||
ClassWithNormalDunder[0]
|
||||
```
|
||||
|
||||
## Operating on instances
|
||||
|
||||
When invoking a dunder method on an instance of a class, it is looked up on the class:
|
||||
|
||||
```py
|
||||
class ClassWithNormalDunder:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class_with_normal_dunder = ClassWithNormalDunder()
|
||||
|
||||
reveal_type(class_with_normal_dunder[0]) # revealed: str
|
||||
```
|
||||
|
||||
Which can be demonstrated by trying to attach a dunder method to an instance, which will not work:
|
||||
|
||||
```py
|
||||
def external_getitem(instance, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class ThisFails:
|
||||
def __init__(self):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
this_fails = ThisFails()
|
||||
|
||||
# error: [non-subscriptable] "Cannot subscript object of type `ThisFails` with no `__getitem__` method"
|
||||
reveal_type(this_fails[0]) # revealed: Unknown
|
||||
```
|
||||
|
||||
However, the attached dunder method *can* be called if accessed directly:
|
||||
|
||||
```py
|
||||
reveal_type(this_fails.__getitem__(this_fails, 0)) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
The instance-level method is also not called when the class-level method is present:
|
||||
|
||||
```py
|
||||
def external_getitem1(instance, key) -> str:
|
||||
return "a"
|
||||
|
||||
def external_getitem2(key) -> int:
|
||||
return 1
|
||||
|
||||
def _(flag: bool):
|
||||
class ThisFails:
|
||||
if flag:
|
||||
__getitem__ = external_getitem1
|
||||
|
||||
def __init__(self):
|
||||
self.__getitem__ = external_getitem2
|
||||
|
||||
this_fails = ThisFails()
|
||||
|
||||
# error: [call-possibly-unbound-method]
|
||||
reveal_type(this_fails[0]) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
## When the dunder is not a method
|
||||
|
||||
A dunder can also be a non-method callable:
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
def __call__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class ClassWithNonMethodDunder:
|
||||
__getitem__: SomeCallable = SomeCallable()
|
||||
|
||||
class_with_callable_dunder = ClassWithNonMethodDunder()
|
||||
|
||||
reveal_type(class_with_callable_dunder[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Dunders are looked up using the descriptor protocol
|
||||
|
||||
Here, we demonstrate that the descriptor protocol is invoked when looking up a dunder method. Note
|
||||
that the `instance` argument is on object of type `ClassWithDescriptorDunder`:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class SomeCallable:
|
||||
def __call__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance: ClassWithDescriptorDunder, owner: type[ClassWithDescriptorDunder]) -> SomeCallable:
|
||||
return SomeCallable()
|
||||
|
||||
class ClassWithDescriptorDunder:
|
||||
__getitem__: Descriptor = Descriptor()
|
||||
|
||||
class_with_descriptor_dunder = ClassWithDescriptorDunder()
|
||||
|
||||
reveal_type(class_with_descriptor_dunder[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Dunders can not be overwritten on instances
|
||||
|
||||
If we attempt to overwrite a dunder method on an instance, it does not affect the behavior of
|
||||
implicit dunder calls:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
def f(self):
|
||||
# TODO: This should emit an `invalid-assignment` diagnostic once we understand the type of `self`
|
||||
self.__getitem__ = None
|
||||
|
||||
# This is still fine, and simply calls the `__getitem__` method on the class
|
||||
reveal_type(C()[0]) # revealed: str
|
||||
```
|
||||
|
||||
## Calling a union of dunder methods
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class C:
|
||||
if flag:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
else:
|
||||
def __getitem__(self, key: int) -> bytes:
|
||||
return bytes()
|
||||
|
||||
c = C()
|
||||
reveal_type(c[0]) # revealed: str | bytes
|
||||
|
||||
if flag:
|
||||
class D:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
else:
|
||||
class D:
|
||||
def __getitem__(self, key: int) -> bytes:
|
||||
return bytes()
|
||||
|
||||
d = D()
|
||||
reveal_type(d[0]) # revealed: str | bytes
|
||||
```
|
||||
|
||||
## Calling a union of types without dunder methods
|
||||
|
||||
We add instance attributes here to make sure that we don't treat the implicit dunder calls here like
|
||||
regular method calls.
|
||||
|
||||
```py
|
||||
def external_getitem(instance, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
class NotSubscriptable1:
|
||||
def __init__(self, value: int):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
class NotSubscriptable2:
|
||||
def __init__(self, value: int):
|
||||
self.__getitem__ = external_getitem
|
||||
|
||||
def _(union: NotSubscriptable1 | NotSubscriptable2):
|
||||
# error: [non-subscriptable]
|
||||
union[0]
|
||||
```
|
||||
|
||||
## Calling a possibly-unbound dunder method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class C:
|
||||
if flag:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return str(key)
|
||||
|
||||
c = C()
|
||||
# error: [call-possibly-unbound-method]
|
||||
reveal_type(c[0]) # revealed: str
|
||||
```
|
335
crates/ty_python_semantic/resources/mdtest/call/function.md
Normal file
335
crates/ty_python_semantic/resources/mdtest/call/function.md
Normal file
|
@ -0,0 +1,335 @@
|
|||
# Call expression
|
||||
|
||||
## Simple
|
||||
|
||||
```py
|
||||
def get_int() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(get_int()) # revealed: int
|
||||
```
|
||||
|
||||
## Async
|
||||
|
||||
```py
|
||||
async def get_int_async() -> int:
|
||||
return 42
|
||||
|
||||
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
|
||||
reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
|
||||
```
|
||||
|
||||
## Generic
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
def get_int[T]() -> int:
|
||||
return 42
|
||||
|
||||
reveal_type(get_int()) # revealed: int
|
||||
```
|
||||
|
||||
## Decorated
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
def decorator(func) -> Callable[[], int]:
|
||||
return foo
|
||||
|
||||
@decorator
|
||||
def bar() -> str:
|
||||
return "bar"
|
||||
|
||||
reveal_type(bar()) # revealed: int
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
|
||||
```py
|
||||
nonsense = 123
|
||||
x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
||||
```
|
||||
|
||||
## Potentially unbound function
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def foo() -> int:
|
||||
return 42
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
## Wrong argument type
|
||||
|
||||
### Positional argument, positional-or-keyword parameter
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Positional argument, positional-only parameter
|
||||
|
||||
```py
|
||||
def f(x: int, /) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Positional argument, variadic parameter
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, positional-or-keyword parameter
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, keyword-only parameter
|
||||
|
||||
```py
|
||||
def f(*, x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Keyword argument, keywords parameter
|
||||
|
||||
```py
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
reveal_type(f(x="foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Correctly match keyword out-of-order
|
||||
|
||||
```py
|
||||
def f(x: int = 1, y: str = "foo") -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[2]`"
|
||||
# error: 20 [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["bar"]`"
|
||||
reveal_type(f(y=2, x="bar")) # revealed: int
|
||||
```
|
||||
|
||||
## Too many positional arguments
|
||||
|
||||
### One too many
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
||||
|
||||
### Two too many
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2"
|
||||
reveal_type(f("foo", "bar")) # revealed: int
|
||||
```
|
||||
|
||||
### No too-many-positional if variadic is taken
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(1, 2, 3)) # revealed: int
|
||||
```
|
||||
|
||||
### Multiple keyword arguments map to keyword variadic parameter
|
||||
|
||||
```py
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(foo=1, bar=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Missing arguments
|
||||
|
||||
### No defaults or variadic
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### With default
|
||||
|
||||
```py
|
||||
def f(x: int, y: str = "foo") -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Defaulted argument is not required
|
||||
|
||||
```py
|
||||
def f(x: int = 1) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### With variadic
|
||||
|
||||
```py
|
||||
def f(x: int, *y: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Variadic argument is not required
|
||||
|
||||
```py
|
||||
def f(*args: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Keywords argument is not required
|
||||
|
||||
```py
|
||||
def f(**kwargs: int) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
### Multiple
|
||||
|
||||
```py
|
||||
def f(x: int, y: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`"
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
## Unknown argument
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`"
|
||||
reveal_type(f(x=1, y=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Parameter already assigned
|
||||
|
||||
```py
|
||||
def f(x: int) -> int:
|
||||
return 1
|
||||
|
||||
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
|
||||
reveal_type(f(1, x=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Special functions
|
||||
|
||||
Some functions require special handling in type inference. Here, we make sure that we still emit
|
||||
proper diagnostics in case of missing or superfluous arguments.
|
||||
|
||||
### `reveal_type`
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
|
||||
reveal_type()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
|
||||
reveal_type(1, 2)
|
||||
```
|
||||
|
||||
### `static_assert`
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
|
||||
static_assert()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
|
||||
static_assert(True, 2, 3)
|
||||
```
|
||||
|
||||
### `len`
|
||||
|
||||
```py
|
||||
# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
|
||||
len()
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
|
||||
len([], 1)
|
||||
```
|
||||
|
||||
### Type API predicates
|
||||
|
||||
```py
|
||||
from ty_extensions import is_subtype_of, is_fully_static
|
||||
|
||||
# error: [missing-argument]
|
||||
is_subtype_of()
|
||||
|
||||
# error: [missing-argument]
|
||||
is_subtype_of(int)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_subtype_of(int, int, int)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_subtype_of(int, int, int, int)
|
||||
|
||||
# error: [missing-argument]
|
||||
is_fully_static()
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
is_fully_static(int, int)
|
||||
```
|
|
@ -0,0 +1,152 @@
|
|||
# `inspect.getattr_static`
|
||||
|
||||
## Basic usage
|
||||
|
||||
`inspect.getattr_static` is a function that returns attributes of an object without invoking the
|
||||
descriptor protocol (for caveats, see the [official documentation]).
|
||||
|
||||
Consider the following example:
|
||||
|
||||
```py
|
||||
import inspect
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance, owner) -> str:
|
||||
return "a"
|
||||
|
||||
class C:
|
||||
normal: int = 1
|
||||
descriptor: Descriptor = Descriptor()
|
||||
```
|
||||
|
||||
If we access attributes on an instance of `C` as usual, the descriptor protocol is invoked, and we
|
||||
get a type of `str` for the `descriptor` attribute:
|
||||
|
||||
```py
|
||||
c = C()
|
||||
|
||||
reveal_type(c.normal) # revealed: int
|
||||
reveal_type(c.descriptor) # revealed: str
|
||||
```
|
||||
|
||||
However, if we use `inspect.getattr_static`, we can see the underlying `Descriptor` type:
|
||||
|
||||
```py
|
||||
reveal_type(inspect.getattr_static(c, "normal")) # revealed: int
|
||||
reveal_type(inspect.getattr_static(c, "descriptor")) # revealed: Descriptor
|
||||
```
|
||||
|
||||
For non-existent attributes, a default value can be provided:
|
||||
|
||||
```py
|
||||
reveal_type(inspect.getattr_static(C, "normal", "default-arg")) # revealed: int
|
||||
reveal_type(inspect.getattr_static(C, "non_existent", "default-arg")) # revealed: Literal["default-arg"]
|
||||
```
|
||||
|
||||
When a non-existent attribute is accessed without a default value, the runtime raises an
|
||||
`AttributeError`. We could emit a diagnostic for this case, but that is currently not supported:
|
||||
|
||||
```py
|
||||
# TODO: we could emit a diagnostic here
|
||||
reveal_type(inspect.getattr_static(C, "non_existent")) # revealed: Never
|
||||
```
|
||||
|
||||
We can access attributes on objects of all kinds:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(inspect.getattr_static(sys, "dont_write_bytecode")) # revealed: bool
|
||||
# revealed: def getattr_static(obj: object, attr: str, default: Any | None = ellipsis) -> Any
|
||||
reveal_type(inspect.getattr_static(inspect, "getattr_static"))
|
||||
|
||||
reveal_type(inspect.getattr_static(1, "real")) # revealed: property
|
||||
```
|
||||
|
||||
(Implicit) instance attributes can also be accessed through `inspect.getattr_static`:
|
||||
|
||||
```py
|
||||
class D:
|
||||
def __init__(self) -> None:
|
||||
self.instance_attr: int = 1
|
||||
|
||||
reveal_type(inspect.getattr_static(D(), "instance_attr")) # revealed: int
|
||||
```
|
||||
|
||||
And attributes on metaclasses can be accessed when probing the class:
|
||||
|
||||
```py
|
||||
class Meta(type):
|
||||
attr: int = 1
|
||||
|
||||
class E(metaclass=Meta): ...
|
||||
|
||||
reveal_type(inspect.getattr_static(E, "attr")) # revealed: int
|
||||
```
|
||||
|
||||
Metaclass attributes can not be added when probing an instance of the class:
|
||||
|
||||
```py
|
||||
reveal_type(inspect.getattr_static(E(), "attr", "non_existent")) # revealed: Literal["non_existent"]
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
We can only infer precise types if the attribute is a literal string. In all other cases, we fall
|
||||
back to `Any`:
|
||||
|
||||
```py
|
||||
import inspect
|
||||
|
||||
class C:
|
||||
x: int = 1
|
||||
|
||||
def _(attr_name: str):
|
||||
reveal_type(inspect.getattr_static(C(), attr_name)) # revealed: Any
|
||||
reveal_type(inspect.getattr_static(C(), attr_name, 1)) # revealed: Any
|
||||
```
|
||||
|
||||
But we still detect errors in the number or type of arguments:
|
||||
|
||||
```py
|
||||
# error: [missing-argument] "No arguments provided for required parameters `obj`, `attr` of function `getattr_static`"
|
||||
inspect.getattr_static()
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `attr`"
|
||||
inspect.getattr_static(C())
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
|
||||
inspect.getattr_static(C(), 1)
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `getattr_static`: expected 3, got 4"
|
||||
inspect.getattr_static(C(), "x", "default-arg", "one too many")
|
||||
```
|
||||
|
||||
## Possibly unbound attributes
|
||||
|
||||
```py
|
||||
import inspect
|
||||
|
||||
def _(flag: bool):
|
||||
class C:
|
||||
if flag:
|
||||
x: int = 1
|
||||
|
||||
reveal_type(inspect.getattr_static(C, "x", "default")) # revealed: int | Literal["default"]
|
||||
```
|
||||
|
||||
## Gradual types
|
||||
|
||||
```py
|
||||
import inspect
|
||||
from typing import Any
|
||||
|
||||
def _(a: Any, tuple_of_any: tuple[Any]):
|
||||
reveal_type(inspect.getattr_static(a, "x", "default")) # revealed: Any | Literal["default"]
|
||||
|
||||
# TODO: Ideally, this would just be `def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int`
|
||||
# revealed: (def index(self, value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int) | Literal["default"]
|
||||
reveal_type(inspect.getattr_static(tuple_of_any, "index", "default"))
|
||||
```
|
||||
|
||||
[official documentation]: https://docs.python.org/3/library/inspect.html#inspect.getattr_static
|
|
@ -0,0 +1,44 @@
|
|||
# Invalid signatures
|
||||
|
||||
## Multiple arguments with the same name
|
||||
|
||||
We always map a keyword argument to the first parameter of that name.
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Duplicate parameter "x""
|
||||
def f(x: int, x: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
|
||||
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
|
||||
reveal_type(f(1, x=2)) # revealed: int
|
||||
```
|
||||
|
||||
## Positional after non-positional
|
||||
|
||||
When parameter kinds are given in an invalid order, we emit a diagnostic and implicitly reorder them
|
||||
to the valid order:
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Parameter cannot follow var-keyword parameter"
|
||||
def f(**kw: int, x: str) -> int:
|
||||
return 1
|
||||
|
||||
# error: 15 [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[1]`"
|
||||
reveal_type(f(1)) # revealed: int
|
||||
```
|
||||
|
||||
## Non-defaulted after defaulted
|
||||
|
||||
We emit a syntax diagnostic for this, but it doesn't cause any problems for binding.
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "Parameter without a default cannot follow a parameter with a default"
|
||||
def f(x: int = 1, y: str) -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f(y="foo")) # revealed: int
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["foo"]`"
|
||||
# error: [missing-argument] "No argument provided for required parameter `y` of function `f`"
|
||||
reveal_type(f("foo")) # revealed: int
|
||||
```
|
433
crates/ty_python_semantic/resources/mdtest/call/methods.md
Normal file
433
crates/ty_python_semantic/resources/mdtest/call/methods.md
Normal file
|
@ -0,0 +1,433 @@
|
|||
# Methods
|
||||
|
||||
## Background: Functions as descriptors
|
||||
|
||||
> Note: See also this related section in the descriptor guide: [Functions and methods].
|
||||
|
||||
Say we have a simple class `C` with a function definition `f` inside its body:
|
||||
|
||||
```py
|
||||
class C:
|
||||
def f(self, x: int) -> str:
|
||||
return "a"
|
||||
```
|
||||
|
||||
Whenever we access the `f` attribute through the class object itself (`C.f`) or through an instance
|
||||
(`C().f`), this access happens via the descriptor protocol. Functions are (non-data) descriptors
|
||||
because they implement a `__get__` method. This is crucial in making sure that method calls work as
|
||||
expected. In general, the signature of the `__get__` method in the descriptor protocol is
|
||||
`__get__(self, instance, owner)`. The `self` argument is the descriptor object itself (`f`). The
|
||||
passed value for the `instance` argument depends on whether the attribute is accessed from the class
|
||||
object (in which case it is `None`), or from an instance (in which case it is the instance of type
|
||||
`C`). The `owner` argument is the class itself (`C` of type `Literal[C]`). To summarize:
|
||||
|
||||
- `C.f` is equivalent to `getattr_static(C, "f").__get__(None, C)`
|
||||
- `C().f` is equivalent to `getattr_static(C, "f").__get__(C(), C)`
|
||||
|
||||
Here, `inspect.getattr_static` is used to bypass the descriptor protocol and directly access the
|
||||
function attribute. The way the special `__get__` method *on functions* works is as follows. In the
|
||||
former case, if the `instance` argument is `None`, `__get__` simply returns the function itself. In
|
||||
the latter case, it returns a *bound method* object:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
reveal_type(getattr_static(C, "f")) # revealed: def f(self, x: int) -> str
|
||||
|
||||
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: def f(self, x: int) -> str
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method C.f(x: int) -> str
|
||||
```
|
||||
|
||||
In conclusion, this is why we see the following two types when accessing the `f` attribute on the
|
||||
class object `C` and on an instance `C()`:
|
||||
|
||||
```py
|
||||
reveal_type(C.f) # revealed: def f(self, x: int) -> str
|
||||
reveal_type(C().f) # revealed: bound method C.f(x: int) -> str
|
||||
```
|
||||
|
||||
A bound method is a callable object that contains a reference to the `instance` that it was called
|
||||
on (can be inspected via `__self__`), and the function object that it refers to (can be inspected
|
||||
via `__func__`):
|
||||
|
||||
```py
|
||||
bound_method = C().f
|
||||
|
||||
reveal_type(bound_method.__self__) # revealed: C
|
||||
reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str
|
||||
```
|
||||
|
||||
When we call the bound method, the `instance` is implicitly passed as the first argument (`self`):
|
||||
|
||||
```py
|
||||
reveal_type(C().f(1)) # revealed: str
|
||||
reveal_type(bound_method(1)) # revealed: str
|
||||
```
|
||||
|
||||
When we call the function object itself, we need to pass the `instance` explicitly:
|
||||
|
||||
```py
|
||||
C.f(1) # error: [missing-argument]
|
||||
|
||||
reveal_type(C.f(C(), 1)) # revealed: str
|
||||
```
|
||||
|
||||
When we access methods from derived classes, they will be bound to instances of the derived class:
|
||||
|
||||
```py
|
||||
class D(C):
|
||||
pass
|
||||
|
||||
reveal_type(D().f) # revealed: bound method D.f(x: int) -> str
|
||||
```
|
||||
|
||||
If we access an attribute on a bound method object itself, it will defer to `types.MethodType`:
|
||||
|
||||
```py
|
||||
reveal_type(bound_method.__hash__) # revealed: bound method MethodType.__hash__() -> int
|
||||
```
|
||||
|
||||
If an attribute is not available on the bound method object, it will be looked up on the underlying
|
||||
function object. We model this explicitly, which means that we can access `__kwdefaults__` on bound
|
||||
methods, even though it is not available on `types.MethodType`:
|
||||
|
||||
```py
|
||||
reveal_type(bound_method.__kwdefaults__) # revealed: dict[str, Any] | None
|
||||
```
|
||||
|
||||
## Basic method calls on class objects and instances
|
||||
|
||||
```py
|
||||
class Base:
|
||||
def method_on_base(self, x: int | None) -> str:
|
||||
return "a"
|
||||
|
||||
class Derived(Base):
|
||||
def method_on_derived(self, x: bytes) -> tuple[int, str]:
|
||||
return (1, "a")
|
||||
|
||||
reveal_type(Base().method_on_base(1)) # revealed: str
|
||||
reveal_type(Base.method_on_base(Base(), 1)) # revealed: str
|
||||
|
||||
Base().method_on_base("incorrect") # error: [invalid-argument-type]
|
||||
Base().method_on_base() # error: [missing-argument]
|
||||
Base().method_on_base(1, 2) # error: [too-many-positional-arguments]
|
||||
|
||||
reveal_type(Derived().method_on_base(1)) # revealed: str
|
||||
reveal_type(Derived().method_on_derived(b"abc")) # revealed: tuple[int, str]
|
||||
reveal_type(Derived.method_on_base(Derived(), 1)) # revealed: str
|
||||
reveal_type(Derived.method_on_derived(Derived(), b"abc")) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
## Method calls on literals
|
||||
|
||||
### Boolean literals
|
||||
|
||||
```py
|
||||
reveal_type(True.bit_length()) # revealed: int
|
||||
reveal_type(True.as_integer_ratio()) # revealed: tuple[int, Literal[1]]
|
||||
```
|
||||
|
||||
### Integer literals
|
||||
|
||||
```py
|
||||
reveal_type((42).bit_length()) # revealed: int
|
||||
```
|
||||
|
||||
### String literals
|
||||
|
||||
```py
|
||||
reveal_type("abcde".find("abc")) # revealed: int
|
||||
reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes
|
||||
|
||||
"abcde".find(123) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Bytes literals
|
||||
|
||||
```py
|
||||
reveal_type(b"abcde".startswith(b"abc")) # revealed: bool
|
||||
```
|
||||
|
||||
## Method calls on `LiteralString`
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
def f(s: LiteralString) -> None:
|
||||
reveal_type(s.find("a")) # revealed: int
|
||||
```
|
||||
|
||||
## Method calls on `tuple`
|
||||
|
||||
```py
|
||||
def f(t: tuple[int, str]) -> None:
|
||||
reveal_type(t.index("a")) # revealed: int
|
||||
```
|
||||
|
||||
## Method calls on unions
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class A:
|
||||
def f(self) -> int:
|
||||
return 1
|
||||
|
||||
class B:
|
||||
def f(self) -> str:
|
||||
return "a"
|
||||
|
||||
def f(a_or_b: A | B, any_or_a: Any | A):
|
||||
reveal_type(a_or_b.f) # revealed: (bound method A.f() -> int) | (bound method B.f() -> str)
|
||||
reveal_type(a_or_b.f()) # revealed: int | str
|
||||
|
||||
reveal_type(any_or_a.f) # revealed: Any | (bound method A.f() -> int)
|
||||
reveal_type(any_or_a.f()) # revealed: Any | int
|
||||
```
|
||||
|
||||
## Method calls on `KnownInstance` types
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr.__or__) # revealed: bound method typing.TypeAliasType.__or__(right: Any) -> _SpecialForm
|
||||
```
|
||||
|
||||
## Error cases: Calling `__get__` for methods
|
||||
|
||||
The `__get__` method on `types.FunctionType` has the following overloaded signature in typeshed:
|
||||
|
||||
```pyi
|
||||
from types import FunctionType, MethodType
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type, /) -> FunctionType: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
|
||||
```
|
||||
|
||||
Here, we test that this signature is enforced correctly:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
class C:
|
||||
def f(self, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
method_wrapper = getattr_static(C, "f").__get__
|
||||
|
||||
reveal_type(method_wrapper) # revealed: <method-wrapper `__get__` of `f`>
|
||||
|
||||
# All of these are fine:
|
||||
method_wrapper(C(), C)
|
||||
method_wrapper(C())
|
||||
method_wrapper(C(), None)
|
||||
method_wrapper(None, C)
|
||||
|
||||
# Passing `None` without an `owner` argument is an
|
||||
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
|
||||
method_wrapper(None)
|
||||
|
||||
# Passing something that is not assignable to `type` as the `owner` argument is an
|
||||
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
|
||||
method_wrapper(None, 1)
|
||||
|
||||
# Passing `None` as the `owner` argument when `instance` is `None` is an
|
||||
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
|
||||
method_wrapper(None, None)
|
||||
|
||||
# Calling `__get__` without any arguments is an
|
||||
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
|
||||
method_wrapper()
|
||||
|
||||
# Calling `__get__` with too many positional arguments is an
|
||||
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
|
||||
method_wrapper(C(), C, "one too many")
|
||||
```
|
||||
|
||||
## Fallback to metaclass
|
||||
|
||||
When a method is accessed on a class object, it is looked up on the metaclass if it is not found on
|
||||
the class itself. This also creates a bound method that is bound to the class object itself:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class Meta(type):
|
||||
def f(cls, arg: int) -> str:
|
||||
return "a"
|
||||
|
||||
class C(metaclass=Meta):
|
||||
pass
|
||||
|
||||
reveal_type(C.f) # revealed: bound method Literal[C].f(arg: int) -> str
|
||||
reveal_type(C.f(1)) # revealed: str
|
||||
```
|
||||
|
||||
The method `f` can not be accessed from an instance of the class:
|
||||
|
||||
```py
|
||||
# error: [unresolved-attribute] "Type `C` has no attribute `f`"
|
||||
C().f
|
||||
```
|
||||
|
||||
A metaclass function can be shadowed by a method on the class:
|
||||
|
||||
```py
|
||||
from typing import Any, Literal
|
||||
|
||||
class D(metaclass=Meta):
|
||||
def f(arg: int) -> Literal["a"]:
|
||||
return "a"
|
||||
|
||||
reveal_type(D.f(1)) # revealed: Literal["a"]
|
||||
```
|
||||
|
||||
If the class method is possibly unbound, we union the return types:
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
class E(metaclass=Meta):
|
||||
if flag():
|
||||
def f(arg: int) -> Any:
|
||||
return "a"
|
||||
|
||||
reveal_type(E.f(1)) # revealed: str | Any
|
||||
```
|
||||
|
||||
## `@classmethod`
|
||||
|
||||
### Basic
|
||||
|
||||
When a `@classmethod` attribute is accessed, it returns a bound method object, even when accessed on
|
||||
the class object itself:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
def f(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(C.f) # revealed: bound method Literal[C].f(x: int) -> str
|
||||
reveal_type(C().f) # revealed: bound method type[C].f(x: int) -> str
|
||||
```
|
||||
|
||||
The `cls` method argument is then implicitly passed as the first argument when calling the method:
|
||||
|
||||
```py
|
||||
reveal_type(C.f(1)) # revealed: str
|
||||
reveal_type(C().f(1)) # revealed: str
|
||||
```
|
||||
|
||||
When the class method is called incorrectly, we detect it:
|
||||
|
||||
```py
|
||||
C.f("incorrect") # error: [invalid-argument-type]
|
||||
C.f() # error: [missing-argument]
|
||||
C.f(1, 2) # error: [too-many-positional-arguments]
|
||||
```
|
||||
|
||||
If the `cls` parameter is wrongly annotated, we emit an error at the call site:
|
||||
|
||||
```py
|
||||
class D:
|
||||
@classmethod
|
||||
def f(cls: D):
|
||||
# This function is wrongly annotated, it should be `type[D]` instead of `D`
|
||||
pass
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `D`, found `Literal[D]`"
|
||||
D.f()
|
||||
```
|
||||
|
||||
When a class method is accessed on a derived class, it is bound to that derived class:
|
||||
|
||||
```py
|
||||
class Derived(C):
|
||||
pass
|
||||
|
||||
reveal_type(Derived.f) # revealed: bound method Literal[Derived].f(x: int) -> str
|
||||
reveal_type(Derived().f) # revealed: bound method type[Derived].f(x: int) -> str
|
||||
|
||||
reveal_type(Derived.f(1)) # revealed: str
|
||||
reveal_type(Derived().f(1)) # revealed: str
|
||||
```
|
||||
|
||||
### Accessing the classmethod as a static member
|
||||
|
||||
Accessing a `@classmethod`-decorated function at runtime returns a `classmethod` object. We
|
||||
currently don't model this explicitly:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
def f(cls): ...
|
||||
|
||||
reveal_type(getattr_static(C, "f")) # revealed: def f(cls) -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
```
|
||||
|
||||
But we correctly model how the `classmethod` descriptor works:
|
||||
|
||||
```py
|
||||
reveal_type(getattr_static(C, "f").__get__(None, C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__(C(), C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
reveal_type(getattr_static(C, "f").__get__(C())) # revealed: bound method type[C].f() -> Unknown
|
||||
```
|
||||
|
||||
The `owner` argument takes precedence over the `instance` argument:
|
||||
|
||||
```py
|
||||
reveal_type(getattr_static(C, "f").__get__("dummy", C)) # revealed: bound method Literal[C].f() -> Unknown
|
||||
```
|
||||
|
||||
### Classmethods mixed with other decorators
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
When a `@classmethod` is additionally decorated with another decorator, it is still treated as a
|
||||
class method:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
def does_nothing[T](f: T) -> T:
|
||||
return f
|
||||
|
||||
class C:
|
||||
@classmethod
|
||||
@does_nothing
|
||||
def f1(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
@does_nothing
|
||||
@classmethod
|
||||
def f2(cls: type[C], x: int) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(C.f1(1)) # revealed: str
|
||||
reveal_type(C().f1(1)) # revealed: str
|
||||
reveal_type(C.f2(1)) # revealed: str
|
||||
reveal_type(C().f2(1)) # revealed: str
|
||||
```
|
||||
|
||||
[functions and methods]: https://docs.python.org/3/howto/descriptor.html#functions-and-methods
|
12
crates/ty_python_semantic/resources/mdtest/call/never.md
Normal file
12
crates/ty_python_semantic/resources/mdtest/call/never.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Never is callable
|
||||
|
||||
The type `Never` is callable with an arbitrary set of arguments. The result is always `Never`.
|
||||
|
||||
```py
|
||||
from typing_extensions import Never
|
||||
|
||||
def f(never: Never):
|
||||
reveal_type(never()) # revealed: Never
|
||||
reveal_type(never(1)) # revealed: Never
|
||||
reveal_type(never(1, "a", never, x=None)) # revealed: Never
|
||||
```
|
|
@ -0,0 +1,50 @@
|
|||
# `str.startswith`
|
||||
|
||||
We special-case `str.startswith` to allow inference of precise Boolean literal types, because those
|
||||
are used in [`sys.platform` checks].
|
||||
|
||||
```py
|
||||
reveal_type("abc".startswith("")) # revealed: Literal[True]
|
||||
reveal_type("abc".startswith("a")) # revealed: Literal[True]
|
||||
reveal_type("abc".startswith("ab")) # revealed: Literal[True]
|
||||
reveal_type("abc".startswith("abc")) # revealed: Literal[True]
|
||||
|
||||
reveal_type("abc".startswith("abcd")) # revealed: Literal[False]
|
||||
reveal_type("abc".startswith("bc")) # revealed: Literal[False]
|
||||
|
||||
reveal_type("AbC".startswith("")) # revealed: Literal[True]
|
||||
reveal_type("AbC".startswith("A")) # revealed: Literal[True]
|
||||
reveal_type("AbC".startswith("Ab")) # revealed: Literal[True]
|
||||
reveal_type("AbC".startswith("AbC")) # revealed: Literal[True]
|
||||
|
||||
reveal_type("AbC".startswith("a")) # revealed: Literal[False]
|
||||
reveal_type("AbC".startswith("aB")) # revealed: Literal[False]
|
||||
|
||||
reveal_type("".startswith("")) # revealed: Literal[True]
|
||||
|
||||
reveal_type("".startswith(" ")) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
Make sure that we fall back to `bool` for more complex cases:
|
||||
|
||||
```py
|
||||
reveal_type("abc".startswith("b", 1)) # revealed: bool
|
||||
reveal_type("abc".startswith("bc", 1, 3)) # revealed: bool
|
||||
|
||||
reveal_type("abc".startswith(("a", "x"))) # revealed: bool
|
||||
```
|
||||
|
||||
And similiarly, we should still infer `bool` if the instance or the prefix are not string literals:
|
||||
|
||||
```py
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
def _(string_instance: str, literalstring: LiteralString):
|
||||
reveal_type(string_instance.startswith("a")) # revealed: bool
|
||||
reveal_type(literalstring.startswith("a")) # revealed: bool
|
||||
|
||||
reveal_type("a".startswith(string_instance)) # revealed: bool
|
||||
reveal_type("a".startswith(literalstring)) # revealed: bool
|
||||
```
|
||||
|
||||
[`sys.platform` checks]: https://docs.python.org/3/library/sys.html#sys.platform
|
|
@ -0,0 +1,52 @@
|
|||
# Call `type[...]`
|
||||
|
||||
## Single class
|
||||
|
||||
### Trivial constructor
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
def _(subclass_of_c: type[C]):
|
||||
reveal_type(subclass_of_c()) # revealed: C
|
||||
```
|
||||
|
||||
### Non-trivial constructor
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, x: int): ...
|
||||
|
||||
def _(subclass_of_c: type[C]):
|
||||
reveal_type(subclass_of_c(1)) # revealed: C
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `Literal["a"]`"
|
||||
reveal_type(subclass_of_c("a")) # revealed: C
|
||||
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||
reveal_type(subclass_of_c()) # revealed: C
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||
reveal_type(subclass_of_c(1, 2)) # revealed: C
|
||||
```
|
||||
|
||||
## Dynamic base
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Unknown
|
||||
|
||||
def _(subclass_of_any: type[Any], subclass_of_unknown: type[Unknown]):
|
||||
reveal_type(subclass_of_any()) # revealed: Any
|
||||
reveal_type(subclass_of_any("any", "args", 1, 2)) # revealed: Any
|
||||
reveal_type(subclass_of_unknown()) # revealed: Unknown
|
||||
reveal_type(subclass_of_unknown("any", "args", 1, 2)) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unions of classes
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def _(subclass_of_ab: type[A | B]):
|
||||
reveal_type(subclass_of_ab()) # revealed: A | B
|
||||
```
|
253
crates/ty_python_semantic/resources/mdtest/call/union.md
Normal file
253
crates/ty_python_semantic/resources/mdtest/call/union.md
Normal file
|
@ -0,0 +1,253 @@
|
|||
# Unions in calls
|
||||
|
||||
## Union of return types
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
def f() -> int:
|
||||
return 1
|
||||
else:
|
||||
def f() -> str:
|
||||
return "foo"
|
||||
reveal_type(f()) # revealed: int | str
|
||||
```
|
||||
|
||||
## Calling with an unknown union
|
||||
|
||||
```py
|
||||
from nonexistent import f # error: [unresolved-import] "Cannot resolve import `nonexistent`"
|
||||
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
if coinflip():
|
||||
def f() -> int:
|
||||
return 1
|
||||
|
||||
reveal_type(f()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Non-callable elements in a union
|
||||
|
||||
Calling a union with a non-callable element should emit a diagnostic.
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Multiple non-callable elements in a union
|
||||
|
||||
Calling a union with multiple non-callable elements should mention all of them in the diagnostic.
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
elif flag2:
|
||||
f = "foo"
|
||||
else:
|
||||
def f() -> int:
|
||||
return 1
|
||||
# TODO we should mention all non-callable elements of the union
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
# revealed: Unknown | int
|
||||
reveal_type(f())
|
||||
```
|
||||
|
||||
## All non-callable union elements
|
||||
|
||||
Calling a union with no callable elements can emit a simpler diagnostic.
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = 1
|
||||
else:
|
||||
f = "foo"
|
||||
|
||||
x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable"
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Mismatching signatures
|
||||
|
||||
Calling a union where the arguments don't match the signature of all variants.
|
||||
|
||||
```py
|
||||
def f1(a: int) -> int:
|
||||
return a
|
||||
|
||||
def f2(a: str) -> str:
|
||||
return a
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = f2
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `str`, found `Literal[3]`"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## Any non-callable variant
|
||||
|
||||
```py
|
||||
def f1(a: int): ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = "This is a string literal"
|
||||
|
||||
# error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union of binding errors
|
||||
|
||||
```py
|
||||
def f1(): ...
|
||||
def f2(): ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = f2
|
||||
|
||||
# TODO: we should show all errors from the union, not arbitrarily pick one union element
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## One not-callable, one wrong argument
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
def f1(): ...
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = f1
|
||||
else:
|
||||
f = C()
|
||||
|
||||
# TODO: we should either show all union errors here, or prioritize the not-callable error
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `f1`: expected 0, got 1"
|
||||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union including a special-cased function
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = str
|
||||
else:
|
||||
f = repr
|
||||
reveal_type(str("string")) # revealed: Literal["string"]
|
||||
reveal_type(repr("string")) # revealed: Literal["'string'"]
|
||||
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
|
||||
```
|
||||
|
||||
## Unions with literals and negations
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from ty_extensions import Not, AlwaysFalsy, static_assert, is_subtype_of, is_assignable_to
|
||||
|
||||
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[AlwaysFalsy]))
|
||||
static_assert(is_subtype_of(Not[AlwaysFalsy], Literal["", "a"] | Not[AlwaysFalsy]))
|
||||
static_assert(is_subtype_of(Literal["a", ""], Not[AlwaysFalsy] | Literal["a", ""]))
|
||||
static_assert(is_subtype_of(Not[AlwaysFalsy], Not[AlwaysFalsy] | Literal["a", ""]))
|
||||
|
||||
static_assert(is_subtype_of(Literal["a", ""], Literal["a", ""] | Not[Literal[""]]))
|
||||
static_assert(is_subtype_of(Not[Literal[""]], Literal["a", ""] | Not[Literal[""]]))
|
||||
static_assert(is_subtype_of(Literal["a", ""], Not[Literal[""]] | Literal["a", ""]))
|
||||
static_assert(is_subtype_of(Not[Literal[""]], Not[Literal[""]] | Literal["a", ""]))
|
||||
|
||||
def _(
|
||||
a: Literal["a", ""] | Not[AlwaysFalsy],
|
||||
b: Literal["a", ""] | Not[Literal[""]],
|
||||
c: Literal[""] | Not[Literal[""]],
|
||||
d: Not[Literal[""]] | Literal[""],
|
||||
e: Literal["a"] | Not[Literal["a"]],
|
||||
f: Literal[b"b"] | Not[Literal[b"b"]],
|
||||
g: Not[Literal[b"b"]] | Literal[b"b"],
|
||||
h: Literal[42] | Not[Literal[42]],
|
||||
i: Not[Literal[42]] | Literal[42],
|
||||
):
|
||||
reveal_type(a) # revealed: Literal[""] | ~AlwaysFalsy
|
||||
reveal_type(b) # revealed: object
|
||||
reveal_type(c) # revealed: object
|
||||
reveal_type(d) # revealed: object
|
||||
reveal_type(e) # revealed: object
|
||||
reveal_type(f) # revealed: object
|
||||
reveal_type(g) # revealed: object
|
||||
reveal_type(h) # revealed: object
|
||||
reveal_type(i) # revealed: object
|
||||
```
|
||||
|
||||
## Cannot use an argument as both a value and a type form
|
||||
|
||||
```py
|
||||
from ty_extensions import is_fully_static
|
||||
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = repr
|
||||
else:
|
||||
f = is_fully_static
|
||||
# error: [conflicting-argument-forms] "Argument is used as both a value and a type form in call"
|
||||
reveal_type(f(int)) # revealed: str | Literal[True]
|
||||
```
|
||||
|
||||
## Size limit on unions of literals
|
||||
|
||||
Beyond a certain size, large unions of literal types collapse to their nearest super-type (`int`,
|
||||
`bytes`, `str`).
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(literals_2: Literal[0, 1], b: bool, flag: bool):
|
||||
literals_4 = 2 * literals_2 + literals_2 # Literal[0, 1, 2, 3]
|
||||
literals_16 = 4 * literals_4 + literals_4 # Literal[0, 1, .., 15]
|
||||
literals_64 = 4 * literals_16 + literals_4 # Literal[0, 1, .., 63]
|
||||
literals_128 = 2 * literals_64 + literals_2 # Literal[0, 1, .., 127]
|
||||
|
||||
# Going beyond the MAX_UNION_LITERALS limit (currently 200):
|
||||
literals_256 = 16 * literals_16 + literals_16
|
||||
reveal_type(literals_256) # revealed: int
|
||||
|
||||
# Going beyond the limit when another type is already part of the union
|
||||
bool_and_literals_128 = b if flag else literals_128 # bool | Literal[0, 1, ..., 127]
|
||||
literals_128_shifted = literals_128 + 128 # Literal[128, 129, ..., 255]
|
||||
|
||||
# Now union the two:
|
||||
reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int
|
||||
```
|
||||
|
||||
## Simplifying gradually-equivalent types
|
||||
|
||||
If two types are gradually equivalent, we can keep just one of them in a union:
|
||||
|
||||
```py
|
||||
from typing import Any, Union
|
||||
from ty_extensions import Intersection, Not
|
||||
|
||||
def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]):
|
||||
reveal_type(x) # revealed: Any & ~int
|
||||
```
|
410
crates/ty_python_semantic/resources/mdtest/class/super.md
Normal file
410
crates/ty_python_semantic/resources/mdtest/class/super.md
Normal file
|
@ -0,0 +1,410 @@
|
|||
# Super
|
||||
|
||||
Python defines the terms *bound super object* and *unbound super object*.
|
||||
|
||||
An **unbound super object** is created when `super` is called with only one argument. (e.g.
|
||||
`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is
|
||||
rarely used in practice.
|
||||
|
||||
A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the
|
||||
implicit form `super()`, where both the pivot class and the owner are inferred. This is the most
|
||||
common usage.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Explicit Super Object
|
||||
|
||||
`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the
|
||||
specified pivot class.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def a(self): ...
|
||||
aa: int = 1
|
||||
|
||||
class B(A):
|
||||
def b(self): ...
|
||||
bb: int = 2
|
||||
|
||||
class C(B):
|
||||
def c(self): ...
|
||||
cc: int = 3
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||
|
||||
super(C, C()).a
|
||||
super(C, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[C], C>` has no attribute `c`"
|
||||
super(C, C()).c
|
||||
|
||||
super(B, C()).a
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `b`"
|
||||
super(B, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `c`"
|
||||
super(B, C()).c
|
||||
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `a`"
|
||||
super(A, C()).a
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `b`"
|
||||
super(A, C()).b
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `c`"
|
||||
super(A, C()).c
|
||||
|
||||
reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
|
||||
reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
|
||||
reveal_type(super(C, C()).aa) # revealed: int
|
||||
reveal_type(super(C, C()).bb) # revealed: int
|
||||
```
|
||||
|
||||
### Implicit Super Object
|
||||
|
||||
The implicit form `super()` is same as `super(__class__, <first argument>)`. The `__class__` refers
|
||||
to the class that contains the function where `super()` is used. The first argument refers to the
|
||||
current method’s first parameter (typically `self` or `cls`).
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __init__(self, a: int): ...
|
||||
@classmethod
|
||||
def f(cls): ...
|
||||
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
# TODO: Once `Self` is supported, this should be `<super: Literal[B], B>`
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
super().__init__(a)
|
||||
|
||||
@classmethod
|
||||
def f(cls):
|
||||
# TODO: Once `Self` is supported, this should be `<super: Literal[B], Literal[B]>`
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
super().f()
|
||||
|
||||
super(B, B(42)).__init__(42)
|
||||
super(B, B).f()
|
||||
```
|
||||
|
||||
### Unbound Super Object
|
||||
|
||||
Calling `super(cls)` without a second argument returns an *unbound super object*. This is treated as
|
||||
a plain `super` instance and does not support name lookup via the MRO.
|
||||
|
||||
```py
|
||||
class A:
|
||||
a: int = 42
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(super(B)) # revealed: super
|
||||
|
||||
# error: [unresolved-attribute] "Type `super` has no attribute `a`"
|
||||
super(B).a
|
||||
```
|
||||
|
||||
## Attribute Assignment
|
||||
|
||||
`super()` objects do not allow attribute assignment — even if the attribute is resolved
|
||||
successfully.
|
||||
|
||||
```py
|
||||
class A:
|
||||
a: int = 3
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(super(B, B()).a) # revealed: int
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: Literal[B], B>`"
|
||||
super(B, B()).a = 3
|
||||
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`"
|
||||
super(B).a = 5
|
||||
```
|
||||
|
||||
## Dynamic Types
|
||||
|
||||
If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a
|
||||
member, it should effectively behave like a dynamic type.
|
||||
|
||||
```py
|
||||
class A:
|
||||
a: int = 1
|
||||
|
||||
def f(x):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
reveal_type(super(x, x)) # revealed: <super: Unknown, Unknown>
|
||||
reveal_type(super(A, x)) # revealed: <super: Literal[A], Unknown>
|
||||
reveal_type(super(x, A())) # revealed: <super: Unknown, A>
|
||||
|
||||
reveal_type(super(x, x).a) # revealed: Unknown
|
||||
reveal_type(super(A, x).a) # revealed: Unknown
|
||||
reveal_type(super(x, A()).a) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Implicit `super()` in Complex Structure
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[A], Unknown>
|
||||
|
||||
class B:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
|
||||
class C(A.B):
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: Literal[C], Unknown>
|
||||
|
||||
def inner(t: C):
|
||||
reveal_type(super()) # revealed: <super: Literal[B], C>
|
||||
lambda x: reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||
```
|
||||
|
||||
## Built-ins and Literals
|
||||
|
||||
```py
|
||||
reveal_type(super(bool, True)) # revealed: <super: Literal[bool], bool>
|
||||
reveal_type(super(bool, bool())) # revealed: <super: Literal[bool], bool>
|
||||
reveal_type(super(int, bool())) # revealed: <super: Literal[int], bool>
|
||||
reveal_type(super(int, 3)) # revealed: <super: Literal[int], int>
|
||||
reveal_type(super(str, "")) # revealed: <super: Literal[str], str>
|
||||
```
|
||||
|
||||
## Descriptor Behavior with Super
|
||||
|
||||
Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can
|
||||
differ depending on whether the second argument to `super` is a class or an instance.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def a1(self): ...
|
||||
@classmethod
|
||||
def a2(cls): ...
|
||||
|
||||
class B(A): ...
|
||||
|
||||
# A.__dict__["a1"].__get__(B(), B)
|
||||
reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown
|
||||
# A.__dict__["a2"].__get__(B(), B)
|
||||
reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown
|
||||
|
||||
# A.__dict__["a1"].__get__(None, B)
|
||||
reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown
|
||||
# A.__dict__["a2"].__get__(None, B)
|
||||
reveal_type(super(B, B).a2) # revealed: bound method Literal[B].a2() -> Unknown
|
||||
```
|
||||
|
||||
## Union of Supers
|
||||
|
||||
When the owner is a union type, `super()` is built separately for each branch, and the resulting
|
||||
super objects are combined into a union.
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
class B:
|
||||
b: int = 42
|
||||
|
||||
class C(A, B): ...
|
||||
class D(B, A): ...
|
||||
|
||||
def f(x: C | D):
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]]
|
||||
|
||||
s = super(A, x)
|
||||
reveal_type(s) # revealed: <super: Literal[A], C> | <super: Literal[A], D>
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: Literal[A], C> | <super: Literal[A], D>` is possibly unbound"
|
||||
s.b
|
||||
|
||||
def f(flag: bool):
|
||||
x = str() if flag else str("hello")
|
||||
reveal_type(x) # revealed: Literal["", "hello"]
|
||||
reveal_type(super(str, x)) # revealed: <super: Literal[str], str>
|
||||
|
||||
def f(x: int | str):
|
||||
# error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call"
|
||||
super(int, x)
|
||||
```
|
||||
|
||||
Even when `super()` is constructed separately for each branch of a union, it should behave correctly
|
||||
in all cases.
|
||||
|
||||
```py
|
||||
def f(flag: bool):
|
||||
if flag:
|
||||
class A:
|
||||
x = 1
|
||||
y: int = 1
|
||||
|
||||
a: str = "hello"
|
||||
|
||||
class B(A): ...
|
||||
s = super(B, B())
|
||||
else:
|
||||
class C:
|
||||
x = 2
|
||||
y: int | str = "test"
|
||||
|
||||
class D(C): ...
|
||||
s = super(D, D())
|
||||
|
||||
reveal_type(s) # revealed: <super: Literal[B], B> | <super: Literal[D], D>
|
||||
|
||||
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
|
||||
reveal_type(s.y) # revealed: int | str
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: Literal[B], B> | <super: Literal[D], D>` is possibly unbound"
|
||||
reveal_type(s.a) # revealed: str
|
||||
```
|
||||
|
||||
## Supers with Generic Classes
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from ty_extensions import TypeOf, static_assert, is_subtype_of
|
||||
|
||||
class A[T]:
|
||||
def f(self, a: T) -> T:
|
||||
return a
|
||||
|
||||
class B[T](A[T]):
|
||||
def f(self, b: T) -> T:
|
||||
return super().f(b)
|
||||
```
|
||||
|
||||
## Invalid Usages
|
||||
|
||||
### Unresolvable `super()` Calls
|
||||
|
||||
If an appropriate class and argument cannot be found, a runtime error will occur.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
reveal_type(super()) # revealed: Unknown
|
||||
|
||||
def f():
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
super()
|
||||
|
||||
# No first argument in its scope
|
||||
class A:
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
s = super()
|
||||
|
||||
def f(self):
|
||||
def g():
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
super()
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
lambda: super()
|
||||
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
(super() for _ in range(10))
|
||||
|
||||
@staticmethod
|
||||
def h():
|
||||
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||
super()
|
||||
```
|
||||
|
||||
### Failing Condition Checks
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`super()` requires its first argument to be a valid class, and its second argument to be either an
|
||||
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
|
||||
runtime.
|
||||
|
||||
```py
|
||||
def f(x: int):
|
||||
# error: [invalid-super-argument] "`int` is not a valid class"
|
||||
super(x, x)
|
||||
|
||||
type IntAlias = int
|
||||
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
|
||||
super(IntAlias, 0)
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(int, str()))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(int, str))
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, A()))
|
||||
|
||||
# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, object()))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, A))
|
||||
|
||||
# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call"
|
||||
# revealed: Unknown
|
||||
reveal_type(super(B, object))
|
||||
|
||||
super(object, object()).__class__
|
||||
```
|
||||
|
||||
### Instance Member Access via `super`
|
||||
|
||||
Accessing instance members through `super()` is not allowed.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __init__(self, a: int):
|
||||
self.a = a
|
||||
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
super().__init__(a)
|
||||
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
|
||||
super().a
|
||||
|
||||
# error: [unresolved-attribute] "Type `<super: Literal[B], B>` has no attribute `a`"
|
||||
super(B, B(42)).a
|
||||
```
|
||||
|
||||
### Dunder Method Resolution
|
||||
|
||||
Dunder methods defined in the `owner` (from `super(pivot_class, owner)`) should not affect the super
|
||||
object itself. In other words, `super` should not be treated as if it inherits attributes of the
|
||||
`owner`.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __getitem__(self, key: int) -> int:
|
||||
return 42
|
||||
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A()[0]) # revealed: int
|
||||
reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int
|
||||
# error: [non-subscriptable] "Cannot subscript object of type `<super: Literal[B], B>` with no `__getitem__` method"
|
||||
super(B, B())[0]
|
||||
```
|
|
@ -0,0 +1,43 @@
|
|||
# Comparison: Byte literals
|
||||
|
||||
These tests assert that we infer precise `Literal` types for comparisons between objects inferred as
|
||||
having `Literal` bytes types:
|
||||
|
||||
```py
|
||||
reveal_type(b"abc" == b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" == b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" != b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"abc" != b"ab") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" < b"abd") # revealed: Literal[True]
|
||||
reveal_type(b"abc" < b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" <= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" <= b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" > b"abd") # revealed: Literal[False]
|
||||
reveal_type(b"abc" > b"abb") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" >= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" >= b"abd") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"" in b"") # revealed: Literal[True]
|
||||
reveal_type(b"" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"") # revealed: Literal[False]
|
||||
reveal_type(b"ab" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"d" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"\x81\x82" in b"\x80\x81\x82") # revealed: Literal[True]
|
||||
reveal_type(b"\x82\x83" in b"\x80\x81\x82") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"ab" not in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" not in b"abc") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" is b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" is not b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is not b"ab") # revealed: Literal[True]
|
||||
```
|
|
@ -0,0 +1,33 @@
|
|||
# Identity tests
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def _(a1: A, a2: A, o: object):
|
||||
n1 = None
|
||||
n2 = None
|
||||
|
||||
reveal_type(a1 is a1) # revealed: bool
|
||||
reveal_type(a1 is a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is n2) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is a1) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is o) # revealed: bool
|
||||
reveal_type(n1 is o) # revealed: bool
|
||||
|
||||
reveal_type(a1 is not a1) # revealed: bool
|
||||
reveal_type(a1 is not a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is not n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is not n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is not a1) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is not o) # revealed: bool
|
||||
reveal_type(n1 is not o) # revealed: bool
|
||||
```
|
|
@ -0,0 +1,204 @@
|
|||
# Comparison: Membership Test
|
||||
|
||||
In Python, the term "membership test operators" refers to the operators `in` and `not in`. To
|
||||
customize their behavior, classes can implement one of the special methods `__contains__`,
|
||||
`__iter__`, or `__getitem__`.
|
||||
|
||||
For references, see:
|
||||
|
||||
- <https://docs.python.org/3/reference/expressions.html#membership-test-details>
|
||||
- <https://docs.python.org/3/reference/datamodel.html#object.__contains__>
|
||||
- <https://snarky.ca/unravelling-membership-testing/>
|
||||
|
||||
## Implements `__contains__`
|
||||
|
||||
Classes can support membership tests by implementing the `__contains__` method:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __contains__(self, item: str) -> bool:
|
||||
return True
|
||||
|
||||
reveal_type("hello" in A()) # revealed: bool
|
||||
reveal_type("hello" not in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `not in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 not in A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Implements `__iter__`
|
||||
|
||||
Classes that don't implement `__contains__`, but do implement `__iter__`, also support containment
|
||||
checks; the needle will be sought in their iterated items:
|
||||
|
||||
```py
|
||||
class StringIterator:
|
||||
def __next__(self) -> str:
|
||||
return "foo"
|
||||
|
||||
class A:
|
||||
def __iter__(self) -> StringIterator:
|
||||
return StringIterator()
|
||||
|
||||
reveal_type("hello" in A()) # revealed: bool
|
||||
reveal_type("hello" not in A()) # revealed: bool
|
||||
reveal_type(42 in A()) # revealed: bool
|
||||
reveal_type(42 not in A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Implements `__getitems__`
|
||||
|
||||
The final fallback is to implement `__getitem__` for integer keys. Python will call `__getitem__`
|
||||
with `0`, `1`, `2`... until either the needle is found (leading the membership test to evaluate to
|
||||
`True`) or `__getitem__` raises `IndexError` (the raised exception is swallowed, but results in the
|
||||
membership test evaluating to `False`).
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __getitem__(self, key: int) -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type("hello" in A()) # revealed: bool
|
||||
reveal_type("hello" not in A()) # revealed: bool
|
||||
reveal_type(42 in A()) # revealed: bool
|
||||
reveal_type(42 not in A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Wrong Return Type
|
||||
|
||||
Python coerces the results of containment checks to `bool`, even if `__contains__` returns a
|
||||
non-bool:
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __contains__(self, item: str) -> str:
|
||||
return "foo"
|
||||
|
||||
reveal_type("hello" in A()) # revealed: bool
|
||||
reveal_type("hello" not in A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Literal Result for `in` and `not in`
|
||||
|
||||
`__contains__` with a literal return type may result in a `BooleanLiteral` outcome.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class AlwaysTrue:
|
||||
def __contains__(self, item: int) -> Literal[1]:
|
||||
return 1
|
||||
|
||||
class AlwaysFalse:
|
||||
def __contains__(self, item: int) -> Literal[""]:
|
||||
return ""
|
||||
|
||||
reveal_type(42 in AlwaysTrue()) # revealed: Literal[True]
|
||||
reveal_type(42 not in AlwaysTrue()) # revealed: Literal[False]
|
||||
|
||||
reveal_type(42 in AlwaysFalse()) # revealed: Literal[False]
|
||||
reveal_type(42 not in AlwaysFalse()) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## No Fallback for `__contains__`
|
||||
|
||||
If `__contains__` is implemented, checking membership of a type it doesn't accept is an error; it
|
||||
doesn't result in a fallback to `__iter__` or `__getitem__`:
|
||||
|
||||
```py
|
||||
class CheckContains: ...
|
||||
class CheckIter: ...
|
||||
class CheckGetItem: ...
|
||||
|
||||
class CheckIterIterator:
|
||||
def __next__(self) -> CheckIter:
|
||||
return CheckIter()
|
||||
|
||||
class A:
|
||||
def __contains__(self, item: CheckContains) -> bool:
|
||||
return True
|
||||
|
||||
def __iter__(self) -> CheckIterIterator:
|
||||
return CheckIterIterator()
|
||||
|
||||
def __getitem__(self, key: int) -> CheckGetItem:
|
||||
return CheckGetItem()
|
||||
|
||||
reveal_type(CheckContains() in A()) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckIter` and `A`"
|
||||
reveal_type(CheckIter() in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckGetItem` and `A`"
|
||||
reveal_type(CheckGetItem() in A()) # revealed: bool
|
||||
|
||||
class B:
|
||||
def __iter__(self) -> CheckIterIterator:
|
||||
return CheckIterIterator()
|
||||
|
||||
def __getitem__(self, key: int) -> CheckGetItem:
|
||||
return CheckGetItem()
|
||||
|
||||
reveal_type(CheckIter() in B()) # revealed: bool
|
||||
# Always use `__iter__`, regardless of iterated type; there's no NotImplemented
|
||||
# in this case, so there's no fallback to `__getitem__`
|
||||
reveal_type(CheckGetItem() in B()) # revealed: bool
|
||||
```
|
||||
|
||||
## Invalid Old-Style Iteration
|
||||
|
||||
If `__getitem__` is implemented but does not accept integer arguments, then the membership test is
|
||||
not supported and should trigger a diagnostic.
|
||||
|
||||
```py
|
||||
class A:
|
||||
def __getitem__(self, key: str) -> str:
|
||||
return "foo"
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
|
||||
reveal_type(42 in A()) # revealed: bool
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `str` and `A`, in comparing `Literal["hello"]` with `A`"
|
||||
reveal_type("hello" in A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Return type that doesn't implement `__bool__` correctly
|
||||
|
||||
`in` and `not in` operations will fail at runtime if the object on the right-hand side of the
|
||||
operation has a `__contains__` method that returns a type which is not convertible to `bool`. This
|
||||
is because of the way these operations are handled by the Python interpreter at runtime. If we
|
||||
assume that `y` is an object that has a `__contains__` method, the Python expression `x in y`
|
||||
desugars to a `contains(y, x)` call, where `contains` looks something like this:
|
||||
|
||||
```ignore
|
||||
def contains(y, x):
|
||||
return bool(type(y).__contains__(y, x))
|
||||
```
|
||||
|
||||
where the `bool()` conversion itself implicitly calls `__bool__` under the hood.
|
||||
|
||||
TODO: Ideally the message would explain to the user what's wrong. E.g,
|
||||
|
||||
```ignore
|
||||
error: [operator] cannot use `in` operator on object of type `WithContains`
|
||||
note: This is because the `in` operator implicitly calls `WithContains.__contains__`, but `WithContains.__contains__` is invalidly defined
|
||||
note: `WithContains.__contains__` is invalidly defined because it returns an instance of `NotBoolable`, which cannot be evaluated in a boolean context
|
||||
note: `NotBoolable` cannot be evaluated in a boolean context because its `__bool__` attribute is not callable
|
||||
```
|
||||
|
||||
It may also be more appropriate to use `unsupported-operator` as the error code.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
class WithContains:
|
||||
def __contains__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 in WithContains()
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 not in WithContains()
|
||||
```
|
|
@ -0,0 +1,391 @@
|
|||
# Comparison: Rich Comparison
|
||||
|
||||
Rich comparison operations (`==`, `!=`, `<`, `<=`, `>`, `>=`) in Python are implemented through
|
||||
double-underscore methods that allow customization of comparison behavior.
|
||||
|
||||
For references, see:
|
||||
|
||||
- <https://docs.python.org/3/reference/datamodel.html#object.__lt__>
|
||||
- <https://snarky.ca/unravelling-rich-comparison-operators/>
|
||||
|
||||
## Rich Comparison Dunder Implementations For Same Class
|
||||
|
||||
Classes can support rich comparison by implementing dunder methods like `__eq__`, `__ne__`, etc. The
|
||||
most common case involves implementing these methods for the same type:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: A) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: A) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: A) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: A) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: A) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: A) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
reveal_type(A() == A()) # revealed: EqReturnType
|
||||
reveal_type(A() != A()) # revealed: NeReturnType
|
||||
reveal_type(A() < A()) # revealed: LtReturnType
|
||||
reveal_type(A() <= A()) # revealed: LeReturnType
|
||||
reveal_type(A() > A()) # revealed: GtReturnType
|
||||
reveal_type(A() >= A()) # revealed: GeReturnType
|
||||
```
|
||||
|
||||
## Rich Comparison Dunder Implementations for Other Class
|
||||
|
||||
In some cases, classes may implement rich comparison dunder methods for comparisons with a different
|
||||
type:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: B) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: B) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: B) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: B) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: B) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: B) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
class B: ...
|
||||
|
||||
reveal_type(A() == B()) # revealed: EqReturnType
|
||||
reveal_type(A() != B()) # revealed: NeReturnType
|
||||
reveal_type(A() < B()) # revealed: LtReturnType
|
||||
reveal_type(A() <= B()) # revealed: LeReturnType
|
||||
reveal_type(A() > B()) # revealed: GtReturnType
|
||||
reveal_type(A() >= B()) # revealed: GeReturnType
|
||||
```
|
||||
|
||||
## Reflected Comparisons
|
||||
|
||||
Fallback to the right-hand side’s comparison methods occurs when the left-hand side does not define
|
||||
them. Note: class `B` has its own `__eq__` and `__ne__` methods to override those of `object`, but
|
||||
these methods will be ignored here because they require a mismatched operand type.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: B) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: B) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: B) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: B) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: B) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: B) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
class Unrelated: ...
|
||||
|
||||
class B:
|
||||
# To override builtins.object.__eq__ and builtins.object.__ne__
|
||||
# TODO these should emit an invalid override diagnostic
|
||||
def __eq__(self, other: Unrelated) -> B:
|
||||
return B()
|
||||
|
||||
def __ne__(self, other: Unrelated) -> B:
|
||||
return B()
|
||||
|
||||
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
|
||||
# this can only happen with an invalid override of these methods,
|
||||
# but we still support it.
|
||||
reveal_type(B() == A()) # revealed: EqReturnType
|
||||
reveal_type(B() != A()) # revealed: NeReturnType
|
||||
|
||||
reveal_type(B() < A()) # revealed: GtReturnType
|
||||
reveal_type(B() <= A()) # revealed: GeReturnType
|
||||
|
||||
reveal_type(B() > A()) # revealed: LtReturnType
|
||||
reveal_type(B() >= A()) # revealed: LeReturnType
|
||||
|
||||
class C:
|
||||
def __gt__(self, other: C) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ge__(self, other: C) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
reveal_type(C() < C()) # revealed: EqReturnType
|
||||
reveal_type(C() <= C()) # revealed: NeReturnType
|
||||
```
|
||||
|
||||
## Reflected Comparisons with Subclasses
|
||||
|
||||
When subclasses override comparison methods, these overridden methods take precedence over those in
|
||||
the parent class. Class `B` inherits from `A` and redefines comparison methods to return types other
|
||||
than `A`.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
def __ne__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
def __lt__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
def __le__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
def __gt__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
def __ge__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
class B(A):
|
||||
def __eq__(self, other: A) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, other: A) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, other: A) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, other: A) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, other: A) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, other: A) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
reveal_type(A() == B()) # revealed: EqReturnType
|
||||
reveal_type(A() != B()) # revealed: NeReturnType
|
||||
|
||||
reveal_type(A() < B()) # revealed: GtReturnType
|
||||
reveal_type(A() <= B()) # revealed: GeReturnType
|
||||
|
||||
reveal_type(A() > B()) # revealed: LtReturnType
|
||||
reveal_type(A() >= B()) # revealed: LeReturnType
|
||||
```
|
||||
|
||||
## Reflected Comparisons with Subclass But Falls Back to LHS
|
||||
|
||||
In the case of a subclass, the right-hand side has priority. However, if the overridden dunder
|
||||
method has an mismatched type to operand, the comparison will fall back to the left-hand side.
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __lt__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
def __gt__(self, other: A) -> A:
|
||||
return A()
|
||||
|
||||
class B(A):
|
||||
def __lt__(self, other: int) -> B:
|
||||
return B()
|
||||
|
||||
def __gt__(self, other: int) -> B:
|
||||
return B()
|
||||
|
||||
reveal_type(A() < B()) # revealed: A
|
||||
reveal_type(A() > B()) # revealed: A
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
|
||||
`Any` and `Unknown` represent a set of possible runtime objects, wherein the bounds of the set are
|
||||
unknown. Whether the left-hand operand's dunder or the right-hand operand's reflected dunder depends
|
||||
on whether the right-hand operand is an instance of a class that is a subclass of the left-hand
|
||||
operand's class and overrides the reflected dunder. In the following example, because of the
|
||||
unknowable nature of `Any`/`Unknown`, we must consider both possibilities: `Any`/`Unknown` might
|
||||
resolve to an unknown third class that inherits from `X` and overrides `__gt__`; but it also might
|
||||
not. Thus, the correct answer here for the `reveal_type` is `int | Unknown`.
|
||||
|
||||
(This test is referenced from `mdtest/binary/instances.md`)
|
||||
|
||||
```py
|
||||
from does_not_exist import Foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(Foo) # revealed: Unknown
|
||||
|
||||
class X:
|
||||
def __lt__(self, other: object) -> int:
|
||||
return 42
|
||||
|
||||
class Y(Foo): ...
|
||||
|
||||
# TODO: Should be `int | Unknown`; see above discussion.
|
||||
reveal_type(X() < Y()) # revealed: int
|
||||
```
|
||||
|
||||
## Equality and Inequality Fallback
|
||||
|
||||
This test confirms that `==` and `!=` comparisons default to identity comparisons (`is`, `is not`)
|
||||
when argument types do not match the method signature.
|
||||
|
||||
Please refer to the [docs](https://docs.python.org/3/reference/datamodel.html#object.__eq__)
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
# TODO both these overrides should emit invalid-override diagnostic
|
||||
def __eq__(self, other: int) -> A:
|
||||
return A()
|
||||
|
||||
def __ne__(self, other: int) -> A:
|
||||
return A()
|
||||
|
||||
reveal_type(A() == A()) # revealed: bool
|
||||
reveal_type(A() != A()) # revealed: bool
|
||||
```
|
||||
|
||||
## Object Comparisons with Typeshed
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
reveal_type(A() == object()) # revealed: bool
|
||||
reveal_type(A() != object()) # revealed: bool
|
||||
reveal_type(object() == A()) # revealed: bool
|
||||
reveal_type(object() != A()) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `object`"
|
||||
# revealed: Unknown
|
||||
reveal_type(A() < object())
|
||||
```
|
||||
|
||||
## Numbers Comparison with typeshed
|
||||
|
||||
```py
|
||||
reveal_type(1 == 1.0) # revealed: bool
|
||||
reveal_type(1 != 1.0) # revealed: bool
|
||||
reveal_type(1 < 1.0) # revealed: bool
|
||||
reveal_type(1 <= 1.0) # revealed: bool
|
||||
reveal_type(1 > 1.0) # revealed: bool
|
||||
reveal_type(1 >= 1.0) # revealed: bool
|
||||
|
||||
reveal_type(1 == 2j) # revealed: bool
|
||||
reveal_type(1 != 2j) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 < 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 <= 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 > 2j) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
|
||||
reveal_type(1 >= 2j) # revealed: Unknown
|
||||
|
||||
def f(x: bool, y: int):
|
||||
reveal_type(x < y) # revealed: bool
|
||||
reveal_type(y < x) # revealed: bool
|
||||
reveal_type(4.2 < x) # revealed: bool
|
||||
reveal_type(x < 4.2) # revealed: bool
|
||||
```
|
||||
|
||||
## Chained comparisons with objects that don't implement `__bool__` correctly
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Python implicitly calls `bool` on the comparison result of preceding elements (but not for the last
|
||||
element) of a chained comparison.
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
def __gt__(self, item) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 < Comparable() < 20
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 < Comparable() < Comparable()
|
||||
|
||||
Comparable() < Comparable() # fine
|
||||
```
|
||||
|
||||
## Callables as comparison dunders
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class AlwaysTrue:
|
||||
def __call__(self, other: object) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class A:
|
||||
__eq__: AlwaysTrue = AlwaysTrue()
|
||||
__lt__: AlwaysTrue = AlwaysTrue()
|
||||
|
||||
reveal_type(A() == A()) # revealed: Literal[True]
|
||||
reveal_type(A() < A()) # revealed: Literal[True]
|
||||
reveal_type(A() > A()) # revealed: Literal[True]
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
# Comparison: Integers
|
||||
|
||||
## Integer literals
|
||||
|
||||
```py
|
||||
reveal_type(1 == 1 == True) # revealed: Literal[True]
|
||||
reveal_type(1 == 1 == 2 == 4) # revealed: Literal[False]
|
||||
reveal_type(False < True <= 2 < 3 != 6) # revealed: Literal[True]
|
||||
reveal_type(1 < 1) # revealed: Literal[False]
|
||||
reveal_type(1 > 1) # revealed: Literal[False]
|
||||
reveal_type(1 is 1) # revealed: bool
|
||||
reveal_type(1 is not 1) # revealed: bool
|
||||
reveal_type(1 is 2) # revealed: Literal[False]
|
||||
reveal_type(1 is not 7) # revealed: Literal[True]
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
|
||||
reveal_type(1 <= "" and 0 < 1) # revealed: (Unknown & ~AlwaysTruthy) | Literal[True]
|
||||
```
|
||||
|
||||
## Integer instance
|
||||
|
||||
```py
|
||||
# TODO: implement lookup of `__eq__` on typeshed `int` stub.
|
||||
def _(a: int, b: int):
|
||||
reveal_type(1 == a) # revealed: bool
|
||||
reveal_type(9 < a) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
```
|
|
@ -0,0 +1,150 @@
|
|||
# Comparison: Intersections
|
||||
|
||||
## Positive contributions
|
||||
|
||||
If we have an intersection type `A & B` and we get a definitive true/false answer for one of the
|
||||
types, we can infer that the result for the intersection type is also true/false:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Base:
|
||||
def __gt__(self, other) -> bool:
|
||||
return False
|
||||
|
||||
class Child1(Base):
|
||||
def __eq__(self, other) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class Child2(Base): ...
|
||||
|
||||
def _(x: Base):
|
||||
c1 = Child1()
|
||||
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
```
|
||||
|
||||
## Negative contributions
|
||||
|
||||
Negative contributions to the intersection type only allow simplifications in a few special cases
|
||||
(equality and identity comparisons).
|
||||
|
||||
### Equality comparisons
|
||||
|
||||
#### Literal strings
|
||||
|
||||
```py
|
||||
x = "x" * 1_000_000_000
|
||||
y = "y" * 1_000_000_000
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
|
||||
if x != "abc":
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
|
||||
|
||||
# TODO: This should be `Literal[False]`
|
||||
reveal_type(x == "abc") # revealed: bool
|
||||
# TODO: This should be `Literal[False]`
|
||||
reveal_type("abc" == x) # revealed: bool
|
||||
reveal_type(x == "something else") # revealed: bool
|
||||
reveal_type("something else" == x) # revealed: bool
|
||||
|
||||
# TODO: This should be `Literal[True]`
|
||||
reveal_type(x != "abc") # revealed: bool
|
||||
# TODO: This should be `Literal[True]`
|
||||
reveal_type("abc" != x) # revealed: bool
|
||||
reveal_type(x != "something else") # revealed: bool
|
||||
reveal_type("something else" != x) # revealed: bool
|
||||
|
||||
reveal_type(x == y) # revealed: bool
|
||||
reveal_type(y == x) # revealed: bool
|
||||
reveal_type(x != y) # revealed: bool
|
||||
reveal_type(y != x) # revealed: bool
|
||||
|
||||
reveal_type(x >= "abc") # revealed: bool
|
||||
reveal_type("abc" >= x) # revealed: bool
|
||||
|
||||
reveal_type(x in "abc") # revealed: bool
|
||||
reveal_type("abc" in x) # revealed: bool
|
||||
```
|
||||
|
||||
#### Integers
|
||||
|
||||
```py
|
||||
def _(x: int):
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
reveal_type(x != 1) # revealed: bool
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
reveal_type(x == 1) # revealed: bool
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity comparisons
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def _(o: object):
|
||||
a = A()
|
||||
n = None
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: ~None
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
|
||||
### Unsupported operators for positive contributions
|
||||
|
||||
Raise an error if any of the positive contributions to the intersection type are unsupported for the
|
||||
given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool:
|
||||
return False
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
### Unsupported operators for negative contributions
|
||||
|
||||
Do *not* raise an error if any of the negative contributions to the intersection type are
|
||||
unsupported for the given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool:
|
||||
return False
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def _(x: object):
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
|
@ -0,0 +1,47 @@
|
|||
# Comparison: Non boolean returns
|
||||
|
||||
Walking through examples:
|
||||
|
||||
- `a = A() < B() < C()`
|
||||
|
||||
1. `A() < B() and B() < C()` - split in N comparison
|
||||
1. `A()` and `B()` - evaluate outcome types
|
||||
1. `bool` and `bool` - evaluate truthiness
|
||||
1. `A | B` - union of "first true" types
|
||||
|
||||
- `b = 0 < 1 < A() < 3`
|
||||
|
||||
1. `0 < 1 and 1 < A() and A() < 3` - split in N comparison
|
||||
1. `True` and `bool` and `A` - evaluate outcome types
|
||||
1. `True` and `bool` and `bool` - evaluate truthiness
|
||||
1. `bool | A` - union of "true" types
|
||||
|
||||
- `c = 10 < 0 < A() < B() < C()` short-circuit to False
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __lt__(self, other) -> A:
|
||||
return self
|
||||
|
||||
def __gt__(self, other) -> bool:
|
||||
return False
|
||||
|
||||
class B:
|
||||
def __lt__(self, other) -> B:
|
||||
return self
|
||||
|
||||
class C:
|
||||
def __lt__(self, other) -> C:
|
||||
return self
|
||||
|
||||
x = A() < B() < C()
|
||||
reveal_type(x) # revealed: (A & ~AlwaysTruthy) | B
|
||||
|
||||
y = 0 < 1 < A() < 3
|
||||
reveal_type(y) # revealed: Literal[False] | A
|
||||
|
||||
z = 10 < 0 < A() < B() < C()
|
||||
reveal_type(z) # revealed: Literal[False]
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
# Comparison: Strings
|
||||
|
||||
## String literals
|
||||
|
||||
```py
|
||||
def _(x: str):
|
||||
reveal_type("abc" == "abc") # revealed: Literal[True]
|
||||
reveal_type("ab_cd" <= "ab_ce") # revealed: Literal[True]
|
||||
reveal_type("abc" in "ab cd") # revealed: Literal[False]
|
||||
reveal_type("" not in "hello") # revealed: Literal[False]
|
||||
reveal_type("--" is "--") # revealed: bool
|
||||
reveal_type("A" is "B") # revealed: Literal[False]
|
||||
reveal_type("--" is not "--") # revealed: bool
|
||||
reveal_type("A" is not "B") # revealed: Literal[True]
|
||||
reveal_type(x < "...") # revealed: bool
|
||||
|
||||
# ensure we're not comparing the interned salsa symbols, which compare by order of declaration.
|
||||
reveal_type("ab" < "ab_cd") # revealed: Literal[True]
|
||||
```
|
394
crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
Normal file
394
crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
Normal file
|
@ -0,0 +1,394 @@
|
|||
# Comparison: Tuples
|
||||
|
||||
## Heterogeneous
|
||||
|
||||
For tuples like `tuple[int, str, Literal[1]]`
|
||||
|
||||
### Value Comparisons
|
||||
|
||||
"Value Comparisons" refers to the operators: `==`, `!=`, `<`, `<=`, `>`, `>=`
|
||||
|
||||
#### Results without Ambiguity
|
||||
|
||||
Cases where the result can be definitively inferred as a `BooleanLiteral`.
|
||||
|
||||
```py
|
||||
a = (1, "test", (3, 13), True)
|
||||
b = (1, "test", (3, 14), False)
|
||||
|
||||
reveal_type(a == a) # revealed: Literal[True]
|
||||
reveal_type(a != a) # revealed: Literal[False]
|
||||
reveal_type(a < a) # revealed: Literal[False]
|
||||
reveal_type(a <= a) # revealed: Literal[True]
|
||||
reveal_type(a > a) # revealed: Literal[False]
|
||||
reveal_type(a >= a) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
Even when tuples have different lengths, comparisons should be handled appropriately.
|
||||
|
||||
```py
|
||||
a = (1, 2, 3)
|
||||
b = (1, 2, 3, 4)
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
|
||||
c = ("a", "b", "c", "d")
|
||||
d = ("a", "b", "c")
|
||||
|
||||
reveal_type(c == d) # revealed: Literal[False]
|
||||
reveal_type(c != d) # revealed: Literal[True]
|
||||
reveal_type(c < d) # revealed: Literal[False]
|
||||
reveal_type(c <= d) # revealed: Literal[False]
|
||||
reveal_type(c > d) # revealed: Literal[True]
|
||||
reveal_type(c >= d) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
#### Results with Ambiguity
|
||||
|
||||
```py
|
||||
def _(x: bool, y: int):
|
||||
a = (x,)
|
||||
b = (y,)
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
reveal_type(a == b) # revealed: bool
|
||||
reveal_type(a != b) # revealed: bool
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
```
|
||||
|
||||
#### Comparison Unsupported
|
||||
|
||||
If two tuples contain types that do not support comparison, the result may be `Unknown`. However,
|
||||
`==` and `!=` are exceptions and can still provide definite results.
|
||||
|
||||
```py
|
||||
a = (1, 2)
|
||||
b = (1, "hello")
|
||||
|
||||
# TODO: should be Literal[False], once we implement (in)equality for mismatched literals
|
||||
reveal_type(a == b) # revealed: bool
|
||||
|
||||
# TODO: should be Literal[True], once we implement (in)equality for mismatched literals
|
||||
reveal_type(a != b) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a < b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a <= b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a > b) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
reveal_type(a >= b) # revealed: Unknown
|
||||
```
|
||||
|
||||
However, if the lexicographic comparison completes without reaching a point where str and int are
|
||||
compared, Python will still produce a result based on the prior elements.
|
||||
|
||||
```py
|
||||
a = (1, 2)
|
||||
b = (999999, "hello")
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
#### Matryoshka Tuples
|
||||
|
||||
```py
|
||||
a = (1, True, "Hello")
|
||||
b = (a, a, a)
|
||||
c = (b, b, b)
|
||||
|
||||
reveal_type(c == c) # revealed: Literal[True]
|
||||
reveal_type(c != c) # revealed: Literal[False]
|
||||
reveal_type(c < c) # revealed: Literal[False]
|
||||
reveal_type(c <= c) # revealed: Literal[True]
|
||||
reveal_type(c > c) # revealed: Literal[False]
|
||||
reveal_type(c >= c) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
#### Non Boolean Rich Comparisons
|
||||
|
||||
Rich comparison methods defined in a class affect tuple comparisons as well. Proper type inference
|
||||
should be possible even in cases where these methods return non-boolean types.
|
||||
|
||||
Note: Tuples use lexicographic comparisons. If the `==` result for all paired elements in the tuple
|
||||
is True, the comparison then considers the tuple’s length. Regardless of the return type of the
|
||||
dunder methods, the final result can still be a boolean value.
|
||||
|
||||
(+cpython: For tuples, `==` and `!=` always produce boolean results, regardless of the return type
|
||||
of the dunder methods.)
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class EqReturnType: ...
|
||||
class NeReturnType: ...
|
||||
class LtReturnType: ...
|
||||
class LeReturnType: ...
|
||||
class GtReturnType: ...
|
||||
class GeReturnType: ...
|
||||
|
||||
class A:
|
||||
def __eq__(self, o: object) -> EqReturnType:
|
||||
return EqReturnType()
|
||||
|
||||
def __ne__(self, o: object) -> NeReturnType:
|
||||
return NeReturnType()
|
||||
|
||||
def __lt__(self, o: A) -> LtReturnType:
|
||||
return LtReturnType()
|
||||
|
||||
def __le__(self, o: A) -> LeReturnType:
|
||||
return LeReturnType()
|
||||
|
||||
def __gt__(self, o: A) -> GtReturnType:
|
||||
return GtReturnType()
|
||||
|
||||
def __ge__(self, o: A) -> GeReturnType:
|
||||
return GeReturnType()
|
||||
|
||||
a = (A(), A())
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: LtReturnType | Literal[False]
|
||||
reveal_type(a <= a) # revealed: LeReturnType | Literal[True]
|
||||
reveal_type(a > a) # revealed: GtReturnType | Literal[False]
|
||||
reveal_type(a >= a) # revealed: GeReturnType | Literal[True]
|
||||
|
||||
# If lexicographic comparison is finished before comparing A()
|
||||
b = ("1_foo", A())
|
||||
c = ("2_bar", A())
|
||||
|
||||
reveal_type(b == c) # revealed: Literal[False]
|
||||
reveal_type(b != c) # revealed: Literal[True]
|
||||
reveal_type(b < c) # revealed: Literal[True]
|
||||
reveal_type(b <= c) # revealed: Literal[True]
|
||||
reveal_type(b > c) # revealed: Literal[False]
|
||||
reveal_type(b >= c) # revealed: Literal[False]
|
||||
|
||||
class LtReturnTypeOnB: ...
|
||||
|
||||
class B:
|
||||
def __lt__(self, o: B) -> LtReturnTypeOnB:
|
||||
return LtReturnTypeOnB()
|
||||
|
||||
reveal_type((A(), B()) < (A(), B())) # revealed: LtReturnType | LtReturnTypeOnB | Literal[False]
|
||||
```
|
||||
|
||||
#### Special Handling of Eq and NotEq in Lexicographic Comparisons
|
||||
|
||||
> Example: `(<int instance>, "foo") == (<int instance>, "bar")`
|
||||
|
||||
`Eq` and `NotEq` have unique behavior compared to other operators in lexicographic comparisons.
|
||||
Specifically, for `Eq`, if any non-equal pair exists within the tuples being compared, we can
|
||||
immediately conclude that the tuples are not equal. Conversely, for `NotEq`, if any non-equal pair
|
||||
exists, we can determine that the tuples are unequal.
|
||||
|
||||
In contrast, with operators like `<` and `>`, the comparison must consider each pair of elements
|
||||
sequentially, and the final outcome might remain ambiguous until all pairs are compared.
|
||||
|
||||
```py
|
||||
def _(x: str, y: int):
|
||||
reveal_type("foo" == "bar") # revealed: Literal[False]
|
||||
reveal_type(("foo",) == ("bar",)) # revealed: Literal[False]
|
||||
reveal_type((4, "foo") == (4, "bar")) # revealed: Literal[False]
|
||||
reveal_type((y, "foo") == (y, "bar")) # revealed: Literal[False]
|
||||
|
||||
a = (x, y, "foo")
|
||||
|
||||
reveal_type(a == a) # revealed: bool
|
||||
reveal_type(a != a) # revealed: bool
|
||||
reveal_type(a < a) # revealed: bool
|
||||
reveal_type(a <= a) # revealed: bool
|
||||
reveal_type(a > a) # revealed: bool
|
||||
reveal_type(a >= a) # revealed: bool
|
||||
|
||||
b = (x, y, "bar")
|
||||
|
||||
reveal_type(a == b) # revealed: Literal[False]
|
||||
reveal_type(a != b) # revealed: Literal[True]
|
||||
reveal_type(a < b) # revealed: bool
|
||||
reveal_type(a <= b) # revealed: bool
|
||||
reveal_type(a > b) # revealed: bool
|
||||
reveal_type(a >= b) # revealed: bool
|
||||
|
||||
c = (x, y, "foo", "different_length")
|
||||
|
||||
reveal_type(a == c) # revealed: Literal[False]
|
||||
reveal_type(a != c) # revealed: Literal[True]
|
||||
reveal_type(a < c) # revealed: bool
|
||||
reveal_type(a <= c) # revealed: bool
|
||||
reveal_type(a > c) # revealed: bool
|
||||
reveal_type(a >= c) # revealed: bool
|
||||
```
|
||||
|
||||
#### Error Propagation
|
||||
|
||||
Errors occurring within a tuple comparison should propagate outward. However, if the tuple
|
||||
comparison can clearly conclude before encountering an error, the error should not be raised.
|
||||
|
||||
```py
|
||||
def _(n: int, s: str):
|
||||
class A: ...
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`"
|
||||
A() < A()
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`"
|
||||
A() <= A()
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`"
|
||||
A() > A()
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`"
|
||||
A() >= A()
|
||||
|
||||
a = (0, n, A())
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a < a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `<=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a <= a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a > a) # revealed: Unknown
|
||||
# error: [unsupported-operator] "Operator `>=` is not supported for types `A` and `A`, in comparing `tuple[Literal[0], int, A]` with `tuple[Literal[0], int, A]`"
|
||||
reveal_type(a >= a) # revealed: Unknown
|
||||
|
||||
# Comparison between `a` and `b` should only involve the first elements, `Literal[0]` and `Literal[99999]`,
|
||||
# and should terminate immediately.
|
||||
b = (99999, n, A())
|
||||
|
||||
reveal_type(a < b) # revealed: Literal[True]
|
||||
reveal_type(a <= b) # revealed: Literal[True]
|
||||
reveal_type(a > b) # revealed: Literal[False]
|
||||
reveal_type(a >= b) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
### Membership Test Comparisons
|
||||
|
||||
"Membership Test Comparisons" refers to the operators `in` and `not in`.
|
||||
|
||||
```py
|
||||
def _(n: int):
|
||||
a = (1, 2)
|
||||
b = ((3, 4), (1, 2))
|
||||
c = ((1, 2, 3), (4, 5, 6))
|
||||
d = ((n, n), (n, n))
|
||||
|
||||
reveal_type(a in b) # revealed: Literal[True]
|
||||
reveal_type(a not in b) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a in c) # revealed: Literal[False]
|
||||
reveal_type(a not in c) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a in d) # revealed: bool
|
||||
reveal_type(a not in d) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity Comparisons
|
||||
|
||||
"Identity Comparisons" refers to `is` and `is not`.
|
||||
|
||||
```py
|
||||
a = (1, 2)
|
||||
b = ("a", "b")
|
||||
c = (1, 2, 3)
|
||||
|
||||
reveal_type(a is (1, 2)) # revealed: bool
|
||||
reveal_type(a is not (1, 2)) # revealed: bool
|
||||
|
||||
# TODO should be Literal[False] once we implement comparison of mismatched literal types
|
||||
reveal_type(a is b) # revealed: bool
|
||||
# TODO should be Literal[True] once we implement comparison of mismatched literal types
|
||||
reveal_type(a is not b) # revealed: bool
|
||||
|
||||
reveal_type(a is c) # revealed: Literal[False]
|
||||
reveal_type(a is not c) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Homogeneous
|
||||
|
||||
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
|
||||
|
||||
// TODO
|
||||
|
||||
## Chained comparisons with elements that incorrectly implement `__bool__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
For an operation `A() < A()` to succeed at runtime, the `A.__lt__` method does not necessarily need
|
||||
to return an object that is convertible to a `bool`. However, the return type _does_ need to be
|
||||
convertible to a `bool` for the operation `A() < A() < A()` (a _chained_ comparison) to succeed.
|
||||
This is because `A() < A() < A()` desugars to something like this, which involves several implicit
|
||||
conversions to `bool`:
|
||||
|
||||
```ignore
|
||||
def compute_chained_comparison():
|
||||
a1 = A()
|
||||
a2 = A()
|
||||
first_comparison = a1 < a2
|
||||
return first_comparison and (a2 < A())
|
||||
```
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 5
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
def __gt__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
a = (1, Comparable())
|
||||
b = (1, Comparable())
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
a < b < b
|
||||
|
||||
a < b # fine
|
||||
```
|
||||
|
||||
## Equality with elements that incorrectly implement `__bool__`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Python does not generally attempt to coerce the result of `==` and `!=` operations between two
|
||||
arbitrary objects to a `bool`, but a comparison of tuples will fail if the result of comparing any
|
||||
pair of elements at equivalent positions cannot be converted to a `bool`:
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
class A:
|
||||
def __eq__(self, other) -> NotBoolable:
|
||||
return NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
(A(),) == (A(),)
|
||||
```
|
|
@ -0,0 +1,79 @@
|
|||
# Comparison: Unions
|
||||
|
||||
## Union on one side of the comparison
|
||||
|
||||
Comparisons on union types need to consider all possible cases:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
one_or_two = 1 if flag else 2
|
||||
|
||||
reveal_type(one_or_two <= 2) # revealed: Literal[True]
|
||||
reveal_type(one_or_two <= 1) # revealed: bool
|
||||
reveal_type(one_or_two <= 0) # revealed: Literal[False]
|
||||
|
||||
reveal_type(2 >= one_or_two) # revealed: Literal[True]
|
||||
reveal_type(1 >= one_or_two) # revealed: bool
|
||||
reveal_type(0 >= one_or_two) # revealed: Literal[False]
|
||||
|
||||
reveal_type(one_or_two < 1) # revealed: Literal[False]
|
||||
reveal_type(one_or_two < 2) # revealed: bool
|
||||
reveal_type(one_or_two < 3) # revealed: Literal[True]
|
||||
|
||||
reveal_type(one_or_two > 0) # revealed: Literal[True]
|
||||
reveal_type(one_or_two > 1) # revealed: bool
|
||||
reveal_type(one_or_two > 2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(one_or_two == 3) # revealed: Literal[False]
|
||||
reveal_type(one_or_two == 1) # revealed: bool
|
||||
|
||||
reveal_type(one_or_two != 3) # revealed: Literal[True]
|
||||
reveal_type(one_or_two != 1) # revealed: bool
|
||||
|
||||
a_or_ab = "a" if flag else "ab"
|
||||
|
||||
reveal_type(a_or_ab in "ab") # revealed: Literal[True]
|
||||
reveal_type("a" in a_or_ab) # revealed: Literal[True]
|
||||
|
||||
reveal_type("c" not in a_or_ab) # revealed: Literal[True]
|
||||
reveal_type("a" not in a_or_ab) # revealed: Literal[False]
|
||||
|
||||
reveal_type("b" in a_or_ab) # revealed: bool
|
||||
reveal_type("b" not in a_or_ab) # revealed: bool
|
||||
|
||||
one_or_none = 1 if flag else None
|
||||
|
||||
reveal_type(one_or_none is None) # revealed: bool
|
||||
reveal_type(one_or_none is not None) # revealed: bool
|
||||
```
|
||||
|
||||
## Union on both sides of the comparison
|
||||
|
||||
With unions on both sides, we need to consider the full cross product of options when building the
|
||||
resulting (union) type:
|
||||
|
||||
```py
|
||||
def _(flag_s: bool, flag_l: bool):
|
||||
small = 1 if flag_s else 2
|
||||
large = 2 if flag_l else 3
|
||||
|
||||
reveal_type(small <= large) # revealed: Literal[True]
|
||||
reveal_type(small >= large) # revealed: bool
|
||||
|
||||
reveal_type(small < large) # revealed: bool
|
||||
reveal_type(small > large) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Unsupported operations
|
||||
|
||||
Make sure we emit a diagnostic if *any* of the possible comparisons is unsupported. For now, we fall
|
||||
back to `bool` for the result type instead of trying to infer something more precise from the other
|
||||
(supported) variants:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = [1, 2] if flag else 1
|
||||
|
||||
result = 1 in x # error: "Operator `in` is not supported"
|
||||
reveal_type(result) # revealed: bool
|
||||
```
|
|
@ -0,0 +1,32 @@
|
|||
# Comparison: Unsupported operators
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
a = 1 in 7 # error: "Operator `in` is not supported for types `Literal[1]` and `Literal[7]`"
|
||||
reveal_type(a) # revealed: bool
|
||||
|
||||
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
|
||||
reveal_type(b) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `object` and `int`, in comparing `object` with `Literal[5]`"
|
||||
c = object() < 5
|
||||
reveal_type(c) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `object`, in comparing `Literal[5]` with `object`"
|
||||
d = 5 < object()
|
||||
reveal_type(d) # revealed: Unknown
|
||||
|
||||
int_literal_or_str_literal = 1 if flag else "foo"
|
||||
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
|
||||
e = 42 in int_literal_or_str_literal
|
||||
reveal_type(e) # revealed: bool
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
|
||||
f = (1, 2) < (1, "hello")
|
||||
reveal_type(f) # revealed: Unknown
|
||||
|
||||
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
|
||||
g = (flag1, A()) < (flag2, A())
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,152 @@
|
|||
# Comprehensions
|
||||
|
||||
## Basic comprehensions
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# revealed: int
|
||||
[reveal_type(x) for x in IntIterable()]
|
||||
|
||||
class IteratorOfIterables:
|
||||
def __next__(self) -> IntIterable:
|
||||
return IntIterable()
|
||||
|
||||
class IterableOfIterables:
|
||||
def __iter__(self) -> IteratorOfIterables:
|
||||
return IteratorOfIterables()
|
||||
|
||||
# revealed: tuple[int, IntIterable]
|
||||
[reveal_type((x, y)) for y in IterableOfIterables() for x in y]
|
||||
|
||||
# revealed: int
|
||||
{reveal_type(x): 0 for x in IntIterable()}
|
||||
|
||||
# revealed: int
|
||||
{0: reveal_type(x) for x in IntIterable()}
|
||||
```
|
||||
|
||||
## Nested comprehension
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# revealed: tuple[int, int]
|
||||
[[reveal_type((x, y)) for x in IntIterable()] for y in IntIterable()]
|
||||
```
|
||||
|
||||
## Comprehension referencing outer comprehension
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
class IteratorOfIterables:
|
||||
def __next__(self) -> IntIterable:
|
||||
return IntIterable()
|
||||
|
||||
class IterableOfIterables:
|
||||
def __iter__(self) -> IteratorOfIterables:
|
||||
return IteratorOfIterables()
|
||||
|
||||
# revealed: tuple[int, IntIterable]
|
||||
[[reveal_type((x, y)) for x in y] for y in IterableOfIterables()]
|
||||
```
|
||||
|
||||
## Comprehension with unbound iterable
|
||||
|
||||
Iterating over an unbound iterable yields `Unknown`:
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference] "Name `x` used when not defined"
|
||||
# revealed: Unknown
|
||||
[reveal_type(z) for z in x]
|
||||
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# error: [not-iterable] "Object of type `int` is not iterable"
|
||||
# revealed: tuple[int, Unknown]
|
||||
[reveal_type((x, z)) for x in IntIterable() for z in x]
|
||||
```
|
||||
|
||||
## Starred expressions
|
||||
|
||||
Starred expressions must be iterable
|
||||
|
||||
```py
|
||||
class NotIterable: ...
|
||||
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
# This is fine:
|
||||
x = [*Iterable()]
|
||||
|
||||
# error: [not-iterable] "Object of type `NotIterable` is not iterable"
|
||||
y = [*NotIterable()]
|
||||
```
|
||||
|
||||
## Async comprehensions
|
||||
|
||||
### Basic
|
||||
|
||||
```py
|
||||
class AsyncIterator:
|
||||
async def __anext__(self) -> int:
|
||||
return 42
|
||||
|
||||
class AsyncIterable:
|
||||
def __aiter__(self) -> AsyncIterator:
|
||||
return AsyncIterator()
|
||||
|
||||
async def _():
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
[reveal_type(x) async for x in AsyncIterable()]
|
||||
```
|
||||
|
||||
### Invalid async comprehension
|
||||
|
||||
This tests that we understand that `async` comprehensions do *not* work according to the synchronous
|
||||
iteration protocol
|
||||
|
||||
```py
|
||||
class Iterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
return Iterator()
|
||||
|
||||
async def _():
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
[reveal_type(x) async for x in Iterable()]
|
||||
```
|
|
@ -0,0 +1,43 @@
|
|||
# Comprehensions with invalid syntax
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 42
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
# Missing 'in' keyword.
|
||||
|
||||
# It's reasonably clear here what they *meant* to write,
|
||||
# so we'll still infer the correct type:
|
||||
|
||||
# error: [invalid-syntax] "Expected 'in', found name"
|
||||
# revealed: int
|
||||
[reveal_type(a) for a IntIterable()]
|
||||
|
||||
|
||||
# Missing iteration variable
|
||||
|
||||
# error: [invalid-syntax] "Expected an identifier, but found a keyword 'in' that cannot be used here"
|
||||
# error: [invalid-syntax] "Expected 'in', found name"
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
[reveal_type(b) for in IntIterable()]
|
||||
|
||||
|
||||
# Missing iterable
|
||||
|
||||
# error: [invalid-syntax] "Expected an expression"
|
||||
# revealed: Unknown
|
||||
[reveal_type(c) for c in]
|
||||
|
||||
|
||||
# Missing 'in' keyword and missing iterable
|
||||
|
||||
# error: [invalid-syntax] "Expected 'in', found ']'"
|
||||
# revealed: Unknown
|
||||
[reveal_type(d) for d]
|
||||
```
|
|
@ -0,0 +1,47 @@
|
|||
# If expressions
|
||||
|
||||
## Simple if-expression
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else 2
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## If-expression with walrus operator
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
y = 0
|
||||
z = 0
|
||||
x = (y := 1) if flag else (z := 2)
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
reveal_type(z) # revealed: Literal[0, 2]
|
||||
```
|
||||
|
||||
## Nested if-expression
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
x = 1 if flag else 2 if flag2 else 3
|
||||
reveal_type(x) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## None
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else None
|
||||
reveal_type(x) # revealed: Literal[1] | None
|
||||
```
|
||||
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
3 if NotBoolable() else 4
|
||||
```
|
|
@ -0,0 +1,163 @@
|
|||
# If statements
|
||||
|
||||
## Simple if
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Simple if-elif-else
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
else:
|
||||
r = y
|
||||
y = 5
|
||||
s = y
|
||||
x = y
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
|
||||
# revealed: Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
|
||||
# revealed: Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
```
|
||||
|
||||
## Single symbol across if-elif-else
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
||||
|
||||
## if-elif-else without else assignment
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
|
||||
if flag:
|
||||
y = 1
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## if-elif-else with intervening assignment
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
|
||||
if flag:
|
||||
y = 1
|
||||
z = 3
|
||||
elif flag2:
|
||||
y = 2
|
||||
else:
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1, 2]
|
||||
```
|
||||
|
||||
## Nested if statement
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 0
|
||||
|
||||
if flag:
|
||||
if flag2:
|
||||
y = 1
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## if-elif without else
|
||||
|
||||
```py
|
||||
def _(flag: bool, flag2: bool):
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
if flag:
|
||||
y = 3
|
||||
elif flag2:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## if-elif with assignment expressions in tests
|
||||
|
||||
```py
|
||||
def check(x: int) -> bool:
|
||||
return bool(x)
|
||||
|
||||
if check(x := 1):
|
||||
x = 2
|
||||
elif check(x := 3):
|
||||
x = 4
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## constraints apply to later test expressions
|
||||
|
||||
```py
|
||||
def check(x) -> bool:
|
||||
return bool(x)
|
||||
|
||||
def _(flag: bool):
|
||||
x = 1 if flag else None
|
||||
y = 0
|
||||
|
||||
if x is None:
|
||||
pass
|
||||
elif check(y := x):
|
||||
pass
|
||||
|
||||
reveal_type(y) # revealed: Literal[0, 1]
|
||||
```
|
||||
|
||||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
if NotBoolable():
|
||||
...
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
elif NotBoolable():
|
||||
...
|
||||
```
|
302
crates/ty_python_semantic/resources/mdtest/conditional/match.md
Normal file
302
crates/ty_python_semantic/resources/mdtest/conditional/match.md
Normal file
|
@ -0,0 +1,302 @@
|
|||
# Pattern matching
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
## With wildcard
|
||||
|
||||
```py
|
||||
def _(target: int):
|
||||
match target:
|
||||
case 1:
|
||||
y = 2
|
||||
case _:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
## Without wildcard
|
||||
|
||||
```py
|
||||
def _(target: int):
|
||||
match target:
|
||||
case 1:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
# revealed: Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
||||
## Basic match
|
||||
|
||||
```py
|
||||
def _(target: int):
|
||||
y = 1
|
||||
y = 2
|
||||
|
||||
match target:
|
||||
case 1:
|
||||
y = 3
|
||||
case 2:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## Value match
|
||||
|
||||
A value pattern matches based on equality: the first `case` branch here will be taken if `subject`
|
||||
is equal to `2`, even if `subject` is not an instance of `int`. We can't know whether `C` here has a
|
||||
custom `__eq__` implementation that might cause it to compare equal to `2`, so we have to consider
|
||||
the possibility that the `case` branch might be taken even though the type `C` is disjoint from the
|
||||
type `Literal[2]`.
|
||||
|
||||
This leads us to infer `Literal[1, 3]` as the type of `y` after the `match` statement, rather than
|
||||
`Literal[1]`:
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class C:
|
||||
pass
|
||||
|
||||
def _(subject: C):
|
||||
y = 1
|
||||
match subject:
|
||||
case 2:
|
||||
y = 3
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## Class match
|
||||
|
||||
A `case` branch with a class pattern is taken if the subject is an instance of the given class, and
|
||||
all subpatterns in the class pattern match.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
class FooSub(Foo):
|
||||
pass
|
||||
|
||||
class Bar:
|
||||
pass
|
||||
|
||||
@final
|
||||
class Baz:
|
||||
pass
|
||||
|
||||
def _(target: FooSub):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Foo():
|
||||
y = 3
|
||||
case Bar():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[3]
|
||||
|
||||
def _(target: FooSub):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Bar():
|
||||
y = 3
|
||||
case Foo():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[3, 4]
|
||||
|
||||
def _(target: FooSub | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz():
|
||||
y = 2
|
||||
case Foo():
|
||||
y = 3
|
||||
case Bar():
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 3, 4]
|
||||
```
|
||||
|
||||
## Singleton match
|
||||
|
||||
Singleton patterns are matched based on identity, not equality comparisons or `isinstance()` checks.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(target: Literal[True, False]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: bool):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[4]
|
||||
|
||||
def _(target: None | Literal[True]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
# TODO: with exhaustiveness checking, this should be Literal[2, 4]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
|
||||
# bool is an int subclass
|
||||
def _(target: int):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case True:
|
||||
y = 2
|
||||
case False:
|
||||
y = 3
|
||||
case None:
|
||||
y = 4
|
||||
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Or match
|
||||
|
||||
A `|` pattern matches if any of the subpatterns match.
|
||||
|
||||
```py
|
||||
from typing import Literal, final
|
||||
|
||||
def _(target: Literal["foo", "baz"]):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case "foo" | "bar":
|
||||
y = 2
|
||||
case "baz":
|
||||
y = 3
|
||||
|
||||
# TODO: with exhaustiveness, this should be Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
|
||||
def _(target: None):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case None | 3:
|
||||
y = 2
|
||||
case "foo" | 4 | True:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[2]
|
||||
|
||||
@final
|
||||
class Baz:
|
||||
pass
|
||||
|
||||
def _(target: int | None | float):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case None | 3:
|
||||
y = 2
|
||||
case Baz():
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
|
||||
def _(target: None | str):
|
||||
y = 1
|
||||
|
||||
match target:
|
||||
case Baz() | True | False:
|
||||
y = 2
|
||||
case int():
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 3]
|
||||
```
|
||||
|
||||
## Guard with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
def _(target: int, flag: NotBoolable):
|
||||
y = 1
|
||||
match target:
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
case 1 if flag:
|
||||
y = 2
|
||||
case 2:
|
||||
y = 3
|
||||
|
||||
reveal_type(y) # revealed: Literal[1, 2, 3]
|
||||
```
|
|
@ -0,0 +1,293 @@
|
|||
# `typing.dataclass_transform`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`dataclass_transform` is a decorator that can be used to let type checkers know that a function,
|
||||
class, or metaclass is a `dataclass`-like construct.
|
||||
|
||||
## Basic example
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T]) -> type[T]:
|
||||
# modify cls
|
||||
return cls
|
||||
|
||||
@my_dataclass
|
||||
class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
Person("Alice", 20)
|
||||
Person("Bob", None)
|
||||
Person("Bob")
|
||||
|
||||
# error: [missing-argument]
|
||||
Person()
|
||||
```
|
||||
|
||||
## Decorating decorators that take parameters themselves
|
||||
|
||||
If we want our `dataclass`-like decorator to also take parameters, that is also possible:
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Callable
|
||||
|
||||
@dataclass_transform()
|
||||
def versioned_class[T](*, version: int = 1):
|
||||
def decorator(cls):
|
||||
# modify cls
|
||||
return cls
|
||||
return decorator
|
||||
|
||||
@versioned_class(version=2)
|
||||
class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
Person("Alice", 20)
|
||||
|
||||
# error: [missing-argument]
|
||||
Person()
|
||||
```
|
||||
|
||||
We properly type-check the arguments to the decorator:
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Callable
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
@versioned_class(version="a string")
|
||||
class C:
|
||||
name: str
|
||||
```
|
||||
|
||||
## Types of decorators
|
||||
|
||||
The examples from this section are straight from the Python documentation on
|
||||
[`typing.dataclass_transform`].
|
||||
|
||||
### Decorating a decorator function
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def create_model[T](cls: type[T]) -> type[T]:
|
||||
...
|
||||
return cls
|
||||
|
||||
@create_model
|
||||
class CustomerModel:
|
||||
id: int
|
||||
name: str
|
||||
|
||||
CustomerModel(id=1, name="Test")
|
||||
```
|
||||
|
||||
### Decorating a metaclass
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
class ModelMeta(type): ...
|
||||
|
||||
class ModelBase(metaclass=ModelMeta): ...
|
||||
|
||||
class CustomerModel(ModelBase):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
CustomerModel(id=1, name="Test")
|
||||
|
||||
# error: [missing-argument]
|
||||
CustomerModel()
|
||||
```
|
||||
|
||||
### Decorating a base class
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
class ModelBase: ...
|
||||
|
||||
class CustomerModel(ModelBase):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
# TODO: this is not supported yet
|
||||
# error: [unknown-argument]
|
||||
# error: [unknown-argument]
|
||||
CustomerModel(id=1, name="Test")
|
||||
```
|
||||
|
||||
## Arguments to `dataclass_transform`
|
||||
|
||||
### `eq_default`
|
||||
|
||||
`eq=True/False` does not have a observable effect (apart from a minor change regarding whether
|
||||
`other` is positional-only or not, which is not modelled at the moment).
|
||||
|
||||
### `order_default`
|
||||
|
||||
The `order_default` argument controls whether methods such as `__lt__` are generated by default.
|
||||
This can be overwritten using the `order` argument to the custom decorator:
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def normal(*, order: bool = False):
|
||||
raise NotImplementedError
|
||||
|
||||
@dataclass_transform(order_default=False)
|
||||
def order_default_false(*, order: bool = False):
|
||||
raise NotImplementedError
|
||||
|
||||
@dataclass_transform(order_default=True)
|
||||
def order_default_true(*, order: bool = True):
|
||||
raise NotImplementedError
|
||||
|
||||
@normal
|
||||
class Normal:
|
||||
inner: int
|
||||
|
||||
Normal(1) < Normal(2) # error: [unsupported-operator]
|
||||
|
||||
@normal(order=True)
|
||||
class NormalOverwritten:
|
||||
inner: int
|
||||
|
||||
NormalOverwritten(1) < NormalOverwritten(2)
|
||||
|
||||
@order_default_false
|
||||
class OrderFalse:
|
||||
inner: int
|
||||
|
||||
OrderFalse(1) < OrderFalse(2) # error: [unsupported-operator]
|
||||
|
||||
@order_default_false(order=True)
|
||||
class OrderFalseOverwritten:
|
||||
inner: int
|
||||
|
||||
OrderFalseOverwritten(1) < OrderFalseOverwritten(2)
|
||||
|
||||
@order_default_true
|
||||
class OrderTrue:
|
||||
inner: int
|
||||
|
||||
OrderTrue(1) < OrderTrue(2)
|
||||
|
||||
@order_default_true(order=False)
|
||||
class OrderTrueOverwritten:
|
||||
inner: int
|
||||
|
||||
# error: [unsupported-operator]
|
||||
OrderTrueOverwritten(1) < OrderTrueOverwritten(2)
|
||||
```
|
||||
|
||||
### `kw_only_default`
|
||||
|
||||
To do
|
||||
|
||||
### `field_specifiers`
|
||||
|
||||
To do
|
||||
|
||||
## Overloaded dataclass-like decorators
|
||||
|
||||
In the case of an overloaded decorator, the `dataclass_transform` decorator can be applied to the
|
||||
implementation, or to *one* of the overloads.
|
||||
|
||||
### Applying `dataclass_transform` to the implementation
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
|
||||
|
||||
T = TypeVar("T", bound=type)
|
||||
|
||||
@overload
|
||||
def versioned_class(
|
||||
cls: T,
|
||||
*,
|
||||
version: int = 1,
|
||||
) -> T: ...
|
||||
@overload
|
||||
def versioned_class(
|
||||
*,
|
||||
version: int = 1,
|
||||
) -> Callable[[T], T]: ...
|
||||
@dataclass_transform()
|
||||
def versioned_class(
|
||||
cls: T | None = None,
|
||||
*,
|
||||
version: int = 1,
|
||||
) -> T | Callable[[T], T]:
|
||||
raise NotImplementedError
|
||||
|
||||
@versioned_class
|
||||
class D1:
|
||||
x: str
|
||||
|
||||
@versioned_class(version=2)
|
||||
class D2:
|
||||
x: str
|
||||
|
||||
D1("a")
|
||||
D2("a")
|
||||
|
||||
D1(1.2) # error: [invalid-argument-type]
|
||||
D2(1.2) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Applying `dataclass_transform` to an overload
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, TypeVar, Callable, overload
|
||||
|
||||
T = TypeVar("T", bound=type)
|
||||
|
||||
@overload
|
||||
@dataclass_transform()
|
||||
def versioned_class(
|
||||
cls: T,
|
||||
*,
|
||||
version: int = 1,
|
||||
) -> T: ...
|
||||
@overload
|
||||
def versioned_class(
|
||||
*,
|
||||
version: int = 1,
|
||||
) -> Callable[[T], T]: ...
|
||||
def versioned_class(
|
||||
cls: T | None = None,
|
||||
*,
|
||||
version: int = 1,
|
||||
) -> T | Callable[[T], T]:
|
||||
raise NotImplementedError
|
||||
|
||||
@versioned_class
|
||||
class D1:
|
||||
x: str
|
||||
|
||||
@versioned_class(version=2)
|
||||
class D2:
|
||||
x: str
|
||||
|
||||
# TODO: these should not be errors
|
||||
D1("a") # error: [too-many-positional-arguments]
|
||||
D2("a") # error: [too-many-positional-arguments]
|
||||
|
||||
# TODO: these should be invalid-argument-type errors
|
||||
D1(1.2) # error: [too-many-positional-arguments]
|
||||
D2(1.2) # error: [too-many-positional-arguments]
|
||||
```
|
||||
|
||||
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
731
crates/ty_python_semantic/resources/mdtest/dataclasses.md
Normal file
731
crates/ty_python_semantic/resources/mdtest/dataclasses.md
Normal file
|
@ -0,0 +1,731 @@
|
|||
# Dataclasses
|
||||
|
||||
## Basic
|
||||
|
||||
Decorating a class with `@dataclass` is a convenient way to add special methods such as `__init__`,
|
||||
`__repr__`, and `__eq__` to a class. The following example shows the basic usage of the `@dataclass`
|
||||
decorator. By default, only the three mentioned methods are generated.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
alice1 = Person("Alice", 30)
|
||||
alice2 = Person(name="Alice", age=30)
|
||||
alice3 = Person(age=30, name="Alice")
|
||||
alice4 = Person("Alice", age=30)
|
||||
|
||||
reveal_type(alice1) # revealed: Person
|
||||
reveal_type(type(alice1)) # revealed: type[Person]
|
||||
|
||||
reveal_type(alice1.name) # revealed: str
|
||||
reveal_type(alice1.age) # revealed: int | None
|
||||
|
||||
reveal_type(repr(alice1)) # revealed: str
|
||||
|
||||
reveal_type(alice1 == alice2) # revealed: bool
|
||||
reveal_type(alice1 == "Alice") # revealed: bool
|
||||
|
||||
bob = Person("Bob")
|
||||
bob2 = Person("Bob", None)
|
||||
bob3 = Person(name="Bob")
|
||||
bob4 = Person(name="Bob", age=None)
|
||||
```
|
||||
|
||||
The signature of the `__init__` method is generated based on the classes attributes. The following
|
||||
calls are not valid:
|
||||
|
||||
```py
|
||||
# error: [missing-argument]
|
||||
Person()
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
Person("Eve", 20, "too many arguments")
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
Person("Eve", "string instead of int")
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
Person(20, "Eve")
|
||||
```
|
||||
|
||||
## Signature of `__init__`
|
||||
|
||||
TODO: All of the following tests are missing the `self` argument in the `__init__` signature.
|
||||
|
||||
Declarations in the class body are used to generate the signature of the `__init__` method. If the
|
||||
attributes are not just declarations, but also bindings, the type inferred from bindings is used as
|
||||
the default value.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
y: str = "default"
|
||||
z: int | None = 1 + 2
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int, y: str = Literal["default"], z: int | None = Literal[3]) -> None
|
||||
```
|
||||
|
||||
This also works if the declaration and binding are split:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int | None
|
||||
x = None
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int | None = None) -> None
|
||||
```
|
||||
|
||||
Non-fully static types are handled correctly:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
x: Any
|
||||
y: int | Any
|
||||
z: tuple[int, Any]
|
||||
|
||||
reveal_type(C.__init__) # revealed: (x: Any, y: int | Any, z: tuple[int, Any]) -> None
|
||||
```
|
||||
|
||||
Variables without annotations are ignored:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
y = 1
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||
```
|
||||
|
||||
If attributes without default values are declared after attributes with default values, a
|
||||
`TypeError` will be raised at runtime. Ideally, we would emit a diagnostic in that case:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int = 1
|
||||
# TODO: this should be an error: field without default defined after field with default
|
||||
y: str
|
||||
```
|
||||
|
||||
Pure class attributes (`ClassVar`) are not included in the signature of `__init__`:
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
y: ClassVar[str] = "default"
|
||||
z: bool
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int, z: bool) -> None
|
||||
|
||||
d = D(1, True)
|
||||
reveal_type(d.x) # revealed: int
|
||||
reveal_type(d.y) # revealed: str
|
||||
reveal_type(d.z) # revealed: bool
|
||||
```
|
||||
|
||||
Function declarations do not affect the signature of `__init__`:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
|
||||
def y(self) -> str:
|
||||
return ""
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||
```
|
||||
|
||||
And neither do nested class declarations:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
|
||||
class Nested:
|
||||
y: str
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||
```
|
||||
|
||||
But if there is a variable annotation with a function or class literal type, the signature of
|
||||
`__init__` will include this field:
|
||||
|
||||
```py
|
||||
from ty_extensions import TypeOf
|
||||
|
||||
class SomeClass: ...
|
||||
|
||||
def some_function() -> None: ...
|
||||
@dataclass
|
||||
class D:
|
||||
function_literal: TypeOf[some_function]
|
||||
class_literal: TypeOf[SomeClass]
|
||||
class_subtype_of: type[SomeClass]
|
||||
|
||||
# revealed: (function_literal: def some_function() -> None, class_literal: Literal[SomeClass], class_subtype_of: type[SomeClass]) -> None
|
||||
reveal_type(D.__init__)
|
||||
```
|
||||
|
||||
More realistically, dataclasses can have `Callable` attributes:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
c: Callable[[int], str]
|
||||
|
||||
reveal_type(D.__init__) # revealed: (c: (int, /) -> str) -> None
|
||||
```
|
||||
|
||||
Implicit instance attributes do not affect the signature of `__init__`:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
x: int
|
||||
|
||||
def f(self, y: str) -> None:
|
||||
self.y: str = y
|
||||
|
||||
reveal_type(D(1).y) # revealed: str
|
||||
|
||||
reveal_type(D.__init__) # revealed: (x: int) -> None
|
||||
```
|
||||
|
||||
Annotating expressions does not lead to an entry in `__annotations__` at runtime, and so it wouldn't
|
||||
be included in the signature of `__init__`. This is a case that we currently don't detect:
|
||||
|
||||
```py
|
||||
@dataclass
|
||||
class D:
|
||||
# (x) is an expression, not a "simple name"
|
||||
(x): int = 1
|
||||
|
||||
# TODO: should ideally not include a `x` parameter
|
||||
reveal_type(D.__init__) # revealed: (x: int = Literal[1]) -> None
|
||||
```
|
||||
|
||||
## `@dataclass` calls with arguments
|
||||
|
||||
The `@dataclass` decorator can take several arguments to customize the existence of the generated
|
||||
methods. The following test makes sure that we still treat the class as a dataclass if (the default)
|
||||
arguments are passed in:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(init=True, repr=True, eq=True)
|
||||
class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
alice = Person("Alice", 30)
|
||||
reveal_type(repr(alice)) # revealed: str
|
||||
reveal_type(alice == alice) # revealed: bool
|
||||
```
|
||||
|
||||
If `init` is set to `False`, no `__init__` method is generated:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(init=False)
|
||||
class C:
|
||||
x: int
|
||||
|
||||
C() # Okay
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
C(1)
|
||||
|
||||
repr(C())
|
||||
|
||||
C() == C()
|
||||
```
|
||||
|
||||
## Other dataclass parameters
|
||||
|
||||
### `repr`
|
||||
|
||||
A custom `__repr__` method is generated by default. It can be disabled by passing `repr=False`, but
|
||||
in that case `__repr__` is still available via `object.__repr__`:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(repr=False)
|
||||
class WithoutRepr:
|
||||
x: int
|
||||
|
||||
reveal_type(WithoutRepr(1).__repr__) # revealed: bound method WithoutRepr.__repr__() -> str
|
||||
```
|
||||
|
||||
### `eq`
|
||||
|
||||
The same is true for `__eq__`. Setting `eq=False` disables the generated `__eq__` method, but
|
||||
`__eq__` is still available via `object.__eq__`:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(eq=False)
|
||||
class WithoutEq:
|
||||
x: int
|
||||
|
||||
reveal_type(WithoutEq(1) == WithoutEq(2)) # revealed: bool
|
||||
```
|
||||
|
||||
### `order`
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
`order` is set to `False` by default. If `order=True`, `__lt__`, `__le__`, `__gt__`, and `__ge__`
|
||||
methods will be generated:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class WithoutOrder:
|
||||
x: int
|
||||
|
||||
WithoutOrder(1) < WithoutOrder(2) # error: [unsupported-operator]
|
||||
WithoutOrder(1) <= WithoutOrder(2) # error: [unsupported-operator]
|
||||
WithoutOrder(1) > WithoutOrder(2) # error: [unsupported-operator]
|
||||
WithoutOrder(1) >= WithoutOrder(2) # error: [unsupported-operator]
|
||||
|
||||
@dataclass(order=True)
|
||||
class WithOrder:
|
||||
x: int
|
||||
|
||||
WithOrder(1) < WithOrder(2)
|
||||
WithOrder(1) <= WithOrder(2)
|
||||
WithOrder(1) > WithOrder(2)
|
||||
WithOrder(1) >= WithOrder(2)
|
||||
```
|
||||
|
||||
Comparisons are only allowed for `WithOrder` instances:
|
||||
|
||||
```py
|
||||
WithOrder(1) < 2 # error: [unsupported-operator]
|
||||
WithOrder(1) <= 2 # error: [unsupported-operator]
|
||||
WithOrder(1) > 2 # error: [unsupported-operator]
|
||||
WithOrder(1) >= 2 # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
This also works for generic dataclasses:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(order=True)
|
||||
class GenericWithOrder[T]:
|
||||
x: T
|
||||
|
||||
GenericWithOrder[int](1) < GenericWithOrder[int](1)
|
||||
|
||||
GenericWithOrder[int](1) < GenericWithOrder[str]("a") # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
If a class already defines one of the comparison methods, a `TypeError` is raised at runtime.
|
||||
Ideally, we would emit a diagnostic in that case:
|
||||
|
||||
```py
|
||||
@dataclass(order=True)
|
||||
class AlreadyHasCustomDunderLt:
|
||||
x: int
|
||||
|
||||
# TODO: Ideally, we would emit a diagnostic here
|
||||
def __lt__(self, other: object) -> bool:
|
||||
return False
|
||||
```
|
||||
|
||||
### `unsafe_hash`
|
||||
|
||||
To do
|
||||
|
||||
### `frozen`
|
||||
|
||||
To do
|
||||
|
||||
### `match_args`
|
||||
|
||||
To do
|
||||
|
||||
### `kw_only`
|
||||
|
||||
To do
|
||||
|
||||
### `slots`
|
||||
|
||||
To do
|
||||
|
||||
### `weakref_slot`
|
||||
|
||||
To do
|
||||
|
||||
## Inheritance
|
||||
|
||||
### Normal class inheriting from a dataclass
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
x: int
|
||||
|
||||
class Derived(Base): ...
|
||||
|
||||
d = Derived(1) # OK
|
||||
reveal_type(d.x) # revealed: int
|
||||
```
|
||||
|
||||
### Dataclass inheriting from normal class
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
class Base:
|
||||
x: int = 1
|
||||
|
||||
@dataclass
|
||||
class Derived(Base):
|
||||
y: str
|
||||
|
||||
d = Derived("a")
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
# error: [invalid-argument-type]
|
||||
Derived(1, "a")
|
||||
```
|
||||
|
||||
### Dataclass inheriting from another dataclass
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
x: int
|
||||
y: str
|
||||
|
||||
@dataclass
|
||||
class Derived(Base):
|
||||
z: bool
|
||||
|
||||
d = Derived(1, "a", True) # OK
|
||||
|
||||
reveal_type(d.x) # revealed: int
|
||||
reveal_type(d.y) # revealed: str
|
||||
reveal_type(d.z) # revealed: bool
|
||||
|
||||
# error: [missing-argument]
|
||||
Derived(1, "a")
|
||||
|
||||
# error: [missing-argument]
|
||||
Derived(True)
|
||||
```
|
||||
|
||||
### Overwriting attributes from base class
|
||||
|
||||
The following example comes from the
|
||||
[Python documentation](https://docs.python.org/3/library/dataclasses.html#inheritance). The `x`
|
||||
attribute appears just once in the `__init__` signature, and the default value is taken from the
|
||||
derived class
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
x: Any = 15.0
|
||||
y: int = 0
|
||||
|
||||
@dataclass
|
||||
class C(Base):
|
||||
z: int = 10
|
||||
x: int = 15
|
||||
|
||||
reveal_type(C.__init__) # revealed: (x: int = Literal[15], y: int = Literal[0], z: int = Literal[10]) -> None
|
||||
```
|
||||
|
||||
## Generic dataclasses
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class DataWithDescription[T]:
|
||||
data: T
|
||||
description: str
|
||||
|
||||
reveal_type(DataWithDescription[int]) # revealed: Literal[DataWithDescription[int]]
|
||||
|
||||
d_int = DataWithDescription[int](1, "description") # OK
|
||||
reveal_type(d_int.data) # revealed: int
|
||||
reveal_type(d_int.description) # revealed: str
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
DataWithDescription[int](None, "description")
|
||||
```
|
||||
|
||||
## Descriptor-typed fields
|
||||
|
||||
### Same type in `__get__` and `__set__`
|
||||
|
||||
For the following descriptor, the return type of `__get__` and the type of the `value` parameter in
|
||||
`__set__` are the same. The generated `__init__` method takes an argument of this type (instead of
|
||||
the type of the descriptor), and the default value is also of this type:
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
from dataclasses import dataclass
|
||||
|
||||
class UppercaseString:
|
||||
_value: str = ""
|
||||
|
||||
def __get__(self, instance: object, owner: None | type) -> str:
|
||||
return self._value
|
||||
|
||||
def __set__(self, instance: object, value: str) -> None:
|
||||
self._value = value.upper()
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
upper: UppercaseString = UppercaseString()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (upper: str = str) -> None
|
||||
|
||||
c = C("abc")
|
||||
reveal_type(c.upper) # revealed: str
|
||||
|
||||
# This is also okay:
|
||||
C()
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
C(1)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
C("a", "b")
|
||||
```
|
||||
|
||||
### Different types in `__get__` and `__set__`
|
||||
|
||||
In general, the type of the `__init__` parameter is determined by the `value` parameter type of the
|
||||
`__set__` method (`str` in the example below). However, the default value is generated by calling
|
||||
the descriptor's `__get__` method as if it had been called on the class itself, i.e. passing `None`
|
||||
for the `instance` argument.
|
||||
|
||||
```py
|
||||
from typing import Literal, overload
|
||||
from dataclasses import dataclass
|
||||
|
||||
class ConvertToLength:
|
||||
_len: int = 0
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type) -> Literal[""]: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type | None) -> int: ...
|
||||
def __get__(self, instance: object | None, owner: type | None) -> str | int:
|
||||
if instance is None:
|
||||
return ""
|
||||
|
||||
return self._len
|
||||
|
||||
def __set__(self, instance, value: str) -> None:
|
||||
self._len = len(value)
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
converter: ConvertToLength = ConvertToLength()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (converter: str = Literal[""]) -> None
|
||||
|
||||
c = C("abc")
|
||||
reveal_type(c.converter) # revealed: int
|
||||
|
||||
# This is also okay:
|
||||
C()
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
C(1)
|
||||
|
||||
# error: [too-many-positional-arguments]
|
||||
C("a", "b")
|
||||
```
|
||||
|
||||
### With overloaded `__set__` method
|
||||
|
||||
If the `__set__` method is overloaded, we determine the type for the `__init__` parameter as the
|
||||
union of all possible `value` parameter types:
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
from dataclasses import dataclass
|
||||
|
||||
class AcceptsStrAndInt:
|
||||
def __get__(self, instance, owner) -> int:
|
||||
return 0
|
||||
|
||||
@overload
|
||||
def __set__(self, instance: object, value: str) -> None: ...
|
||||
@overload
|
||||
def __set__(self, instance: object, value: int) -> None: ...
|
||||
def __set__(self, instance: object, value) -> None:
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
field: AcceptsStrAndInt = AcceptsStrAndInt()
|
||||
|
||||
reveal_type(C.__init__) # revealed: (field: str | int = int) -> None
|
||||
```
|
||||
|
||||
## `dataclasses.field`
|
||||
|
||||
To do
|
||||
|
||||
## Other special cases
|
||||
|
||||
### `dataclasses.dataclass`
|
||||
|
||||
We also understand dataclasses if they are decorated with the fully qualified name:
|
||||
|
||||
```py
|
||||
import dataclasses
|
||||
|
||||
@dataclasses.dataclass
|
||||
class C:
|
||||
x: str
|
||||
|
||||
reveal_type(C.__init__) # revealed: (x: str) -> None
|
||||
```
|
||||
|
||||
### Dataclass with custom `__init__` method
|
||||
|
||||
If a class already defines `__init__`, it is not replaced by the `dataclass` decorator.
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(init=True)
|
||||
class C:
|
||||
x: str
|
||||
|
||||
def __init__(self, x: int) -> None:
|
||||
self.x = str(x)
|
||||
|
||||
C(1) # OK
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
C("a")
|
||||
```
|
||||
|
||||
Similarly, if we set `init=False`, we still recognize the custom `__init__` method:
|
||||
|
||||
```py
|
||||
@dataclass(init=False)
|
||||
class D:
|
||||
def __init__(self, x: int) -> None:
|
||||
self.x = str(x)
|
||||
|
||||
D(1) # OK
|
||||
D() # error: [missing-argument]
|
||||
```
|
||||
|
||||
### Accessing instance attributes on the class itself
|
||||
|
||||
Just like for normal classes, accessing instance attributes on the class itself is not allowed:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
x: int
|
||||
|
||||
# error: [unresolved-attribute] "Attribute `x` can only be accessed on instances, not on the class object `Literal[C]` itself."
|
||||
C.x
|
||||
```
|
||||
|
||||
### Return type of `dataclass(...)`
|
||||
|
||||
A call like `dataclass(order=True)` returns a callable itself, which is then used as the decorator.
|
||||
We can store the callable in a variable and later use it as a decorator:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
dataclass_with_order = dataclass(order=True)
|
||||
|
||||
reveal_type(dataclass_with_order) # revealed: <decorator produced by dataclass-like function>
|
||||
|
||||
@dataclass_with_order
|
||||
class C:
|
||||
x: int
|
||||
|
||||
C(1) < C(2) # ok
|
||||
```
|
||||
|
||||
### Using `dataclass` as a function
|
||||
|
||||
To do
|
||||
|
||||
## Internals
|
||||
|
||||
The `dataclass` decorator returns the class itself. This means that the type of `Person` is `type`,
|
||||
and attributes like the MRO are unchanged:
|
||||
|
||||
```py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class Person:
|
||||
name: str
|
||||
age: int | None = None
|
||||
|
||||
reveal_type(type(Person)) # revealed: Literal[type]
|
||||
reveal_type(Person.__mro__) # revealed: tuple[Literal[Person], Literal[object]]
|
||||
```
|
||||
|
||||
The generated methods have the following signatures:
|
||||
|
||||
```py
|
||||
# TODO: `self` is missing here
|
||||
reveal_type(Person.__init__) # revealed: (name: str, age: int | None = None) -> None
|
||||
|
||||
reveal_type(Person.__repr__) # revealed: def __repr__(self) -> str
|
||||
|
||||
reveal_type(Person.__eq__) # revealed: def __eq__(self, value: object, /) -> bool
|
||||
```
|
|
@ -0,0 +1,75 @@
|
|||
# Errors while declaring
|
||||
|
||||
## Violates previous assignment
|
||||
|
||||
```py
|
||||
x = 1
|
||||
x: str # error: [invalid-declaration] "Cannot declare type `str` for inferred type `Literal[1]`"
|
||||
```
|
||||
|
||||
## Incompatible declarations
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Incompatible declarations for 2 (out of 3) types
|
||||
|
||||
```py
|
||||
def _(flag1: bool, flag2: bool):
|
||||
if flag1:
|
||||
x: str
|
||||
elif flag2:
|
||||
x: int
|
||||
|
||||
# Here, the declared type for `x` is `int | str | Unknown`.
|
||||
x = 1 # error: [conflicting-declarations] "Conflicting declared types for `x`: str, int"
|
||||
```
|
||||
|
||||
## Incompatible declarations with bad assignment
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: str
|
||||
else:
|
||||
x: int
|
||||
|
||||
# error: [conflicting-declarations]
|
||||
# error: [invalid-assignment]
|
||||
x = b"foo"
|
||||
```
|
||||
|
||||
## No errors
|
||||
|
||||
Currently, we avoid raising the conflicting-declarations for the following cases:
|
||||
|
||||
### Partial declarations
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x: int
|
||||
|
||||
x = 1
|
||||
```
|
||||
|
||||
### Partial declarations in try-except
|
||||
|
||||
Refer to <https://github.com/astral-sh/ruff/issues/13966>
|
||||
|
||||
```py
|
||||
def _():
|
||||
try:
|
||||
x: int = 1
|
||||
except:
|
||||
x = 2
|
||||
|
||||
x = 3
|
||||
```
|
237
crates/ty_python_semantic/resources/mdtest/decorators.md
Normal file
237
crates/ty_python_semantic/resources/mdtest/decorators.md
Normal file
|
@ -0,0 +1,237 @@
|
|||
# Decorators
|
||||
|
||||
Decorators are a way to modify function and class behavior. A decorator is a callable that takes the
|
||||
function or class as an argument and returns a modified version of it.
|
||||
|
||||
## Basic example
|
||||
|
||||
A decorated function definition is conceptually similar to `def f(x): ...` followed by
|
||||
`f = decorator(f)`. This means that the type of a decorated function is the same as the return type
|
||||
of the decorator (which does not necessarily need to be a callable type):
|
||||
|
||||
```py
|
||||
def custom_decorator(f) -> int:
|
||||
return 1
|
||||
|
||||
@custom_decorator
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: int
|
||||
```
|
||||
|
||||
## Type-annotated decorator
|
||||
|
||||
More commonly, a decorator returns a modified callable type:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def ensure_positive(wrapped: Callable[[int], bool]) -> Callable[[int], bool]:
|
||||
return lambda x: wrapped(x) and x > 0
|
||||
|
||||
@ensure_positive
|
||||
def even(x: int) -> bool:
|
||||
return x % 2 == 0
|
||||
|
||||
reveal_type(even) # revealed: (int, /) -> bool
|
||||
reveal_type(even(4)) # revealed: bool
|
||||
```
|
||||
|
||||
## Decorators which take arguments
|
||||
|
||||
Decorators can be arbitrary expressions. This is often useful when the decorator itself takes
|
||||
arguments:
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def ensure_larger_than(lower_bound: int) -> Callable[[Callable[[int], bool]], Callable[[int], bool]]:
|
||||
def decorator(wrapped: Callable[[int], bool]) -> Callable[[int], bool]:
|
||||
return lambda x: wrapped(x) and x >= lower_bound
|
||||
return decorator
|
||||
|
||||
@ensure_larger_than(10)
|
||||
def even(x: int) -> bool:
|
||||
return x % 2 == 0
|
||||
|
||||
reveal_type(even) # revealed: (int, /) -> bool
|
||||
reveal_type(even(14)) # revealed: bool
|
||||
```
|
||||
|
||||
## Multiple decorators
|
||||
|
||||
Multiple decorators can be applied to a single function. They are applied in "bottom-up" order,
|
||||
meaning that the decorator closest to the function definition is applied first:
|
||||
|
||||
```py
|
||||
def maps_to_str(f) -> str:
|
||||
return "a"
|
||||
|
||||
def maps_to_int(f) -> int:
|
||||
return 1
|
||||
|
||||
def maps_to_bytes(f) -> bytes:
|
||||
return b"a"
|
||||
|
||||
@maps_to_str
|
||||
@maps_to_int
|
||||
@maps_to_bytes
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Decorating with a class
|
||||
|
||||
When a function is decorated with a class-based decorator, the decorated function turns into an
|
||||
instance of the class (see also: [properties](properties.md)). Attributes of the class can be
|
||||
accessed on the decorated function.
|
||||
|
||||
```py
|
||||
class accept_strings:
|
||||
custom_attribute: str = "a"
|
||||
|
||||
def __init__(self, f):
|
||||
self.f = f
|
||||
|
||||
def __call__(self, x: str | int) -> bool:
|
||||
return self.f(int(x))
|
||||
|
||||
@accept_strings
|
||||
def even(x: int) -> bool:
|
||||
return x > 0
|
||||
|
||||
reveal_type(even) # revealed: accept_strings
|
||||
reveal_type(even.custom_attribute) # revealed: str
|
||||
reveal_type(even("1")) # revealed: bool
|
||||
reveal_type(even(1)) # revealed: bool
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
even(None)
|
||||
```
|
||||
|
||||
## Common decorator patterns
|
||||
|
||||
### `functools.wraps`
|
||||
|
||||
This test mainly makes sure that we do not emit any diagnostics in a case where the decorator is
|
||||
implemented using `functools.wraps`.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
from functools import wraps
|
||||
|
||||
def custom_decorator(f) -> Callable[[int], str]:
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
print("Calling decorated function")
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
@custom_decorator
|
||||
def f(x: int) -> str:
|
||||
return str(x)
|
||||
|
||||
reveal_type(f) # revealed: (int, /) -> str
|
||||
```
|
||||
|
||||
### `functools.cache`
|
||||
|
||||
```py
|
||||
from functools import cache
|
||||
|
||||
@cache
|
||||
def f(x: int) -> int:
|
||||
return x**2
|
||||
|
||||
# TODO: Should be `_lru_cache_wrapper[int]`
|
||||
reveal_type(f) # revealed: _lru_cache_wrapper[_T]
|
||||
|
||||
# TODO: Should be `int`
|
||||
reveal_type(f(1)) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Lambdas as decorators
|
||||
|
||||
```py
|
||||
@lambda f: f
|
||||
def g(x: int) -> str:
|
||||
return "a"
|
||||
|
||||
# TODO: This should be `Literal[g]` or `(int, /) -> str`
|
||||
reveal_type(g) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
### Unknown decorator
|
||||
|
||||
```py
|
||||
# error: [unresolved-reference] "Name `unknown_decorator` used when not defined"
|
||||
@unknown_decorator
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Error in the decorator expression
|
||||
|
||||
```py
|
||||
# error: [unsupported-operator]
|
||||
@(1 + "a")
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Non-callable decorator
|
||||
|
||||
```py
|
||||
non_callable = 1
|
||||
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
@non_callable
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Wrong signature
|
||||
|
||||
#### Wrong argument type
|
||||
|
||||
Here, we emit a diagnostic since `wrong_signature` takes an `int` instead of a callable type as the
|
||||
first argument:
|
||||
|
||||
```py
|
||||
def wrong_signature(f: int) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `def f(x) -> Unknown`"
|
||||
@wrong_signature
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
#### Wrong number of arguments
|
||||
|
||||
Decorators need to be callable with a single argument. If they are not, we emit a diagnostic:
|
||||
|
||||
```py
|
||||
def takes_two_arguments(f, g) -> str:
|
||||
return "a"
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `g` of function `takes_two_arguments`"
|
||||
@takes_two_arguments
|
||||
def f(x): ...
|
||||
|
||||
reveal_type(f) # revealed: str
|
||||
|
||||
def takes_no_argument() -> str:
|
||||
return "a"
|
||||
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `takes_no_argument`: expected 0, got 1"
|
||||
@takes_no_argument
|
||||
def g(x): ...
|
||||
```
|
|
@ -0,0 +1,800 @@
|
|||
# Descriptor protocol
|
||||
|
||||
[Descriptors] let objects customize attribute lookup, storage, and deletion.
|
||||
|
||||
A descriptor is an attribute value that has one of the methods in the descriptor protocol. Those
|
||||
methods are `__get__()`, `__set__()`, and `__delete__()`. If any of those methods are defined for an
|
||||
attribute, it is said to be a descriptor.
|
||||
|
||||
## Basic properties
|
||||
|
||||
### Example
|
||||
|
||||
An introductory example, modeled after a [simple example] in the primer on descriptors, involving a
|
||||
descriptor that returns a constant value:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Ten:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
|
||||
return 10
|
||||
|
||||
def __set__(self, instance: object, value: Literal[10]) -> None:
|
||||
pass
|
||||
|
||||
class C:
|
||||
ten: Ten = Ten()
|
||||
|
||||
c = C()
|
||||
|
||||
reveal_type(c.ten) # revealed: Literal[10]
|
||||
|
||||
reveal_type(C.ten) # revealed: Literal[10]
|
||||
|
||||
# This is fine:
|
||||
c.ten = 10
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `ten` on type `C` with custom `__set__` method"
|
||||
c.ten = 11
|
||||
```
|
||||
|
||||
When assigning to the `ten` attribute from the class object, we get an error. The descriptor
|
||||
protocol is *not* triggered in this case. Since the attribute is declared as `Ten` in the class
|
||||
body, we do not allow these assignments, preventing users from accidentally overwriting the data
|
||||
descriptor, which is what would happen at runtime:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Object of type `Literal[10]` is not assignable to attribute `ten` of type `Ten`"
|
||||
C.ten = 10
|
||||
# error: [invalid-assignment] "Object of type `Literal[11]` is not assignable to attribute `ten` of type `Ten`"
|
||||
C.ten = 11
|
||||
```
|
||||
|
||||
### Different types for `__get__` and `__set__`
|
||||
|
||||
The return type of `__get__` and the value type of `__set__` can be different:
|
||||
|
||||
```py
|
||||
class FlexibleInt:
|
||||
def __init__(self):
|
||||
self._value: int | None = None
|
||||
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int | None:
|
||||
return self._value
|
||||
|
||||
def __set__(self, instance: object, value: int | str) -> None:
|
||||
self._value = int(value)
|
||||
|
||||
class C:
|
||||
flexible_int: FlexibleInt = FlexibleInt()
|
||||
|
||||
c = C()
|
||||
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
|
||||
c.flexible_int = 42 # okay
|
||||
c.flexible_int = "42" # also okay!
|
||||
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `flexible_int` on type `C` with custom `__set__` method"
|
||||
c.flexible_int = None # not okay
|
||||
|
||||
reveal_type(c.flexible_int) # revealed: int | None
|
||||
```
|
||||
|
||||
### Data and non-data descriptors
|
||||
|
||||
Descriptors that define `__set__` or `__delete__` are called *data descriptors*. An example of a
|
||||
data descriptor is a `property` with a setter and/or a deleter. Descriptors that only define
|
||||
`__get__`, meanwhile, are called *non-data descriptors*. Examples include functions, `classmethod`
|
||||
or `staticmethod`.
|
||||
|
||||
The precedence chain for attribute access is (1) data descriptors, (2) instance attributes, and (3)
|
||||
non-data descriptors.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
|
||||
return "data"
|
||||
|
||||
def __set__(self, instance: object, value: int) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
|
||||
return "non-data"
|
||||
|
||||
class C:
|
||||
data_descriptor = DataDescriptor()
|
||||
non_data_descriptor = NonDataDescriptor()
|
||||
|
||||
def f(self):
|
||||
# This explains why data descriptors come first in the precedence chain. If
|
||||
# instance attributes would take priority, we would override the descriptor
|
||||
# here. Instead, this calls `DataDescriptor.__set__`, i.e. it does not affect
|
||||
# the type of the `data_descriptor` attribute.
|
||||
self.data_descriptor = 1
|
||||
|
||||
# However, for non-data descriptors, instance attributes do take precedence.
|
||||
# So it is possible to override them.
|
||||
self.non_data_descriptor = 1
|
||||
|
||||
c = C()
|
||||
|
||||
reveal_type(c.data_descriptor) # revealed: Unknown | Literal["data"]
|
||||
|
||||
reveal_type(c.non_data_descriptor) # revealed: Unknown | Literal["non-data", 1]
|
||||
|
||||
reveal_type(C.data_descriptor) # revealed: Unknown | Literal["data"]
|
||||
|
||||
reveal_type(C.non_data_descriptor) # revealed: Unknown | Literal["non-data"]
|
||||
|
||||
# It is possible to override data descriptors via class objects. The following
|
||||
# assignment does not call `DataDescriptor.__set__`. For this reason, we infer
|
||||
# `Unknown | …` for all (descriptor) attributes.
|
||||
C.data_descriptor = "something else" # This is okay
|
||||
```
|
||||
|
||||
### Partial fall back
|
||||
|
||||
Our implementation of the descriptor protocol takes into account that symbols can be possibly
|
||||
unbound. In those cases, we fall back to lower precedence steps of the descriptor protocol and union
|
||||
all possible results accordingly. We start by defining a data and a non-data descriptor:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
|
||||
return "data"
|
||||
|
||||
def __set__(self, instance: object, value: int) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
|
||||
return "non-data"
|
||||
```
|
||||
|
||||
Then, we demonstrate that we fall back to an instance attribute if a data descriptor is possibly
|
||||
unbound:
|
||||
|
||||
```py
|
||||
def f1(flag: bool):
|
||||
class C1:
|
||||
if flag:
|
||||
attr = DataDescriptor()
|
||||
|
||||
def f(self):
|
||||
self.attr = "normal"
|
||||
|
||||
reveal_type(C1().attr) # revealed: Unknown | Literal["data", "normal"]
|
||||
|
||||
# Assigning to the attribute also causes no `possibly-unbound` diagnostic:
|
||||
C1().attr = 1
|
||||
```
|
||||
|
||||
We never treat implicit instance attributes as definitely bound, so we fall back to the non-data
|
||||
descriptor here:
|
||||
|
||||
```py
|
||||
class C2:
|
||||
def f(self):
|
||||
self.attr = "normal"
|
||||
attr = NonDataDescriptor()
|
||||
|
||||
reveal_type(C2().attr) # revealed: Unknown | Literal["non-data", "normal"]
|
||||
|
||||
# Assignments always go to the instance attribute in this case
|
||||
C2().attr = 1
|
||||
```
|
||||
|
||||
### Descriptors only work when used as class variables
|
||||
|
||||
Descriptors only work when used as class variables. When put in instances, they have no effect.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Ten:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
|
||||
return 10
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.ten: Ten = Ten()
|
||||
|
||||
reveal_type(C().ten) # revealed: Ten
|
||||
|
||||
C().ten = Ten()
|
||||
|
||||
# The instance attribute is declared as `Ten`, so this is an
|
||||
# error: [invalid-assignment] "Object of type `Literal[10]` is not assignable to attribute `ten` of type `Ten`"
|
||||
C().ten = 10
|
||||
```
|
||||
|
||||
## Descriptor protocol for class objects
|
||||
|
||||
When attributes are accessed on a class object, the following [precedence chain] is used:
|
||||
|
||||
- Data descriptor on the metaclass
|
||||
- Data or non-data descriptor on the class
|
||||
- Class attribute
|
||||
- Non-data descriptor on the metaclass
|
||||
- Metaclass attribute
|
||||
|
||||
To verify this, we define a data and a non-data descriptor:
|
||||
|
||||
```py
|
||||
from typing import Literal, Any
|
||||
|
||||
class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["data"]:
|
||||
return "data"
|
||||
|
||||
def __set__(self, instance: object, value: int) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["non-data"]:
|
||||
return "non-data"
|
||||
```
|
||||
|
||||
First, we make sure that the descriptors are correctly accessed when defined on the metaclass or the
|
||||
class:
|
||||
|
||||
```py
|
||||
class Meta1(type):
|
||||
meta_data_descriptor: DataDescriptor = DataDescriptor()
|
||||
meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
|
||||
|
||||
class C1(metaclass=Meta1):
|
||||
class_data_descriptor: DataDescriptor = DataDescriptor()
|
||||
class_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
|
||||
|
||||
reveal_type(C1.meta_data_descriptor) # revealed: Literal["data"]
|
||||
reveal_type(C1.meta_non_data_descriptor) # revealed: Literal["non-data"]
|
||||
|
||||
reveal_type(C1.class_data_descriptor) # revealed: Literal["data"]
|
||||
reveal_type(C1.class_non_data_descriptor) # revealed: Literal["non-data"]
|
||||
```
|
||||
|
||||
Assignments to class object attribute only trigger the descriptor protocol if the data descriptor is
|
||||
on the metaclass:
|
||||
|
||||
```py
|
||||
C1.meta_data_descriptor = 1
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor` on type `Literal[C1]` with custom `__set__` method"
|
||||
C1.meta_data_descriptor = "invalid"
|
||||
```
|
||||
|
||||
When writing to a class-level data descriptor from the class object itself, the descriptor protocol
|
||||
is *not* triggered (this is in contrast to what happens when you read class-level descriptor
|
||||
attributes!). So the following assignment does not call `__set__`. At runtime, the assignment would
|
||||
overwrite the data descriptor, but the attribute is declared as `DataDescriptor` in the class body,
|
||||
so we do not allow this:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `class_data_descriptor` of type `DataDescriptor`"
|
||||
C1.class_data_descriptor = 1
|
||||
```
|
||||
|
||||
We now demonstrate that a *metaclass data descriptor* takes precedence over all class-level
|
||||
attributes:
|
||||
|
||||
```py
|
||||
class Meta2(type):
|
||||
meta_data_descriptor1: DataDescriptor = DataDescriptor()
|
||||
meta_data_descriptor2: DataDescriptor = DataDescriptor()
|
||||
|
||||
class ClassLevelDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal["class level data descriptor"]:
|
||||
return "class level data descriptor"
|
||||
|
||||
def __set__(self, instance: object, value: str) -> None:
|
||||
pass
|
||||
|
||||
class C2(metaclass=Meta2):
|
||||
meta_data_descriptor1: Literal["value on class"] = "value on class"
|
||||
meta_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
|
||||
|
||||
reveal_type(C2.meta_data_descriptor1) # revealed: Literal["data"]
|
||||
reveal_type(C2.meta_data_descriptor2) # revealed: Literal["data"]
|
||||
|
||||
C2.meta_data_descriptor1 = 1
|
||||
C2.meta_data_descriptor2 = 1
|
||||
|
||||
# error: [invalid-assignment]
|
||||
C2.meta_data_descriptor1 = "invalid"
|
||||
# error: [invalid-assignment]
|
||||
C2.meta_data_descriptor2 = "invalid"
|
||||
```
|
||||
|
||||
On the other hand, normal metaclass attributes and metaclass non-data descriptors are shadowed by
|
||||
class-level attributes (descriptor or not):
|
||||
|
||||
```py
|
||||
class Meta3(type):
|
||||
meta_attribute1: Literal["value on metaclass"] = "value on metaclass"
|
||||
meta_attribute2: Literal["value on metaclass"] = "value on metaclass"
|
||||
meta_non_data_descriptor1: NonDataDescriptor = NonDataDescriptor()
|
||||
meta_non_data_descriptor2: NonDataDescriptor = NonDataDescriptor()
|
||||
|
||||
class C3(metaclass=Meta3):
|
||||
meta_attribute1: Literal["value on class"] = "value on class"
|
||||
meta_attribute2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
|
||||
meta_non_data_descriptor1: Literal["value on class"] = "value on class"
|
||||
meta_non_data_descriptor2: ClassLevelDataDescriptor = ClassLevelDataDescriptor()
|
||||
|
||||
reveal_type(C3.meta_attribute1) # revealed: Literal["value on class"]
|
||||
reveal_type(C3.meta_attribute2) # revealed: Literal["class level data descriptor"]
|
||||
reveal_type(C3.meta_non_data_descriptor1) # revealed: Literal["value on class"]
|
||||
reveal_type(C3.meta_non_data_descriptor2) # revealed: Literal["class level data descriptor"]
|
||||
```
|
||||
|
||||
Finally, metaclass attributes and metaclass non-data descriptors are only accessible when they are
|
||||
not shadowed by class-level attributes:
|
||||
|
||||
```py
|
||||
class Meta4(type):
|
||||
meta_attribute: Literal["value on metaclass"] = "value on metaclass"
|
||||
meta_non_data_descriptor: NonDataDescriptor = NonDataDescriptor()
|
||||
|
||||
class C4(metaclass=Meta4): ...
|
||||
|
||||
reveal_type(C4.meta_attribute) # revealed: Literal["value on metaclass"]
|
||||
reveal_type(C4.meta_non_data_descriptor) # revealed: Literal["non-data"]
|
||||
```
|
||||
|
||||
When a metaclass data descriptor is possibly unbound, we union the result type of its `__get__`
|
||||
method with an underlying class level attribute, if present:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Meta5(type):
|
||||
if flag:
|
||||
meta_data_descriptor1: DataDescriptor = DataDescriptor()
|
||||
meta_data_descriptor2: DataDescriptor = DataDescriptor()
|
||||
|
||||
class C5(metaclass=Meta5):
|
||||
meta_data_descriptor1: Literal["value on class"] = "value on class"
|
||||
|
||||
reveal_type(C5.meta_data_descriptor1) # revealed: Literal["data", "value on class"]
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(C5.meta_data_descriptor2) # revealed: Literal["data"]
|
||||
|
||||
# TODO: We currently emit two diagnostics here, corresponding to the two states of `flag`. The diagnostics are not
|
||||
# wrong, but they could be subsumed under a higher-level diagnostic.
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `meta_data_descriptor1` on type `Literal[C5]` with custom `__set__` method"
|
||||
# error: [invalid-assignment] "Object of type `None` is not assignable to attribute `meta_data_descriptor1` of type `Literal["value on class"]`"
|
||||
C5.meta_data_descriptor1 = None
|
||||
|
||||
# error: [possibly-unbound-attribute]
|
||||
C5.meta_data_descriptor2 = 1
|
||||
```
|
||||
|
||||
When a class-level attribute is possibly unbound, we union its (descriptor protocol) type with the
|
||||
metaclass attribute (unless it's a data descriptor, which always takes precedence):
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def _(flag: bool):
|
||||
class Meta6(type):
|
||||
attribute1: DataDescriptor = DataDescriptor()
|
||||
attribute2: NonDataDescriptor = NonDataDescriptor()
|
||||
attribute3: Literal["value on metaclass"] = "value on metaclass"
|
||||
|
||||
class C6(metaclass=Meta6):
|
||||
if flag:
|
||||
attribute1: Literal["value on class"] = "value on class"
|
||||
attribute2: Literal["value on class"] = "value on class"
|
||||
attribute3: Literal["value on class"] = "value on class"
|
||||
attribute4: Literal["value on class"] = "value on class"
|
||||
|
||||
reveal_type(C6.attribute1) # revealed: Literal["data"]
|
||||
reveal_type(C6.attribute2) # revealed: Literal["non-data", "value on class"]
|
||||
reveal_type(C6.attribute3) # revealed: Literal["value on metaclass", "value on class"]
|
||||
# error: [possibly-unbound-attribute]
|
||||
reveal_type(C6.attribute4) # revealed: Literal["value on class"]
|
||||
```
|
||||
|
||||
Finally, we can also have unions of various types of attributes:
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class Meta7(type):
|
||||
if flag:
|
||||
union_of_metaclass_attributes: Literal[1] = 1
|
||||
union_of_metaclass_data_descriptor_and_attribute: DataDescriptor = DataDescriptor()
|
||||
else:
|
||||
union_of_metaclass_attributes: Literal[2] = 2
|
||||
union_of_metaclass_data_descriptor_and_attribute: Literal[2] = 2
|
||||
|
||||
class C7(metaclass=Meta7):
|
||||
if flag:
|
||||
union_of_class_attributes: Literal[1] = 1
|
||||
union_of_class_data_descriptor_and_attribute: DataDescriptor = DataDescriptor()
|
||||
else:
|
||||
union_of_class_attributes: Literal[2] = 2
|
||||
union_of_class_data_descriptor_and_attribute: Literal[2] = 2
|
||||
|
||||
reveal_type(C7.union_of_metaclass_attributes) # revealed: Literal[1, 2]
|
||||
reveal_type(C7.union_of_metaclass_data_descriptor_and_attribute) # revealed: Literal["data", 2]
|
||||
reveal_type(C7.union_of_class_attributes) # revealed: Literal[1, 2]
|
||||
reveal_type(C7.union_of_class_data_descriptor_and_attribute) # revealed: Literal["data", 2]
|
||||
|
||||
C7.union_of_metaclass_attributes = 2 if flag else 1
|
||||
C7.union_of_metaclass_data_descriptor_and_attribute = 2 if flag else 100
|
||||
C7.union_of_class_attributes = 2 if flag else 1
|
||||
C7.union_of_class_data_descriptor_and_attribute = 2 if flag else DataDescriptor()
|
||||
```
|
||||
|
||||
## Descriptors distinguishing between class and instance access
|
||||
|
||||
Overloads can be used to distinguish between when a descriptor is accessed on a class object and
|
||||
when it is accessed on an instance. A real-world example of this is the `__get__` method on
|
||||
`types.FunctionType`.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString, overload
|
||||
|
||||
class Descriptor:
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type, /) -> Literal["called on class object"]: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type | None = None, /) -> Literal["called on instance"]: ...
|
||||
def __get__(self, instance, owner=None, /) -> LiteralString:
|
||||
if instance:
|
||||
return "called on instance"
|
||||
else:
|
||||
return "called on class object"
|
||||
|
||||
class C:
|
||||
d: Descriptor = Descriptor()
|
||||
|
||||
reveal_type(C.d) # revealed: Literal["called on class object"]
|
||||
|
||||
reveal_type(C().d) # revealed: Literal["called on instance"]
|
||||
```
|
||||
|
||||
## Descriptor protocol for dunder methods
|
||||
|
||||
Dunder methods are always looked up on the meta-type. There is no instance fallback. This means that
|
||||
an implicit dunder call on an instance-like object will not only look up the dunder method on the
|
||||
class object, without considering instance attributes. And an implicit dunder call on a class object
|
||||
will look up the dunder method on the metaclass, without considering class attributes.
|
||||
|
||||
```py
|
||||
class SomeCallable:
|
||||
def __call__(self, x: int) -> str:
|
||||
return "a"
|
||||
|
||||
class Descriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> SomeCallable:
|
||||
return SomeCallable()
|
||||
|
||||
class B:
|
||||
__call__: Descriptor = Descriptor()
|
||||
|
||||
b_instance = B()
|
||||
reveal_type(b_instance(1)) # revealed: str
|
||||
|
||||
b_instance("bla") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Special descriptors
|
||||
|
||||
### Built-in `property` descriptor
|
||||
|
||||
The built-in `property` decorator creates a descriptor. The names for attribute reads/writes are
|
||||
determined by the return type of the `name` method and the parameter type of the setter,
|
||||
respectively.
|
||||
|
||||
```py
|
||||
class C:
|
||||
_name: str | None = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name or "Unset"
|
||||
|
||||
@name.setter
|
||||
def name(self, value: str | None) -> None:
|
||||
self._value = value
|
||||
|
||||
c = C()
|
||||
|
||||
reveal_type(c._name) # revealed: str | None
|
||||
reveal_type(c.name) # revealed: str
|
||||
reveal_type(C.name) # revealed: property
|
||||
|
||||
c.name = "new"
|
||||
c.name = None
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to data descriptor attribute `name` on type `C` with custom `__set__` method"
|
||||
c.name = 42
|
||||
```
|
||||
|
||||
### Built-in `classmethod` descriptor
|
||||
|
||||
Similarly to `property`, `classmethod` decorator creates an implicit descriptor that binds the first
|
||||
argument to the class instead of the instance.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, value: str) -> None:
|
||||
self._name: str = value
|
||||
|
||||
@classmethod
|
||||
def factory(cls, value: str) -> "C":
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def get_name(cls) -> str:
|
||||
return cls.__name__
|
||||
|
||||
c1 = C.factory("test") # okay
|
||||
|
||||
reveal_type(c1) # revealed: C
|
||||
|
||||
reveal_type(C.get_name()) # revealed: str
|
||||
|
||||
reveal_type(C("42").get_name()) # revealed: str
|
||||
```
|
||||
|
||||
### Functions as descriptors
|
||||
|
||||
Functions are descriptors because they implement a `__get__` method. This is crucial in making sure
|
||||
that method calls work as expected. See [this test suite](./call/methods.md) for more information.
|
||||
Here, we only demonstrate how `__get__` works on functions:
|
||||
|
||||
```py
|
||||
from inspect import getattr_static
|
||||
|
||||
def f(x: object) -> str:
|
||||
return "a"
|
||||
|
||||
reveal_type(f) # revealed: def f(x: object) -> str
|
||||
reveal_type(f.__get__) # revealed: <method-wrapper `__get__` of `f`>
|
||||
reveal_type(f.__get__(None, type(f))) # revealed: def f(x: object) -> str
|
||||
reveal_type(f.__get__(None, type(f))(1)) # revealed: str
|
||||
|
||||
wrapper_descriptor = getattr_static(f, "__get__")
|
||||
|
||||
reveal_type(wrapper_descriptor) # revealed: <wrapper-descriptor `__get__` of `function` objects>
|
||||
reveal_type(wrapper_descriptor(f, None, type(f))) # revealed: def f(x: object) -> str
|
||||
|
||||
# Attribute access on the method-wrapper `f.__get__` falls back to `MethodWrapperType`:
|
||||
reveal_type(f.__get__.__hash__) # revealed: bound method MethodWrapperType.__hash__() -> int
|
||||
|
||||
# Attribute access on the wrapper-descriptor falls back to `WrapperDescriptorType`:
|
||||
reveal_type(wrapper_descriptor.__qualname__) # revealed: str
|
||||
```
|
||||
|
||||
We can also bind the free function `f` to an instance of a class `C`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
bound_method = wrapper_descriptor(f, C(), C)
|
||||
|
||||
reveal_type(bound_method) # revealed: bound method C.f() -> str
|
||||
```
|
||||
|
||||
We can then call it, and the instance of `C` is implicitly passed to the first parameter of `f`
|
||||
(`x`):
|
||||
|
||||
```py
|
||||
reveal_type(bound_method()) # revealed: str
|
||||
```
|
||||
|
||||
Finally, we test some error cases for the call to the wrapper descriptor:
|
||||
|
||||
```py
|
||||
# Calling the wrapper descriptor without any arguments is an
|
||||
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
|
||||
wrapper_descriptor()
|
||||
|
||||
# Calling it without the `instance` argument is an also an
|
||||
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
|
||||
wrapper_descriptor(f)
|
||||
|
||||
# Calling it without the `owner` argument if `instance` is not `None` is an
|
||||
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
|
||||
wrapper_descriptor(f, None)
|
||||
|
||||
# But calling it with an instance is fine (in this case, the `owner` argument is optional):
|
||||
wrapper_descriptor(f, C())
|
||||
|
||||
# Calling it with something that is not a `FunctionType` as the first argument is an
|
||||
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
|
||||
wrapper_descriptor(1, None, type(f))
|
||||
|
||||
# Calling it with something that is not a `type` as the `owner` argument is an
|
||||
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
|
||||
wrapper_descriptor(f, None, f)
|
||||
|
||||
# Calling it with too many positional arguments is an
|
||||
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
|
||||
wrapper_descriptor(f, None, type(f), "one too many")
|
||||
```
|
||||
|
||||
## Error handling and edge cases
|
||||
|
||||
### `__get__` is called with correct arguments
|
||||
|
||||
This test makes sure that we call `__get__` with the right argument types for various scenarios:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class TailoredForClassObjectAccess:
|
||||
def __get__(self, instance: None, owner: type[C]) -> int:
|
||||
return 1
|
||||
|
||||
class TailoredForInstanceAccess:
|
||||
def __get__(self, instance: C, owner: type[C] | None = None) -> str:
|
||||
return "a"
|
||||
|
||||
class TailoredForMetaclassAccess:
|
||||
def __get__(self, instance: type[C], owner: type[Meta]) -> bytes:
|
||||
return b"a"
|
||||
|
||||
class Meta(type):
|
||||
metaclass_access: TailoredForMetaclassAccess = TailoredForMetaclassAccess()
|
||||
|
||||
class C(metaclass=Meta):
|
||||
class_object_access: TailoredForClassObjectAccess = TailoredForClassObjectAccess()
|
||||
instance_access: TailoredForInstanceAccess = TailoredForInstanceAccess()
|
||||
|
||||
reveal_type(C.class_object_access) # revealed: int
|
||||
reveal_type(C().instance_access) # revealed: str
|
||||
reveal_type(C.metaclass_access) # revealed: bytes
|
||||
|
||||
# TODO: These should emit a diagnostic
|
||||
reveal_type(C().class_object_access) # revealed: TailoredForClassObjectAccess
|
||||
reveal_type(C.instance_access) # revealed: TailoredForInstanceAccess
|
||||
```
|
||||
|
||||
### Descriptors with incorrect `__get__` signature
|
||||
|
||||
```py
|
||||
class Descriptor:
|
||||
# `__get__` method with missing parameters:
|
||||
def __get__(self) -> int:
|
||||
return 1
|
||||
|
||||
class C:
|
||||
descriptor: Descriptor = Descriptor()
|
||||
|
||||
# TODO: This should be an error
|
||||
reveal_type(C.descriptor) # revealed: Descriptor
|
||||
|
||||
# TODO: This should be an error
|
||||
reveal_type(C().descriptor) # revealed: Descriptor
|
||||
```
|
||||
|
||||
### Undeclared descriptor arguments
|
||||
|
||||
If a descriptor attribute is not declared, we union with `Unknown`, just like for regular
|
||||
attributes, since that attribute could be overwritten externally. Even a data descriptor with a
|
||||
`__set__` method can be overwritten when accessed through a class object.
|
||||
|
||||
```py
|
||||
class Descriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int:
|
||||
return 1
|
||||
|
||||
def __set__(self, instance: object, value: int) -> None:
|
||||
pass
|
||||
|
||||
class C:
|
||||
descriptor = Descriptor()
|
||||
|
||||
C.descriptor = "something else"
|
||||
|
||||
# This could also be `Literal["something else"]` if we support narrowing of attribute types based on assignments
|
||||
reveal_type(C.descriptor) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
### Possibly unbound descriptor attributes
|
||||
|
||||
```py
|
||||
class DataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int:
|
||||
return 1
|
||||
|
||||
def __set__(self, instance: int, value) -> None:
|
||||
pass
|
||||
|
||||
class NonDataDescriptor:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int:
|
||||
return 1
|
||||
|
||||
def _(flag: bool):
|
||||
class PossiblyUnbound:
|
||||
if flag:
|
||||
non_data: NonDataDescriptor = NonDataDescriptor()
|
||||
data: DataDescriptor = DataDescriptor()
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `Literal[PossiblyUnbound]` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound.non_data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `non_data` on type `PossiblyUnbound` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound().non_data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `data` on type `Literal[PossiblyUnbound]` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound.data) # revealed: int
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `data` on type `PossiblyUnbound` is possibly unbound"
|
||||
reveal_type(PossiblyUnbound().data) # revealed: int
|
||||
```
|
||||
|
||||
### Possibly-unbound `__get__` method
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class MaybeDescriptor:
|
||||
if flag:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int:
|
||||
return 1
|
||||
|
||||
class C:
|
||||
descriptor: MaybeDescriptor = MaybeDescriptor()
|
||||
|
||||
reveal_type(C.descriptor) # revealed: int | MaybeDescriptor
|
||||
|
||||
reveal_type(C().descriptor) # revealed: int | MaybeDescriptor
|
||||
```
|
||||
|
||||
### Descriptors with non-function `__get__` callables that are descriptors themselves
|
||||
|
||||
The descriptor protocol is recursive, i.e. looking up `__get__` can involve triggering the
|
||||
descriptor protocol on the callable's `__call__` method:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class ReturnedCallable2:
|
||||
def __call__(self, descriptor: Descriptor1, instance: None, owner: type[C]) -> int:
|
||||
return 1
|
||||
|
||||
class ReturnedCallable1:
|
||||
def __call__(self, descriptor: Descriptor2, instance: Callable1, owner: type[Callable1]) -> ReturnedCallable2:
|
||||
return ReturnedCallable2()
|
||||
|
||||
class Callable3:
|
||||
def __call__(self, descriptor: Descriptor3, instance: Callable2, owner: type[Callable2]) -> ReturnedCallable1:
|
||||
return ReturnedCallable1()
|
||||
|
||||
class Descriptor3:
|
||||
__get__: Callable3 = Callable3()
|
||||
|
||||
class Callable2:
|
||||
__call__: Descriptor3 = Descriptor3()
|
||||
|
||||
class Descriptor2:
|
||||
__get__: Callable2 = Callable2()
|
||||
|
||||
class Callable1:
|
||||
__call__: Descriptor2 = Descriptor2()
|
||||
|
||||
class Descriptor1:
|
||||
__get__: Callable1 = Callable1()
|
||||
|
||||
class C:
|
||||
d: Descriptor1 = Descriptor1()
|
||||
|
||||
reveal_type(C.d) # revealed: int
|
||||
```
|
||||
|
||||
[descriptors]: https://docs.python.org/3/howto/descriptor.html
|
||||
[precedence chain]: https://github.com/python/cpython/blob/3.13/Objects/typeobject.c#L5393-L5481
|
||||
[simple example]: https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant
|
|
@ -0,0 +1,149 @@
|
|||
# Attribute assignment
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
This test suite demonstrates various kinds of diagnostics that can be emitted in a
|
||||
`obj.attr = value` assignment.
|
||||
|
||||
## Instance attributes with class-level defaults
|
||||
|
||||
These can be set on instances and on class objects.
|
||||
|
||||
```py
|
||||
class C:
|
||||
attr: int = 0
|
||||
|
||||
instance = C()
|
||||
instance.attr = 1 # fine
|
||||
instance.attr = "wrong" # error: [invalid-assignment]
|
||||
|
||||
C.attr = 1 # fine
|
||||
C.attr = "wrong" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Pure instance attributes
|
||||
|
||||
These can only be set on instances. When trying to set them on class objects, we generate a useful
|
||||
diagnostic that mentions that the attribute is only available on instances.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.attr: int = 0
|
||||
|
||||
instance = C()
|
||||
instance.attr = 1 # fine
|
||||
instance.attr = "wrong" # error: [invalid-assignment]
|
||||
|
||||
C.attr = 1 # error: [invalid-attribute-access]
|
||||
```
|
||||
|
||||
## `ClassVar`s
|
||||
|
||||
These can only be set on class objects. When trying to set them on instances, we generate a useful
|
||||
diagnostic that mentions that the attribute is only available on class objects.
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
class C:
|
||||
attr: ClassVar[int] = 0
|
||||
|
||||
C.attr = 1 # fine
|
||||
C.attr = "wrong" # error: [invalid-assignment]
|
||||
|
||||
instance = C()
|
||||
instance.attr = 1 # error: [invalid-attribute-access]
|
||||
```
|
||||
|
||||
## Unknown attributes
|
||||
|
||||
When trying to set an attribute that is not defined, we also emit errors:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
C.non_existent = 1 # error: [unresolved-attribute]
|
||||
|
||||
instance = C()
|
||||
instance.non_existent = 1 # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
## Possibly-unbound attributes
|
||||
|
||||
When trying to set an attribute that is not defined in all branches, we emit errors:
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
class C:
|
||||
if flag:
|
||||
attr: int = 0
|
||||
|
||||
C.attr = 1 # error: [possibly-unbound-attribute]
|
||||
|
||||
instance = C()
|
||||
instance.attr = 1 # error: [possibly-unbound-attribute]
|
||||
```
|
||||
|
||||
## Data descriptors
|
||||
|
||||
When assigning to a data descriptor attribute, we implicitly call the descriptor's `__set__` method.
|
||||
This can lead to various kinds of diagnostics.
|
||||
|
||||
### Invalid argument type
|
||||
|
||||
```py
|
||||
class Descriptor:
|
||||
def __set__(self, instance: object, value: int) -> None:
|
||||
pass
|
||||
|
||||
class C:
|
||||
attr: Descriptor = Descriptor()
|
||||
|
||||
instance = C()
|
||||
instance.attr = 1 # fine
|
||||
|
||||
# TODO: ideally, we would mention why this is an invalid assignment (wrong argument type for `value` parameter)
|
||||
instance.attr = "wrong" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
### Invalid `__set__` method signature
|
||||
|
||||
```py
|
||||
class WrongDescriptor:
|
||||
def __set__(self, instance: object, value: int, extra: int) -> None:
|
||||
pass
|
||||
|
||||
class C:
|
||||
attr: WrongDescriptor = WrongDescriptor()
|
||||
|
||||
instance = C()
|
||||
|
||||
# TODO: ideally, we would mention why this is an invalid assignment (wrong number of arguments for `__set__`)
|
||||
instance.attr = 1 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Setting attributes on union types
|
||||
|
||||
```py
|
||||
def _(flag: bool) -> None:
|
||||
if flag:
|
||||
class C1:
|
||||
attr: int = 0
|
||||
|
||||
else:
|
||||
class C1:
|
||||
attr: str = ""
|
||||
|
||||
# TODO: The error message here could be improved to explain why the assignment fails.
|
||||
C1.attr = 1 # error: [invalid-assignment]
|
||||
|
||||
class C2:
|
||||
if flag:
|
||||
attr: int = 0
|
||||
else:
|
||||
attr: str = ""
|
||||
|
||||
# TODO: This should be an error
|
||||
C2.attr = 1
|
||||
```
|
|
@ -0,0 +1,197 @@
|
|||
# Invalid argument type diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Basic
|
||||
|
||||
This is a basic test demonstrating that a diagnostic points to the function definition corresponding
|
||||
to the invalid argument.
|
||||
|
||||
```py
|
||||
def foo(x: int) -> int:
|
||||
return x * x
|
||||
|
||||
foo("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Different source order
|
||||
|
||||
This is like the basic test, except we put the call site above the function definition.
|
||||
|
||||
```py
|
||||
def bar():
|
||||
foo("hello") # error: [invalid-argument-type]
|
||||
|
||||
def foo(x: int) -> int:
|
||||
return x * x
|
||||
```
|
||||
|
||||
## Different files
|
||||
|
||||
This tests that a diagnostic can point to a function definition in a different file in which an
|
||||
invalid call site was found.
|
||||
|
||||
`package.py`:
|
||||
|
||||
```py
|
||||
def foo(x: int) -> int:
|
||||
return x * x
|
||||
```
|
||||
|
||||
```py
|
||||
import package
|
||||
|
||||
package.foo("hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Many parameters
|
||||
|
||||
This checks that a diagnostic renders reasonably when there are multiple parameters.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, z: int) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Many parameters across multiple lines
|
||||
|
||||
This checks that a diagnostic renders reasonably when there are multiple parameters spread out
|
||||
across multiple lines.
|
||||
|
||||
```py
|
||||
def foo(
|
||||
x: int,
|
||||
y: int,
|
||||
z: int,
|
||||
) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Many parameters with multiple invalid arguments
|
||||
|
||||
This checks that a diagnostic renders reasonably when there are multiple parameters and multiple
|
||||
invalid argument types.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, z: int) -> int:
|
||||
return x * y * z
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
# error: [invalid-argument-type]
|
||||
foo("a", "b", "c")
|
||||
```
|
||||
|
||||
At present (2025-02-18), this renders three different diagnostic messages. But arguably, these could
|
||||
all be folded into one diagnostic. Fixing this requires at least better support for multi-spans in
|
||||
the diagnostic model and possibly also how diagnostics are emitted by the type checker itself.
|
||||
|
||||
## Test calling a function whose type is vendored from `typeshed`
|
||||
|
||||
This tests that diagnostic rendering is reasonable when the function being called is from the
|
||||
standard library.
|
||||
|
||||
```py
|
||||
import json
|
||||
|
||||
json.loads(5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Tests for a variety of argument types
|
||||
|
||||
These tests check that diagnostic output is reasonable regardless of the kinds of arguments used in
|
||||
a function definition.
|
||||
|
||||
### Only positional
|
||||
|
||||
Tests a function definition with only positional parameters.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, z: int, /) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, "hello", 3) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Variadic arguments
|
||||
|
||||
Tests a function definition with variadic arguments.
|
||||
|
||||
```py
|
||||
def foo(*numbers: int) -> int:
|
||||
return len(numbers)
|
||||
|
||||
foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Keyword only arguments
|
||||
|
||||
Tests a function definition with keyword-only arguments.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, *, z: int = 0) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### One keyword argument
|
||||
|
||||
Tests a function definition with keyword-only arguments.
|
||||
|
||||
```py
|
||||
def foo(x: int, y: int, z: int = 0) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, 2, "hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Variadic keyword arguments
|
||||
|
||||
```py
|
||||
def foo(**numbers: int) -> int:
|
||||
return len(numbers)
|
||||
|
||||
foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Mix of arguments
|
||||
|
||||
Tests a function definition with multiple different kinds of arguments.
|
||||
|
||||
```py
|
||||
def foo(x: int, /, y: int, *, z: int = 0) -> int:
|
||||
return x * y * z
|
||||
|
||||
foo(1, 2, z="hello") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
### Synthetic arguments
|
||||
|
||||
Tests a function call with synthetic arguments.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __call__(self, x: int) -> int:
|
||||
return 1
|
||||
|
||||
c = C()
|
||||
c("wrong") # error: [invalid-argument-type]
|
||||
```
|
||||
|
||||
## Calls to methods
|
||||
|
||||
Tests that we also see a reference to a function if the callable is a bound method.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def square(self, x: int) -> int:
|
||||
return x * x
|
||||
|
||||
c = C()
|
||||
c.square("hello") # error: [invalid-argument-type]
|
||||
```
|
|
@ -0,0 +1,14 @@
|
|||
# No matching overload diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Calls to overloaded functions
|
||||
|
||||
TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in
|
||||
real Python code. We are instead testing a special-cased function where we create an overloaded
|
||||
signature internally. Update this to an `@overload` function in the Python snippet itself once we
|
||||
can.
|
||||
|
||||
```py
|
||||
type("Foo", ()) # error: [no-matching-overload]
|
||||
```
|
|
@ -0,0 +1,221 @@
|
|||
# Semantic syntax error diagnostics
|
||||
|
||||
## `async` comprehensions in synchronous comprehensions
|
||||
|
||||
### Python 3.10
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Before Python 3.11, `async` comprehensions could not be used within outer sync comprehensions, even
|
||||
within an `async` function ([CPython issue](https://github.com/python/cpython/issues/77527)):
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
async def elements(n):
|
||||
yield n
|
||||
|
||||
async def f():
|
||||
# error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)"
|
||||
return {n: [x async for x in elements(n)] for n in range(3)}
|
||||
```
|
||||
|
||||
If all of the comprehensions are `async`, on the other hand, the code was still valid:
|
||||
|
||||
```py
|
||||
async def test():
|
||||
return [[x async for x in elements(n)] async for n in range(3)]
|
||||
```
|
||||
|
||||
These are a couple of tricky but valid cases to check that nested scope handling is wired up
|
||||
correctly in the `SemanticSyntaxContext` trait:
|
||||
|
||||
```py
|
||||
async def f():
|
||||
[x for x in [1]] and [x async for x in elements(1)]
|
||||
|
||||
async def f():
|
||||
def g():
|
||||
pass
|
||||
[x async for x in elements(1)]
|
||||
```
|
||||
|
||||
### Python 3.11
|
||||
|
||||
All of these same examples are valid after Python 3.11:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
async def elements(n):
|
||||
yield n
|
||||
|
||||
async def f():
|
||||
return {n: [x async for x in elements(n)] for n in range(3)}
|
||||
```
|
||||
|
||||
## Late `__future__` import
|
||||
|
||||
```py
|
||||
from collections import namedtuple
|
||||
|
||||
# error: [invalid-syntax] "__future__ imports must be at the top of the file"
|
||||
from __future__ import print_function
|
||||
```
|
||||
|
||||
## Invalid annotation
|
||||
|
||||
This one might be a bit redundant with the `invalid-type-form` error.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
# error: [invalid-type-form] "Named expressions are not allowed in type expressions"
|
||||
# error: [invalid-syntax] "named expression cannot be used within a type annotation"
|
||||
def f() -> (y := 3): ...
|
||||
```
|
||||
|
||||
## Duplicate `match` key
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
match 2:
|
||||
# error: [invalid-syntax] "mapping pattern checks duplicate key `"x"`"
|
||||
case {"x": 1, "x": 2}:
|
||||
...
|
||||
```
|
||||
|
||||
## `return`, `yield`, `yield from`, and `await` outside function
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "`return` statement outside of a function"
|
||||
return
|
||||
|
||||
# error: [invalid-syntax] "`yield` statement outside of a function"
|
||||
yield
|
||||
|
||||
# error: [invalid-syntax] "`yield from` statement outside of a function"
|
||||
yield from []
|
||||
|
||||
# error: [invalid-syntax] "`await` statement outside of a function"
|
||||
# error: [invalid-syntax] "`await` outside of an asynchronous function"
|
||||
await 1
|
||||
|
||||
def f():
|
||||
# error: [invalid-syntax] "`await` outside of an asynchronous function"
|
||||
await 1
|
||||
```
|
||||
|
||||
Generators are evaluated lazily, so `await` is allowed, even outside of a function.
|
||||
|
||||
```py
|
||||
async def g():
|
||||
yield 1
|
||||
|
||||
(x async for x in g())
|
||||
```
|
||||
|
||||
## Rebound comprehension variable
|
||||
|
||||
Walrus operators cannot rebind variables already in use as iterators:
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable"
|
||||
[x := 2 for x in range(10)]
|
||||
|
||||
# error: [invalid-syntax] "assignment expression cannot rebind comprehension variable"
|
||||
{y := 5 for y in range(10)}
|
||||
```
|
||||
|
||||
## Multiple case assignments
|
||||
|
||||
Variable names in pattern matching must be unique within a single pattern:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
x = [1, 2]
|
||||
match x:
|
||||
# error: [invalid-syntax] "multiple assignments to name `a` in pattern"
|
||||
case [a, a]:
|
||||
pass
|
||||
case _:
|
||||
pass
|
||||
|
||||
d = {"key": "value"}
|
||||
match d:
|
||||
# error: [invalid-syntax] "multiple assignments to name `b` in pattern"
|
||||
case {"key": b, "other": b}:
|
||||
pass
|
||||
```
|
||||
|
||||
## Duplicate type parameter
|
||||
|
||||
Type parameter names must be unique in a generic class or function definition:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "duplicate type parameter"
|
||||
class C[T, T]:
|
||||
pass
|
||||
|
||||
# error: [invalid-syntax] "duplicate type parameter"
|
||||
def f[X, Y, X]():
|
||||
pass
|
||||
```
|
||||
|
||||
## `await` outside async function
|
||||
|
||||
This error includes `await`, `async for`, `async with`, and `async` comprehensions.
|
||||
|
||||
```python
|
||||
async def elements(n):
|
||||
yield n
|
||||
|
||||
def _():
|
||||
# error: [invalid-syntax] "`await` outside of an asynchronous function"
|
||||
await 1
|
||||
# error: [invalid-syntax] "`async for` outside of an asynchronous function"
|
||||
async for _ in elements(1):
|
||||
...
|
||||
# error: [invalid-syntax] "`async with` outside of an asynchronous function"
|
||||
async with elements(1) as x:
|
||||
...
|
||||
# error: [invalid-syntax] "cannot use an asynchronous comprehension outside of an asynchronous function on Python 3.9 (syntax was added in 3.11)"
|
||||
# error: [invalid-syntax] "asynchronous comprehension outside of an asynchronous function"
|
||||
[x async for x in elements(1)]
|
||||
```
|
||||
|
||||
## Load before `global` declaration
|
||||
|
||||
This should be an error, but it's not yet.
|
||||
|
||||
TODO implement `SemanticSyntaxContext::global`
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
global x
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
# Shadowing
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Implicit class shadowing
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
C = 1 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Implicit function shadowing
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
f = 1 # error: [invalid-assignment]
|
||||
```
|
|
@ -0,0 +1,27 @@
|
|||
# Unpacking
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Right hand side not iterable
|
||||
|
||||
```py
|
||||
a, b = 1 # error: [not-iterable]
|
||||
```
|
||||
|
||||
## Exactly too many values to unpack
|
||||
|
||||
```py
|
||||
a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Exactly too few values to unpack
|
||||
|
||||
```py
|
||||
a, b = (1,) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Too few values to unpack
|
||||
|
||||
```py
|
||||
[a, *b, c, d] = (1, 2) # error: [invalid-assignment]
|
||||
```
|
|
@ -0,0 +1,87 @@
|
|||
# Unresolved import diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Using `from` with an unresolvable module
|
||||
|
||||
This example demonstrates the diagnostic when a `from` style import is used with a module that could
|
||||
not be found:
|
||||
|
||||
```py
|
||||
from does_not_exist import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with too many leading dots
|
||||
|
||||
This example demonstrates the diagnostic when a `from` style import is used with a presumptively
|
||||
valid path, but where there are too many leading dots.
|
||||
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
def add(x, y):
|
||||
return x + y
|
||||
```
|
||||
|
||||
`package/subpackage/subsubpackage/__init__.py`:
|
||||
|
||||
```py
|
||||
from ....foo import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with an unknown current module
|
||||
|
||||
This is another case handled separately in ty, where a `.` provokes relative module name resolution,
|
||||
but where the module name is not resolvable.
|
||||
|
||||
```py
|
||||
from .does_not_exist import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with an unknown nested module
|
||||
|
||||
Like the previous test, but with sub-modules to ensure the span is correct.
|
||||
|
||||
```py
|
||||
from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with a resolvable module but unresolvable item
|
||||
|
||||
This ensures that diagnostics for an unresolvable item inside a resolvable import highlight the item
|
||||
and not the entire `from ... import ...` statement.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
does_exist1 = 1
|
||||
does_exist2 = 2
|
||||
```
|
||||
|
||||
```py
|
||||
from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
```
|
||||
|
||||
## An unresolvable import that does not use `from`
|
||||
|
||||
This ensures that an unresolvable `import ...` statement highlights just the module name and not the
|
||||
entire statement.
|
||||
|
||||
```py
|
||||
import does_not_exist # error: [unresolved-import]
|
||||
|
||||
x = does_not_exist.foo
|
||||
```
|
|
@ -0,0 +1,61 @@
|
|||
<!-- snapshot-diagnostics -->
|
||||
|
||||
# Different ways that `unsupported-bool-conversion` can occur
|
||||
|
||||
## Has a `__bool__` method, but has incorrect parameters
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
def __bool__(self, foo):
|
||||
return False
|
||||
|
||||
a = NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Has a `__bool__` method, but has an incorrect return type
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
def __bool__(self) -> str:
|
||||
return "wat"
|
||||
|
||||
a = NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Has a `__bool__` attribute, but it's not callable
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
a = NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 and a and True
|
||||
```
|
||||
|
||||
## Part of a union where at least one member has incorrect `__bool__` method
|
||||
|
||||
```py
|
||||
class NotBoolable1:
|
||||
def __bool__(self) -> str:
|
||||
return "wat"
|
||||
|
||||
class NotBoolable2:
|
||||
pass
|
||||
|
||||
class NotBoolable3:
|
||||
__bool__: int = 3
|
||||
|
||||
def get() -> NotBoolable1 | NotBoolable2 | NotBoolable3:
|
||||
return NotBoolable2()
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
10 and get() and True
|
||||
```
|
|
@ -0,0 +1,37 @@
|
|||
# Version-related syntax error diagnostics
|
||||
|
||||
## `match` statement
|
||||
|
||||
The `match` statement was introduced in Python 3.10.
|
||||
|
||||
### Before 3.10
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
We should emit a syntax error before 3.10.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.9"
|
||||
```
|
||||
|
||||
```py
|
||||
match 2: # error: 1 [invalid-syntax] "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)"
|
||||
case 1:
|
||||
print("it's one")
|
||||
```
|
||||
|
||||
### After 3.10
|
||||
|
||||
On or after 3.10, no error should be reported.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
match 2:
|
||||
case 1:
|
||||
print("it's one")
|
||||
```
|
|
@ -0,0 +1,111 @@
|
|||
# `assert_never`
|
||||
|
||||
## Basic functionality
|
||||
|
||||
`assert_never` makes sure that the type of the argument is `Never`. If it is not, a
|
||||
`type-assertion-failure` diagnostic is emitted.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_never, Never, Any
|
||||
from ty_extensions import Unknown
|
||||
|
||||
def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
|
||||
assert_never(never) # fine
|
||||
|
||||
assert_never(0) # error: [type-assertion-failure]
|
||||
assert_never("") # error: [type-assertion-failure]
|
||||
assert_never(None) # error: [type-assertion-failure]
|
||||
assert_never([]) # error: [type-assertion-failure]
|
||||
assert_never({}) # error: [type-assertion-failure]
|
||||
assert_never(()) # error: [type-assertion-failure]
|
||||
assert_never(1 if flag else never) # error: [type-assertion-failure]
|
||||
|
||||
assert_never(any_) # error: [type-assertion-failure]
|
||||
assert_never(unknown) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
## Use case: Type narrowing and exhaustiveness checking
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases
|
||||
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_never, Literal
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def if_else_isinstance_success(obj: A | B):
|
||||
if isinstance(obj, A):
|
||||
pass
|
||||
elif isinstance(obj, B):
|
||||
pass
|
||||
elif isinstance(obj, C):
|
||||
pass
|
||||
else:
|
||||
assert_never(obj)
|
||||
|
||||
def if_else_isinstance_error(obj: A | B):
|
||||
if isinstance(obj, A):
|
||||
pass
|
||||
# B is missing
|
||||
elif isinstance(obj, C):
|
||||
pass
|
||||
else:
|
||||
# error: [type-assertion-failure] "Expected type `Never`, got `B & ~A & ~C` instead"
|
||||
assert_never(obj)
|
||||
|
||||
def if_else_singletons_success(obj: Literal[1, "a"] | None):
|
||||
if obj == 1:
|
||||
pass
|
||||
elif obj == "a":
|
||||
pass
|
||||
elif obj is None:
|
||||
pass
|
||||
else:
|
||||
assert_never(obj)
|
||||
|
||||
def if_else_singletons_error(obj: Literal[1, "a"] | None):
|
||||
if obj == 1:
|
||||
pass
|
||||
elif obj is "A": # "A" instead of "a"
|
||||
pass
|
||||
elif obj is None:
|
||||
pass
|
||||
else:
|
||||
# error: [type-assertion-failure] "Expected type `Never`, got `Literal["a"]` instead"
|
||||
assert_never(obj)
|
||||
|
||||
def match_singletons_success(obj: Literal[1, "a"] | None):
|
||||
match obj:
|
||||
case 1:
|
||||
pass
|
||||
case "a":
|
||||
pass
|
||||
case None:
|
||||
pass
|
||||
case _ as obj:
|
||||
# TODO: Ideally, we would not emit an error here
|
||||
# error: [type-assertion-failure] "Expected type `Never`, got `@Todo"
|
||||
assert_never(obj)
|
||||
|
||||
def match_singletons_error(obj: Literal[1, "a"] | None):
|
||||
match obj:
|
||||
case 1:
|
||||
pass
|
||||
case "A": # "A" instead of "a"
|
||||
pass
|
||||
case None:
|
||||
pass
|
||||
case _ as obj:
|
||||
# TODO: We should emit an error here, but the message should
|
||||
# show the type `Literal["a"]` instead of `@Todo(…)`.
|
||||
# error: [type-assertion-failure] "Expected type `Never`, got `@Todo"
|
||||
assert_never(obj)
|
||||
```
|
|
@ -0,0 +1,138 @@
|
|||
# `assert_type`
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(x: int):
|
||||
assert_type(x, int) # fine
|
||||
assert_type(x, str) # error: [type-assertion-failure]
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
The asserted type is checked against the inferred type, not the declared type.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(x: int | str):
|
||||
if isinstance(x, int):
|
||||
reveal_type(x) # revealed: int
|
||||
assert_type(x, int) # fine
|
||||
```
|
||||
|
||||
## Equivalence
|
||||
|
||||
The actual type must match the asserted type precisely.
|
||||
|
||||
```py
|
||||
from typing import Any, Type, Union
|
||||
from typing_extensions import assert_type
|
||||
|
||||
# Subtype does not count
|
||||
def _(x: bool):
|
||||
assert_type(x, int) # error: [type-assertion-failure]
|
||||
|
||||
def _(a: type[int], b: type[Any]):
|
||||
assert_type(a, type[Any]) # error: [type-assertion-failure]
|
||||
assert_type(b, type[int]) # error: [type-assertion-failure]
|
||||
|
||||
# The expression constructing the type is not taken into account
|
||||
def _(a: type[int]):
|
||||
assert_type(a, Type[int]) # fine
|
||||
```
|
||||
|
||||
## Gradual types
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import Literal, assert_type
|
||||
|
||||
from ty_extensions import Unknown
|
||||
|
||||
# Any and Unknown are considered equivalent
|
||||
def _(a: Unknown, b: Any):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
assert_type(a, Any) # fine
|
||||
|
||||
reveal_type(b) # revealed: Any
|
||||
assert_type(b, Unknown) # fine
|
||||
|
||||
def _(a: type[Unknown], b: type[Any]):
|
||||
reveal_type(a) # revealed: type[Unknown]
|
||||
assert_type(a, type[Any]) # fine
|
||||
|
||||
reveal_type(b) # revealed: type[Any]
|
||||
assert_type(b, type[Unknown]) # fine
|
||||
```
|
||||
|
||||
## Tuples
|
||||
|
||||
Tuple types with the same elements are the same.
|
||||
|
||||
```py
|
||||
from typing_extensions import Any, assert_type
|
||||
|
||||
from ty_extensions import Unknown
|
||||
|
||||
def _(a: tuple[int, str, bytes]):
|
||||
assert_type(a, tuple[int, str, bytes]) # fine
|
||||
|
||||
assert_type(a, tuple[int, str]) # error: [type-assertion-failure]
|
||||
assert_type(a, tuple[int, str, bytes, None]) # error: [type-assertion-failure]
|
||||
assert_type(a, tuple[int, bytes, str]) # error: [type-assertion-failure]
|
||||
|
||||
def _(a: tuple[Any, ...], b: tuple[Unknown, ...]):
|
||||
assert_type(a, tuple[Any, ...]) # fine
|
||||
assert_type(a, tuple[Unknown, ...]) # fine
|
||||
|
||||
assert_type(b, tuple[Unknown, ...]) # fine
|
||||
assert_type(b, tuple[Any, ...]) # fine
|
||||
```
|
||||
|
||||
## Unions
|
||||
|
||||
Unions with the same elements are the same, regardless of order.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
def _(a: str | int):
|
||||
assert_type(a, str | int)
|
||||
assert_type(a, int | str)
|
||||
```
|
||||
|
||||
## Intersections
|
||||
|
||||
Intersections are the same when their positive and negative parts are respectively the same,
|
||||
regardless of order.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
|
||||
from ty_extensions import Intersection, Not
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
|
||||
def _(a: A):
|
||||
if isinstance(a, B) and not isinstance(a, C) and not isinstance(a, D):
|
||||
reveal_type(a) # revealed: A & B & ~C & ~D
|
||||
|
||||
assert_type(a, Intersection[A, B, Not[C], Not[D]])
|
||||
assert_type(a, Intersection[B, A, Not[D], Not[C]])
|
||||
```
|
|
@ -0,0 +1,70 @@
|
|||
# `cast`
|
||||
|
||||
`cast()` takes two arguments, one type and one value, and returns a value of the given type.
|
||||
|
||||
The (inferred) type of the value and the given type do not need to have any correlation.
|
||||
|
||||
```py
|
||||
from typing import Literal, cast, Any
|
||||
|
||||
reveal_type(True) # revealed: Literal[True]
|
||||
reveal_type(cast(str, True)) # revealed: str
|
||||
reveal_type(cast("str", True)) # revealed: str
|
||||
|
||||
reveal_type(cast(int | str, 1)) # revealed: int | str
|
||||
|
||||
reveal_type(cast(val="foo", typ=int)) # revealed: int
|
||||
|
||||
# error: [invalid-type-form]
|
||||
reveal_type(cast(Literal, True)) # revealed: Unknown
|
||||
|
||||
# error: [invalid-type-form]
|
||||
reveal_type(cast(1, True)) # revealed: Unknown
|
||||
|
||||
# error: [missing-argument] "No argument provided for required parameter `val` of function `cast`"
|
||||
cast(str)
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to function `cast`: expected 2, got 3"
|
||||
cast(str, b"ar", "foo")
|
||||
|
||||
def function_returning_int() -> int:
|
||||
return 10
|
||||
|
||||
# error: [redundant-cast] "Value is already of type `int`"
|
||||
cast(int, function_returning_int())
|
||||
|
||||
def function_returning_any() -> Any:
|
||||
return "blah"
|
||||
|
||||
# error: [redundant-cast] "Value is already of type `Any`"
|
||||
cast(Any, function_returning_any())
|
||||
```
|
||||
|
||||
Complex type expressions (which may be unsupported) do not lead to spurious `[redundant-cast]`
|
||||
diagnostics.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def f(x: Callable[[dict[str, int]], None], y: tuple[dict[str, int]]):
|
||||
a = cast(Callable[[list[bytes]], None], x)
|
||||
b = cast(tuple[list[bytes]], y)
|
||||
```
|
||||
|
||||
A cast from `Todo` or `Unknown` to `Any` is not considered a "redundant cast": even if these are
|
||||
understood as gradually equivalent types by ty, they are understood as different types by human
|
||||
readers of ty's output. For `Unknown` in particular, we may consider it differently in the context
|
||||
of some opt-in diagnostics, as it indicates that the gradual type has come about due to an invalid
|
||||
annotation, missing annotation or missing type argument somewhere.
|
||||
|
||||
```py
|
||||
from ty_extensions import Unknown
|
||||
|
||||
def f(x: Any, y: Unknown, z: Any | str | int):
|
||||
a = cast(dict[str, Any], x)
|
||||
reveal_type(a) # revealed: dict[str, Any]
|
||||
|
||||
b = cast(Any, y)
|
||||
reveal_type(b) # revealed: Any
|
||||
|
||||
c = cast(str | int | Any, z) # error: [redundant-cast]
|
||||
```
|
2
crates/ty_python_semantic/resources/mdtest/doc/README.md
Normal file
2
crates/ty_python_semantic/resources/mdtest/doc/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
This directory contains user-facing documentation, but also doubles as an extended test suite that
|
||||
makes sure that our documentation stays up to date.
|
|
@ -0,0 +1,125 @@
|
|||
# Public type of undeclared symbols
|
||||
|
||||
## Summary
|
||||
|
||||
One major deviation from the behavior of existing Python type checkers is our handling of 'public'
|
||||
types for undeclared symbols. This is best illustrated with an example:
|
||||
|
||||
```py
|
||||
class Wrapper:
|
||||
value = None
|
||||
|
||||
wrapper = Wrapper()
|
||||
|
||||
reveal_type(wrapper.value) # revealed: Unknown | None
|
||||
|
||||
wrapper.value = 1
|
||||
```
|
||||
|
||||
Mypy and Pyright both infer a type of `None` for the type of `wrapper.value`. Consequently, both
|
||||
tools emit an error when trying to assign `1` to `wrapper.value`. But there is nothing wrong with
|
||||
this program. Emitting an error here violates the [gradual guarantee] which states that *"Removing
|
||||
type annotations (making the program more dynamic) should not result in additional static type
|
||||
errors."*: If `value` were annotated with `int | None` here, Mypy and Pyright would not emit any
|
||||
errors.
|
||||
|
||||
By inferring `Unknown | None` instead, we allow arbitrary values to be assigned to `wrapper.value`.
|
||||
This is a deliberate choice to prevent false positive errors on untyped code.
|
||||
|
||||
More generally, we infer `Unknown | T_inferred` for undeclared symbols, where `T_inferred` is the
|
||||
inferred type of the right-hand side of the assignment. This gradual type represents an *unknown*
|
||||
fully-static type that is *at least as large as* `T_inferred`. It accurately describes our static
|
||||
knowledge about this type. In the example above, we don't know what values `wrapper.value` could
|
||||
possibly contain, but we *do know* that `None` is a possibility. This allows us to catch errors
|
||||
where `wrapper.value` is used in a way that is incompatible with `None`:
|
||||
|
||||
```py
|
||||
def accepts_int(i: int) -> None:
|
||||
pass
|
||||
|
||||
def f(w: Wrapper) -> None:
|
||||
# This is fine
|
||||
v: int | None = w.value
|
||||
|
||||
# This function call is incorrect, because `w.value` could be `None`. We therefore emit the following
|
||||
# error: "Argument to this function is incorrect: Expected `int`, found `Unknown | None`"
|
||||
c = accepts_int(w.value)
|
||||
```
|
||||
|
||||
## Explicit lack of knowledge
|
||||
|
||||
The following example demonstrates how Mypy and Pyright's type inference of fully-static types in
|
||||
these situations can lead to false-negatives, even though everything appears to be (statically)
|
||||
typed. To make this a bit more realistic, imagine that `OptionalInt` is imported from an external,
|
||||
untyped module:
|
||||
|
||||
`optional_int.py`:
|
||||
|
||||
```py
|
||||
class OptionalInt:
|
||||
value = 10
|
||||
|
||||
def reset(o):
|
||||
o.value = None
|
||||
```
|
||||
|
||||
It is then used like this:
|
||||
|
||||
```py
|
||||
from optional_int import OptionalInt, reset
|
||||
|
||||
o = OptionalInt()
|
||||
reset(o) # Oh no...
|
||||
|
||||
# Mypy and Pyright infer a fully-static type of `int` here, which appears to make the
|
||||
# subsequent division operation safe -- but it is not. We infer the following type:
|
||||
reveal_type(o.value) # revealed: Unknown | Literal[10]
|
||||
|
||||
print(o.value // 2) # Runtime error!
|
||||
```
|
||||
|
||||
We do not catch this mistake either, but we accurately reflect our lack of knowledge about
|
||||
`o.value`. Together with a possible future type-checker mode that would detect the prevalence of
|
||||
dynamic types, this could help developers catch such mistakes.
|
||||
|
||||
## Stricter behavior
|
||||
|
||||
Users can always opt in to stricter behavior by adding type annotations. For the `OptionalInt`
|
||||
class, this would probably be:
|
||||
|
||||
```py
|
||||
class OptionalInt:
|
||||
value: int | None = 10
|
||||
|
||||
o = OptionalInt()
|
||||
|
||||
# The following public type is now
|
||||
# revealed: int | None
|
||||
reveal_type(o.value)
|
||||
|
||||
# Incompatible assignments are now caught:
|
||||
# error: "Object of type `Literal["a"]` is not assignable to attribute `value` of type `int | None`"
|
||||
o.value = "a"
|
||||
```
|
||||
|
||||
## What is meant by 'public' type?
|
||||
|
||||
We apply different semantics depending on whether a symbol is accessed from the same scope in which
|
||||
it was originally defined, or whether it is accessed from an external scope. External scopes will
|
||||
see the symbol's "public type", which has been discussed above. But within the same scope the symbol
|
||||
was defined in, we use a narrower type of `T_inferred` for undeclared symbols. This is because, from
|
||||
the perspective of this scope, there is no way that the value of the symbol could have been
|
||||
reassigned from external scopes. For example:
|
||||
|
||||
```py
|
||||
class Wrapper:
|
||||
value = None
|
||||
|
||||
# Type as seen from the same scope:
|
||||
reveal_type(value) # revealed: None
|
||||
|
||||
# Type as seen from another scope:
|
||||
reveal_type(Wrapper.value) # revealed: Unknown | None
|
||||
```
|
||||
|
||||
[gradual guarantee]: https://typing.python.org/en/latest/spec/concepts.html#the-gradual-guarantee
|
179
crates/ty_python_semantic/resources/mdtest/exception/basic.md
Normal file
179
crates/ty_python_semantic/resources/mdtest/exception/basic.md
Normal file
|
@ -0,0 +1,179 @@
|
|||
# Exception Handling
|
||||
|
||||
## Single Exception
|
||||
|
||||
```py
|
||||
import re
|
||||
|
||||
try:
|
||||
help()
|
||||
except NameError as e:
|
||||
reveal_type(e) # revealed: NameError
|
||||
except re.error as f:
|
||||
reveal_type(f) # revealed: error
|
||||
```
|
||||
|
||||
## Unknown type in except handler does not cause spurious diagnostic
|
||||
|
||||
```py
|
||||
from nonexistent_module import foo # error: [unresolved-import]
|
||||
|
||||
try:
|
||||
help()
|
||||
except foo as e:
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
reveal_type(e) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Multiple Exceptions in a Tuple
|
||||
|
||||
```py
|
||||
EXCEPTIONS = (AttributeError, TypeError)
|
||||
|
||||
try:
|
||||
help()
|
||||
except (RuntimeError, OSError) as e:
|
||||
reveal_type(e) # revealed: RuntimeError | OSError
|
||||
except EXCEPTIONS as f:
|
||||
reveal_type(f) # revealed: AttributeError | TypeError
|
||||
```
|
||||
|
||||
## Dynamic exception types
|
||||
|
||||
```py
|
||||
def foo(
|
||||
x: type[AttributeError],
|
||||
y: tuple[type[OSError], type[RuntimeError]],
|
||||
z: tuple[type[BaseException], ...],
|
||||
):
|
||||
try:
|
||||
help()
|
||||
except x as e:
|
||||
reveal_type(e) # revealed: AttributeError
|
||||
except y as f:
|
||||
reveal_type(f) # revealed: OSError | RuntimeError
|
||||
except z as g:
|
||||
# TODO: should be `BaseException`
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
## Invalid exception handlers
|
||||
|
||||
```py
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[3]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
except 3 as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
try:
|
||||
pass
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal["foo"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
# error: [invalid-exception-caught] "Cannot catch object of type `Literal[b"bar"]` in an exception handler (must be a `BaseException` subclass or a tuple of `BaseException` subclasses)"
|
||||
except (ValueError, OSError, "foo", b"bar") as e:
|
||||
reveal_type(e) # revealed: ValueError | OSError | Unknown
|
||||
|
||||
def foo(
|
||||
x: type[str],
|
||||
y: tuple[type[OSError], type[RuntimeError], int],
|
||||
z: tuple[type[str], ...],
|
||||
):
|
||||
try:
|
||||
help()
|
||||
# error: [invalid-exception-caught]
|
||||
except x as e:
|
||||
reveal_type(e) # revealed: Unknown
|
||||
# error: [invalid-exception-caught]
|
||||
except y as f:
|
||||
reveal_type(f) # revealed: OSError | RuntimeError | Unknown
|
||||
except z as g:
|
||||
# TODO: should emit a diagnostic here:
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
## Object raised is not an exception
|
||||
|
||||
```py
|
||||
try:
|
||||
raise AttributeError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise FloatingPointError # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise 1 # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise int # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise e # fine
|
||||
|
||||
def _(e: Exception | type[Exception] | None):
|
||||
raise e # error: [invalid-raise]
|
||||
```
|
||||
|
||||
## Exception cause is not an exception
|
||||
|
||||
```py
|
||||
def _():
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
|
||||
def _():
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise ModuleNotFoundError from e # fine
|
||||
|
||||
def _(e: Exception | type[Exception] | None):
|
||||
raise IndexError from e # fine
|
||||
|
||||
def _(e: int | None):
|
||||
raise IndexError from e # error: [invalid-raise]
|
||||
```
|
|
@ -0,0 +1,611 @@
|
|||
# Control flow for exception handlers
|
||||
|
||||
These tests assert that we understand the possible "definition states" (which symbols might or might
|
||||
not be defined) in the various branches of a `try`/`except`/`else`/`finally` block.
|
||||
|
||||
For a full writeup on the semantics of exception handlers, see [this document][1].
|
||||
|
||||
The tests throughout this Markdown document use functions with names starting with `could_raise_*`
|
||||
to mark definitions that might or might not succeed (as the function could raise an exception). A
|
||||
type checker must assume that any arbitrary function call could raise an exception in Python; this
|
||||
is just a naming convention used in these tests for clarity, and to future-proof the tests against
|
||||
possible future improvements whereby certain statements or expressions could potentially be inferred
|
||||
as being incapable of causing an exception to be raised.
|
||||
|
||||
## A single bare `except`
|
||||
|
||||
Consider the following `try`/`except` block, with a single bare `except:`. There are different types
|
||||
for the variable `x` in the two branches of this block, and we can't determine which branch might
|
||||
have been taken from the perspective of code following this block. The inferred type after the
|
||||
block's conclusion is therefore the union of the type at the end of the `try` suite (`str`) and the
|
||||
type at the end of the `except` suite (`Literal[2]`).
|
||||
|
||||
*Within* the `except` suite, we must infer a union of all possible "definition states" we could have
|
||||
been in at any point during the `try` suite. This is because control flow could have jumped to the
|
||||
`except` suite without any of the `try`-suite definitions successfully completing, with only *some*
|
||||
of the `try`-suite definitions successfully completing, or indeed with *all* of them successfully
|
||||
completing. The type of `x` at the beginning of the `except` suite in this example is therefore
|
||||
`Literal[1] | str`, taking into account that we might have jumped to the `except` suite before the
|
||||
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
|
||||
*after* that redefinition.
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
except:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
reveal_type(x) # revealed: str | Literal[2]
|
||||
```
|
||||
|
||||
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
|
||||
inferred as having a union type following the `try`/`except` block:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
try:
|
||||
x = could_raise_returns_str()
|
||||
except:
|
||||
x = could_raise_returns_str()
|
||||
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
## A non-bare `except`
|
||||
|
||||
For simple `try`/`except` blocks, an `except TypeError:` handler has the same control flow semantics
|
||||
as an `except:` handler. An `except TypeError:` handler will not catch *all* exceptions: if this is
|
||||
the only handler, it opens up the possibility that an exception might occur that would not be
|
||||
handled. However, as described in [the document on exception-handling semantics][1], that would lead
|
||||
to termination of the scope. It's therefore irrelevant to consider this possibility when it comes to
|
||||
control-flow analysis.
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
reveal_type(x) # revealed: str | Literal[2]
|
||||
```
|
||||
|
||||
## Multiple `except` branches
|
||||
|
||||
If the scope reaches the final `reveal_type` call in this example, either the `try`-block suite of
|
||||
statements was executed in its entirety, or exactly one `except` suite was executed in its entirety.
|
||||
The inferred type of `x` at this point is the union of the types at the end of the three suites:
|
||||
|
||||
- At the end of `try`, `type(x) == str`
|
||||
- At the end of `except TypeError`, `x == 2`
|
||||
- At the end of `except ValueError`, `x == 3`
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = 3
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
|
||||
reveal_type(x) # revealed: str | Literal[2, 3]
|
||||
```
|
||||
|
||||
## Exception handlers with `else` branches (but no `finally`)
|
||||
|
||||
If we reach the `reveal_type` call at the end of this scope, either the `try` and `else` suites were
|
||||
both executed in their entireties, or the `except` suite was executed in its entirety. The type of
|
||||
`x` at this point is the union of the type at the end of the `else` suite and the type at the end of
|
||||
the `except` suite:
|
||||
|
||||
- At the end of `else`, `x == 3`
|
||||
- At the end of `except`, `x == 2`
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
x = 3
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
```
|
||||
|
||||
For a block that has multiple `except` branches and an `else` branch, the same principle applies. In
|
||||
order to reach the final `reveal_type` call, either exactly one of the `except` suites must have
|
||||
been executed in its entirety, or the `try` suite and the `else` suite must both have been executed
|
||||
in their entireties:
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | str
|
||||
x = 3
|
||||
reveal_type(x) # revealed: Literal[3]
|
||||
else:
|
||||
reveal_type(x) # revealed: str
|
||||
x = 4
|
||||
reveal_type(x) # revealed: Literal[4]
|
||||
|
||||
reveal_type(x) # revealed: Literal[2, 3, 4]
|
||||
```
|
||||
|
||||
## Exception handlers with `finally` branches (but no `except` branches)
|
||||
|
||||
A `finally` suite is *always* executed. As such, if we reach the `reveal_type` call at the end of
|
||||
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
|
||||
type of `x` at the end of the example is therefore `Literal[2]`:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
finally:
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
If `x` was *not* redefined in the `finally` suite, however, things are somewhat more complicated. If
|
||||
we reach the final `reveal_type` call, unlike the state when we're visiting the `finally` suite, we
|
||||
know that the `try`-block suite ran to completion. This means that there are fewer possible states
|
||||
at this point than there were when we were inside the `finally` block.
|
||||
|
||||
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
|
||||
still a TODO item for us.)
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_str()
|
||||
reveal_type(x) # revealed: str
|
||||
finally:
|
||||
# TODO: should be Literal[1] | str
|
||||
reveal_type(x) # revealed: str
|
||||
|
||||
reveal_type(x) # revealed: str
|
||||
```
|
||||
|
||||
## Combining an `except` branch with a `finally` branch
|
||||
|
||||
As previously stated, we do not yet have accurate inference for types *inside* `finally` suites.
|
||||
When we do, however, we will have to take account of the following possibilities inside `finally`
|
||||
suites:
|
||||
|
||||
- The `try` suite could have run to completion
|
||||
- Or we could have jumped from halfway through the `try` suite to an `except` suite, and the
|
||||
`except` suite ran to completion
|
||||
- Or we could have jumped from halfway through the `try` suite straight to the `finally` suite due
|
||||
to an unhandled exception
|
||||
- Or we could have jumped from halfway through the `try` suite to an `except` suite, only for an
|
||||
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
|
||||
`except` suite ran to completion
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def could_raise_returns_A() -> A:
|
||||
return A()
|
||||
|
||||
def could_raise_returns_B() -> B:
|
||||
return B()
|
||||
|
||||
def could_raise_returns_C() -> C:
|
||||
return C()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | A | B | C`
|
||||
reveal_type(x) # revealed: A | C
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
Now for an example without a redefinition in the `finally` suite. As before, there *should* be fewer
|
||||
possibilities after completion of the `finally` suite than there were during the `finally` suite
|
||||
itself. (In some control-flow possibilities, some exceptions were merely *suspended* during the
|
||||
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
|
||||
suite.)
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | A | B | C`
|
||||
reveal_type(x) # revealed: A | C
|
||||
|
||||
reveal_type(x) # revealed: A | C
|
||||
```
|
||||
|
||||
An example with multiple `except` branches and a `finally` branch:
|
||||
|
||||
```py
|
||||
class D: ...
|
||||
class E: ...
|
||||
|
||||
def could_raise_returns_D() -> D:
|
||||
return D()
|
||||
|
||||
def could_raise_returns_E() -> E:
|
||||
return E()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | A | B | C | D | E`
|
||||
reveal_type(x) # revealed: A | C | E
|
||||
|
||||
reveal_type(x) # revealed: A | C | E
|
||||
```
|
||||
|
||||
## Combining `except`, `else` and `finally` branches
|
||||
|
||||
If the exception handler has an `else` branch, we must also take into account the possibility that
|
||||
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
|
||||
an exception raised *there*.
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
class E: ...
|
||||
|
||||
def could_raise_returns_A() -> A:
|
||||
return A()
|
||||
|
||||
def could_raise_returns_B() -> B:
|
||||
return B()
|
||||
|
||||
def could_raise_returns_C() -> C:
|
||||
return C()
|
||||
|
||||
def could_raise_returns_D() -> D:
|
||||
return D()
|
||||
|
||||
def could_raise_returns_E() -> E:
|
||||
return E()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | A | B | C | D | E`
|
||||
reveal_type(x) # revealed: C | E
|
||||
|
||||
reveal_type(x) # revealed: C | E
|
||||
```
|
||||
|
||||
The same again, this time with multiple `except` branches:
|
||||
|
||||
```py
|
||||
class F: ...
|
||||
class G: ...
|
||||
|
||||
def could_raise_returns_F() -> F:
|
||||
return F()
|
||||
|
||||
def could_raise_returns_G() -> G:
|
||||
return G()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
x = could_raise_returns_F()
|
||||
reveal_type(x) # revealed: F
|
||||
x = could_raise_returns_G()
|
||||
reveal_type(x) # revealed: G
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | A | B | C | D | E | F | G`
|
||||
reveal_type(x) # revealed: C | E | G
|
||||
|
||||
reveal_type(x) # revealed: C | E | G
|
||||
```
|
||||
|
||||
## Nested `try`/`except` blocks
|
||||
|
||||
It would take advanced analysis, which we are not yet capable of, to be able to determine that an
|
||||
exception handler always suppresses all exceptions. This is partly because it is possible for
|
||||
statements in `except`, `else` and `finally` suites to raise exceptions as well as statements in
|
||||
`try` suites. This means that if an exception handler is nested inside the `try` statement of an
|
||||
enclosing exception handler, it should (at least for now) be treated the same as any other node: as
|
||||
a suite containing statements that could possibly raise exceptions, which would lead to control flow
|
||||
jumping out of that suite prior to the suite running to completion.
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
class E: ...
|
||||
class F: ...
|
||||
class G: ...
|
||||
class H: ...
|
||||
class I: ...
|
||||
class J: ...
|
||||
class K: ...
|
||||
|
||||
def could_raise_returns_A() -> A:
|
||||
return A()
|
||||
|
||||
def could_raise_returns_B() -> B:
|
||||
return B()
|
||||
|
||||
def could_raise_returns_C() -> C:
|
||||
return C()
|
||||
|
||||
def could_raise_returns_D() -> D:
|
||||
return D()
|
||||
|
||||
def could_raise_returns_E() -> E:
|
||||
return E()
|
||||
|
||||
def could_raise_returns_F() -> F:
|
||||
return F()
|
||||
|
||||
def could_raise_returns_G() -> G:
|
||||
return G()
|
||||
|
||||
def could_raise_returns_H() -> H:
|
||||
return H()
|
||||
|
||||
def could_raise_returns_I() -> I:
|
||||
return I()
|
||||
|
||||
def could_raise_returns_J() -> J:
|
||||
return J()
|
||||
|
||||
def could_raise_returns_K() -> K:
|
||||
return K()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
try:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
x = could_raise_returns_A()
|
||||
reveal_type(x) # revealed: A
|
||||
except TypeError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
except ValueError:
|
||||
reveal_type(x) # revealed: Literal[1] | A
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
else:
|
||||
reveal_type(x) # revealed: A
|
||||
x = could_raise_returns_F()
|
||||
reveal_type(x) # revealed: F
|
||||
x = could_raise_returns_G()
|
||||
reveal_type(x) # revealed: G
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | A | B | C | D | E | F | G`
|
||||
reveal_type(x) # revealed: C | E | G
|
||||
x = 2
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
except:
|
||||
reveal_type(x) # revealed: Literal[1, 2] | A | B | C | D | E | F | G
|
||||
x = could_raise_returns_H()
|
||||
reveal_type(x) # revealed: H
|
||||
x = could_raise_returns_I()
|
||||
reveal_type(x) # revealed: I
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
x = could_raise_returns_J()
|
||||
reveal_type(x) # revealed: J
|
||||
x = could_raise_returns_K()
|
||||
reveal_type(x) # revealed: K
|
||||
finally:
|
||||
# TODO: should be `Literal[1, 2] | A | B | C | D | E | F | G | H | I | J | K`
|
||||
reveal_type(x) # revealed: I | K
|
||||
|
||||
# Either one `except` branch or the `else`
|
||||
# must have been taken and completed to get here:
|
||||
reveal_type(x) # revealed: I | K
|
||||
```
|
||||
|
||||
## Nested scopes inside `try` blocks
|
||||
|
||||
Shadowing a variable in an inner scope has no effect on type inference of the variable by that name
|
||||
in the outer scope:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
class E: ...
|
||||
|
||||
def could_raise_returns_A() -> A:
|
||||
return A()
|
||||
|
||||
def could_raise_returns_B() -> B:
|
||||
return B()
|
||||
|
||||
def could_raise_returns_C() -> C:
|
||||
return C()
|
||||
|
||||
def could_raise_returns_D() -> D:
|
||||
return D()
|
||||
|
||||
def could_raise_returns_E() -> E:
|
||||
return E()
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
|
||||
def foo(param=could_raise_returns_A()):
|
||||
x = could_raise_returns_A()
|
||||
|
||||
try:
|
||||
reveal_type(x) # revealed: A
|
||||
x = could_raise_returns_B()
|
||||
reveal_type(x) # revealed: B
|
||||
except:
|
||||
reveal_type(x) # revealed: A | B
|
||||
x = could_raise_returns_C()
|
||||
reveal_type(x) # revealed: C
|
||||
x = could_raise_returns_D()
|
||||
reveal_type(x) # revealed: D
|
||||
finally:
|
||||
# TODO: should be `A | B | C | D`
|
||||
reveal_type(x) # revealed: B | D
|
||||
reveal_type(x) # revealed: B | D
|
||||
x = foo
|
||||
reveal_type(x) # revealed: def foo(param=A) -> Unknown
|
||||
except:
|
||||
reveal_type(x) # revealed: Literal[1] | (def foo(param=A) -> Unknown)
|
||||
|
||||
class Bar:
|
||||
x = could_raise_returns_E()
|
||||
reveal_type(x) # revealed: E
|
||||
|
||||
x = Bar
|
||||
reveal_type(x) # revealed: Literal[Bar]
|
||||
finally:
|
||||
# TODO: should be `Literal[1] | Literal[foo] | Literal[Bar]`
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | Literal[Bar]
|
||||
|
||||
reveal_type(x) # revealed: (def foo(param=A) -> Unknown) | Literal[Bar]
|
||||
```
|
||||
|
||||
[1]: https://astral-sh.notion.site/Exception-handler-control-flow-11348797e1ca80bb8ce1e9aedbbe439d
|
|
@ -0,0 +1,63 @@
|
|||
# `except*`
|
||||
|
||||
`except*` is only available in Python 3.11 and later:
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
## `except*` with `BaseException`
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* BaseException as e:
|
||||
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
|
||||
```
|
||||
|
||||
## `except*` with specific exception
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* OSError as e:
|
||||
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
|
||||
# (needs homogeneous tuples + generics)
|
||||
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
|
||||
```
|
||||
|
||||
## `except*` with multiple exceptions
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* (TypeError, AttributeError) as e:
|
||||
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
|
||||
# (needs homogeneous tuples + generics)
|
||||
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
|
||||
```
|
||||
|
||||
## `except*` with mix of `Exception`s and `BaseException`s
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* (KeyboardInterrupt, AttributeError) as e:
|
||||
# TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex
|
||||
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
|
||||
```
|
||||
|
||||
## Invalid `except*` handlers
|
||||
|
||||
```py
|
||||
try:
|
||||
help()
|
||||
except* 3 as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
|
||||
|
||||
try:
|
||||
help()
|
||||
except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
|
||||
```
|
|
@ -0,0 +1,12 @@
|
|||
# Exception Handling
|
||||
|
||||
## Invalid syntax
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
try:
|
||||
print
|
||||
except as e: # error: [invalid-syntax]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
## Condition with object that implements `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
assert NotBoolable()
|
||||
```
|
|
@ -0,0 +1,34 @@
|
|||
# Attribute access
|
||||
|
||||
## Boundness
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
class A:
|
||||
always_bound: int = 1
|
||||
|
||||
if flag:
|
||||
union = 1
|
||||
else:
|
||||
union = "abc"
|
||||
|
||||
if flag:
|
||||
union_declared: int = 1
|
||||
else:
|
||||
union_declared: str = "abc"
|
||||
|
||||
if flag:
|
||||
possibly_unbound: str = "abc"
|
||||
|
||||
reveal_type(A.always_bound) # revealed: int
|
||||
|
||||
reveal_type(A.union) # revealed: Unknown | Literal[1, "abc"]
|
||||
|
||||
reveal_type(A.union_declared) # revealed: int | str
|
||||
|
||||
# error: [possibly-unbound-attribute] "Attribute `possibly_unbound` on type `Literal[A]` is possibly unbound"
|
||||
reveal_type(A.possibly_unbound) # revealed: str
|
||||
|
||||
# error: [unresolved-attribute] "Type `Literal[A]` has no attribute `non_existent`"
|
||||
reveal_type(A.non_existent) # revealed: Unknown
|
||||
```
|
155
crates/ty_python_semantic/resources/mdtest/expression/boolean.md
Normal file
155
crates/ty_python_semantic/resources/mdtest/expression/boolean.md
Normal file
|
@ -0,0 +1,155 @@
|
|||
# Expressions
|
||||
|
||||
## OR
|
||||
|
||||
```py
|
||||
def _(foo: str):
|
||||
reveal_type(True or False) # revealed: Literal[True]
|
||||
reveal_type("x" or "y" or "z") # revealed: Literal["x"]
|
||||
reveal_type("" or "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type(False or "z") # revealed: Literal["z"]
|
||||
reveal_type(False or True) # revealed: Literal[True]
|
||||
reveal_type(False or False) # revealed: Literal[False]
|
||||
reveal_type(foo or False) # revealed: (str & ~AlwaysFalsy) | Literal[False]
|
||||
reveal_type(foo or True) # revealed: (str & ~AlwaysFalsy) | Literal[True]
|
||||
```
|
||||
|
||||
## AND
|
||||
|
||||
```py
|
||||
def _(foo: str):
|
||||
reveal_type(True and False) # revealed: Literal[False]
|
||||
reveal_type(False and True) # revealed: Literal[False]
|
||||
reveal_type(foo and False) # revealed: (str & ~AlwaysTruthy) | Literal[False]
|
||||
reveal_type(foo and True) # revealed: (str & ~AlwaysTruthy) | Literal[True]
|
||||
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" and "") # revealed: Literal[""]
|
||||
reveal_type("" and "y") # revealed: Literal[""]
|
||||
```
|
||||
|
||||
## Simple function calls to bool
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
x = True
|
||||
else:
|
||||
x = False
|
||||
|
||||
reveal_type(x) # revealed: bool
|
||||
```
|
||||
|
||||
## Complex
|
||||
|
||||
```py
|
||||
reveal_type("x" and "y" or "z") # revealed: Literal["y"]
|
||||
reveal_type("x" or "y" and "z") # revealed: Literal["x"]
|
||||
reveal_type("" and "y" or "z") # revealed: Literal["z"]
|
||||
reveal_type("" or "y" and "z") # revealed: Literal["z"]
|
||||
reveal_type("x" and "y" or "") # revealed: Literal["y"]
|
||||
reveal_type("x" or "y" and "") # revealed: Literal["x"]
|
||||
```
|
||||
|
||||
## `bool()` function
|
||||
|
||||
## Evaluates to builtin
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
redefined_builtin_bool: type[bool] = bool
|
||||
|
||||
def my_bool(x) -> bool:
|
||||
return True
|
||||
```
|
||||
|
||||
```py
|
||||
from a import redefined_builtin_bool, my_bool
|
||||
|
||||
reveal_type(redefined_builtin_bool(0)) # revealed: Literal[False]
|
||||
reveal_type(my_bool(0)) # revealed: bool
|
||||
```
|
||||
|
||||
## Truthy values
|
||||
|
||||
```py
|
||||
reveal_type(bool(1)) # revealed: Literal[True]
|
||||
reveal_type(bool((0,))) # revealed: Literal[True]
|
||||
reveal_type(bool("NON EMPTY")) # revealed: Literal[True]
|
||||
reveal_type(bool(True)) # revealed: Literal[True]
|
||||
|
||||
def foo(): ...
|
||||
|
||||
reveal_type(bool(foo)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Falsy values
|
||||
|
||||
```py
|
||||
reveal_type(bool(0)) # revealed: Literal[False]
|
||||
reveal_type(bool(())) # revealed: Literal[False]
|
||||
reveal_type(bool(None)) # revealed: Literal[False]
|
||||
reveal_type(bool("")) # revealed: Literal[False]
|
||||
reveal_type(bool(False)) # revealed: Literal[False]
|
||||
reveal_type(bool()) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
## Ambiguous values
|
||||
|
||||
```py
|
||||
reveal_type(bool([])) # revealed: bool
|
||||
reveal_type(bool({})) # revealed: bool
|
||||
reveal_type(bool(set())) # revealed: bool
|
||||
```
|
||||
|
||||
## `__bool__` returning `NoReturn`
|
||||
|
||||
```py
|
||||
from typing import NoReturn
|
||||
|
||||
class NotBoolable:
|
||||
def __bool__(self) -> NoReturn:
|
||||
raise NotImplementedError("This object can't be converted to a boolean")
|
||||
|
||||
# TODO: This should emit an error that `NotBoolable` can't be converted to a bool but it currently doesn't
|
||||
# because `Never` is assignable to `bool`. This probably requires dead code analysis to fix.
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Not callable `__bool__`
|
||||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Not-boolable union
|
||||
|
||||
```py
|
||||
def test(cond: bool):
|
||||
class NotBoolable:
|
||||
__bool__: int | None = None if cond else 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
||||
## Union with some variants implementing `__bool__` incorrectly
|
||||
|
||||
```py
|
||||
def test(cond: bool):
|
||||
class NotBoolable:
|
||||
__bool__: None = None
|
||||
|
||||
a = 10 if cond else NotBoolable()
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `Literal[10] | NotBoolable`"
|
||||
if a:
|
||||
...
|
||||
```
|
41
crates/ty_python_semantic/resources/mdtest/expression/if.md
Normal file
41
crates/ty_python_semantic/resources/mdtest/expression/if.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
# If expression
|
||||
|
||||
## Union
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
reveal_type(1 if flag else 2) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Statically known conditions in if-expressions
|
||||
|
||||
```py
|
||||
reveal_type(1 if True else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if "not empty" else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if (1,) else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if 1 else 2) # revealed: Literal[1]
|
||||
|
||||
reveal_type(1 if False else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if None else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if "" else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if 0 else 2) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## Leaked Narrowing Constraint
|
||||
|
||||
(issue #14588)
|
||||
|
||||
The test inside an if expression should not affect code outside of the expression.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(flag: bool):
|
||||
x: Literal[42, "hello"] = 42 if flag else "hello"
|
||||
|
||||
reveal_type(x) # revealed: Literal[42, "hello"]
|
||||
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
reveal_type(x) # revealed: Literal[42, "hello"]
|
||||
```
|
118
crates/ty_python_semantic/resources/mdtest/expression/lambda.md
Normal file
118
crates/ty_python_semantic/resources/mdtest/expression/lambda.md
Normal file
|
@ -0,0 +1,118 @@
|
|||
# `lambda` expression
|
||||
|
||||
## No parameters
|
||||
|
||||
`lambda` expressions can be defined without any parameters.
|
||||
|
||||
```py
|
||||
reveal_type(lambda: 1) # revealed: () -> Unknown
|
||||
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(lambda: a) # revealed: () -> Unknown
|
||||
```
|
||||
|
||||
## With parameters
|
||||
|
||||
Unlike parameters in function definition, the parameters in a `lambda` expression cannot be
|
||||
annotated.
|
||||
|
||||
```py
|
||||
reveal_type(lambda a: a) # revealed: (a) -> Unknown
|
||||
reveal_type(lambda a, b: a + b) # revealed: (a, b) -> Unknown
|
||||
```
|
||||
|
||||
But, it can have default values:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a=1: a) # revealed: (a=Literal[1]) -> Unknown
|
||||
reveal_type(lambda a, b=2: a) # revealed: (a, b=Literal[2]) -> Unknown
|
||||
```
|
||||
|
||||
And, positional-only parameters:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a, b, /, c: c) # revealed: (a, b, /, c) -> Unknown
|
||||
```
|
||||
|
||||
And, keyword-only parameters:
|
||||
|
||||
```py
|
||||
reveal_type(lambda a, *, b=2, c: b) # revealed: (a, *, b=Literal[2], c) -> Unknown
|
||||
```
|
||||
|
||||
And, variadic parameter:
|
||||
|
||||
```py
|
||||
reveal_type(lambda *args: args) # revealed: (*args) -> Unknown
|
||||
```
|
||||
|
||||
And, keyword-varidic parameter:
|
||||
|
||||
```py
|
||||
reveal_type(lambda **kwargs: kwargs) # revealed: (**kwargs) -> Unknown
|
||||
```
|
||||
|
||||
Mixing all of them together:
|
||||
|
||||
```py
|
||||
# revealed: (a, b, /, c=Literal[True], *args, *, d=Literal["default"], e=Literal[5], **kwargs) -> Unknown
|
||||
reveal_type(lambda a, b, /, c=True, *args, d="default", e=5, **kwargs: None)
|
||||
```
|
||||
|
||||
## Parameter type
|
||||
|
||||
In addition to correctly inferring the `lambda` expression, the parameters should also be inferred
|
||||
correctly.
|
||||
|
||||
Using a parameter with no default value:
|
||||
|
||||
```py
|
||||
lambda x: reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
Using a parameter with default value:
|
||||
|
||||
```py
|
||||
lambda x=1: reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
Using a variadic parameter:
|
||||
|
||||
```py
|
||||
# TODO: should be `tuple[Unknown, ...]` (needs generics)
|
||||
lambda *args: reveal_type(args) # revealed: tuple
|
||||
```
|
||||
|
||||
Using a keyword-variadic parameter:
|
||||
|
||||
```py
|
||||
lambda **kwargs: reveal_type(kwargs) # revealed: dict[str, Unknown]
|
||||
```
|
||||
|
||||
## Nested `lambda` expressions
|
||||
|
||||
Here, a `lambda` expression is used as the default value for a parameter in another `lambda`
|
||||
expression.
|
||||
|
||||
```py
|
||||
reveal_type(lambda a=lambda x, y: 0: 2) # revealed: (a=(x, y) -> Unknown) -> Unknown
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
This does not enumerate all combinations of parameter kinds as that should be covered by the
|
||||
[subtype tests for callable types](./../type_properties/is_subtype_of.md#callable).
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
a1: Callable[[], None] = lambda: None
|
||||
a2: Callable[[int], None] = lambda x: None
|
||||
a3: Callable[[int, int], None] = lambda x, y, z=1: None
|
||||
a4: Callable[[int, int], None] = lambda *args: None
|
||||
|
||||
# error: [invalid-assignment]
|
||||
a5: Callable[[], None] = lambda x: None
|
||||
# error: [invalid-assignment]
|
||||
a6: Callable[[int], None] = lambda: None
|
||||
```
|
234
crates/ty_python_semantic/resources/mdtest/expression/len.md
Normal file
234
crates/ty_python_semantic/resources/mdtest/expression/len.md
Normal file
|
@ -0,0 +1,234 @@
|
|||
# Length (`len()`)
|
||||
|
||||
## Literal and constructed iterables
|
||||
|
||||
### Strings and bytes literals
|
||||
|
||||
```py
|
||||
reveal_type(len("no\rmal")) # revealed: Literal[6]
|
||||
reveal_type(len(r"aw stri\ng")) # revealed: Literal[10]
|
||||
reveal_type(len(r"conca\t" "ena\tion")) # revealed: Literal[14]
|
||||
reveal_type(len(b"ytes lite" rb"al")) # revealed: Literal[11]
|
||||
reveal_type(len("𝒰𝕹🄸©🕲𝕕ℇ")) # revealed: Literal[7]
|
||||
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
r"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[7]
|
||||
len(
|
||||
b"""foo
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
reveal_type( # revealed: Literal[9]
|
||||
len(
|
||||
rb"""foo\r
|
||||
bar"""
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Tuples
|
||||
|
||||
```py
|
||||
reveal_type(len(())) # revealed: Literal[0]
|
||||
reveal_type(len((1,))) # revealed: Literal[1]
|
||||
reveal_type(len((1, 2))) # revealed: Literal[2]
|
||||
|
||||
# TODO: Handle constructor calls
|
||||
reveal_type(len(tuple())) # revealed: int
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[],))) # revealed: Literal[1]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[1]
|
||||
reveal_type( # revealed: Literal[2]
|
||||
len(
|
||||
(
|
||||
*[],
|
||||
1,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[2]
|
||||
reveal_type(len((*[], 1, 2))) # revealed: Literal[3]
|
||||
|
||||
# TODO: Handle star unpacks; Should be: Literal[0]
|
||||
reveal_type(len((*[], *{}))) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
### Lists, sets and dictionaries
|
||||
|
||||
```py
|
||||
reveal_type(len([])) # revealed: int
|
||||
reveal_type(len([1])) # revealed: int
|
||||
reveal_type(len([1, 2])) # revealed: int
|
||||
reveal_type(len([*{}, *dict()])) # revealed: int
|
||||
|
||||
reveal_type(len({})) # revealed: int
|
||||
reveal_type(len({**{}})) # revealed: int
|
||||
reveal_type(len({**{}, **{}})) # revealed: int
|
||||
|
||||
reveal_type(len({1})) # revealed: int
|
||||
reveal_type(len({1, 2})) # revealed: int
|
||||
reveal_type(len({*[], 2})) # revealed: int
|
||||
|
||||
reveal_type(len(list())) # revealed: int
|
||||
reveal_type(len(set())) # revealed: int
|
||||
reveal_type(len(dict())) # revealed: int
|
||||
reveal_type(len(frozenset())) # revealed: int
|
||||
```
|
||||
|
||||
## `__len__`
|
||||
|
||||
The returned value of `__len__` is implicitly and recursively converted to `int`.
|
||||
|
||||
### Literal integers
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Zero:
|
||||
def __len__(self) -> Literal[0]:
|
||||
return 0
|
||||
|
||||
class ZeroOrOne:
|
||||
def __len__(self) -> Literal[0, 1]:
|
||||
return 0
|
||||
|
||||
class ZeroOrTrue:
|
||||
def __len__(self) -> Literal[0, True]:
|
||||
return 0
|
||||
|
||||
class OneOrFalse:
|
||||
def __len__(self) -> Literal[1] | Literal[False]:
|
||||
return 1
|
||||
|
||||
class OneOrFoo:
|
||||
def __len__(self) -> Literal[1, "foo"]:
|
||||
return 1
|
||||
|
||||
class ZeroOrStr:
|
||||
def __len__(self) -> Literal[0] | str:
|
||||
return 0
|
||||
|
||||
reveal_type(len(Zero())) # revealed: Literal[0]
|
||||
reveal_type(len(ZeroOrOne())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(ZeroOrTrue())) # revealed: Literal[0, 1]
|
||||
reveal_type(len(OneOrFalse())) # revealed: Literal[1, 0]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(OneOrFoo())) # revealed: int
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(ZeroOrStr())) # revealed: int
|
||||
```
|
||||
|
||||
### Literal booleans
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class LiteralTrue:
|
||||
def __len__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class LiteralFalse:
|
||||
def __len__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
reveal_type(len(LiteralTrue())) # revealed: Literal[1]
|
||||
reveal_type(len(LiteralFalse())) # revealed: Literal[0]
|
||||
```
|
||||
|
||||
### Enums
|
||||
|
||||
```py
|
||||
from enum import Enum, auto
|
||||
from typing import Literal
|
||||
|
||||
class SomeEnum(Enum):
|
||||
AUTO = auto()
|
||||
INT = 2
|
||||
STR = "4"
|
||||
TUPLE = (8, "16")
|
||||
INT_2 = 3_2
|
||||
|
||||
class Auto:
|
||||
def __len__(self) -> Literal[SomeEnum.AUTO]:
|
||||
return SomeEnum.AUTO
|
||||
|
||||
class Int:
|
||||
def __len__(self) -> Literal[SomeEnum.INT]:
|
||||
return SomeEnum.INT
|
||||
|
||||
class Str:
|
||||
def __len__(self) -> Literal[SomeEnum.STR]:
|
||||
return SomeEnum.STR
|
||||
|
||||
class Tuple:
|
||||
def __len__(self) -> Literal[SomeEnum.TUPLE]:
|
||||
return SomeEnum.TUPLE
|
||||
|
||||
class IntUnion:
|
||||
def __len__(self) -> Literal[SomeEnum.INT, SomeEnum.INT_2]:
|
||||
return SomeEnum.INT
|
||||
|
||||
reveal_type(len(Auto())) # revealed: int
|
||||
reveal_type(len(Int())) # revealed: int
|
||||
reveal_type(len(Str())) # revealed: int
|
||||
reveal_type(len(Tuple())) # revealed: int
|
||||
reveal_type(len(IntUnion())) # revealed: int
|
||||
```
|
||||
|
||||
### Negative integers
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Negative:
|
||||
def __len__(self) -> Literal[-1]:
|
||||
return -1
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(Negative())) # revealed: int
|
||||
```
|
||||
|
||||
### Wrong signature
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class SecondOptionalArgument:
|
||||
def __len__(self, v: int = 0) -> Literal[0]:
|
||||
return 0
|
||||
|
||||
class SecondRequiredArgument:
|
||||
def __len__(self, v: int) -> Literal[1]:
|
||||
return 1
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(SecondOptionalArgument())) # revealed: Literal[0]
|
||||
|
||||
# TODO: Emit a diagnostic
|
||||
reveal_type(len(SecondRequiredArgument())) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### No `__len__`
|
||||
|
||||
```py
|
||||
class NoDunderLen: ...
|
||||
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(len(NoDunderLen())) # revealed: int
|
||||
```
|
31
crates/ty_python_semantic/resources/mdtest/final.md
Normal file
31
crates/ty_python_semantic/resources/mdtest/final.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Tests for the `@typing(_extensions).final` decorator
|
||||
|
||||
## Cannot subclass
|
||||
|
||||
Don't do this:
|
||||
|
||||
```py
|
||||
import typing_extensions
|
||||
from typing import final
|
||||
|
||||
@final
|
||||
class A: ...
|
||||
|
||||
class B(A): ... # error: 9 [subclass-of-final-class] "Class `B` cannot inherit from final class `A`"
|
||||
|
||||
@typing_extensions.final
|
||||
class C: ...
|
||||
|
||||
class D(C): ... # error: [subclass-of-final-class]
|
||||
class E: ...
|
||||
class F: ...
|
||||
class G: ...
|
||||
|
||||
# fmt: off
|
||||
class H(
|
||||
E,
|
||||
F,
|
||||
A, # error: [subclass-of-final-class]
|
||||
G,
|
||||
): ...
|
||||
```
|
|
@ -0,0 +1,114 @@
|
|||
# Function parameter types
|
||||
|
||||
Within a function scope, the declared type of each parameter is its annotated type (or Unknown if
|
||||
not annotated). The initial inferred type is the union of the declared type with the type of the
|
||||
default value expression (if any). If both are fully static types, this union should simplify to the
|
||||
annotated type (since the default value type must be assignable to the annotated type, and for fully
|
||||
static types this means subtype-of, which simplifies in unions). But if the annotated type is
|
||||
Unknown or another non-fully-static type, the default value type may still be relevant as lower
|
||||
bound.
|
||||
|
||||
The variadic parameter is a variadic tuple of its annotated type; the variadic-keywords parameter is
|
||||
a dictionary from strings to its annotated type.
|
||||
|
||||
## Parameter kinds
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5, h: Literal[6] = 6, **kwargs: str):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: int
|
||||
reveal_type(c) # revealed: Unknown | Literal[1]
|
||||
reveal_type(d) # revealed: int
|
||||
reveal_type(e) # revealed: Unknown | Literal[3]
|
||||
reveal_type(f) # revealed: Literal[4]
|
||||
reveal_type(g) # revealed: Unknown | Literal[5]
|
||||
reveal_type(h) # revealed: Literal[6]
|
||||
# TODO: should be `tuple[object, ...]` (needs generics)
|
||||
reveal_type(args) # revealed: tuple
|
||||
reveal_type(kwargs) # revealed: dict[str, str]
|
||||
```
|
||||
|
||||
## Unannotated variadic parameters
|
||||
|
||||
...are inferred as tuple of Unknown or dict from string to Unknown.
|
||||
|
||||
```py
|
||||
def g(*args, **kwargs):
|
||||
# TODO: should be `tuple[Unknown, ...]` (needs generics)
|
||||
reveal_type(args) # revealed: tuple
|
||||
reveal_type(kwargs) # revealed: dict[str, Unknown]
|
||||
```
|
||||
|
||||
## Annotation is present but not a fully static type
|
||||
|
||||
The default value type should be a lower bound on the inferred type.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(x: Any = 1):
|
||||
reveal_type(x) # revealed: Any | Literal[1]
|
||||
```
|
||||
|
||||
## Default value type must be assignable to annotated type
|
||||
|
||||
The default value type must be assignable to the annotated type. If not, we emit a diagnostic, and
|
||||
fall back to inferring the annotated type, ignoring the default value type.
|
||||
|
||||
```py
|
||||
# error: [invalid-parameter-default]
|
||||
def f(x: int = "foo"):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# The check is assignable-to, not subtype-of, so this is fine:
|
||||
from typing import Any
|
||||
|
||||
def g(x: Any = "foo"):
|
||||
reveal_type(x) # revealed: Any | Literal["foo"]
|
||||
```
|
||||
|
||||
## Stub functions
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
### In Protocol
|
||||
|
||||
```py
|
||||
from typing import Protocol
|
||||
|
||||
class Foo(Protocol):
|
||||
def x(self, y: bool = ...): ...
|
||||
def y[T](self, y: T = ...) -> T: ...
|
||||
|
||||
class GenericFoo[T](Protocol):
|
||||
def x(self, y: bool = ...) -> T: ...
|
||||
```
|
||||
|
||||
### In abstract method
|
||||
|
||||
```py
|
||||
from abc import abstractmethod
|
||||
|
||||
class Bar:
|
||||
@abstractmethod
|
||||
def x(self, y: bool = ...): ...
|
||||
@abstractmethod
|
||||
def y[T](self, y: T = ...) -> T: ...
|
||||
```
|
||||
|
||||
### In function overload
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def x(y: None = ...) -> None: ...
|
||||
@overload
|
||||
def x(y: int) -> str: ...
|
||||
def x(y: int | None = None) -> str | None: ...
|
||||
```
|
|
@ -0,0 +1,342 @@
|
|||
# Function return type
|
||||
|
||||
When a function's return type is annotated, all return statements are checked to ensure that the
|
||||
type of the returned value is assignable to the annotated return type.
|
||||
|
||||
## Basic examples
|
||||
|
||||
A return value assignable to the annotated return type is valid.
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return 1
|
||||
```
|
||||
|
||||
The type of the value obtained by calling a function is the annotated return type, not the inferred
|
||||
return type.
|
||||
|
||||
```py
|
||||
reveal_type(f()) # revealed: int
|
||||
```
|
||||
|
||||
A `raise` is equivalent to a return of `Never`, which is assignable to any annotated return type.
|
||||
|
||||
```py
|
||||
def f() -> str:
|
||||
raise ValueError()
|
||||
|
||||
reveal_type(f()) # revealed: str
|
||||
```
|
||||
|
||||
## Stub functions
|
||||
|
||||
"Stub" function definitions (that is, function definitions with an empty body) are permissible in
|
||||
stub files, or in a few other locations: Protocol method definitions, abstract methods, and
|
||||
overloads. In this case the function body is considered to be omitted (thus no return type checking
|
||||
is performed on it), not assumed to implicitly return `None`.
|
||||
|
||||
A stub function's "empty" body may contain only an optional docstring, followed (optionally) by an
|
||||
ellipsis (`...`) or `pass`.
|
||||
|
||||
### In stub file
|
||||
|
||||
```pyi
|
||||
def f() -> int: ...
|
||||
|
||||
def f() -> int:
|
||||
pass
|
||||
|
||||
def f() -> int:
|
||||
"""Some docstring"""
|
||||
|
||||
def f() -> int:
|
||||
"""Some docstring"""
|
||||
...
|
||||
```
|
||||
|
||||
### In Protocol
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Protocol, TypeVar
|
||||
|
||||
class Bar(Protocol):
|
||||
def f(self) -> int: ...
|
||||
|
||||
class Baz(Bar):
|
||||
# error: [invalid-return-type]
|
||||
def f(self) -> int: ...
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Qux(Protocol[T]):
|
||||
def f(self) -> int: ...
|
||||
|
||||
class Foo(Protocol):
|
||||
def f[T](self, v: T) -> T: ...
|
||||
|
||||
t = (Protocol, int)
|
||||
reveal_type(t[0]) # revealed: typing.Protocol
|
||||
|
||||
class Lorem(t[0]):
|
||||
def f(self) -> int: ...
|
||||
```
|
||||
|
||||
### In abstract method
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class Foo(ABC):
|
||||
@abstractmethod
|
||||
def f(self) -> int: ...
|
||||
@abstractmethod
|
||||
def g[T](self, x: T) -> T: ...
|
||||
|
||||
class Bar[T](ABC):
|
||||
@abstractmethod
|
||||
def f(self) -> int: ...
|
||||
@abstractmethod
|
||||
def g[T](self, x: T) -> T: ...
|
||||
|
||||
# error: [invalid-return-type]
|
||||
def f() -> int: ...
|
||||
@abstractmethod # Semantically meaningless, accepted nevertheless
|
||||
def g() -> int: ...
|
||||
```
|
||||
|
||||
### In overload
|
||||
|
||||
```py
|
||||
from typing import overload
|
||||
|
||||
@overload
|
||||
def f(x: int) -> int: ...
|
||||
@overload
|
||||
def f(x: str) -> str: ...
|
||||
def f(x: int | str):
|
||||
return x
|
||||
```
|
||||
|
||||
## Conditional return type
|
||||
|
||||
```py
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
return 1
|
||||
else:
|
||||
return 2
|
||||
|
||||
def f(cond: bool) -> int | None:
|
||||
if cond:
|
||||
return 1
|
||||
else:
|
||||
return
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
return 1
|
||||
else:
|
||||
raise ValueError()
|
||||
|
||||
def f(cond: bool) -> str | int:
|
||||
if cond:
|
||||
return "a"
|
||||
else:
|
||||
return 1
|
||||
```
|
||||
|
||||
## Implicit return type
|
||||
|
||||
```py
|
||||
def f(cond: bool) -> int | None:
|
||||
if cond:
|
||||
return 1
|
||||
|
||||
# no implicit return
|
||||
def f() -> int:
|
||||
if True:
|
||||
return 1
|
||||
|
||||
# no implicit return
|
||||
def f(cond: bool) -> int:
|
||||
cond = True
|
||||
if cond:
|
||||
return 1
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
cond = True
|
||||
else:
|
||||
return 1
|
||||
if cond:
|
||||
return 2
|
||||
```
|
||||
|
||||
## Invalid return type
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
# error: [invalid-return-type]
|
||||
def f() -> int:
|
||||
1
|
||||
|
||||
def f() -> str:
|
||||
# error: [invalid-return-type]
|
||||
return 1
|
||||
|
||||
def f() -> int:
|
||||
# error: [invalid-return-type]
|
||||
return
|
||||
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [invalid-return-type]
|
||||
def m(x: T) -> T: ...
|
||||
```
|
||||
|
||||
## Invalid return type in stub file
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```pyi
|
||||
def f() -> int:
|
||||
# error: [invalid-return-type]
|
||||
return ...
|
||||
|
||||
# error: [invalid-return-type]
|
||||
def foo() -> int:
|
||||
print("...")
|
||||
...
|
||||
|
||||
# error: [invalid-return-type]
|
||||
def foo() -> int:
|
||||
f"""{foo} is a function that ..."""
|
||||
...
|
||||
```
|
||||
|
||||
## Invalid conditional return type
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
def f(cond: bool) -> str:
|
||||
if cond:
|
||||
return "a"
|
||||
else:
|
||||
# error: [invalid-return-type]
|
||||
return 1
|
||||
|
||||
def f(cond: bool) -> str:
|
||||
if cond:
|
||||
# error: [invalid-return-type]
|
||||
return 1
|
||||
else:
|
||||
# error: [invalid-return-type]
|
||||
return 2
|
||||
```
|
||||
|
||||
## Invalid implicit return type
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
def f() -> None:
|
||||
if False:
|
||||
# error: [invalid-return-type]
|
||||
return 1
|
||||
|
||||
# error: [invalid-return-type]
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
return 1
|
||||
|
||||
# error: [invalid-return-type]
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
raise ValueError()
|
||||
|
||||
# error: [invalid-return-type]
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
cond = False
|
||||
else:
|
||||
return 1
|
||||
if cond:
|
||||
return 2
|
||||
```
|
||||
|
||||
## NotImplemented
|
||||
|
||||
### Default Python version
|
||||
|
||||
`NotImplemented` is a special symbol in Python. It is commonly used to control the fallback behavior
|
||||
of special dunder methods. You can find more details in the
|
||||
[documentation](https://docs.python.org/3/library/numbers.html#implementing-the-arithmetic-operations).
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def __add__(self, o: A) -> A:
|
||||
return NotImplemented
|
||||
```
|
||||
|
||||
However, as shown below, `NotImplemented` should not cause issues with the declared return type.
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return NotImplemented
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
if cond:
|
||||
return 1
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def f(x: int) -> int | str:
|
||||
if x < 0:
|
||||
return -1
|
||||
elif x == 0:
|
||||
return NotImplemented
|
||||
else:
|
||||
return "test"
|
||||
|
||||
def f(cond: bool) -> str:
|
||||
return "hello" if cond else NotImplemented
|
||||
|
||||
def f(cond: bool) -> int:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `int`, found `Literal["hello"]`"
|
||||
return "hello" if cond else NotImplemented
|
||||
```
|
||||
|
||||
### Python 3.10+
|
||||
|
||||
Unlike Ellipsis, `_NotImplementedType` remains in `builtins.pyi` regardless of the Python version.
|
||||
Even if `builtins._NotImplementedType` is fully replaced by `types.NotImplementedType` in the
|
||||
future, it should still work as expected.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.10"
|
||||
```
|
||||
|
||||
```py
|
||||
def f() -> int:
|
||||
return NotImplemented
|
||||
|
||||
def f(cond: bool) -> str:
|
||||
return "hello" if cond else NotImplemented
|
||||
```
|
|
@ -0,0 +1,35 @@
|
|||
# Generic builtins
|
||||
|
||||
## Variadic keyword arguments with a custom `dict`
|
||||
|
||||
When we define `dict` in a custom typeshed, we must take care to define it as a generic class in the
|
||||
same way as in the real typeshed.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class object: ...
|
||||
class int: ...
|
||||
class dict[K, V, Extra]: ...
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
If we don't, then we won't be able to infer the types of variadic keyword arguments correctly.
|
||||
|
||||
```py
|
||||
def f(**kwargs):
|
||||
reveal_type(kwargs) # revealed: Unknown
|
||||
|
||||
def g(**kwargs: int):
|
||||
reveal_type(kwargs) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,443 @@
|
|||
# Generic classes: Legacy syntax
|
||||
|
||||
## Defining a generic class
|
||||
|
||||
At its simplest, to define a generic class using the legacy syntax, you inherit from the
|
||||
`typing.Generic` special form, which is "specialized" with the generic class's type variables.
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
class SingleTypevar(Generic[T]): ...
|
||||
class MultipleTypevars(Generic[T, S]): ...
|
||||
|
||||
reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S]
|
||||
```
|
||||
|
||||
You cannot use the same typevar more than once.
|
||||
|
||||
```py
|
||||
# TODO: error
|
||||
class RepeatedTypevar(Generic[T, T]): ...
|
||||
```
|
||||
|
||||
You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples).
|
||||
|
||||
```py
|
||||
# error: [invalid-argument-type] "`Literal[int]` is not a valid argument to `typing.Generic`"
|
||||
class GenericOfType(Generic[int]): ...
|
||||
```
|
||||
|
||||
You can also define a generic class by inheriting from some _other_ generic class, and specializing
|
||||
it with typevars.
|
||||
|
||||
```py
|
||||
class InheritedGeneric(MultipleTypevars[T, S]): ...
|
||||
class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ...
|
||||
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...
|
||||
|
||||
reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[T, S]
|
||||
reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None
|
||||
```
|
||||
|
||||
If you don't specialize a generic base class, we use the default specialization, which maps each
|
||||
typevar to its default value or `Any`. Since that base class is fully specialized, it does not make
|
||||
the inheriting class generic.
|
||||
|
||||
```py
|
||||
class InheritedGenericDefaultSpecialization(MultipleTypevars): ...
|
||||
|
||||
reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None
|
||||
```
|
||||
|
||||
When inheriting from a generic class, you can optionally inherit from `typing.Generic` as well. But
|
||||
if you do, you have to mention all of the typevars that you use in your other base classes.
|
||||
|
||||
```py
|
||||
class ExplicitInheritedGeneric(MultipleTypevars[T, S], Generic[T, S]): ...
|
||||
|
||||
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
|
||||
class ExplicitInheritedGenericMissingTypevar(MultipleTypevars[T, S], Generic[T]): ...
|
||||
class ExplicitInheritedGenericPartiallySpecialized(MultipleTypevars[T, int], Generic[T]): ...
|
||||
class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[T, int], Generic[T, S]): ...
|
||||
|
||||
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
|
||||
class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ...
|
||||
|
||||
reveal_type(generic_context(ExplicitInheritedGeneric)) # revealed: tuple[T, S]
|
||||
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) # revealed: tuple[T, S]
|
||||
```
|
||||
|
||||
## Specializing generic classes explicitly
|
||||
|
||||
The type parameter can be specified explicitly:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
x: T
|
||||
|
||||
reveal_type(C[int]()) # revealed: C[int]
|
||||
```
|
||||
|
||||
The specialization must match the generic types:
|
||||
|
||||
```py
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
|
||||
reveal_type(C[int, int]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the type variable has an upper bound, the specialized type must satisfy that bound:
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
BoundedT = TypeVar("BoundedT", bound=int)
|
||||
BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str])
|
||||
|
||||
class Bounded(Generic[BoundedT]): ...
|
||||
class BoundedByUnion(Generic[BoundedByUnionT]): ...
|
||||
class IntSubclass(int): ...
|
||||
|
||||
reveal_type(Bounded[int]()) # revealed: Bounded[int]
|
||||
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
|
||||
|
||||
# TODO: update this diagnostic to talk about type parameters and specializations
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`"
|
||||
reveal_type(Bounded[str]()) # revealed: Unknown
|
||||
|
||||
# TODO: update this diagnostic to talk about type parameters and specializations
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`"
|
||||
reveal_type(Bounded[int | str]()) # revealed: Unknown
|
||||
|
||||
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
|
||||
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
|
||||
reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str]
|
||||
reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str]
|
||||
```
|
||||
|
||||
If the type variable is constrained, the specialized type must satisfy those constraints:
|
||||
|
||||
```py
|
||||
ConstrainedT = TypeVar("ConstrainedT", int, str)
|
||||
|
||||
class Constrained(Generic[ConstrainedT]): ...
|
||||
|
||||
reveal_type(Constrained[int]()) # revealed: Constrained[int]
|
||||
|
||||
# TODO: error: [invalid-argument-type]
|
||||
# TODO: revealed: Constrained[Unknown]
|
||||
reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass]
|
||||
|
||||
reveal_type(Constrained[str]()) # revealed: Constrained[str]
|
||||
|
||||
# TODO: error: [invalid-argument-type]
|
||||
# TODO: revealed: Unknown
|
||||
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
|
||||
|
||||
# TODO: update this diagnostic to talk about type parameters and specializations
|
||||
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`"
|
||||
reveal_type(Constrained[object]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring generic class parameters
|
||||
|
||||
We can infer the type parameter from a type context:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
x: T
|
||||
|
||||
c: C[int] = C()
|
||||
# TODO: revealed: C[int]
|
||||
reveal_type(c) # revealed: C[Unknown]
|
||||
```
|
||||
|
||||
The typevars of a fully specialized generic class should no longer be visible:
|
||||
|
||||
```py
|
||||
# TODO: revealed: int
|
||||
reveal_type(c.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the type parameter is not specified explicitly, and there are no constraints that let us infer a
|
||||
specific type, we infer the typevar's default type:
|
||||
|
||||
```py
|
||||
DefaultT = TypeVar("DefaultT", default=int)
|
||||
|
||||
class D(Generic[DefaultT]): ...
|
||||
|
||||
reveal_type(D()) # revealed: D[int]
|
||||
```
|
||||
|
||||
If a typevar does not provide a default, we use `Unknown`:
|
||||
|
||||
```py
|
||||
reveal_type(C()) # revealed: C[Unknown]
|
||||
```
|
||||
|
||||
## Inferring generic class parameters from constructors
|
||||
|
||||
If the type of a constructor parameter is a class typevar, we can use that to infer the type
|
||||
parameter. The types inferred from a type context and from a constructor parameter must be
|
||||
consistent with each other.
|
||||
|
||||
### `__new__` only
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### `__init__` only
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### Identical `__new__` and `__init__` signatures
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### Compatible `__new__` and `__init__` signatures
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __new__(cls, *args, **kwargs) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
|
||||
class D(Generic[T]):
|
||||
def __new__(cls, x: T) -> "D[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
|
||||
reveal_type(D(1)) # revealed: D[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
|
||||
wrong_innards: D[int] = D("five")
|
||||
```
|
||||
|
||||
### Both present, `__new__` inherited from a generic base class
|
||||
|
||||
If either method comes from a generic base class, we don't currently use its inferred specialization
|
||||
to specialize the class.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
V = TypeVar("V")
|
||||
|
||||
class C(Generic[T, U]):
|
||||
def __new__(cls, *args, **kwargs) -> "C[T, U]":
|
||||
return object.__new__(cls)
|
||||
|
||||
class D(C[V, int]):
|
||||
def __init__(self, x: V) -> None: ...
|
||||
|
||||
reveal_type(D(1)) # revealed: D[Literal[1]]
|
||||
```
|
||||
|
||||
### `__init__` is itself generic
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
S = TypeVar("S")
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __init__(self, x: T, y: S) -> None: ...
|
||||
|
||||
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
|
||||
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
|
||||
reveal_type(C(1, True)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five", 1)
|
||||
```
|
||||
|
||||
## Generic subclass
|
||||
|
||||
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
|
||||
propagate through:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]):
|
||||
x: T | None = None
|
||||
|
||||
class ExplicitlyGenericSub(Base[T], Generic[T]): ...
|
||||
class ImplicitlyGenericSub(Base[T]): ...
|
||||
|
||||
reveal_type(Base[int].x) # revealed: int | None
|
||||
reveal_type(ExplicitlyGenericSub[int].x) # revealed: int | None
|
||||
reveal_type(ImplicitlyGenericSub[int].x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Generic methods
|
||||
|
||||
Generic classes can contain methods that are themselves generic. The generic methods can refer to
|
||||
the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in
|
||||
scope for the method.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
class C(Generic[T]):
|
||||
def method(self, u: U) -> U:
|
||||
return u
|
||||
|
||||
c: C[int] = C[int]()
|
||||
reveal_type(c.method("string")) # revealed: Literal["string"]
|
||||
```
|
||||
|
||||
## Cyclic class definitions
|
||||
|
||||
### F-bounded quantification
|
||||
|
||||
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
|
||||
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
|
||||
|
||||
#### In a stub file
|
||||
|
||||
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
|
||||
|
||||
```pyi
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]): ...
|
||||
class Sub(Base[Sub]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
#### With string forward references
|
||||
|
||||
A similar case can work in a non-stub file, if forward references are stringified:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]): ...
|
||||
class Sub(Base["Sub"]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
#### Without string forward references
|
||||
|
||||
In a non-stub file, without stringified forward references, this raises a `NameError`:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]): ...
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class Sub(Base[Sub]): ...
|
||||
```
|
||||
|
||||
### Cyclic inheritance as a generic parameter
|
||||
|
||||
```pyi
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Derived(list[Derived[T]], Generic[T]): ...
|
||||
```
|
||||
|
||||
### Direct cyclic inheritance
|
||||
|
||||
Inheritance that would result in a cyclic MRO is detected as an error.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class C(C, Generic[T]): ...
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class D(D[int], Generic[T]): ...
|
||||
```
|
||||
|
||||
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
|
||||
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification
|
|
@ -0,0 +1,272 @@
|
|||
# Generic functions: Legacy syntax
|
||||
|
||||
## Typevar must be used at least twice
|
||||
|
||||
If you're only using a typevar for a single parameter, you don't need the typevar — just use
|
||||
`object` (or the typevar's upper bound):
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# TODO: error, should be (x: object)
|
||||
def typevar_not_needed(x: T) -> None:
|
||||
pass
|
||||
|
||||
BoundedT = TypeVar("BoundedT", bound=int)
|
||||
|
||||
# TODO: error, should be (x: int)
|
||||
def bounded_typevar_not_needed(x: BoundedT) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
Typevars are only needed if you use them more than once. For instance, to specify that two
|
||||
parameters must both have the same type:
|
||||
|
||||
```py
|
||||
def two_params(x: T, y: T) -> T:
|
||||
return x
|
||||
```
|
||||
|
||||
or to specify that a return value is the same as a parameter:
|
||||
|
||||
```py
|
||||
def return_value(x: T) -> T:
|
||||
return x
|
||||
```
|
||||
|
||||
Each typevar must also appear _somewhere_ in the parameter list:
|
||||
|
||||
```py
|
||||
def absurd() -> T:
|
||||
# There's no way to construct a T!
|
||||
raise ValueError("absurd")
|
||||
```
|
||||
|
||||
## Inferring generic function parameter types
|
||||
|
||||
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
|
||||
is bound to at each call site.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: Literal[1]
|
||||
reveal_type(f(1.0)) # revealed: float
|
||||
reveal_type(f(True)) # revealed: Literal[True]
|
||||
reveal_type(f("string")) # revealed: Literal["string"]
|
||||
```
|
||||
|
||||
## Inferring “deep” generic parameter types
|
||||
|
||||
The matching up of call arguments and discovery of constraints on typevars can be a recursive
|
||||
process for arbitrarily-nested generic types in parameters.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(x: list[T]) -> T:
|
||||
return x[0]
|
||||
|
||||
# TODO: revealed: float
|
||||
reveal_type(f([1.0, 2.0])) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring a bound typevar
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
T = TypeVar("T", bound=int)
|
||||
|
||||
def f(x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: Literal[1]
|
||||
reveal_type(f(True)) # revealed: Literal[True]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("string")) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring a constrained typevar
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
T = TypeVar("T", int, None)
|
||||
|
||||
def f(x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: int
|
||||
reveal_type(f(True)) # revealed: int
|
||||
reveal_type(f(None)) # revealed: None
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("string")) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Typevar constraints
|
||||
|
||||
If a type parameter has an upper bound, that upper bound constrains which types can be used for that
|
||||
typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar
|
||||
in the function.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", bound=int)
|
||||
|
||||
def good_param(x: T) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
If the function is annotated as returning the typevar, this means that the upper bound is _not_
|
||||
assignable to that typevar, since return types are contravariant. In `bad`, we can infer that
|
||||
`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the
|
||||
return value is not guaranteed to be compatible for all `T: int`.
|
||||
|
||||
```py
|
||||
def good_return(x: T) -> T:
|
||||
return x
|
||||
|
||||
def bad_return(x: T) -> T:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`"
|
||||
return x + 1
|
||||
```
|
||||
|
||||
## All occurrences of the same typevar have the same type
|
||||
|
||||
If a typevar appears multiple times in a function signature, all occurrences have the same type.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
def different_types(cond: bool, t: T, s: S) -> T:
|
||||
if cond:
|
||||
return t
|
||||
else:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`"
|
||||
return s
|
||||
|
||||
def same_types(cond: bool, t1: T, t2: T) -> T:
|
||||
if cond:
|
||||
return t1
|
||||
else:
|
||||
return t2
|
||||
```
|
||||
|
||||
## All occurrences of the same constrained typevar have the same type
|
||||
|
||||
The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__`
|
||||
methods that are compatible with the return type, so the `return` expression is always well-typed:
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", int, str)
|
||||
|
||||
def same_constrained_types(t1: T, t2: T) -> T:
|
||||
# TODO: no error
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`"
|
||||
return t1 + t2
|
||||
```
|
||||
|
||||
This is _not_ the same as a union type, because of this additional constraint that the two
|
||||
occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types,
|
||||
and an `int` and a `str` cannot be added together:
|
||||
|
||||
```py
|
||||
def unions_are_different(t1: int | str, t2: int | str) -> int | str:
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
|
||||
return t1 + t2
|
||||
```
|
||||
|
||||
## Typevar inference is a unification problem
|
||||
|
||||
When inferring typevar assignments in a generic function call, we cannot simply solve constraints
|
||||
eagerly for each parameter in turn. We must solve a unification problem involving all of the
|
||||
parameters simultaneously.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def two_params(x: T, y: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]
|
||||
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
|
||||
```
|
||||
|
||||
When one of the parameters is a union, we attempt to find the smallest specialization that satisfies
|
||||
all of the constraints.
|
||||
|
||||
```py
|
||||
def union_param(x: T | None) -> T:
|
||||
if x is None:
|
||||
raise ValueError
|
||||
return x
|
||||
|
||||
reveal_type(union_param("a")) # revealed: Literal["a"]
|
||||
reveal_type(union_param(1)) # revealed: Literal[1]
|
||||
reveal_type(union_param(None)) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py
|
||||
def union_and_nonunion_params(x: T | int, y: T) -> T:
|
||||
return y
|
||||
|
||||
reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"]
|
||||
reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"]
|
||||
reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1]
|
||||
reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1]
|
||||
reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1]
|
||||
```
|
||||
|
||||
```py
|
||||
S = TypeVar("S")
|
||||
|
||||
def tuple_param(x: T | S, y: tuple[T, S]) -> tuple[T, S]:
|
||||
return y
|
||||
|
||||
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
|
||||
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
|
||||
```
|
||||
|
||||
## Inferring nested generic function calls
|
||||
|
||||
We can infer type assignments in nested calls to multiple generic functions. If they use the same
|
||||
type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(x: T) -> tuple[T, int]:
|
||||
return (x, 1)
|
||||
|
||||
def g(x: T) -> T | None:
|
||||
return x
|
||||
|
||||
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
|
||||
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
|
||||
```
|
|
@ -0,0 +1,126 @@
|
|||
# Legacy type variables
|
||||
|
||||
The tests in this file focus on how type variables are defined using the legacy notation. Most
|
||||
_uses_ of type variables are tested in other files in this directory; we do not duplicate every test
|
||||
for both type variable syntaxes.
|
||||
|
||||
Unless otherwise specified, all quotations come from the [Generics] section of the typing spec.
|
||||
|
||||
## Type variables
|
||||
|
||||
### Defining legacy type variables
|
||||
|
||||
> Generics can be parameterized by using a factory available in `typing` called `TypeVar`.
|
||||
|
||||
This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available
|
||||
in newer Python releases.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
reveal_type(type(T)) # revealed: Literal[TypeVar]
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
|
||||
### Directly assigned to a variable
|
||||
|
||||
> A `TypeVar()` expression must always directly be assigned to a variable (it should not be used as
|
||||
> part of a larger expression).
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
# TODO: no error
|
||||
# error: [invalid-legacy-type-variable]
|
||||
U: TypeVar = TypeVar("U")
|
||||
|
||||
# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
|
||||
TestList = list[TypeVar("W")]
|
||||
```
|
||||
|
||||
### `TypeVar` parameter must match variable name
|
||||
|
||||
> The argument to `TypeVar()` must be a string equal to the variable name to which it is assigned.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)"
|
||||
T = TypeVar("Q")
|
||||
```
|
||||
|
||||
### No redefinition
|
||||
|
||||
> Type variables must not be redefined.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# TODO: error
|
||||
T = TypeVar("T")
|
||||
```
|
||||
|
||||
### Type variables with a default
|
||||
|
||||
Note that the `__default__` property is only available in Python ≥3.13.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", default=int)
|
||||
reveal_type(T.__default__) # revealed: int
|
||||
reveal_type(T.__bound__) # revealed: None
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
|
||||
S = TypeVar("S")
|
||||
reveal_type(S.__default__) # revealed: NoDefault
|
||||
```
|
||||
|
||||
### Type variables with an upper bound
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", bound=int)
|
||||
reveal_type(T.__bound__) # revealed: int
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
|
||||
S = TypeVar("S")
|
||||
reveal_type(S.__bound__) # revealed: None
|
||||
```
|
||||
|
||||
### Type variables with constraints
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", int, str)
|
||||
reveal_type(T.__constraints__) # revealed: tuple[int, str]
|
||||
|
||||
S = TypeVar("S")
|
||||
reveal_type(S.__constraints__) # revealed: tuple[()]
|
||||
```
|
||||
|
||||
### Cannot have only one constraint
|
||||
|
||||
> `TypeVar` supports constraining parametric types to a fixed set of possible types...There should
|
||||
> be at least two constraints, if any; specifying a single constraint is disallowed.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
# TODO: error: [invalid-type-variable-constraints]
|
||||
T = TypeVar("T", int)
|
||||
```
|
||||
|
||||
[generics]: https://typing.python.org/en/latest/spec/generics.html
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue