Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions crates/red_knot_python_semantic/resources/mdtest/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,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
Expand All @@ -244,9 +246,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
Expand All @@ -259,6 +260,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:
Expand Down Expand Up @@ -304,8 +314,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
Expand Down Expand Up @@ -346,8 +355,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
Expand Down
12 changes: 10 additions & 2 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/red_knot_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -2545,14 +2545,14 @@ impl<'db> KnownInstanceType<'db> {
| Self::Counter
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::Optional
| Self::Union
| Self::NoReturn
| Self::Tuple
| Self::Type
| Self::Callable => module.is_typing(),
Self::Annotated
| Self::Protocol
| Self::Literal
| Self::LiteralString
| Self::Never
Expand Down
Loading