[red-knot] Understand typing.Protocol and typing_extensions.Protocol as equivalent (#17446)
Some checks are pending
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[Knot Playground] Release / publish (push) Waiting to run

This commit is contained in:
Alex Waygood 2025-04-17 21:54:22 +01:00 committed by GitHub
parent 58807b2980
commit 9965cee998
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 29 additions and 13 deletions

View file

@ -227,13 +227,15 @@ reveal_type(issubclass(MyProtocol, Protocol)) # revealed: bool
```py
import typing
import typing_extensions
from knot_extensions import static_assert, is_equivalent_to
from knot_extensions import static_assert, is_equivalent_to, TypeOf
static_assert(is_equivalent_to(TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol]))
static_assert(is_equivalent_to(int | str | TypeOf[typing.Protocol], TypeOf[typing_extensions.Protocol] | str | int))
class Foo(typing.Protocol):
x: int
# TODO: should not error
class Bar(typing_extensions.Protocol): # error: [invalid-base]
class Bar(typing_extensions.Protocol):
x: int
# TODO: these should pass
@ -249,9 +251,8 @@ The same goes for `typing.runtime_checkable` and `typing_extensions.runtime_chec
class RuntimeCheckableFoo(typing.Protocol):
x: int
# TODO: should not error
@typing.runtime_checkable
class RuntimeCheckableBar(typing_extensions.Protocol): # error: [invalid-base]
class RuntimeCheckableBar(typing_extensions.Protocol):
x: int
# TODO: these should pass
@ -264,6 +265,15 @@ isinstance(object(), RuntimeCheckableFoo)
isinstance(object(), RuntimeCheckableBar)
```
However, we understand that they are not necessarily the same symbol at the same memory address at
runtime -- these reveal `bool` rather than `Literal[True]` or `Literal[False]`, which would be
incorrect:
```py
reveal_type(typing.Protocol is typing_extensions.Protocol) # revealed: bool
reveal_type(typing.Protocol is not typing_extensions.Protocol) # revealed: bool
```
## Calls to protocol classes
Neither `Protocol`, nor any protocol class, can be directly instantiated:
@ -309,8 +319,7 @@ via `typing_extensions`.
```py
from typing_extensions import Protocol, get_protocol_members
# TODO: should not error
class Foo(Protocol): # error: [invalid-base]
class Foo(Protocol):
x: int
@property
@ -351,8 +360,7 @@ Certain special attributes and methods are not considered protocol members at ru
not be considered protocol members by type checkers either:
```py
# TODO: should not error
class Lumberjack(Protocol): # error: [invalid-base]
class Lumberjack(Protocol):
__slots__ = ()
__match_args__ = ()
x: int

View file

@ -1949,8 +1949,16 @@ impl<'db> Type<'db> {
| Type::WrapperDescriptor(..)
| Type::ClassLiteral(..)
| Type::GenericAlias(..)
| Type::ModuleLiteral(..)
| Type::KnownInstance(..) => true,
| Type::ModuleLiteral(..) => true,
Type::KnownInstance(known_instance) => {
// Nearly all `KnownInstance` types are singletons, but if a symbol could validly
// originate from either `typing` or `typing_extensions` then this is not guaranteed.
// E.g. `typing.Protocol` is equivalent to `typing_extensions.Protocol`, so both are treated
// as inhabiting the type `KnownInstanceType::Protocol` in our model, but they are actually
// distinct symbols at different memory addresses at runtime.
!(known_instance.check_module(KnownModule::Typing)
&& known_instance.check_module(KnownModule::TypingExtensions))
}
Type::Callable(_) => {
// A callable type is never a singleton because for any given signature,
// there could be any number of distinct objects that are all callable with that

View file

@ -2532,7 +2532,7 @@ impl<'db> KnownInstanceType<'db> {
///
/// Most variants can only exist in one module, which is the same as `self.class().canonical_module()`.
/// Some variants could validly be defined in either `typing` or `typing_extensions`, however.
fn check_module(self, module: KnownModule) -> bool {
pub(super) fn check_module(self, module: KnownModule) -> bool {
match self {
Self::Any
| Self::ClassVar
@ -2545,7 +2545,6 @@ impl<'db> KnownInstanceType<'db> {
| Self::Counter
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::Optional
| Self::Union
| Self::NoReturn
@ -2553,6 +2552,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::Type
| Self::Callable => module.is_typing(),
Self::Annotated
| Self::Protocol
| Self::Literal
| Self::LiteralString
| Self::Never