mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 11:41:21 +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
|
|
@ -33,7 +33,7 @@ g(None)
|
|||
We also support unions in type aliases:
|
||||
|
||||
```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
|
||||
|
||||
IntOrStr = int | str
|
||||
|
|
@ -41,6 +41,8 @@ IntOrStrOrBytes1 = int | str | bytes
|
|||
IntOrStrOrBytes2 = (int | str) | bytes
|
||||
IntOrStrOrBytes3 = int | (str | bytes)
|
||||
IntOrStrOrBytes4 = IntOrStr | bytes
|
||||
IntOrStrOrBytes5 = int | Union[str, bytes]
|
||||
IntOrStrOrBytes6 = Union[int, str] | bytes
|
||||
BytesOrIntOrStr = bytes | IntOrStr
|
||||
IntOrNone = int | None
|
||||
NoneOrInt = None | int
|
||||
|
|
@ -70,6 +72,8 @@ reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
|
|||
reveal_type(IntOrStrOrBytes2) # revealed: types.UnionType
|
||||
reveal_type(IntOrStrOrBytes3) # 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(IntOrNone) # revealed: types.UnionType
|
||||
reveal_type(NoneOrInt) # revealed: types.UnionType
|
||||
|
|
@ -100,6 +104,8 @@ def _(
|
|||
int_or_str_or_bytes2: IntOrStrOrBytes2,
|
||||
int_or_str_or_bytes3: IntOrStrOrBytes3,
|
||||
int_or_str_or_bytes4: IntOrStrOrBytes4,
|
||||
int_or_str_or_bytes5: IntOrStrOrBytes5,
|
||||
int_or_str_or_bytes6: IntOrStrOrBytes6,
|
||||
bytes_or_int_or_str: BytesOrIntOrStr,
|
||||
int_or_none: IntOrNone,
|
||||
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_bytes3) # 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(int_or_none) # revealed: int | None
|
||||
reveal_type(none_or_int) # revealed: None | int
|
||||
|
|
@ -505,13 +513,90 @@ def _(
|
|||
|
||||
## `Tuple`
|
||||
|
||||
We support implicit type aliases using `typing.Tuple`:
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
|
||||
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(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?
|
||||
|
|
@ -544,10 +629,19 @@ We *do* support stringified annotations if they appear in a position where a typ
|
|||
syntactically expected:
|
||||
|
||||
```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(str_or_style) # revealed: str | Style
|
||||
```
|
||||
|
||||
## Recursive
|
||||
|
|
|
|||
|
|
@ -104,3 +104,27 @@ from typing import Callable
|
|||
def _(c: Callable[]):
|
||||
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) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
let inferred_as = list.inferred_as(db);
|
||||
for element in list.elements(db) {
|
||||
builder = builder.add(element.in_type_expression(
|
||||
db,
|
||||
scope_id,
|
||||
typevar_binding_context,
|
||||
)?);
|
||||
builder = builder.add(if inferred_as.type_expression() {
|
||||
*element
|
||||
} else {
|
||||
element.in_type_expression(db, scope_id, typevar_binding_context)?
|
||||
});
|
||||
}
|
||||
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.
|
||||
///
|
||||
/// # Ordering
|
||||
|
|
@ -9174,6 +9190,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
|
|||
pub struct InternedTypes<'db> {
|
||||
#[returns(deref)]
|
||||
elements: Box<[Type<'db>]>,
|
||||
inferred_as: InferredAs,
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for InternedTypes<'_> {}
|
||||
|
|
@ -9182,8 +9199,9 @@ impl<'db> InternedTypes<'db> {
|
|||
pub(crate) fn from_elements(
|
||||
db: &'db dyn Db,
|
||||
elements: impl IntoIterator<Item = Type<'db>>,
|
||||
inferred_as: InferredAs,
|
||||
) -> 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 {
|
||||
|
|
@ -9193,6 +9211,7 @@ impl<'db> InternedTypes<'db> {
|
|||
.iter()
|
||||
.map(|ty| ty.normalized_impl(db, visitor))
|
||||
.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::{
|
||||
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
|
||||
DynamicType, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, KnownClass,
|
||||
KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter,
|
||||
ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness,
|
||||
Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
|
||||
DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType,
|
||||
KnownClass, KnownInstanceType, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType,
|
||||
Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet,
|
||||
Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
|
||||
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
|
||||
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
|
||||
binding_type, todo_type,
|
||||
|
|
@ -9234,7 +9234,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
Some(left_ty)
|
||||
} else {
|
||||
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) =>
|
||||
{
|
||||
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(
|
||||
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