[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
This commit is contained in:
Alex Waygood 2025-09-22 08:29:03 +01:00 committed by GitHub
parent 3033d1e5a5
commit f1aacd0f2c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 30 additions and 3 deletions

View file

@ -264,9 +264,21 @@ def f(
# fmt: on # 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 ```py
import abc
import typing
from ty_extensions import TypeOf
reveal_type(type(Protocol)) # revealed: <class '_ProtocolMeta'>
# revealed: tuple[<class '_ProtocolMeta'>, <class 'ABCMeta'>, <class 'type'>, <class 'object'>]
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: # Could also be `Literal[True]`, but `bool` is fine:
reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
``` ```

View file

@ -3694,6 +3694,7 @@ pub enum KnownClass {
ParamSpec, ParamSpec,
ParamSpecArgs, ParamSpecArgs,
ParamSpecKwargs, ParamSpecKwargs,
ProtocolMeta,
TypeVarTuple, TypeVarTuple,
TypeAliasType, TypeAliasType,
NoDefaultType, NoDefaultType,
@ -3821,6 +3822,7 @@ impl KnownClass {
| Self::NamedTupleFallback | Self::NamedTupleFallback
| Self::NamedTupleLike | Self::NamedTupleLike
| Self::ConstraintSet | Self::ConstraintSet
| Self::ProtocolMeta
| Self::TypedDictFallback => Some(Truthiness::Ambiguous), | Self::TypedDictFallback => Some(Truthiness::Ambiguous),
Self::Tuple => None, Self::Tuple => None,
@ -3903,6 +3905,7 @@ impl KnownClass {
| KnownClass::ConstraintSet | KnownClass::ConstraintSet
| KnownClass::TypedDictFallback | KnownClass::TypedDictFallback
| KnownClass::BuiltinFunctionType | KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
| KnownClass::Template => false, | KnownClass::Template => false,
} }
} }
@ -3982,6 +3985,7 @@ impl KnownClass {
| KnownClass::ConstraintSet | KnownClass::ConstraintSet
| KnownClass::TypedDictFallback | KnownClass::TypedDictFallback
| KnownClass::BuiltinFunctionType | KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
| KnownClass::Template => false, | KnownClass::Template => false,
} }
} }
@ -4060,6 +4064,7 @@ impl KnownClass {
| KnownClass::NamedTupleFallback | KnownClass::NamedTupleFallback
| KnownClass::ConstraintSet | KnownClass::ConstraintSet
| KnownClass::BuiltinFunctionType | KnownClass::BuiltinFunctionType
| KnownClass::ProtocolMeta
| KnownClass::Template => false, | KnownClass::Template => false,
} }
} }
@ -4151,6 +4156,7 @@ impl KnownClass {
| Self::ConstraintSet | Self::ConstraintSet
| Self::TypedDictFallback | Self::TypedDictFallback
| Self::BuiltinFunctionType | Self::BuiltinFunctionType
| Self::ProtocolMeta
| Self::Template => false, | Self::Template => false,
} }
} }
@ -4250,6 +4256,7 @@ impl KnownClass {
Self::ConstraintSet => "ConstraintSet", Self::ConstraintSet => "ConstraintSet",
Self::TypedDictFallback => "TypedDictFallback", Self::TypedDictFallback => "TypedDictFallback",
Self::Template => "Template", Self::Template => "Template",
Self::ProtocolMeta => "_ProtocolMeta",
} }
} }
@ -4477,6 +4484,7 @@ impl KnownClass {
| Self::StdlibAlias | Self::StdlibAlias
| Self::Iterable | Self::Iterable
| Self::Iterator | Self::Iterator
| Self::ProtocolMeta
| Self::SupportsIndex => KnownModule::Typing, | Self::SupportsIndex => KnownModule::Typing,
Self::TypeAliasType Self::TypeAliasType
| Self::TypeVarTuple | Self::TypeVarTuple
@ -4596,6 +4604,7 @@ impl KnownClass {
| Self::ConstraintSet | Self::ConstraintSet
| Self::TypedDictFallback | Self::TypedDictFallback
| Self::BuiltinFunctionType | Self::BuiltinFunctionType
| Self::ProtocolMeta
| Self::Template => Some(false), | Self::Template => Some(false),
Self::Tuple => None, Self::Tuple => None,
@ -4680,6 +4689,7 @@ impl KnownClass {
| Self::ConstraintSet | Self::ConstraintSet
| Self::TypedDictFallback | Self::TypedDictFallback
| Self::BuiltinFunctionType | Self::BuiltinFunctionType
| Self::ProtocolMeta
| Self::Template => false, | Self::Template => false,
} }
} }
@ -4773,6 +4783,7 @@ impl KnownClass {
"ConstraintSet" => Self::ConstraintSet, "ConstraintSet" => Self::ConstraintSet,
"TypedDictFallback" => Self::TypedDictFallback, "TypedDictFallback" => Self::TypedDictFallback,
"Template" => Self::Template, "Template" => Self::Template,
"_ProtocolMeta" => Self::ProtocolMeta,
_ => return None, _ => return None,
}; };
@ -4855,9 +4866,9 @@ impl KnownClass {
| Self::TypeVarTuple | Self::TypeVarTuple
| Self::Iterable | Self::Iterable
| Self::Iterator | Self::Iterator
| Self::ProtocolMeta
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions), | Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions), Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions),
} }
} }

View file

@ -160,9 +160,13 @@ impl SpecialFormType {
| Self::Bottom | Self::Bottom
| Self::Intersection | Self::Intersection
| Self::CallableTypeOf | Self::CallableTypeOf
| Self::Protocol // actually `_ProtocolMeta` at runtime but this is what typeshed says
| Self::ReadOnly => KnownClass::SpecialForm, | 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::Generic | Self::Any => KnownClass::Type,
Self::List Self::List