From a2de81cb27541b7f09d57644539cea566cc0ece1 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 10 Jun 2025 13:25:08 -0700 Subject: [PATCH] [ty] implement disjointness of Callable vs SpecialForm (#18503) ## Summary Fixes https://github.com/astral-sh/ty/issues/557 ## Test Plan Stable property tests succeed with a million iterations. Added mdtests. --------- Co-authored-by: Alex Waygood --- .../type_properties/is_disjoint_from.md | 47 ++++++++++++++++ crates/ty_python_semantic/src/types.rs | 9 ++++ .../src/types/special_form.rs | 53 +++++++++++++++++++ 3 files changed, 109 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 a39826671b..20725ebd89 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,50 @@ 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, except for callable special forms. + +```py +from ty_extensions import is_disjoint_from, static_assert, TypeOf +from typing_extensions import Any, Callable, TypedDict +from typing import Literal, Union, Optional, Final, Type, ChainMap, Counter, OrderedDict, DefaultDict, Deque + +# 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])) + +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])) + +# 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 9a2d1b9243..6169de067e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1926,6 +1926,15 @@ impl<'db> Type<'db> { 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() + } + ( Type::Callable(_) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_), instance @ Type::NominalInstance(NominalInstanceType { class, .. }), diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index a17531cb13..be8995018d 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 types + 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 {