diff --git a/synapse/events/py_protocol.py b/synapse/events/py_protocol.py index d9ac8c066f..a34170cd93 100644 --- a/synapse/events/py_protocol.py +++ b/synapse/events/py_protocol.py @@ -33,7 +33,6 @@ predicates here when a new room-version feature gates access to additional attributes. """ -import abc from typing import ( TYPE_CHECKING, Sequence, @@ -42,12 +41,14 @@ from typing import ( from typing_extensions import TypeIs from synapse.events import EventBase +from synapse.synapse_rust.events import Event +from synapse.types import StrCollection if TYPE_CHECKING: from synapse.events.snapshot import EventContext, EventPersistencePair -class _DisableIsInstance(abc.ABCMeta): +class _DisableIsInstance(type): """Metaclass which disables isinstance checks on classes which use it, by making isinstance() raise NotImplementedError. @@ -61,15 +62,38 @@ class _DisableIsInstance(abc.ABCMeta): raise NotImplementedError("Instance cannot be used.") -class EventProtocol(EventBase, metaclass=_DisableIsInstance): - """Helper subclass that allows type narrowing for `EventBase` objects.""" +# We now define `EventProtocol` as a helper class for type narrowing. +# +# During type checking, we want the type narrowed event classes to still have +# all the fields as a normal `Event`, so we make `EventProtocol` a subclass of +# `Event`. +# +# However, at runtime we a) can't subclass `Event` because it's a Rust class, +# and b) don't want to allow `isinstance` checks against `EventProtocol` (as +# it's purely a type annotation helper, not a real class). So at runtime, we +# make `EventProtocol` a class with a metaclass that raises on `isinstance` +# checks. +if TYPE_CHECKING: + + class EventProtocol(Event): + """Helper subclass that allows type narrowing for `EventBase` objects.""" + +else: + + class EventProtocol(metaclass=_DisableIsInstance): + """Helper subclass that allows type narrowing for `EventBase` objects.""" + + def __new__(cls): + raise NotImplementedError( + f"{cls.__name__} cannot be instantiated as it is not a real class." + ) class MSC4242Event(EventProtocol): """Marker protocol for events in MSC4242 rooms. This allows us to narrow the type of events.""" - prev_state_events: list[str] + prev_state_events: StrCollection def supports_msc4242_state_dag(event: EventBase) -> TypeIs[MSC4242Event]: diff --git a/tests/events/test_py_protocol.py b/tests/events/test_py_protocol.py index 1bc80b6ed9..ee1e060236 100644 --- a/tests/events/test_py_protocol.py +++ b/tests/events/test_py_protocol.py @@ -18,7 +18,6 @@ from unittest.mock import Mock from synapse.api.room_versions import RoomVersion, RoomVersions from synapse.events import EventBase from synapse.events.py_protocol import ( - EventProtocol, MSC4242Event, all_supports_msc4242_state_dag, supports_msc4242_state_dag, @@ -49,9 +48,6 @@ class TestMetaClass(TestCase): # EventBase should work as normal self.assertFalse(isinstance(object(), EventBase)) - with self.assertRaises(NotImplementedError): - isinstance(object(), EventProtocol) - with self.assertRaises(NotImplementedError): isinstance(object(), MSC4242Event)