From 5bed87bd6b603c5bca57fc078e576e4f9cf81aa7 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 6 Jun 2025 09:48:13 -0700 Subject: [PATCH 1/4] [ty] implement disjointness of Callable vs SpecialForm --- .../type_properties/is_disjoint_from.md | 28 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 8 ++++++ 2 files changed, 36 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index a39826671be6f..061617919fe62 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -498,3 +498,31 @@ def possibly_unbound_with_invalid_type(flag: bool): static_assert(is_disjoint_from(G, Callable[..., Any])) static_assert(is_disjoint_from(Callable[..., Any], G)) ``` + +A callable type is disjoint from special form types. + +```py +from ty_extensions import is_disjoint_from, static_assert, TypeOf +from typing_extensions import Any, Callable +from typing import Literal, Union, Optional, Final, Type + +# All special forms are disjoint from callable types because special forms +# are type constructors/annotations that are subscripted, not called. +static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Literal])) +static_assert(is_disjoint_from(TypeOf[Literal], Callable[..., Any])) + +static_assert(is_disjoint_from(Callable[[], None], TypeOf[Union])) +static_assert(is_disjoint_from(TypeOf[Union], Callable[[], None])) + +static_assert(is_disjoint_from(Callable[[int], str], TypeOf[Optional])) +static_assert(is_disjoint_from(TypeOf[Optional], Callable[[int], str])) + +static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Type])) +static_assert(is_disjoint_from(TypeOf[Type], Callable[..., Any])) + +static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Final])) +static_assert(is_disjoint_from(TypeOf[Final], Callable[..., Any])) + +static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Callable])) +static_assert(is_disjoint_from(TypeOf[Callable], Callable[..., Any])) +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9a2d1b9243e2e..6d1be9c20b076 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1926,6 +1926,14 @@ impl<'db> Type<'db> { true } + (Type::Callable(_), Type::SpecialForm(_)) + | (Type::SpecialForm(_), Type::Callable(_)) => { + // A callable type is disjoint from special form types. Special forms are + // type constructors/annotations (like `typing.Literal`, `typing.Union`, etc.) + // that are subscripted, not called. They do not have `__call__` methods. + true + } + ( Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), instance @ Type::NominalInstance(NominalInstanceType { class, .. }), From 547868e7d7588640e0e70919bdddd5220b7ac123 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Fri, 6 Jun 2025 19:07:08 -0700 Subject: [PATCH 2/4] collections aliases are callable --- .../type_properties/is_disjoint_from.md | 29 ++++++++-- crates/ty_python_semantic/src/types.rs | 13 ++--- .../src/types/special_form.rs | 53 +++++++++++++++++++ 3 files changed, 84 insertions(+), 11 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md index 061617919fe62..20725ebd89f1f 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_disjoint_from.md @@ -499,15 +499,15 @@ def possibly_unbound_with_invalid_type(flag: bool): static_assert(is_disjoint_from(Callable[..., Any], G)) ``` -A callable type is disjoint from special form types. +A callable type is disjoint from special form types, except for callable special forms. ```py from ty_extensions import is_disjoint_from, static_assert, TypeOf -from typing_extensions import Any, Callable -from typing import Literal, Union, Optional, Final, Type +from typing_extensions import Any, Callable, TypedDict +from typing import Literal, Union, Optional, Final, Type, ChainMap, Counter, OrderedDict, DefaultDict, Deque -# All special forms are disjoint from callable types because special forms -# are type constructors/annotations that are subscripted, not called. +# Most special forms are disjoint from callable types because they are +# type constructors/annotations that are subscripted, not called. static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Literal])) static_assert(is_disjoint_from(TypeOf[Literal], Callable[..., Any])) @@ -525,4 +525,23 @@ static_assert(is_disjoint_from(TypeOf[Final], Callable[..., Any])) static_assert(is_disjoint_from(Callable[..., Any], TypeOf[Callable])) static_assert(is_disjoint_from(TypeOf[Callable], Callable[..., Any])) + +# However, some special forms are callable (TypedDict and collection constructors) +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[TypedDict])) +static_assert(not is_disjoint_from(TypeOf[TypedDict], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[ChainMap])) +static_assert(not is_disjoint_from(TypeOf[ChainMap], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[Counter])) +static_assert(not is_disjoint_from(TypeOf[Counter], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[DefaultDict])) +static_assert(not is_disjoint_from(TypeOf[DefaultDict], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[Deque])) +static_assert(not is_disjoint_from(TypeOf[Deque], Callable[..., Any])) + +static_assert(not is_disjoint_from(Callable[..., Any], TypeOf[OrderedDict])) +static_assert(not is_disjoint_from(TypeOf[OrderedDict], Callable[..., Any])) ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6d1be9c20b076..6169de067e735 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1926,12 +1926,13 @@ impl<'db> Type<'db> { true } - (Type::Callable(_), Type::SpecialForm(_)) - | (Type::SpecialForm(_), Type::Callable(_)) => { - // A callable type is disjoint from special form types. Special forms are - // type constructors/annotations (like `typing.Literal`, `typing.Union`, etc.) - // that are subscripted, not called. They do not have `__call__` methods. - true + (Type::Callable(_), Type::SpecialForm(special_form)) + | (Type::SpecialForm(special_form), Type::Callable(_)) => { + // A callable type is disjoint from special form types, except for special forms + // that are callable (like TypedDict and collection constructors). + // Most special forms are type constructors/annotations (like `typing.Literal`, + // `typing.Union`, etc.) that are subscripted, not called. + !special_form.is_callable() } ( diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index a17531cb1339f..0d5a5cbd72031 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -247,6 +247,59 @@ impl SpecialFormType { self.class().to_class_literal(db) } + /// Return true if this special form is callable at runtime. + /// Most special forms are not callable (they are type constructors that are subscripted), + /// but some like TypedDict and collection constructors can be called. + pub(super) const fn is_callable(self) -> bool { + match self { + // TypedDict can be called as a constructor to create TypedDict instances + Self::TypedDict + // Collection constructors are callable + // TODO actually implement support for calling them + | Self::ChainMap + | Self::Counter + | Self::DefaultDict + | Self::Deque + | Self::OrderedDict => true, + + // All other special forms are not callable + Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Never + | Self::Tuple + | Self::List + | Self::Dict + | Self::Set + | Self::FrozenSet + | Self::Type + | Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf + | Self::Callable + | Self::TypingSelf + | Self::Final + | Self::ClassVar + | Self::Concatenate + | Self::Unpack + | Self::Required + | Self::NotRequired + | Self::TypeAlias + | Self::TypeGuard + | Self::TypeIs + | Self::ReadOnly + | Self::Protocol + | Self::Generic => false, + } + } + /// Return the repr of the symbol at runtime pub(super) const fn repr(self) -> &'static str { match self { From 63b91dfd50689b8385f4588c662a64f64f8c2a35 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 10 Jun 2025 07:56:12 -0700 Subject: [PATCH 3/4] Update crates/ty_python_semantic/src/types/special_form.rs Co-authored-by: Alex Waygood --- crates/ty_python_semantic/src/types/special_form.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 0d5a5cbd72031..0e74f6ea4af0a 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -252,7 +252,7 @@ impl SpecialFormType { /// but some like TypedDict and collection constructors can be called. pub(super) const fn is_callable(self) -> bool { match self { - // TypedDict can be called as a constructor to create TypedDict instances + // TypedDict can be called as a constructor to create TypedDict types Self::TypedDict // Collection constructors are callable // TODO actually implement support for calling them From f7a847960e52a7f6b896a449fa7035d8fae925c3 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 10 Jun 2025 08:48:24 -0700 Subject: [PATCH 4/4] clippy --- crates/ty_python_semantic/src/types/special_form.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index 0e74f6ea4af0a..be8995018d895 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -249,7 +249,7 @@ impl SpecialFormType { /// Return true if this special form is callable at runtime. /// Most special forms are not callable (they are type constructors that are subscripted), - /// but some like TypedDict and collection constructors can be called. + /// but some like `TypedDict` and collection constructors can be called. pub(super) const fn is_callable(self) -> bool { match self { // TypedDict can be called as a constructor to create TypedDict types