From f1aacd0f2c061e82164c19c1136fe81453fa650e Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 22 Sep 2025 08:29:03 +0100 Subject: [PATCH] [ty] The runtime object `typing.Protocol` is an instance of `_ProtocolMeta` (#20488) ## Summary Fixes https://github.com/astral-sh/ty/issues/1218. This bug doesn't currently cause us any real-world issues, because we don't yet understand the signatures typeshed gives us for `isinstance()` and `issubclass()` (typeshed's annotations there use PEP-613 type aliases). #20107 demonstrates that this will start causing us issues as soon as we add support for PEP-613 aliases, however, so it makes sense to fix it now. ## Test Plan Added mdtests --- .../resources/mdtest/protocols.md | 14 +++++++++++++- crates/ty_python_semantic/src/types/class.rs | 13 ++++++++++++- .../ty_python_semantic/src/types/special_form.rs | 6 +++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/protocols.md b/crates/ty_python_semantic/resources/mdtest/protocols.md index 97b8e6b9ad..27cdde1017 100644 --- a/crates/ty_python_semantic/resources/mdtest/protocols.md +++ b/crates/ty_python_semantic/resources/mdtest/protocols.md @@ -264,9 +264,21 @@ def f( # fmt: on ``` -Nonetheless, `Protocol` can still be used as the second argument to `issubclass()` at runtime: +Nonetheless, `Protocol` is an instance of `type` at runtime, and therefore can still be used as the +second argument to `issubclass()` at runtime: ```py +import abc +import typing +from ty_extensions import TypeOf + +reveal_type(type(Protocol)) # revealed: +# revealed: tuple[, , , ] +reveal_type(type(Protocol).__mro__) +static_assert(is_subtype_of(TypeOf[Protocol], type)) +static_assert(is_subtype_of(TypeOf[Protocol], abc.ABCMeta)) +static_assert(is_subtype_of(TypeOf[Protocol], typing._ProtocolMeta)) + # Could also be `Literal[True]`, but `bool` is fine: reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool ``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 48766457d7..9a45eb6b7d 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -3694,6 +3694,7 @@ pub enum KnownClass { ParamSpec, ParamSpecArgs, ParamSpecKwargs, + ProtocolMeta, TypeVarTuple, TypeAliasType, NoDefaultType, @@ -3821,6 +3822,7 @@ impl KnownClass { | Self::NamedTupleFallback | Self::NamedTupleLike | Self::ConstraintSet + | Self::ProtocolMeta | Self::TypedDictFallback => Some(Truthiness::Ambiguous), Self::Tuple => None, @@ -3903,6 +3905,7 @@ impl KnownClass { | KnownClass::ConstraintSet | KnownClass::TypedDictFallback | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta | KnownClass::Template => false, } } @@ -3982,6 +3985,7 @@ impl KnownClass { | KnownClass::ConstraintSet | KnownClass::TypedDictFallback | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta | KnownClass::Template => false, } } @@ -4060,6 +4064,7 @@ impl KnownClass { | KnownClass::NamedTupleFallback | KnownClass::ConstraintSet | KnownClass::BuiltinFunctionType + | KnownClass::ProtocolMeta | KnownClass::Template => false, } } @@ -4151,6 +4156,7 @@ impl KnownClass { | Self::ConstraintSet | Self::TypedDictFallback | Self::BuiltinFunctionType + | Self::ProtocolMeta | Self::Template => false, } } @@ -4250,6 +4256,7 @@ impl KnownClass { Self::ConstraintSet => "ConstraintSet", Self::TypedDictFallback => "TypedDictFallback", Self::Template => "Template", + Self::ProtocolMeta => "_ProtocolMeta", } } @@ -4477,6 +4484,7 @@ impl KnownClass { | Self::StdlibAlias | Self::Iterable | Self::Iterator + | Self::ProtocolMeta | Self::SupportsIndex => KnownModule::Typing, Self::TypeAliasType | Self::TypeVarTuple @@ -4596,6 +4604,7 @@ impl KnownClass { | Self::ConstraintSet | Self::TypedDictFallback | Self::BuiltinFunctionType + | Self::ProtocolMeta | Self::Template => Some(false), Self::Tuple => None, @@ -4680,6 +4689,7 @@ impl KnownClass { | Self::ConstraintSet | Self::TypedDictFallback | Self::BuiltinFunctionType + | Self::ProtocolMeta | Self::Template => false, } } @@ -4773,6 +4783,7 @@ impl KnownClass { "ConstraintSet" => Self::ConstraintSet, "TypedDictFallback" => Self::TypedDictFallback, "Template" => Self::Template, + "_ProtocolMeta" => Self::ProtocolMeta, _ => return None, }; @@ -4855,9 +4866,9 @@ impl KnownClass { | Self::TypeVarTuple | Self::Iterable | Self::Iterator + | Self::ProtocolMeta | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), - } } diff --git a/crates/ty_python_semantic/src/types/special_form.rs b/crates/ty_python_semantic/src/types/special_form.rs index de4dc4d3f1..721def0dee 100644 --- a/crates/ty_python_semantic/src/types/special_form.rs +++ b/crates/ty_python_semantic/src/types/special_form.rs @@ -160,9 +160,13 @@ impl SpecialFormType { | Self::Bottom | Self::Intersection | Self::CallableTypeOf - | Self::Protocol // actually `_ProtocolMeta` at runtime but this is what typeshed says | Self::ReadOnly => KnownClass::SpecialForm, + // Typeshed says it's an instance of `_SpecialForm`, + // but then we wouldn't recognise things like `issubclass(`X, Protocol)` + // as being valid. + Self::Protocol => KnownClass::ProtocolMeta, + Self::Generic | Self::Any => KnownClass::Type, Self::List