[ty] support imported PEP 613 type aliases

This commit is contained in:
Carl Meyer 2025-11-11 17:21:43 -08:00
parent fb5b8c3653
commit df0c8e202d
No known key found for this signature in database
GPG key ID: 2D1FB7916A52E121
20 changed files with 297 additions and 252 deletions

View file

@ -143,7 +143,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
525,
600,
);
static PANDAS: Benchmark = Benchmark::new(
@ -163,7 +163,7 @@ static PANDAS: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
3000,
4000,
);
static PYDANTIC: Benchmark = Benchmark::new(
@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY39,
},
5000,
7000,
);
static SYMPY: Benchmark = Benchmark::new(

View file

@ -12,11 +12,8 @@ P = ParamSpec("P")
Ts = TypeVarTuple("Ts")
R_co = TypeVar("R_co", covariant=True)
Alias: TypeAlias = int
def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
reveal_type(args) # revealed: tuple[@Todo(`Unpack[]` special form), ...]
reveal_type(Alias) # revealed: @Todo(Support for `typing.TypeAlias`)
return args
def g() -> TypeGuard[int]: ...

View file

@ -2208,9 +2208,9 @@ reveal_type(False.real) # revealed: Literal[0]
All attribute access on literal `bytes` types is currently delegated to `builtins.bytes`:
```py
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[@Todo(Support for `typing.TypeAlias`)], /) -> bytes
# revealed: bound method Literal[b"foo"].join(iterable_of_bytes: Iterable[Buffer], /) -> bytes
reveal_type(b"foo".join)
# revealed: bound method Literal[b"foo"].endswith(suffix: @Todo(Support for `typing.TypeAlias`) | tuple[@Todo(Support for `typing.TypeAlias`), ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
# revealed: bound method Literal[b"foo"].endswith(suffix: Buffer | tuple[Buffer, ...], start: SupportsIndex | None = None, end: SupportsIndex | None = None, /) -> bool
reveal_type(b"foo".endswith)
```

View file

@ -313,8 +313,7 @@ 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(b"foo" + A()) # revealed: A
reveal_type(A() + ()) # revealed: A
reveal_type(() + A()) # revealed: A

View file

@ -54,10 +54,8 @@ 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
reveal_type(2**x) # revealed: Any
reveal_type(x**x) # revealed: Any
```
If the second argument is \<0, a `float` is returned at runtime. If the first argument is \<0 but

View file

@ -598,9 +598,9 @@ from typing_extensions import Self
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
reveal_type(int.__new__)
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
reveal_type((42).__new__)
class X:

View file

@ -10,13 +10,13 @@ import pickle
reveal_type(open("")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(open("", "r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(open("", "rb")) # revealed: @Todo(`builtins.open` return type)
reveal_type(open("", "rb")) # revealed: BufferedReader[_BufferedReaderStream]
with open("foo.pickle", "rb") as f:
x = pickle.load(f) # fine
def _(mode: str):
reveal_type(open("", mode)) # revealed: @Todo(`builtins.open` return type)
reveal_type(open("", mode)) # revealed: IO[Any]
```
## `os.fdopen`
@ -29,7 +29,7 @@ import os
reveal_type(os.fdopen(0)) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(os.fdopen(0, "r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(os.fdopen(0, "rb")) # revealed: @Todo(`os.fdopen` return type)
reveal_type(os.fdopen(0, "rb")) # revealed: BufferedReader[_BufferedReaderStream]
with os.fdopen(0, "rb") as f:
x = pickle.load(f) # fine
@ -43,9 +43,9 @@ And similarly for `Path.open()`:
from pathlib import Path
import pickle
reveal_type(Path("").open()) # revealed: @Todo(`Path.open` return type)
reveal_type(Path("").open("r")) # revealed: @Todo(`Path.open` return type)
reveal_type(Path("").open("rb")) # revealed: @Todo(`Path.open` return type)
reveal_type(Path("").open()) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(Path("").open("r")) # revealed: TextIOWrapper[_WrappedBuffer]
reveal_type(Path("").open("rb")) # revealed: BufferedReader[_BufferedReaderStream]
with Path("foo.pickle").open("rb") as f:
x = pickle.load(f) # fine
@ -61,7 +61,7 @@ import pickle
reveal_type(NamedTemporaryFile()) # revealed: _TemporaryFileWrapper[bytes]
reveal_type(NamedTemporaryFile("r")) # revealed: _TemporaryFileWrapper[str]
reveal_type(NamedTemporaryFile("rb")) # revealed: @Todo(`tempfile.NamedTemporaryFile` return type)
reveal_type(NamedTemporaryFile("rb")) # revealed: _TemporaryFileWrapper[bytes]
with NamedTemporaryFile("rb") as f:
x = pickle.load(f) # fine

View file

@ -127,7 +127,7 @@ x = lambda y: y
reveal_type(x.__code__) # revealed: CodeType
reveal_type(x.__name__) # revealed: str
reveal_type(x.__defaults__) # revealed: tuple[Any, ...] | None
reveal_type(x.__annotations__) # revealed: dict[str, @Todo(Support for `typing.TypeAlias`)]
reveal_type(x.__annotations__) # revealed: dict[str, Any]
reveal_type(x.__dict__) # revealed: dict[str, Any]
reveal_type(x.__doc__) # revealed: str | None
reveal_type(x.__kwdefaults__) # revealed: dict[str, Any] | None

View file

@ -1,8 +1,82 @@
# PEP 613 type aliases
## No panics
PEP 613 type aliases are simple assignment statements, annotated with `typing.TypeAlias` to mark
them as a type alias. At runtime, they behave the same as implicit type aliases. Our support for
them is currently the same as for implicit type aliases, but we don't reproduce the full
implicit-type-alias test suite here, just some particularly interesting cases.
We do not fully support PEP 613 type aliases yet. For now, just make sure that we don't panic:
## Basic
### as `TypeAlias`
```py
from typing import TypeAlias
IntOrStr: TypeAlias = int | str
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
### as `typing.TypeAlias`
```py
import typing
IntOrStr: typing.TypeAlias = int | str
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
## Can be used as value
Because PEP 613 type aliases are just annotated assignments, they can be used as values, like a
legacy type expression (and unlike a PEP 695 type alias). We might prefer this wasn't allowed, but
people do use it.
```py
from typing import TypeAlias
MyExc: TypeAlias = Exception
try:
raise MyExc("error")
except MyExc as e:
reveal_type(e) # revealed: Exception
```
## Imported
`alias.py`:
```py
from typing import TypeAlias
MyAlias: TypeAlias = int | str
```
`main.py`:
```py
from alias import MyAlias
def _(x: MyAlias):
reveal_type(x) # revealed: int | str
```
## String literal in RHS
```py
from typing import TypeAlias
IntOrStr: TypeAlias = "int | str"
def _(x: IntOrStr):
reveal_type(x) # revealed: int | str
```
## Cyclic
```py
from typing import TypeAlias
@ -18,6 +92,26 @@ def _(rec: RecursiveHomogeneousTuple):
reveal_type(rec) # revealed: tuple[Divergent, ...]
```
## Conditionally imported on Python < 3.10
```toml
[environment]
python-version = "3.9"
```
```py
try:
# error: [unresolved-import]
from typing import TypeAlias
except ImportError:
from typing_extensions import TypeAlias
MyAlias: TypeAlias = int
def _(x: MyAlias):
reveal_type(x) # revealed: int
```
## PEP-613 aliases in stubs are deferred
Although the right-hand side of a PEP-613 alias is a value expression, inference of this value is
@ -46,7 +140,31 @@ f(stub.B())
class Unrelated: ...
# TODO: we should emit `[invalid-argument-type]` here
# (the alias is a `@Todo` because it's imported from another file)
# error: [invalid-argument-type]
f(Unrelated())
```
## Invalid position
`typing.TypeAlias` must be used as the sole annotation in an annotated assignment. Use in any other
context is an error.
```py
from typing import TypeAlias
# error: [invalid-type-form]
def _(x: TypeAlias):
reveal_type(x) # revealed: Unknown
# error: [invalid-type-form]
y: list[TypeAlias] = []
```
## RHS is required
```py
from typing import TypeAlias
# error: [invalid-type-form]
Empty: TypeAlias
```

View file

@ -28,7 +28,7 @@ def f() -> None:
```py
type IntOrStr = int | str
reveal_type(IntOrStr.__value__) # revealed: @Todo(Support for `typing.TypeAlias`)
reveal_type(IntOrStr.__value__) # revealed: Any
```
## Invalid assignment

View file

@ -122,7 +122,7 @@ properties on instance types:
```py
reveal_type(sys.version_info.micro) # revealed: int
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(Support for `typing.TypeAlias`)
reveal_type(sys.version_info.releaselevel) # revealed: Literal["alpha", "beta", "candidate", "final"]
reveal_type(sys.version_info.serial) # revealed: int
```

View file

@ -4369,11 +4369,6 @@ impl<'db> Type<'db> {
Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(literal)),
)
.into(),
Type::NominalInstance(instance)
if instance.has_known_class(db, KnownClass::Path) && name == "open" =>
{
Place::bound(Type::KnownBoundMethod(KnownBoundMethodType::PathOpen)).into()
}
Type::ClassLiteral(class)
if name == "range" && class.is_known(db, KnownClass::ConstraintSet) =>
@ -6737,6 +6732,7 @@ impl<'db> Type<'db> {
Ok(ty.inner(db).to_meta_type(db))
}
KnownInstanceType::LiteralStringAlias(ty) => Ok(ty.inner(db)),
},
Type::SpecialForm(special_form) => match special_form {
@ -6791,7 +6787,15 @@ impl<'db> Type<'db> {
Ok(typing_self(db, scope_id, typevar_binding_context, class).unwrap_or(*self))
}
SpecialFormType::TypeAlias => Ok(Type::Dynamic(DynamicType::TodoTypeAlias)),
// We ensure that `typing.TypeAlias` used in the expected position (annotating an
// annotated assignment statement) doesn't reach here. Using it in any other type
// expression is an error.
SpecialFormType::TypeAlias => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec_inline![
InvalidTypeExpression::TypeAlias
],
fallback_type: Type::unknown(),
}),
SpecialFormType::TypedDict => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec_inline![
InvalidTypeExpression::TypedDict
@ -7269,7 +7273,6 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::KnownBoundMethod(
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@ -7429,7 +7432,6 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(_)
| Type::KnownBoundMethod(
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@ -7980,6 +7982,9 @@ pub enum KnownInstanceType<'db> {
/// An instance of `typing.GenericAlias` representing a `type[...]` expression.
TypeGenericAlias(InternedType<'db>),
/// A literal string which is the right-hand side of a PEP 613 `TypeAlias`.
LiteralStringAlias(InternedType<'db>),
/// An identity callable created with `typing.NewType(name, base)`, which behaves like a
/// subtype of `base` in type expressions. See the `struct NewType` payload for an example.
NewType(NewType<'db>),
@ -8016,7 +8021,8 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
}
KnownInstanceType::Literal(ty)
| KnownInstanceType::Annotated(ty)
| KnownInstanceType::TypeGenericAlias(ty) => {
| KnownInstanceType::TypeGenericAlias(ty)
| KnownInstanceType::LiteralStringAlias(ty) => {
visitor.visit_type(db, ty.inner(db));
}
KnownInstanceType::NewType(newtype) => {
@ -8064,6 +8070,9 @@ impl<'db> KnownInstanceType<'db> {
Self::Literal(ty) => Self::Literal(ty.normalized_impl(db, visitor)),
Self::Annotated(ty) => Self::Annotated(ty.normalized_impl(db, visitor)),
Self::TypeGenericAlias(ty) => Self::TypeGenericAlias(ty.normalized_impl(db, visitor)),
Self::LiteralStringAlias(ty) => {
Self::LiteralStringAlias(ty.normalized_impl(db, visitor))
}
Self::NewType(newtype) => Self::NewType(
newtype
.map_base_class_type(db, |class_type| class_type.normalized_impl(db, visitor)),
@ -8089,6 +8098,7 @@ impl<'db> KnownInstanceType<'db> {
Self::Literal(_) | Self::Annotated(_) | Self::TypeGenericAlias(_) => {
KnownClass::GenericAlias
}
Self::LiteralStringAlias(_) => KnownClass::Str,
Self::NewType(_) => KnownClass::NewType,
}
}
@ -8175,6 +8185,7 @@ impl<'db> KnownInstanceType<'db> {
f.write_str("<typing.Annotated special form>")
}
KnownInstanceType::TypeGenericAlias(_) => f.write_str("GenericAlias"),
KnownInstanceType::LiteralStringAlias(_) => f.write_str("str"),
KnownInstanceType::NewType(declaration) => {
write!(f, "<NewType pseudo-class '{}'>", declaration.name(self.db))
}
@ -8216,9 +8227,6 @@ pub enum DynamicType<'db> {
///
/// This variant should be created with the `todo_type!` macro.
Todo(TodoType),
/// A special Todo-variant for type aliases declared using `typing.TypeAlias`.
/// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions.
TodoTypeAlias,
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
TodoUnpack,
/// A type that is determined to be divergent during type inference for a recursive function.
@ -8250,13 +8258,6 @@ impl std::fmt::Display for DynamicType<'_> {
f.write_str("@Todo")
}
}
DynamicType::TodoTypeAlias => {
if cfg!(debug_assertions) {
f.write_str("@Todo(Support for `typing.TypeAlias`)")
} else {
f.write_str("@Todo")
}
}
DynamicType::Divergent(_) => f.write_str("Divergent"),
}
}
@ -8415,6 +8416,9 @@ enum InvalidTypeExpression<'db> {
ConstraintSet,
/// Same for `typing.TypedDict`
TypedDict,
/// Same for `typing.TypeAlias`, anywhere except for as the sole annotation on an annotated
/// assignment
TypeAlias,
/// Type qualifiers are always invalid in *type expressions*,
/// but these ones are okay with 0 arguments in *annotation expressions*
TypeQualifier(SpecialFormType),
@ -8470,6 +8474,11 @@ impl<'db> InvalidTypeExpression<'db> {
"The special form `typing.TypedDict` is not allowed in type expressions. \
Did you mean to use a concrete TypedDict or `collections.abc.Mapping[str, object]` instead?")
}
InvalidTypeExpression::TypeAlias => {
f.write_str(
"`typing.TypeAlias` is only allowed as the sole annotation on an annotated assignment",
)
}
InvalidTypeExpression::TypeQualifier(qualifier) => write!(
f,
"Type qualifier `{qualifier}` is not allowed in type expressions \
@ -10910,8 +10919,6 @@ pub enum KnownBoundMethodType<'db> {
/// this allows us to understand statically known branches for common tests such as
/// `if sys.platform.startswith("freebsd")`.
StrStartswith(StringLiteralType<'db>),
/// Method wrapper for `Path.open`,
PathOpen,
// ConstraintSet methods
ConstraintSetRange,
@ -10943,8 +10950,7 @@ pub(super) fn walk_method_wrapper_type<'db, V: visitor::TypeVisitor<'db> + ?Size
KnownBoundMethodType::StrStartswith(string_literal) => {
visitor.visit_type(db, Type::StringLiteral(string_literal));
}
KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
| KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_)
@ -11001,8 +11007,7 @@ impl<'db> KnownBoundMethodType<'db> {
ConstraintSet::from(self == other)
}
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen)
| (
(
KnownBoundMethodType::ConstraintSetRange,
KnownBoundMethodType::ConstraintSetRange,
)
@ -11033,7 +11038,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@ -11045,7 +11049,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@ -11087,8 +11090,7 @@ impl<'db> KnownBoundMethodType<'db> {
ConstraintSet::from(self == other)
}
(KnownBoundMethodType::PathOpen, KnownBoundMethodType::PathOpen)
| (
(
KnownBoundMethodType::ConstraintSetRange,
KnownBoundMethodType::ConstraintSetRange,
)
@ -11122,7 +11124,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@ -11134,7 +11135,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_)
| KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@ -11160,7 +11160,6 @@ impl<'db> KnownBoundMethodType<'db> {
KnownBoundMethodType::PropertyDunderSet(property.normalized_impl(db, visitor))
}
KnownBoundMethodType::StrStartswith(_)
| KnownBoundMethodType::PathOpen
| KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@ -11178,7 +11177,6 @@ impl<'db> KnownBoundMethodType<'db> {
| KnownBoundMethodType::PropertyDunderGet(_)
| KnownBoundMethodType::PropertyDunderSet(_) => KnownClass::MethodWrapperType,
KnownBoundMethodType::StrStartswith(_) => KnownClass::BuiltinFunctionType,
KnownBoundMethodType::PathOpen => KnownClass::MethodType,
KnownBoundMethodType::ConstraintSetRange
| KnownBoundMethodType::ConstraintSetAlways
| KnownBoundMethodType::ConstraintSetNever
@ -11283,9 +11281,6 @@ impl<'db> KnownBoundMethodType<'db> {
Some(KnownClass::Bool.to_instance(db)),
)))
}
KnownBoundMethodType::PathOpen => {
Either::Right(std::iter::once(Signature::todo("`Path.open` return type")))
}
KnownBoundMethodType::ConstraintSetRange => {
Either::Right(std::iter::once(Signature::new(

View file

@ -48,9 +48,7 @@ impl<'db> ClassBase<'db> {
ClassBase::Class(class) => class.name(db),
ClassBase::Dynamic(DynamicType::Any) => "Any",
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
ClassBase::Dynamic(
DynamicType::Todo(_) | DynamicType::TodoTypeAlias | DynamicType::TodoUnpack,
) => "@Todo",
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack) => "@Todo",
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
ClassBase::Protocol => "Protocol",
ClassBase::Generic => "Generic",
@ -176,6 +174,7 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::ConstraintSet(_)
| KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_)
| KnownInstanceType::LiteralStringAlias(_)
// A class inheriting from a newtype would make intuitive sense, but newtype
// wrappers are just identity callables at runtime, so this sort of inheritance
// doesn't work and isn't allowed.

View file

@ -520,9 +520,6 @@ impl Display for DisplayRepresentation<'_> {
Type::KnownBoundMethod(KnownBoundMethodType::StrStartswith(_)) => {
f.write_str("<method-wrapper `startswith` of `str` object>")
}
Type::KnownBoundMethod(KnownBoundMethodType::PathOpen) => {
f.write_str("bound method `Path.open`")
}
Type::KnownBoundMethod(KnownBoundMethodType::ConstraintSetRange) => {
f.write_str("bound method `ConstraintSet.range`")
}

View file

@ -83,7 +83,7 @@ use crate::types::{
ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor,
HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType,
NormalizedVisitor, SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation,
UnionBuilder, binding_type, todo_type, walk_signature,
UnionBuilder, binding_type, walk_signature,
};
use crate::{Db, FxOrderSet, ModuleName, resolve_module};
@ -1152,70 +1152,6 @@ fn is_instance_truthiness<'db>(
}
}
/// Return true, if the type passed as `mode` would require us to pick a non-trivial overload of
/// `builtins.open` / `os.fdopen` / `Path.open`.
fn is_mode_with_nontrivial_return_type<'db>(db: &'db dyn Db, mode: Type<'db>) -> bool {
// Return true for any mode that doesn't match typeshed's
// `OpenTextMode` type alias (<https://github.com/python/typeshed/blob/6937a9b193bfc2f0696452d58aad96d7627aa29a/stdlib/_typeshed/__init__.pyi#L220>).
mode.as_string_literal().is_none_or(|mode| {
!matches!(
mode.value(db),
"r+" | "+r"
| "rt+"
| "r+t"
| "+rt"
| "tr+"
| "t+r"
| "+tr"
| "w+"
| "+w"
| "wt+"
| "w+t"
| "+wt"
| "tw+"
| "t+w"
| "+tw"
| "a+"
| "+a"
| "at+"
| "a+t"
| "+at"
| "ta+"
| "t+a"
| "+ta"
| "x+"
| "+x"
| "xt+"
| "x+t"
| "+xt"
| "tx+"
| "t+x"
| "+tx"
| "w"
| "wt"
| "tw"
| "a"
| "at"
| "ta"
| "x"
| "xt"
| "tx"
| "r"
| "rt"
| "tr"
| "U"
| "rU"
| "Ur"
| "rtU"
| "rUt"
| "Urt"
| "trU"
| "tUr"
| "Utr"
)
})
}
fn signature_cycle_initial<'db>(
_db: &'db dyn Db,
_id: salsa::Id,
@ -1268,16 +1204,6 @@ pub enum KnownFunction {
DunderImport,
/// `importlib.import_module`, which returns the submodule.
ImportModule,
/// `builtins.open`
Open,
/// `os.fdopen`
Fdopen,
/// `tempfile.NamedTemporaryFile`
#[strum(serialize = "NamedTemporaryFile")]
NamedTemporaryFile,
/// `typing(_extensions).final`
Final,
/// `typing(_extensions).disjoint_base`
@ -1376,7 +1302,6 @@ impl KnownFunction {
| Self::HasAttr
| Self::Len
| Self::Repr
| Self::Open
| Self::DunderImport => module.is_builtins(),
Self::AssertType
| Self::AssertNever
@ -1396,12 +1321,6 @@ impl KnownFunction {
Self::AbstractMethod => {
matches!(module, KnownModule::Abc)
}
Self::Fdopen => {
matches!(module, KnownModule::Os)
}
Self::NamedTemporaryFile => {
matches!(module, KnownModule::Tempfile)
}
Self::Dataclass | Self::Field => {
matches!(module, KnownModule::Dataclasses)
}
@ -1871,38 +1790,6 @@ impl KnownFunction {
overload.set_return_type(Type::module_literal(db, file, module));
}
KnownFunction::Open => {
// TODO: Temporary special-casing for `builtins.open` to avoid an excessive number of
// false positives in lieu of proper support for PEP-613 type aliases.
if let [_, Some(mode), ..] = parameter_types
&& is_mode_with_nontrivial_return_type(db, *mode)
{
overload.set_return_type(todo_type!("`builtins.open` return type"));
}
}
KnownFunction::Fdopen => {
// TODO: Temporary special-casing for `os.fdopen` to avoid an excessive number of
// false positives in lieu of proper support for PEP-613 type aliases.
if let [_, Some(mode), ..] = parameter_types
&& is_mode_with_nontrivial_return_type(db, *mode)
{
overload.set_return_type(todo_type!("`os.fdopen` return type"));
}
}
KnownFunction::NamedTemporaryFile => {
// TODO: Temporary special-casing for `tempfile.NamedTemporaryFile` to avoid an excessive number of
// false positives in lieu of proper support for PEP-613 type aliases.
if let [Some(mode), ..] = parameter_types
&& is_mode_with_nontrivial_return_type(db, *mode)
{
overload
.set_return_type(todo_type!("`tempfile.NamedTemporaryFile` return type"));
}
}
_ => {}
}
}
@ -1929,15 +1816,10 @@ pub(crate) mod tests {
| KnownFunction::IsInstance
| KnownFunction::HasAttr
| KnownFunction::IsSubclass
| KnownFunction::Open
| KnownFunction::DunderImport => KnownModule::Builtins,
KnownFunction::AbstractMethod => KnownModule::Abc,
KnownFunction::Fdopen => KnownModule::Os,
KnownFunction::NamedTemporaryFile => KnownModule::Tempfile,
KnownFunction::Dataclass | KnownFunction::Field => KnownModule::Dataclasses,
KnownFunction::GetattrStatic => KnownModule::Inspect,

View file

@ -15,7 +15,7 @@ use crate::types::generics::Specialization;
use crate::types::signatures::Signature;
use crate::types::{CallDunderError, UnionType};
use crate::types::{
ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type, TypeContext,
ClassBase, ClassLiteral, KnownClass, KnownInstanceType, Type, TypeContext,
TypeVarBoundOrConstraints, class::CodeGeneratorKind,
};
use crate::{Db, HasType, NameKind, SemanticModel};
@ -299,9 +299,10 @@ impl<'db> AllMembers<'db> {
Type::KnownInstance(
KnownInstanceType::TypeVar(_)
| KnownInstanceType::TypeAliasType(_)
| KnownInstanceType::UnionType(_),
| KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_)
| KnownInstanceType::Annotated(_),
) => continue,
Type::Dynamic(DynamicType::TodoTypeAlias) => continue,
_ => {}
}
}

View file

@ -5390,7 +5390,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let target = assignment.target(self.module());
let value = assignment.value(self.module());
let mut declared = self.infer_annotation_expression(
let mut declared = self.infer_annotation_expression_allow_pep_613(
annotation,
DeferredExpressionState::from(self.defer_annotations()),
);
@ -5442,6 +5442,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
declared.inner = Type::BooleanLiteral(true);
}
// Check if this is a PEP 613 `TypeAlias`. (This must come below the SpecialForm handling
// immediately below, since that can overwrite the type to be `TypeAlias`.)
let is_pep_613_type_alias = matches!(
declared.inner_type(),
Type::SpecialForm(SpecialFormType::TypeAlias)
);
// Handle various singletons.
if let Some(name_expr) = target.as_name_expr() {
if let Some(special_form) =
@ -5487,20 +5494,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
// We defer the r.h.s. of PEP-613 `TypeAlias` assignments in stub files.
let declared_type = declared.inner_type();
let previous_deferred_state = self.deferred_state;
if matches!(
declared_type,
Type::SpecialForm(SpecialFormType::TypeAlias)
| Type::Dynamic(DynamicType::TodoTypeAlias)
) && self.in_stub()
{
if is_pep_613_type_alias && self.in_stub() {
self.deferred_state = DeferredExpressionState::Deferred;
}
let inferred_ty = self
.infer_maybe_standalone_expression(value, TypeContext::new(Some(declared_type)));
let inferred_ty = if is_pep_613_type_alias && value.is_string_literal_expr() {
let aliased_type = self.infer_type_expression(value);
Type::KnownInstance(KnownInstanceType::LiteralStringAlias(InternedType::new(
self.db(),
aliased_type,
)))
} else {
self.infer_maybe_standalone_expression(
value,
TypeContext::new(Some(declared.inner_type())),
)
};
self.deferred_state = previous_deferred_state;
@ -5517,6 +5528,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
inferred_ty
};
if is_pep_613_type_alias {
self.add_declaration_with_binding(
target.into(),
definition,
&DeclaredAndInferredType::AreTheSame(TypeAndQualifiers::declared(inferred_ty)),
);
} else {
self.add_declaration_with_binding(
target.into(),
definition,
@ -5525,9 +5543,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
inferred_ty,
},
);
}
self.store_expression_type(target, inferred_ty);
} else {
if is_pep_613_type_alias {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, annotation) {
builder.into_diagnostic(
"`TypeAlias` must be assigned a value in annotated assignments",
);
}
declared.inner = Type::unknown();
}
if self.in_stub() {
self.add_declaration_with_binding(
target.into(),
@ -9286,20 +9313,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(unknown @ Type::Dynamic(DynamicType::Unknown), _, _)
| (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown),
(
todo @ Type::Dynamic(
DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoTypeAlias,
),
_,
_,
)
| (
_,
todo @ Type::Dynamic(
DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoTypeAlias,
),
_,
) => Some(todo),
(todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack), _, _)
| (_, todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack), _) => {
Some(todo)
}
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),

View file

@ -18,21 +18,17 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
annotation: &ast::Expr,
deferred_state: DeferredExpressionState,
) -> TypeAndQualifiers<'db> {
// `DeferredExpressionState::InStringAnnotation` takes precedence over other deferred states.
// However, if it's not a stringified annotation, we must still ensure that annotation expressions
// are always deferred in stub files.
let state = if deferred_state.in_string_annotation() {
deferred_state
} else if self.in_stub() {
DeferredExpressionState::Deferred
} else {
deferred_state
};
self.infer_annotation_expression_inner(annotation, deferred_state, false)
}
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state);
let annotation_ty = self.infer_annotation_expression_impl(annotation);
self.deferred_state = previous_deferred_state;
annotation_ty
/// Infer the type of an annotation expression with the given [`DeferredExpressionState`],
/// allowing a PEP 613 `typing.TypeAlias` annotation.
pub(super) fn infer_annotation_expression_allow_pep_613(
&mut self,
annotation: &ast::Expr,
deferred_state: DeferredExpressionState,
) -> TypeAndQualifiers<'db> {
self.infer_annotation_expression_inner(annotation, deferred_state, true)
}
/// Similar to [`infer_annotation_expression`], but accepts an optional annotation expression
@ -47,17 +43,42 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
annotation.map(|expr| self.infer_annotation_expression(expr, deferred_state))
}
fn infer_annotation_expression_inner(
&mut self,
annotation: &ast::Expr,
deferred_state: DeferredExpressionState,
allow_pep_613: bool,
) -> TypeAndQualifiers<'db> {
// `DeferredExpressionState::InStringAnnotation` takes precedence over other deferred states.
// However, if it's not a stringified annotation, we must still ensure that annotation expressions
// are always deferred in stub files.
let state = if deferred_state.in_string_annotation() {
deferred_state
} else if self.in_stub() {
DeferredExpressionState::Deferred
} else {
deferred_state
};
let previous_deferred_state = std::mem::replace(&mut self.deferred_state, state);
let annotation_ty = self.infer_annotation_expression_impl(annotation, allow_pep_613);
self.deferred_state = previous_deferred_state;
annotation_ty
}
/// Implementation of [`infer_annotation_expression`].
///
/// [`infer_annotation_expression`]: TypeInferenceBuilder::infer_annotation_expression
fn infer_annotation_expression_impl(
&mut self,
annotation: &ast::Expr,
allow_pep_613: bool,
) -> TypeAndQualifiers<'db> {
fn infer_name_or_attribute<'db>(
ty: Type<'db>,
annotation: &ast::Expr,
builder: &TypeInferenceBuilder<'db, '_>,
allow_pep_613: bool,
) -> TypeAndQualifiers<'db> {
match ty {
Type::SpecialForm(SpecialFormType::ClassVar) => TypeAndQualifiers::new(
@ -85,6 +106,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
TypeOrigin::Declared,
TypeQualifiers::READ_ONLY,
),
Type::SpecialForm(SpecialFormType::TypeAlias) if allow_pep_613 => {
TypeAndQualifiers::declared(ty)
}
// Conditional import of `typing.TypeAlias` or `typing_extensions.TypeAlias` on a
// Python version where the former doesn't exist.
Type::Union(union)
if allow_pep_613
&& union.elements(builder.db()).iter().all(|ty| {
matches!(
ty,
Type::SpecialForm(SpecialFormType::TypeAlias) | Type::Dynamic(_)
)
}) =>
{
TypeAndQualifiers::declared(Type::SpecialForm(SpecialFormType::TypeAlias))
}
Type::ClassLiteral(class) if class.is_known(builder.db(), KnownClass::InitVar) => {
if let Some(builder) =
builder.context.report_lint(&INVALID_TYPE_FORM, annotation)
@ -148,6 +185,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_attribute_expression(attribute),
annotation,
self,
allow_pep_613,
),
ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
@ -156,9 +194,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
},
ast::Expr::Name(name) => match name.ctx {
ast::ExprContext::Load => {
infer_name_or_attribute(self.infer_name_expression(name), annotation, self)
}
ast::ExprContext::Load => infer_name_or_attribute(
self.infer_name_expression(name),
annotation,
self,
allow_pep_613,
),
ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()),
ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared(
todo_type!("Name expression annotation in Store/Del context"),
@ -189,7 +230,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
let inner_annotation_ty =
self.infer_annotation_expression_impl(inner_annotation);
self.infer_annotation_expression_impl(inner_annotation, false);
self.store_expression_type(slice, inner_annotation_ty.inner_type());
inner_annotation_ty
@ -202,7 +243,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
}
} else {
report_invalid_arguments_to_annotated(&self.context, subscript);
self.infer_annotation_expression_impl(slice)
self.infer_annotation_expression_impl(slice, false)
}
}
Type::SpecialForm(
@ -220,7 +261,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let num_arguments = arguments.len();
let type_and_qualifiers = if num_arguments == 1 {
let mut type_and_qualifiers =
self.infer_annotation_expression_impl(slice);
self.infer_annotation_expression_impl(slice, false);
match type_qualifier {
SpecialFormType::ClassVar => {
@ -243,7 +284,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
type_and_qualifiers
} else {
for element in arguments {
self.infer_annotation_expression_impl(element);
self.infer_annotation_expression_impl(element, false);
}
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
@ -269,12 +310,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
let num_arguments = arguments.len();
let type_and_qualifiers = if num_arguments == 1 {
let mut type_and_qualifiers =
self.infer_annotation_expression_impl(slice);
self.infer_annotation_expression_impl(slice, false);
type_and_qualifiers.add_qualifier(TypeQualifiers::INIT_VAR);
type_and_qualifiers
} else {
for element in arguments {
self.infer_annotation_expression_impl(element);
self.infer_annotation_expression_impl(element, false);
}
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, subscript)

View file

@ -825,6 +825,10 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_type_expression(slice);
todo_type!("Generic manual PEP-695 type alias")
}
KnownInstanceType::LiteralStringAlias(_) => {
self.infer_type_expression(slice);
todo_type!("Generic stringified PEP-613 type alias")
}
KnownInstanceType::UnionType(_) => {
self.infer_type_expression(slice);
todo_type!("Generic specialization of types.UnionType")

View file

@ -269,9 +269,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
(DynamicType::TodoUnpack, _) => Ordering::Less,
(_, DynamicType::TodoUnpack) => Ordering::Greater,
(DynamicType::TodoTypeAlias, _) => Ordering::Less,
(_, DynamicType::TodoTypeAlias) => Ordering::Greater,
(DynamicType::Divergent(left), DynamicType::Divergent(right)) => {
left.scope.cmp(&right.scope)
}