Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 20 additions & 40 deletions crates/ty_python_semantic/resources/mdtest/annotations/literal.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,30 +181,20 @@ def _(
bool2: Literal[Bool2],
multiple: Literal[SingleInt, SingleStr, SingleEnum],
):
# TODO should be `Literal[1]`
reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal["foo"]`
reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[b"bar"]`
reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[True]`
reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `None`
reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[E.A]`
reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[1, "foo", b"bar", True, E.A] | None`
reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form)
reveal_type(single_int) # revealed: Literal[1]
reveal_type(single_str) # revealed: Literal["foo"]
reveal_type(single_bytes) # revealed: Literal[b"bar"]
reveal_type(single_bool) # revealed: Literal[True]
reveal_type(single_none) # revealed: None
reveal_type(single_enum) # revealed: Literal[E.A]
reveal_type(union_literals) # revealed: Literal[1, "foo", b"bar", True, E.A] | None
# Could also be `E`
reveal_type(an_enum1) # revealed: Unknown
# TODO should be `E`
reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form)
reveal_type(an_enum2) # revealed: E
# Could also be `bool`
reveal_type(bool1) # revealed: Unknown
# TODO should be `bool`
reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[1, "foo", E.A]`
reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form)
reveal_type(bool2) # revealed: bool
reveal_type(multiple) # revealed: Literal[1, "foo", E.A]
```

### Implicit type alias
Expand Down Expand Up @@ -246,28 +236,18 @@ def _(
bool2: Literal[Bool2],
multiple: Literal[SingleInt, SingleStr, SingleEnum],
):
# TODO should be `Literal[1]`
reveal_type(single_int) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal["foo"]`
reveal_type(single_str) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[b"bar"]`
reveal_type(single_bytes) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[True]`
reveal_type(single_bool) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `None`
reveal_type(single_none) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[E.A]`
reveal_type(single_enum) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[1, "foo", b"bar", True, E.A] | None`
reveal_type(union_literals) # revealed: @Todo(Inference of subscript on special form)
reveal_type(single_int) # revealed: Literal[1]
reveal_type(single_str) # revealed: Literal["foo"]
reveal_type(single_bytes) # revealed: Literal[b"bar"]
reveal_type(single_bool) # revealed: Literal[True]
reveal_type(single_none) # revealed: None
reveal_type(single_enum) # revealed: Literal[E.A]
reveal_type(union_literals) # revealed: Literal[1, "foo", b"bar", True, E.A] | None
reveal_type(an_enum1) # revealed: Unknown
# TODO should be `E`
reveal_type(an_enum2) # revealed: @Todo(Inference of subscript on special form)
reveal_type(an_enum2) # revealed: E
reveal_type(bool1) # revealed: Unknown
# TODO should be `bool`
reveal_type(bool2) # revealed: @Todo(Inference of subscript on special form)
# TODO should be `Literal[1, "foo", E.A]`
reveal_type(multiple) # revealed: @Todo(Inference of subscript on special form)
reveal_type(bool2) # revealed: bool
reveal_type(multiple) # revealed: Literal[1, "foo", E.A]
```

## Shortening unions of literals
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ g(None)
We also support unions in type aliases:

```py
from typing_extensions import Any, Never
from typing_extensions import Any, Never, Literal
from ty_extensions import Unknown

IntOrStr = int | str
Expand All @@ -54,6 +54,8 @@ NeverOrAny = Never | Any
AnyOrNever = Any | Never
UnknownOrInt = Unknown | int
IntOrUnknown = int | Unknown
StrOrZero = str | Literal[0]
ZeroOrStr = Literal[0] | str

reveal_type(IntOrStr) # revealed: types.UnionType
reveal_type(IntOrStrOrBytes1) # revealed: types.UnionType
Expand All @@ -73,6 +75,8 @@ reveal_type(NeverOrAny) # revealed: types.UnionType
reveal_type(AnyOrNever) # revealed: types.UnionType
reveal_type(UnknownOrInt) # revealed: types.UnionType
reveal_type(IntOrUnknown) # revealed: types.UnionType
reveal_type(StrOrZero) # revealed: types.UnionType
reveal_type(ZeroOrStr) # revealed: types.UnionType

def _(
int_or_str: IntOrStr,
Expand All @@ -93,6 +97,8 @@ def _(
any_or_never: AnyOrNever,
unknown_or_int: UnknownOrInt,
int_or_unknown: IntOrUnknown,
str_or_zero: StrOrZero,
zero_or_str: ZeroOrStr,
):
reveal_type(int_or_str) # revealed: int | str
reveal_type(int_or_str_or_bytes1) # revealed: int | str | bytes
Expand All @@ -112,6 +118,8 @@ def _(
reveal_type(any_or_never) # revealed: Any
reveal_type(unknown_or_int) # revealed: Unknown | int
reveal_type(int_or_unknown) # revealed: int | Unknown
reveal_type(str_or_zero) # revealed: str | Literal[0]
reveal_type(zero_or_str) # revealed: Literal[0] | str
```

If a type is unioned with itself in a value expression, the result is just that type. No
Expand Down Expand Up @@ -255,6 +263,68 @@ def _(list_or_tuple: ListOrTuple[int]):
reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType)
```

## `Literal`s

We also support `typing.Literal` in implicit type aliases.

```py
from typing import Literal
from enum import Enum

IntLiteral1 = Literal[26]
IntLiteral2 = Literal[0x1A]
IntLiterals = Literal[-1, 0, 1]
NestedLiteral = Literal[Literal[1]]
StringLiteral = Literal["a"]
BytesLiteral = Literal[b"b"]
BoolLiteral = Literal[True]
MixedLiterals = Literal[1, "a", True, None]

class Color(Enum):
RED = 0
GREEN = 1
BLUE = 2

EnumLiteral = Literal[Color.RED]

def _(
int_literal1: IntLiteral1,
int_literal2: IntLiteral2,
int_literals: IntLiterals,
nested_literal: NestedLiteral,
string_literal: StringLiteral,
bytes_literal: BytesLiteral,
bool_literal: BoolLiteral,
mixed_literals: MixedLiterals,
enum_literal: EnumLiteral,
):
reveal_type(int_literal1) # revealed: Literal[26]
reveal_type(int_literal2) # revealed: Literal[26]
reveal_type(int_literals) # revealed: Literal[-1, 0, 1]
reveal_type(nested_literal) # revealed: Literal[1]
reveal_type(string_literal) # revealed: Literal["a"]
reveal_type(bytes_literal) # revealed: Literal[b"b"]
reveal_type(bool_literal) # revealed: Literal[True]
reveal_type(mixed_literals) # revealed: Literal[1, "a", True] | None
reveal_type(enum_literal) # revealed: Literal[Color.RED]
```

We reject invalid uses:

```py
# error: [invalid-type-form] "Type arguments for `Literal` must be `None`, a literal value (int, bool, str, or bytes), or an enum member"
LiteralInt = Literal[int]

reveal_type(LiteralInt) # revealed: Unknown

def _(weird: LiteralInt):
reveal_type(weird) # revealed: Unknown

# error: [invalid-type-form] "`Literal[26]` is not a generic class"
def _(weird: IntLiteral1[int]):
reveal_type(weird) # revealed: Unknown
```

## Stringified annotations?

From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):
Expand Down
57 changes: 39 additions & 18 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6444,9 +6444,9 @@ impl<'db> Type<'db> {
invalid_expressions: smallvec::smallvec_inline![InvalidTypeExpression::Generic],
fallback_type: Type::unknown(),
}),
KnownInstanceType::UnionType(union_type) => {
KnownInstanceType::UnionType(list) => {
let mut builder = UnionBuilder::new(db);
for element in union_type.elements(db) {
for element in list.elements(db) {
builder = builder.add(element.in_type_expression(
db,
scope_id,
Expand All @@ -6455,6 +6455,7 @@ impl<'db> Type<'db> {
}
Ok(builder.build())
}
KnownInstanceType::Literal(list) => Ok(list.to_union(db)),
},

Type::SpecialForm(special_form) => match special_form {
Expand Down Expand Up @@ -7668,7 +7669,10 @@ pub enum KnownInstanceType<'db> {

/// A single instance of `types.UnionType`, which stores the left- and
/// right-hand sides of a PEP 604 union.
UnionType(UnionTypeInstance<'db>),
UnionType(TypeList<'db>),

/// A single instance of `typing.Literal`
Literal(TypeList<'db>),
}

fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
Expand All @@ -7695,9 +7699,9 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
visitor.visit_type(db, default_ty);
}
}
KnownInstanceType::UnionType(union_type) => {
for element in union_type.elements(db) {
visitor.visit_type(db, element);
KnownInstanceType::UnionType(list) | KnownInstanceType::Literal(list) => {
for element in list.elements(db) {
visitor.visit_type(db, *element);
}
}
}
Expand Down Expand Up @@ -7736,7 +7740,8 @@ impl<'db> KnownInstanceType<'db> {
// Nothing to normalize
Self::ConstraintSet(set)
}
Self::UnionType(union_type) => Self::UnionType(union_type.normalized_impl(db, visitor)),
Self::UnionType(list) => Self::UnionType(list.normalized_impl(db, visitor)),
Self::Literal(list) => Self::Literal(list.normalized_impl(db, visitor)),
}
}

Expand All @@ -7752,6 +7757,7 @@ impl<'db> KnownInstanceType<'db> {
Self::Field(_) => KnownClass::Field,
Self::ConstraintSet(_) => KnownClass::ConstraintSet,
Self::UnionType(_) => KnownClass::UnionType,
Self::Literal(_) => KnownClass::GenericAlias,
}
}

Expand Down Expand Up @@ -7826,6 +7832,7 @@ impl<'db> KnownInstanceType<'db> {
)
}
KnownInstanceType::UnionType(_) => f.write_str("types.UnionType"),
KnownInstanceType::Literal(_) => f.write_str("typing.Literal"),
}
}
}
Expand Down Expand Up @@ -8949,32 +8956,46 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
}
}

/// An instance of `types.UnionType`.
/// A salsa-interned list of types.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not wild about the fact that -- depending on context -- this type can sometimes hold a list of value-expression types (if it's the wrapped data inside a KnownInstance::UnionType variant) and sometimes hold a list of type-expression types (if it's the wrapped data inside a KnownInstance::Literal variant). That feels a little bug-prone? E.g. it would be incorrect to call the .to_union() method on a TypeList wrapped inside a KnownInstance::Union variant, because you need to first convert them all to type expressions. But it would be incorrect to convert them all to type expressions on the elements in a TypeList inside a KnownInstance::Literal variant, because those have all already been interpreted as type expressions eagerly when building the type list.

I think I'd prefer to have different structs for the wrapped data inside the two variants, since they have pretty different semantics.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. I will change this in major ways in my next PR, and will keep this in mind.

///
/// # Ordering
/// Ordering is based on the context's salsa-assigned id and not on its values.
/// The id may change between runs, or when the context was garbage collected and recreated.
#[salsa::interned(debug)]
#[derive(PartialOrd, Ord)]
pub struct UnionTypeInstance<'db> {
left: Type<'db>,
right: Type<'db>,
pub struct TypeList<'db> {
#[returns(deref)]
elements: Box<[Type<'db>]>,
}

impl get_size2::GetSize for UnionTypeInstance<'_> {}
impl get_size2::GetSize for TypeList<'_> {}

impl<'db> TypeList<'db> {
pub(crate) fn from_elements(
db: &'db dyn Db,
elements: impl IntoIterator<Item = Type<'db>>,
) -> TypeList<'db> {
TypeList::new(db, elements.into_iter().collect::<Box<[_]>>())
}

impl<'db> UnionTypeInstance<'db> {
pub(crate) fn elements(self, db: &'db dyn Db) -> [Type<'db>; 2] {
[self.left(db), self.right(db)]
pub(crate) fn singleton(db: &'db dyn Db, element: Type<'db>) -> TypeList<'db> {
TypeList::from_elements(db, [element])
}

pub(crate) fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
UnionTypeInstance::new(
TypeList::new(
db,
self.left(db).normalized_impl(db, visitor),
self.right(db).normalized_impl(db, visitor),
self.elements(db)
.iter()
.map(|ty| ty.normalized_impl(db, visitor))
.collect::<Box<[_]>>(),
)
}

/// Turn this list of types `[T1, T2, ...]` into a union type `T1 | T2 | ...`.
pub(crate) fn to_union(self, db: &'db dyn Db) -> Type<'db> {
UnionType::from_elements(db, self.elements(db))
}
}

/// Error returned if a type is not awaitable.
Expand Down
3 changes: 2 additions & 1 deletion crates/ty_python_semantic/src/types/class_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ impl<'db> ClassBase<'db> {
| KnownInstanceType::Deprecated(_)
| KnownInstanceType::Field(_)
| KnownInstanceType::ConstraintSet(_)
| KnownInstanceType::UnionType(_) => None,
| KnownInstanceType::UnionType(_)
| KnownInstanceType::Literal(_) => None,
},

Type::SpecialForm(special_form) => match special_form {
Expand Down
Loading
Loading