mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 12:16:43 +00:00
[ty] Implicit type aliases: Add support for typing.Union (#21363)
## Summary Add support for `typing.Union` in implicit type aliases / in value position. ## Typing conformance tests Two new tests are passing ## Ecosystem impact * The 2k new `invalid-key` diagnostics on pydantic are caused by https://github.com/astral-sh/ty/issues/1479#issuecomment-3513854645. * Everything else I've checked is either a known limitation (often related to type narrowing, because union types are often narrowed down to a subset of options), or a true positive. ## Test Plan New Markdown tests
This commit is contained in:
parent
f5cf672ed4
commit
e8e8180888
5 changed files with 201 additions and 19 deletions
|
|
@ -181,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
|
||||||
max_dep_date: "2025-06-17",
|
max_dep_date: "2025-06-17",
|
||||||
python_version: PythonVersion::PY39,
|
python_version: PythonVersion::PY39,
|
||||||
},
|
},
|
||||||
1000,
|
3000,
|
||||||
);
|
);
|
||||||
|
|
||||||
static SYMPY: Benchmark = Benchmark::new(
|
static SYMPY: Benchmark = Benchmark::new(
|
||||||
|
|
@ -223,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||||
max_dep_date: "2025-08-09",
|
max_dep_date: "2025-08-09",
|
||||||
python_version: PythonVersion::PY311,
|
python_version: PythonVersion::PY311,
|
||||||
},
|
},
|
||||||
800,
|
900,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ g(None)
|
||||||
We also support unions in type aliases:
|
We also support unions in type aliases:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional
|
from typing_extensions import Any, Never, Literal, LiteralString, Tuple, Annotated, Optional, Union
|
||||||
from ty_extensions import Unknown
|
from ty_extensions import Unknown
|
||||||
|
|
||||||
IntOrStr = int | str
|
IntOrStr = int | str
|
||||||
|
|
@ -41,6 +41,8 @@ IntOrStrOrBytes1 = int | str | bytes
|
||||||
IntOrStrOrBytes2 = (int | str) | bytes
|
IntOrStrOrBytes2 = (int | str) | bytes
|
||||||
IntOrStrOrBytes3 = int | (str | bytes)
|
IntOrStrOrBytes3 = int | (str | bytes)
|
||||||
IntOrStrOrBytes4 = IntOrStr | bytes
|
IntOrStrOrBytes4 = IntOrStr | bytes
|
||||||
|
IntOrStrOrBytes5 = int | Union[str, bytes]
|
||||||
|
IntOrStrOrBytes6 = Union[int, str] | bytes
|
||||||
BytesOrIntOrStr = bytes | IntOrStr
|
BytesOrIntOrStr = bytes | IntOrStr
|
||||||
IntOrNone = int | None
|
IntOrNone = int | None
|
||||||
NoneOrInt = None | int
|
NoneOrInt = None | int
|
||||||
|
|
@ -70,6 +72,8 @@ reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
|
||||||
reveal_type(IntOrStrOrBytes2) # revealed: types.UnionType
|
reveal_type(IntOrStrOrBytes2) # revealed: types.UnionType
|
||||||
reveal_type(IntOrStrOrBytes3) # revealed: types.UnionType
|
reveal_type(IntOrStrOrBytes3) # revealed: types.UnionType
|
||||||
reveal_type(IntOrStrOrBytes4) # revealed: types.UnionType
|
reveal_type(IntOrStrOrBytes4) # revealed: types.UnionType
|
||||||
|
reveal_type(IntOrStrOrBytes5) # revealed: types.UnionType
|
||||||
|
reveal_type(IntOrStrOrBytes6) # revealed: types.UnionType
|
||||||
reveal_type(BytesOrIntOrStr) # revealed: types.UnionType
|
reveal_type(BytesOrIntOrStr) # revealed: types.UnionType
|
||||||
reveal_type(IntOrNone) # revealed: types.UnionType
|
reveal_type(IntOrNone) # revealed: types.UnionType
|
||||||
reveal_type(NoneOrInt) # revealed: types.UnionType
|
reveal_type(NoneOrInt) # revealed: types.UnionType
|
||||||
|
|
@ -100,6 +104,8 @@ def _(
|
||||||
int_or_str_or_bytes2: IntOrStrOrBytes2,
|
int_or_str_or_bytes2: IntOrStrOrBytes2,
|
||||||
int_or_str_or_bytes3: IntOrStrOrBytes3,
|
int_or_str_or_bytes3: IntOrStrOrBytes3,
|
||||||
int_or_str_or_bytes4: IntOrStrOrBytes4,
|
int_or_str_or_bytes4: IntOrStrOrBytes4,
|
||||||
|
int_or_str_or_bytes5: IntOrStrOrBytes5,
|
||||||
|
int_or_str_or_bytes6: IntOrStrOrBytes6,
|
||||||
bytes_or_int_or_str: BytesOrIntOrStr,
|
bytes_or_int_or_str: BytesOrIntOrStr,
|
||||||
int_or_none: IntOrNone,
|
int_or_none: IntOrNone,
|
||||||
none_or_int: NoneOrInt,
|
none_or_int: NoneOrInt,
|
||||||
|
|
@ -129,6 +135,8 @@ def _(
|
||||||
reveal_type(int_or_str_or_bytes2) # revealed: int | str | bytes
|
reveal_type(int_or_str_or_bytes2) # revealed: int | str | bytes
|
||||||
reveal_type(int_or_str_or_bytes3) # revealed: int | str | bytes
|
reveal_type(int_or_str_or_bytes3) # revealed: int | str | bytes
|
||||||
reveal_type(int_or_str_or_bytes4) # revealed: int | str | bytes
|
reveal_type(int_or_str_or_bytes4) # revealed: int | str | bytes
|
||||||
|
reveal_type(int_or_str_or_bytes5) # revealed: int | str | bytes
|
||||||
|
reveal_type(int_or_str_or_bytes6) # revealed: int | str | bytes
|
||||||
reveal_type(bytes_or_int_or_str) # revealed: bytes | int | str
|
reveal_type(bytes_or_int_or_str) # revealed: bytes | int | str
|
||||||
reveal_type(int_or_none) # revealed: int | None
|
reveal_type(int_or_none) # revealed: int | None
|
||||||
reveal_type(none_or_int) # revealed: None | int
|
reveal_type(none_or_int) # revealed: None | int
|
||||||
|
|
@ -505,13 +513,90 @@ def _(
|
||||||
|
|
||||||
## `Tuple`
|
## `Tuple`
|
||||||
|
|
||||||
|
We support implicit type aliases using `typing.Tuple`:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
IntAndStr = Tuple[int, str]
|
IntAndStr = Tuple[int, str]
|
||||||
|
SingleInt = Tuple[int]
|
||||||
|
Ints = Tuple[int, ...]
|
||||||
|
EmptyTuple = Tuple[()]
|
||||||
|
|
||||||
def _(int_and_str: IntAndStr):
|
def _(int_and_str: IntAndStr, single_int: SingleInt, ints: Ints, empty_tuple: EmptyTuple):
|
||||||
reveal_type(int_and_str) # revealed: tuple[int, str]
|
reveal_type(int_and_str) # revealed: tuple[int, str]
|
||||||
|
reveal_type(single_int) # revealed: tuple[int]
|
||||||
|
reveal_type(ints) # revealed: tuple[int, ...]
|
||||||
|
reveal_type(empty_tuple) # revealed: tuple[()]
|
||||||
|
```
|
||||||
|
|
||||||
|
Invalid uses cause diagnostics:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||||
|
Invalid = Tuple[int, 1]
|
||||||
|
|
||||||
|
def _(invalid: Invalid):
|
||||||
|
reveal_type(invalid) # revealed: tuple[int, Unknown]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `Union`
|
||||||
|
|
||||||
|
We support implicit type aliases using `typing.Union`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
IntOrStr = Union[int, str]
|
||||||
|
IntOrStrOrBytes = Union[int, Union[str, bytes]]
|
||||||
|
|
||||||
|
reveal_type(IntOrStr) # revealed: types.UnionType
|
||||||
|
reveal_type(IntOrStrOrBytes) # revealed: types.UnionType
|
||||||
|
|
||||||
|
def _(
|
||||||
|
int_or_str: IntOrStr,
|
||||||
|
int_or_str_or_bytes: IntOrStrOrBytes,
|
||||||
|
):
|
||||||
|
reveal_type(int_or_str) # revealed: int | str
|
||||||
|
reveal_type(int_or_str_or_bytes) # revealed: int | str | bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
If a single type is given, no `types.UnionType` instance is created:
|
||||||
|
|
||||||
|
```py
|
||||||
|
JustInt = Union[int]
|
||||||
|
|
||||||
|
reveal_type(JustInt) # revealed: <class 'int'>
|
||||||
|
|
||||||
|
def _(just_int: JustInt):
|
||||||
|
reveal_type(just_int) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
An empty `typing.Union` leads to a `TypeError` at runtime, so we emit an error. We still infer
|
||||||
|
`Never` when used as a type expression, which seems reasonable for an empty union:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [invalid-type-form] "`typing.Union` requires at least one type argument"
|
||||||
|
EmptyUnion = Union[()]
|
||||||
|
|
||||||
|
reveal_type(EmptyUnion) # revealed: types.UnionType
|
||||||
|
|
||||||
|
def _(empty: EmptyUnion):
|
||||||
|
reveal_type(empty) # revealed: Never
|
||||||
|
```
|
||||||
|
|
||||||
|
Other invalid uses are also caught:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
|
||||||
|
Invalid = Union[str, 1]
|
||||||
|
|
||||||
|
def _(
|
||||||
|
invalid: Invalid,
|
||||||
|
):
|
||||||
|
reveal_type(invalid) # revealed: str | Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
## Stringified annotations?
|
## Stringified annotations?
|
||||||
|
|
@ -544,10 +629,19 @@ We *do* support stringified annotations if they appear in a position where a typ
|
||||||
syntactically expected:
|
syntactically expected:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
ListOfInts = list["int"]
|
from typing import Union
|
||||||
|
|
||||||
def _(list_of_ints: ListOfInts):
|
ListOfInts = list["int"]
|
||||||
|
StrOrStyle = Union[str, "Style"]
|
||||||
|
|
||||||
|
class Style: ...
|
||||||
|
|
||||||
|
def _(
|
||||||
|
list_of_ints: ListOfInts,
|
||||||
|
str_or_style: StrOrStyle,
|
||||||
|
):
|
||||||
reveal_type(list_of_ints) # revealed: list[int]
|
reveal_type(list_of_ints) # revealed: list[int]
|
||||||
|
reveal_type(str_or_style) # revealed: str | Style
|
||||||
```
|
```
|
||||||
|
|
||||||
## Recursive
|
## Recursive
|
||||||
|
|
|
||||||
|
|
@ -104,3 +104,27 @@ from typing import Callable
|
||||||
def _(c: Callable[]):
|
def _(c: Callable[]):
|
||||||
reveal_type(c) # revealed: (...) -> Unknown
|
reveal_type(c) # revealed: (...) -> Unknown
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `typing.Tuple`
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
# error: [invalid-syntax] "Expected index or slice expression"
|
||||||
|
InvalidEmptyTuple = Tuple[]
|
||||||
|
|
||||||
|
def _(t: InvalidEmptyTuple):
|
||||||
|
reveal_type(t) # revealed: tuple[Unknown]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `typing.Union`
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
# error: [invalid-syntax] "Expected index or slice expression"
|
||||||
|
InvalidEmptyUnion = Union[]
|
||||||
|
|
||||||
|
def _(u: InvalidEmptyUnion):
|
||||||
|
reveal_type(u) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -6586,12 +6586,13 @@ impl<'db> Type<'db> {
|
||||||
}),
|
}),
|
||||||
KnownInstanceType::UnionType(list) => {
|
KnownInstanceType::UnionType(list) => {
|
||||||
let mut builder = UnionBuilder::new(db);
|
let mut builder = UnionBuilder::new(db);
|
||||||
|
let inferred_as = list.inferred_as(db);
|
||||||
for element in list.elements(db) {
|
for element in list.elements(db) {
|
||||||
builder = builder.add(element.in_type_expression(
|
builder = builder.add(if inferred_as.type_expression() {
|
||||||
db,
|
*element
|
||||||
scope_id,
|
} else {
|
||||||
typevar_binding_context,
|
element.in_type_expression(db, scope_id, typevar_binding_context)?
|
||||||
)?);
|
});
|
||||||
}
|
}
|
||||||
Ok(builder.build())
|
Ok(builder.build())
|
||||||
}
|
}
|
||||||
|
|
@ -9164,6 +9165,21 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a given type originates from value expression inference or type expression inference.
|
||||||
|
/// For example, the symbol `int` would be inferred as `<class 'int'>` in value expression context,
|
||||||
|
/// and as `int` (i.e. an instance of the class `int`) in type expression context.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)]
|
||||||
|
pub enum InferredAs {
|
||||||
|
ValueExpression,
|
||||||
|
TypeExpression,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InferredAs {
|
||||||
|
pub const fn type_expression(self) -> bool {
|
||||||
|
matches!(self, InferredAs::TypeExpression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A salsa-interned list of types.
|
/// A salsa-interned list of types.
|
||||||
///
|
///
|
||||||
/// # Ordering
|
/// # Ordering
|
||||||
|
|
@ -9174,6 +9190,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
|
||||||
pub struct InternedTypes<'db> {
|
pub struct InternedTypes<'db> {
|
||||||
#[returns(deref)]
|
#[returns(deref)]
|
||||||
elements: Box<[Type<'db>]>,
|
elements: Box<[Type<'db>]>,
|
||||||
|
inferred_as: InferredAs,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl get_size2::GetSize for InternedTypes<'_> {}
|
impl get_size2::GetSize for InternedTypes<'_> {}
|
||||||
|
|
@ -9182,8 +9199,9 @@ impl<'db> InternedTypes<'db> {
|
||||||
pub(crate) fn from_elements(
|
pub(crate) fn from_elements(
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
elements: impl IntoIterator<Item = Type<'db>>,
|
elements: impl IntoIterator<Item = Type<'db>>,
|
||||||
|
inferred_as: InferredAs,
|
||||||
) -> InternedTypes<'db> {
|
) -> InternedTypes<'db> {
|
||||||
InternedTypes::new(db, elements.into_iter().collect::<Box<[_]>>())
|
InternedTypes::new(db, elements.into_iter().collect::<Box<[_]>>(), inferred_as)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
|
pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
|
||||||
|
|
@ -9193,6 +9211,7 @@ impl<'db> InternedTypes<'db> {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ty| ty.normalized_impl(db, visitor))
|
.map(|ty| ty.normalized_impl(db, visitor))
|
||||||
.collect::<Box<[_]>>(),
|
.collect::<Box<[_]>>(),
|
||||||
|
self.inferred_as(db),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,10 +101,10 @@ use crate::types::typed_dict::{
|
||||||
use crate::types::visitor::any_over_type;
|
use crate::types::visitor::any_over_type;
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
|
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
|
||||||
DynamicType, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, KnownClass,
|
DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType,
|
||||||
KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
|
KnownClass, KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
|
||||||
ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness,
|
Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet,
|
||||||
Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
|
Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
|
||||||
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
|
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
|
||||||
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
|
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
|
||||||
binding_type, todo_type,
|
binding_type, todo_type,
|
||||||
|
|
@ -9234,7 +9234,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
Some(left_ty)
|
Some(left_ty)
|
||||||
} else {
|
} else {
|
||||||
Some(Type::KnownInstance(KnownInstanceType::UnionType(
|
Some(Type::KnownInstance(KnownInstanceType::UnionType(
|
||||||
InternedTypes::from_elements(self.db(), [left_ty, right_ty]),
|
InternedTypes::from_elements(
|
||||||
|
self.db(),
|
||||||
|
[left_ty, right_ty],
|
||||||
|
InferredAs::ValueExpression,
|
||||||
|
),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -9259,7 +9263,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
&& instance.has_known_class(self.db(), KnownClass::NoneType) =>
|
&& instance.has_known_class(self.db(), KnownClass::NoneType) =>
|
||||||
{
|
{
|
||||||
Some(Type::KnownInstance(KnownInstanceType::UnionType(
|
Some(Type::KnownInstance(KnownInstanceType::UnionType(
|
||||||
InternedTypes::from_elements(self.db(), [left_ty, right_ty]),
|
InternedTypes::from_elements(
|
||||||
|
self.db(),
|
||||||
|
[left_ty, right_ty],
|
||||||
|
InferredAs::ValueExpression,
|
||||||
|
),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10476,9 +10484,46 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Type::KnownInstance(KnownInstanceType::UnionType(
|
return Type::KnownInstance(KnownInstanceType::UnionType(
|
||||||
InternedTypes::from_elements(self.db(), [ty, Type::none(self.db())]),
|
InternedTypes::from_elements(
|
||||||
|
self.db(),
|
||||||
|
[ty, Type::none(self.db())],
|
||||||
|
InferredAs::ValueExpression,
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
Type::SpecialForm(SpecialFormType::Union) => {
|
||||||
|
let db = self.db();
|
||||||
|
|
||||||
|
match **slice {
|
||||||
|
ast::Expr::Tuple(ref tuple) => {
|
||||||
|
let mut elements = tuple
|
||||||
|
.elts
|
||||||
|
.iter()
|
||||||
|
.map(|elt| self.infer_type_expression(elt))
|
||||||
|
.peekable();
|
||||||
|
|
||||||
|
let is_empty = elements.peek().is_none();
|
||||||
|
let union_type = Type::KnownInstance(KnownInstanceType::UnionType(
|
||||||
|
InternedTypes::from_elements(db, elements, InferredAs::TypeExpression),
|
||||||
|
));
|
||||||
|
|
||||||
|
if is_empty {
|
||||||
|
if let Some(builder) =
|
||||||
|
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
|
||||||
|
{
|
||||||
|
builder.into_diagnostic(
|
||||||
|
"`typing.Union` requires at least one type argument",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return union_type;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return self.infer_expression(slice, TypeContext::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue