diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md index c6b4af5f52..a7b354688f 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/new_types.md @@ -235,8 +235,8 @@ Foo(3.14) Foo(42) Foo("hello") # error: [invalid-argument-type] "Argument is incorrect: Expected `int | float`, found `Literal["hello"]`" -reveal_type(Foo(3.14).__class__) # revealed: type[int] | type[float] -reveal_type(Foo(42).__class__) # revealed: type[int] | type[float] +reveal_type(Foo(3.14).__class__) # revealed: type[int | float] +reveal_type(Foo(42).__class__) # revealed: type[int | float] static_assert(is_assignable_to(Foo, float)) static_assert(is_assignable_to(Foo, int | float)) static_assert(is_assignable_to(Foo, int | float | None)) @@ -253,9 +253,9 @@ Bar(3.14) Bar(42) Bar("goodbye") # error: [invalid-argument-type] -reveal_type(Bar(1 + 2j).__class__) # revealed: type[int] | type[float] | type[complex] -reveal_type(Bar(3.14).__class__) # revealed: type[int] | type[float] | type[complex] -reveal_type(Bar(42).__class__) # revealed: type[int] | type[float] | type[complex] +reveal_type(Bar(1 + 2j).__class__) # revealed: type[int | float | complex] +reveal_type(Bar(3.14).__class__) # revealed: type[int | float | complex] +reveal_type(Bar(42).__class__) # revealed: type[int | float | complex] static_assert(is_assignable_to(Bar, complex)) static_assert(is_assignable_to(Bar, int | float | complex)) static_assert(is_assignable_to(Bar, int | float | complex | None)) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index d12d243aef..1823203da1 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2094,8 +2094,8 @@ def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]): reveal_type(b.__class__) # revealed: reveal_type(type(b)) # revealed: - reveal_type(c.__class__) # revealed: type[int] | type[str] - reveal_type(type(c)) # revealed: type[int] | type[str] + reveal_type(c.__class__) # revealed: type[int | str] + reveal_type(type(c)) # revealed: type[int | str] # `type[type]`, a.k.a., either the class `type` or some subclass of `type`. # It would be incorrect to infer `Literal[type]` here, diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 5d744d0493..6b33a1ff09 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -408,7 +408,7 @@ def f(x: type[B]) -> B: ... from overloaded import A, B, f def _(x: type[A | B]): - reveal_type(x) # revealed: type[A] | type[B] + reveal_type(x) # revealed: type[A | B] reveal_type(f(x)) # revealed: A | B reveal_type(f(*(x,))) # revealed: A | B ``` diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index e3fff99f0d..1fe375910f 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -1139,8 +1139,8 @@ SubclassOfP = type[P] reveal_type(SubclassOfA) # revealed: reveal_type(SubclassOfAny) # revealed: reveal_type(SubclassOfAOrB1) # revealed: -reveal_type(SubclassOfAOrB2) # revealed: -reveal_type(SubclassOfAOrB3) # revealed: +reveal_type(SubclassOfAOrB2) # revealed: +reveal_type(SubclassOfAOrB3) # revealed: reveal_type(SubclassOfG) # revealed: reveal_type(SubclassOfGInt) # revealed: reveal_type(SubclassOfP) # revealed: @@ -1161,13 +1161,13 @@ def _( reveal_type(subclass_of_any) # revealed: type[Any] reveal_type(subclass_of_any()) # revealed: Any - reveal_type(subclass_of_a_or_b1) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b1) # revealed: type[A | B] reveal_type(subclass_of_a_or_b1()) # revealed: A | B - reveal_type(subclass_of_a_or_b2) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b2) # revealed: type[A | B] reveal_type(subclass_of_a_or_b2()) # revealed: A | B - reveal_type(subclass_of_a_or_b3) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b3) # revealed: type[A | B] reveal_type(subclass_of_a_or_b3()) # revealed: A | B reveal_type(subclass_of_g) # revealed: type[G[Unknown]] @@ -1200,10 +1200,10 @@ def _( subclass_of_union_alias1: SubclassOfUnionAlias1, subclass_of_union_alias2: SubclassOfUnionAlias2, ): - reveal_type(subclass_of_union_alias1) # revealed: type[C] | type[D] + reveal_type(subclass_of_union_alias1) # revealed: type[C | D] reveal_type(subclass_of_union_alias1()) # revealed: C | D - reveal_type(subclass_of_union_alias2) # revealed: type[C] | type[D] + reveal_type(subclass_of_union_alias2) # revealed: type[C | D] reveal_type(subclass_of_union_alias2()) # revealed: C | D ``` @@ -1255,8 +1255,8 @@ SubclassOfP = Type[P] reveal_type(SubclassOfA) # revealed: reveal_type(SubclassOfAny) # revealed: reveal_type(SubclassOfAOrB1) # revealed: -reveal_type(SubclassOfAOrB2) # revealed: -reveal_type(SubclassOfAOrB3) # revealed: +reveal_type(SubclassOfAOrB2) # revealed: +reveal_type(SubclassOfAOrB3) # revealed: reveal_type(SubclassOfG) # revealed: reveal_type(SubclassOfGInt) # revealed: reveal_type(SubclassOfP) # revealed: @@ -1277,13 +1277,13 @@ def _( reveal_type(subclass_of_any) # revealed: type[Any] reveal_type(subclass_of_any()) # revealed: Any - reveal_type(subclass_of_a_or_b1) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b1) # revealed: type[A | B] reveal_type(subclass_of_a_or_b1()) # revealed: A | B - reveal_type(subclass_of_a_or_b2) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b2) # revealed: type[A | B] reveal_type(subclass_of_a_or_b2()) # revealed: A | B - reveal_type(subclass_of_a_or_b3) # revealed: type[A] | type[B] + reveal_type(subclass_of_a_or_b3) # revealed: type[A | B] reveal_type(subclass_of_a_or_b3()) # revealed: A | B reveal_type(subclass_of_g) # revealed: type[G[Unknown]] diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md index d07dee6a22..ff6d116da5 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md @@ -141,7 +141,7 @@ python-version = "3.10" ```py def f(x: type[int | str | bytes | range]): if issubclass(x, int | str): - reveal_type(x) # revealed: type[int] | type[str] + reveal_type(x) # revealed: type[int | str] elif issubclass(x, bytes | memoryview): reveal_type(x) # revealed: type[bytes] else: @@ -154,7 +154,7 @@ runtime a special exception is made for `None` so that `issubclass(x, int | None ```py def _(x: type): if issubclass(x, int | str | None): - reveal_type(x) # revealed: type[int] | type[str] | + reveal_type(x) # revealed: type[int | str] | else: reveal_type(x) # revealed: type & ~type[int] & ~type[str] & ~ ``` @@ -176,9 +176,9 @@ python-version = "3.10" def _(x: type[int | list | bytes]): # error: [invalid-argument-type] if issubclass(x, int | list[int]): - reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] + reveal_type(x) # revealed: type[int | list[Unknown] | bytes] else: - reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] + reveal_type(x) # revealed: type[int | list[Unknown] | bytes] ``` ## PEP-604 unions on Python \<3.10 @@ -213,7 +213,7 @@ reveal_type(IntOrStr) # revealed: def f(x: type[int | str | bytes | range]): if issubclass(x, IntOrStr): - reveal_type(x) # revealed: type[int] | type[str] + reveal_type(x) # revealed: type[int | str] elif issubclass(x, Union[bytes, memoryview]): reveal_type(x) # revealed: type[bytes] else: diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md index a7666dcf53..27b14b8188 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/truthiness.md @@ -262,11 +262,11 @@ def _( af: type[AmbiguousClass] | type[FalsyClass], flag: bool, ): - reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] + reveal_type(ta) # revealed: type[TruthyClass | AmbiguousClass] if ta: reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy) - reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass] + reveal_type(af) # revealed: type[AmbiguousClass | FalsyClass] if af: reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub…_-_`classinfo`_is_an_in…_(7bb66a0f412caac1).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub…_-_`classinfo`_is_an_in…_(7bb66a0f412caac1).snap index e6f6140a53..f98df2862d 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub…_-_`classinfo`_is_an_in…_(7bb66a0f412caac1).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/issubclass.md_-_Narrowing_for_`issub…_-_`classinfo`_is_an_in…_(7bb66a0f412caac1).snap @@ -16,9 +16,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md 1 | def _(x: type[int | list | bytes]): 2 | # error: [invalid-argument-type] 3 | if issubclass(x, int | list[int]): -4 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] +4 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] 5 | else: -6 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] +6 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] ``` # Diagnostics @@ -33,7 +33,7 @@ error[invalid-argument-type]: Invalid second argument to `issubclass` | ^^^^^^^^^^^^^^---------------^ | | | This `UnionType` instance contains non-class elements -4 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes] +4 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes] 5 | else: | info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects diff --git a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md index 6d747cb6f1..58ae422fb8 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/type_of/basic.md @@ -93,7 +93,7 @@ class A: class C: ... def _(u: type[BasicUser | ProUser | A.B.C]): - # revealed: type[BasicUser] | type[ProUser] | type[C] + # revealed: type[BasicUser | ProUser | C] reveal_type(u) ``` @@ -110,9 +110,9 @@ class A: class C: ... def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Union[BasicUser, Union[ProUser, A.B.C]]]): - reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C] + reveal_type(a) # revealed: type[BasicUser | ProUser | C] reveal_type(b) # revealed: type[str] - reveal_type(c) # revealed: type[BasicUser] | type[ProUser] | type[C] + reveal_type(c) # revealed: type[BasicUser | ProUser | C] ``` ## New-style and old-style unions in combination @@ -128,8 +128,8 @@ class A: class C: ... def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Union[ProUser, A.B.C | str]]]): - reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C] - reveal_type(b) # revealed: type[BasicUser] | type[ProUser] | type[C] | type[str] + reveal_type(a) # revealed: type[BasicUser | ProUser | C] + reveal_type(b) # revealed: type[BasicUser | ProUser | C | str] ``` ## Illegal parameters diff --git a/crates/ty_python_semantic/resources/mdtest/union_types.md b/crates/ty_python_semantic/resources/mdtest/union_types.md index dc4da435f5..63f63d3553 100644 --- a/crates/ty_python_semantic/resources/mdtest/union_types.md +++ b/crates/ty_python_semantic/resources/mdtest/union_types.md @@ -7,9 +7,10 @@ This test suite covers certain basic properties and simplification strategies fo ```py from typing import Literal -def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None: +def _(u1: int | str, u2: Literal[0] | Literal[1], u3: type[int] | type[str]) -> None: reveal_type(u1) # revealed: int | str reveal_type(u2) # revealed: Literal[0, 1] + reveal_type(u3) # revealed: type[int | str] ``` ## Duplicate elements are collapsed diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 5f1cb6e56e..0d8d4836e5 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -28,8 +28,8 @@ use crate::types::visitor::TypeVisitor; use crate::types::{ BindingContext, BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType, KnownClass, KnownInstanceType, MaterializationKind, Protocol, - ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, Type, TypeGuardLike, - TypedDictType, UnionType, WrapperDescriptorKind, visitor, + ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType, + Type, TypeGuardLike, TypedDictType, UnionType, WrapperDescriptorKind, visitor, }; /// Settings for displaying types and signatures @@ -2151,15 +2151,20 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { } let elements = self.ty.elements(self.db); + let mut condensed_types = vec![]; + let mut subclass_of_types = vec![]; - let condensed_types = elements - .iter() - .copied() - .filter(|element| is_condensable(*element)) - .collect::>(); + for element in elements.iter().copied() { + if is_condensable(element) { + condensed_types.push(element); + } else if let Type::SubclassOf(subclass_of) = element { + subclass_of_types.push(subclass_of); + } + } - let total_entries = - usize::from(!condensed_types.is_empty()) + elements.len() - condensed_types.len(); + let total_entries = elements.len() - condensed_types.len() - subclass_of_types.len() + + usize::from(!condensed_types.is_empty()) + + usize::from(!subclass_of_types.is_empty()); assert_ne!(total_entries, 0); @@ -2170,6 +2175,7 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); let mut condensed_types = Some(condensed_types); + let mut subclass_of_types = Some(subclass_of_types); let mut displayed_entries = 0usize; for element in elements { @@ -2186,6 +2192,15 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { settings: self.settings.singleline(), }); } + } else if element.is_subclass_of() { + if let Some(subclass_of_types) = subclass_of_types.take() { + displayed_entries += 1; + join.entry(&DisplaySubclassOfGroup { + types: subclass_of_types, + db: self.db, + settings: self.settings.singleline(), + }); + } } else { displayed_entries += 1; join.entry(&DisplayMaybeParenthesizedType { @@ -2221,6 +2236,61 @@ impl fmt::Debug for DisplayUnionType<'_, '_> { Display::fmt(self, f) } } + +struct DisplaySubclassOfGroup<'db> { + types: Vec>, + db: &'db dyn Db, + settings: DisplaySettings<'db>, +} + +impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'db> { + fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + f.write_str("type[")?; + let total_entries = self.types.len(); + let display_limit = + UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); + let mut join = f.join(" | "); + for subclass_of in self.types.iter().take(display_limit) { + match subclass_of.subclass_of() { + SubclassOfInner::Class(ClassType::NonGeneric(class)) => { + join.entry(&class.display_with(self.db, self.settings.singleline())); + } + SubclassOfInner::Class(ClassType::Generic(alias)) => { + join.entry(&alias.display_with(self.db, self.settings.singleline())); + } + SubclassOfInner::Dynamic(dynamic) => { + let rep = + Type::Dynamic(dynamic).representation(self.db, self.settings.singleline()); + join.entry(&rep); + } + SubclassOfInner::TypeVar(bound_typevar) => { + let rep = Type::TypeVar(bound_typevar) + .representation(self.db, self.settings.singleline()); + join.entry(&rep); + } + } + } + if !self.settings.preserve_full_unions { + let omitted_entries = total_entries.saturating_sub(display_limit); + if omitted_entries > 0 { + join.entry(&DisplayOmitted { + count: omitted_entries, + singular: "type", + plural: "types", + }); + } + } + join.finish()?; + f.write_str("]") + } +} + +impl Display for DisplaySubclassOfGroup<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.fmt_detailed(&mut TypeWriter::Formatter(f)) + } +} + struct DisplayLiteralGroup<'db> { literals: Vec>, db: &'db dyn Db,