From df24e0f30244b1c423f4130d64c6008be341d0b7 Mon Sep 17 00:00:00 2001 From: Devon Hudson Date: Fri, 12 Dec 2025 15:34:13 +0000 Subject: [PATCH] Fix support for older versions of zope-interface (#19274) Fixes #19269 Versions of zope-interface from RHEL, Ubuntu LTS 22 & 24 and OpenSuse don't support the new python union `X | Y` syntax for interfaces. This PR partially reverts the change over to fully use the new syntax, adds a minimum supported version of zope-interface to Synapse's dependency list, and removes the linter auto-upgrades which prefer the newer syntax. ### Pull Request Checklist * [X] Pull request is based on the develop branch * [X] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [X] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --------- Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/19274.bugfix | 1 + poetry.lock | 2 +- pyproject.toml | 12 ++++++------ synapse/app/_base.py | 3 ++- synapse/app/admin_cmd.py | 4 ++-- synapse/app/generic_worker.py | 3 ++- synapse/app/homeserver.py | 4 ++-- synapse/handlers/delayed_events.py | 4 ++-- synapse/handlers/message.py | 4 ++-- synapse/handlers/user_directory.py | 4 ++-- synapse/http/client.py | 9 +++++---- synapse/http/federation/matrix_federation_agent.py | 4 ++-- synapse/http/matrixfederationclient.py | 3 ++- synapse/http/proxy.py | 4 ++-- synapse/http/proxyagent.py | 14 +++++++------- synapse/http/replicationagent.py | 3 ++- synapse/logging/_remote.py | 4 ++-- synapse/logging/handlers.py | 4 ++-- synapse/media/_base.py | 3 ++- synapse/push/emailpusher.py | 4 ++-- synapse/push/httppusher.py | 4 ++-- synapse/server.py | 5 +++-- synapse/util/file_consumer.py | 6 +++--- tests/server.py | 10 +++++----- tests/unittest.py | 3 ++- 25 files changed, 65 insertions(+), 56 deletions(-) create mode 100644 changelog.d/19274.bugfix diff --git a/changelog.d/19274.bugfix b/changelog.d/19274.bugfix new file mode 100644 index 0000000000..92aaa0fe6d --- /dev/null +++ b/changelog.d/19274.bugfix @@ -0,0 +1 @@ +Fix bug introduced in 1.143.0 that broke support for versions of `zope-interface` older than 6.2. diff --git a/poetry.lock b/poetry.lock index 4dacae38a4..3f8607b687 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3542,4 +3542,4 @@ url-preview = ["lxml"] [metadata] lock-version = "2.1" python-versions = ">=3.10.0,<4.0.0" -content-hash = "abbbdff591a306b56cc8890dbb2f477ac5f1a2d328baa6409e01084abc655bbf" +content-hash = "1caa5072f6304122c89377420f993a54f54587f3618ccc8094ec31642264592c" diff --git a/pyproject.toml b/pyproject.toml index a9832ccabf..09ca2a9e77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,12 @@ dependencies = [ "pyrsistent>=0.18.0", # via jsonschema "requests>=2.16.0", # 2.16.0+ no longer vendors urllib3, avoiding Python 3.10+ incompatibility "urllib3>=1.26.5", # via treq; 1.26.5 fixes Python 3.10+ collections.abc compatibility - "zope-interface>=6.2", # via twisted + # 5.2 is the current version in Debian oldstable. If we don't care to support that, then 5.4 is + # the minimum version from Ubuntu 22.04 and RHEL 9. (as of 2025-12) + # When bumping this version to 6.2 or above, refer to https://github.com/element-hq/synapse/pull/19274 + # for details of Synapse improvements that may be unlocked. Particularly around the use of `|` + # syntax with zope interface types. + "zope-interface>=5.2", # via twisted ] [project.optional-dependencies] @@ -383,15 +388,10 @@ select = [ "G", # pyupgrade "UP006", - "UP007", - "UP045", ] extend-safe-fixes = [ # pyupgrade rules compatible with Python >= 3.9 "UP006", - "UP007", - # pyupgrade rules compatible with Python >= 3.10 - "UP045", # Allow ruff to automatically fix trailing spaces within a multi-line string/comment. "W293" ] diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 98d051bf04..c64c41e9d2 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -36,6 +36,7 @@ from typing import ( Awaitable, Callable, NoReturn, + Optional, cast, ) from wsgiref.simple_server import WSGIServer @@ -455,7 +456,7 @@ def listen_http( root_resource: Resource, version_string: str, max_request_body_size: int, - context_factory: IOpenSSLContextFactory | None, + context_factory: Optional[IOpenSSLContextFactory], reactor: ISynapseReactor = reactor, ) -> list[Port]: """ diff --git a/synapse/app/admin_cmd.py b/synapse/app/admin_cmd.py index facc98164e..0614c805da 100644 --- a/synapse/app/admin_cmd.py +++ b/synapse/app/admin_cmd.py @@ -24,7 +24,7 @@ import logging import os import sys import tempfile -from typing import Mapping, Sequence +from typing import Mapping, Optional, Sequence from twisted.internet import defer, task @@ -291,7 +291,7 @@ def load_config(argv_options: list[str]) -> tuple[HomeServerConfig, argparse.Nam def create_homeserver( config: HomeServerConfig, - reactor: ISynapseReactor | None = None, + reactor: Optional[ISynapseReactor] = None, ) -> AdminCmdServer: """ Create a homeserver instance for the Synapse admin command process. diff --git a/synapse/app/generic_worker.py b/synapse/app/generic_worker.py index 9939c0fe7d..0a4abd1839 100644 --- a/synapse/app/generic_worker.py +++ b/synapse/app/generic_worker.py @@ -21,6 +21,7 @@ # import logging import sys +from typing import Optional from twisted.web.resource import Resource @@ -335,7 +336,7 @@ def load_config(argv_options: list[str]) -> HomeServerConfig: def create_homeserver( config: HomeServerConfig, - reactor: ISynapseReactor | None = None, + reactor: Optional[ISynapseReactor] = None, ) -> GenericWorkerServer: """ Create a homeserver instance for the Synapse worker process. diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py index bd2956d9e1..2b1760416b 100644 --- a/synapse/app/homeserver.py +++ b/synapse/app/homeserver.py @@ -22,7 +22,7 @@ import logging import os import sys -from typing import Iterable +from typing import Iterable, Optional from twisted.internet.tcp import Port from twisted.web.resource import EncodingResourceWrapper, Resource @@ -350,7 +350,7 @@ def load_or_generate_config(argv_options: list[str]) -> HomeServerConfig: def create_homeserver( config: HomeServerConfig, - reactor: ISynapseReactor | None = None, + reactor: Optional[ISynapseReactor] = None, ) -> SynapseHomeServer: """ Create a homeserver instance for the Synapse main process. diff --git a/synapse/handlers/delayed_events.py b/synapse/handlers/delayed_events.py index cb0a4dd6b2..c58d1d42bc 100644 --- a/synapse/handlers/delayed_events.py +++ b/synapse/handlers/delayed_events.py @@ -13,7 +13,7 @@ # import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from twisted.internet.interfaces import IDelayedCall @@ -74,7 +74,7 @@ class DelayedEventsHandler: cfg=self._config.ratelimiting.rc_delayed_event_mgmt, ) - self._next_delayed_event_call: IDelayedCall | None = None + self._next_delayed_event_call: Optional[IDelayedCall] = None # The current position in the current_state_delta stream self._event_pos: int | None = None diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index a6499de3a8..7808bd68cb 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -22,7 +22,7 @@ import logging import random from http import HTTPStatus -from typing import TYPE_CHECKING, Any, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence from canonicaljson import encode_canonical_json @@ -111,7 +111,7 @@ class MessageHandler: # The scheduled call to self._expire_event. None if no call is currently # scheduled. - self._scheduled_expiry: IDelayedCall | None = None + self._scheduled_expiry: Optional[IDelayedCall] = None if not hs.config.worker.worker_app: self.hs.run_as_background_process( diff --git a/synapse/handlers/user_directory.py b/synapse/handlers/user_directory.py index 36b037e8e1..e5c4de03c5 100644 --- a/synapse/handlers/user_directory.py +++ b/synapse/handlers/user_directory.py @@ -21,7 +21,7 @@ import logging from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from twisted.internet.interfaces import IDelayedCall @@ -125,7 +125,7 @@ class UserDirectoryHandler(StateDeltasHandler): # Guard to ensure we only have one process for refreshing remote profiles self._is_refreshing_remote_profiles = False # Handle to cancel the `call_later` of `kick_off_remote_profile_refresh_process` - self._refresh_remote_profiles_call_later: IDelayedCall | None = None + self._refresh_remote_profiles_call_later: Optional[IDelayedCall] = None # Guard to ensure we only have one process for refreshing remote profiles # for the given servers. diff --git a/synapse/http/client.py b/synapse/http/client.py index f0b9201086..05c5f13a87 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -28,6 +28,7 @@ from typing import ( BinaryIO, Callable, Mapping, + Optional, Protocol, ) @@ -313,7 +314,7 @@ class BlocklistingAgentWrapper(Agent): method: bytes, uri: bytes, headers: Headers | None = None, - bodyProducer: IBodyProducer | None = None, + bodyProducer: Optional[IBodyProducer] = None, ) -> defer.Deferred: h = urllib.parse.urlparse(uri.decode("ascii")) @@ -1033,7 +1034,7 @@ class BodyExceededMaxSize(Exception): class _DiscardBodyWithMaxSizeProtocol(protocol.Protocol): """A protocol which immediately errors upon receiving data.""" - transport: ITCPTransport | None = None + transport: Optional[ITCPTransport] = None def __init__(self, deferred: defer.Deferred): self.deferred = deferred @@ -1075,7 +1076,7 @@ class _MultipartParserProtocol(protocol.Protocol): Protocol to read and parse a MSC3916 multipart/mixed response """ - transport: ITCPTransport | None = None + transport: Optional[ITCPTransport] = None def __init__( self, @@ -1188,7 +1189,7 @@ class _MultipartParserProtocol(protocol.Protocol): class _ReadBodyWithMaxSizeProtocol(protocol.Protocol): """A protocol which reads body to a stream, erroring if the body exceeds a maximum size.""" - transport: ITCPTransport | None = None + transport: Optional[ITCPTransport] = None def __init__( self, stream: ByteWriteable, deferred: defer.Deferred, max_size: int | None diff --git a/synapse/http/federation/matrix_federation_agent.py b/synapse/http/federation/matrix_federation_agent.py index c3ba26fe03..a0167659f1 100644 --- a/synapse/http/federation/matrix_federation_agent.py +++ b/synapse/http/federation/matrix_federation_agent.py @@ -19,7 +19,7 @@ # import logging import urllib.parse -from typing import Any, Generator +from typing import Any, Generator, Optional from urllib.request import ( # type: ignore[attr-defined] proxy_bypass_environment, ) @@ -173,7 +173,7 @@ class MatrixFederationAgent: method: bytes, uri: bytes, headers: Headers | None = None, - bodyProducer: IBodyProducer | None = None, + bodyProducer: Optional[IBodyProducer] = None, ) -> Generator[defer.Deferred, Any, IResponse]: """ Args: diff --git a/synapse/http/matrixfederationclient.py b/synapse/http/matrixfederationclient.py index 7090960cfb..dbd4f1e4b6 100644 --- a/synapse/http/matrixfederationclient.py +++ b/synapse/http/matrixfederationclient.py @@ -33,6 +33,7 @@ from typing import ( Callable, Generic, Literal, + Optional, TextIO, TypeVar, cast, @@ -691,7 +692,7 @@ class MatrixFederationHttpClient: destination_bytes, method_bytes, url_to_sign_bytes, json ) data = encode_canonical_json(json) - producer: IBodyProducer | None = QuieterFileBodyProducer( + producer: Optional[IBodyProducer] = QuieterFileBodyProducer( BytesIO(data), cooperator=self._cooperator ) else: diff --git a/synapse/http/proxy.py b/synapse/http/proxy.py index c7f5e39dd8..b3a2f84f29 100644 --- a/synapse/http/proxy.py +++ b/synapse/http/proxy.py @@ -22,7 +22,7 @@ import json import logging import urllib.parse -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Optional, cast from twisted.internet import protocol from twisted.internet.interfaces import ITCPTransport @@ -237,7 +237,7 @@ class _ProxyResponseBody(protocol.Protocol): request. """ - transport: ITCPTransport | None = None + transport: Optional[ITCPTransport] = None def __init__(self, request: "SynapseRequest") -> None: self._request = request diff --git a/synapse/http/proxyagent.py b/synapse/http/proxyagent.py index d315ce8475..1f8e58efbc 100644 --- a/synapse/http/proxyagent.py +++ b/synapse/http/proxyagent.py @@ -21,7 +21,7 @@ import logging import random import re -from typing import Any, Collection, Sequence, cast +from typing import Any, Collection, Optional, Sequence, cast from urllib.parse import urlparse from urllib.request import ( # type: ignore[attr-defined] proxy_bypass_environment, @@ -119,8 +119,8 @@ class ProxyAgent(_AgentBase): self, *, reactor: IReactorCore, - proxy_reactor: IReactorCore | None = None, - contextFactory: IPolicyForHTTPS | None = None, + proxy_reactor: Optional[IReactorCore] = None, + contextFactory: Optional[IPolicyForHTTPS] = None, connectTimeout: float | None = None, bindAddress: bytes | None = None, pool: HTTPConnectionPool | None = None, @@ -175,7 +175,7 @@ class ProxyAgent(_AgentBase): self._policy_for_https = contextFactory self._reactor = cast(IReactorTime, reactor) - self._federation_proxy_endpoint: IStreamClientEndpoint | None = None + self._federation_proxy_endpoint: Optional[IStreamClientEndpoint] = None self._federation_proxy_credentials: ProxyCredentials | None = None if federation_proxy_locations: assert federation_proxy_credentials is not None, ( @@ -221,7 +221,7 @@ class ProxyAgent(_AgentBase): method: bytes, uri: bytes, headers: Headers | None = None, - bodyProducer: IBodyProducer | None = None, + bodyProducer: Optional[IBodyProducer] = None, ) -> "defer.Deferred[IResponse]": """ Issue a request to the server indicated by the given uri. @@ -365,11 +365,11 @@ class ProxyAgent(_AgentBase): def http_proxy_endpoint( proxy: bytes | None, reactor: IReactorCore, - tls_options_factory: IPolicyForHTTPS | None, + tls_options_factory: Optional[IPolicyForHTTPS], timeout: float = 30, bindAddress: bytes | str | tuple[bytes | str, int] | None = None, attemptDelay: float | None = None, -) -> tuple[IStreamClientEndpoint | None, ProxyCredentials | None]: +) -> tuple[Optional[IStreamClientEndpoint], ProxyCredentials | None]: """Parses an http proxy setting and returns an endpoint for the proxy Args: diff --git a/synapse/http/replicationagent.py b/synapse/http/replicationagent.py index 708e4c386b..3d47107cf2 100644 --- a/synapse/http/replicationagent.py +++ b/synapse/http/replicationagent.py @@ -20,6 +20,7 @@ # import logging +from typing import Optional from zope.interface import implementer @@ -149,7 +150,7 @@ class ReplicationAgent(_AgentBase): method: bytes, uri: bytes, headers: Headers | None = None, - bodyProducer: IBodyProducer | None = None, + bodyProducer: Optional[IBodyProducer] = None, ) -> "defer.Deferred[IResponse]": """ Issue a request to the server indicated by the given uri. diff --git a/synapse/logging/_remote.py b/synapse/logging/_remote.py index e3e0ba4beb..72faa3c746 100644 --- a/synapse/logging/_remote.py +++ b/synapse/logging/_remote.py @@ -25,7 +25,7 @@ import traceback from collections import deque from ipaddress import IPv4Address, IPv6Address, ip_address from math import floor -from typing import Callable +from typing import Callable, Optional import attr from zope.interface import implementer @@ -113,7 +113,7 @@ class RemoteHandler(logging.Handler): port: int, maximum_buffer: int = 1000, level: int = logging.NOTSET, - _reactor: IReactorTime | None = None, + _reactor: Optional[IReactorTime] = None, ): super().__init__(level=level) self.host = host diff --git a/synapse/logging/handlers.py b/synapse/logging/handlers.py index 976c7075d4..984d7c2238 100644 --- a/synapse/logging/handlers.py +++ b/synapse/logging/handlers.py @@ -3,7 +3,7 @@ import time from logging import Handler, LogRecord from logging.handlers import MemoryHandler from threading import Thread -from typing import cast +from typing import Optional, cast from twisted.internet.interfaces import IReactorCore @@ -26,7 +26,7 @@ class PeriodicallyFlushingMemoryHandler(MemoryHandler): target: Handler | None = None, flushOnClose: bool = True, period: float = 5.0, - reactor: IReactorCore | None = None, + reactor: Optional[IReactorCore] = None, ) -> None: """ period: the period between automatic flushes diff --git a/synapse/media/_base.py b/synapse/media/_base.py index 0fe2e5b529..7884930876 100644 --- a/synapse/media/_base.py +++ b/synapse/media/_base.py @@ -30,6 +30,7 @@ from typing import ( Awaitable, BinaryIO, Generator, + Optional, ) import attr @@ -705,7 +706,7 @@ class ThreadedFileSender: self.file: BinaryIO | None = None self.deferred: "Deferred[None]" = Deferred() - self.consumer: interfaces.IConsumer | None = None + self.consumer: Optional[IConsumer] = None # Signals if the thread should keep reading/sending data. Set means # continue, clear means pause. diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index ce4a2102e4..c44222f6ea 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -20,7 +20,7 @@ # import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from twisted.internet.error import AlreadyCalled, AlreadyCancelled from twisted.internet.interfaces import IDelayedCall @@ -71,7 +71,7 @@ class EmailPusher(Pusher): self.server_name = hs.hostname self.store = self.hs.get_datastores().main self.email = pusher_config.pushkey - self.timed_call: IDelayedCall | None = None + self.timed_call: Optional[IDelayedCall] = None self.throttle_params: dict[str, ThrottleParams] = {} self._inited = False diff --git a/synapse/push/httppusher.py b/synapse/push/httppusher.py index 1e7e742ddd..fdfae234be 100644 --- a/synapse/push/httppusher.py +++ b/synapse/push/httppusher.py @@ -21,7 +21,7 @@ import logging import random import urllib.parse -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional from prometheus_client import Counter @@ -120,7 +120,7 @@ class HttpPusher(Pusher): self.data = pusher_config.data self.backoff_delay = HttpPusher.INITIAL_BACKOFF_SEC self.failing_since = pusher_config.failing_since - self.timed_call: IDelayedCall | None = None + self.timed_call: Optional[IDelayedCall] = None self._is_processing = False self._group_unread_count_by_room = ( hs.config.push.push_group_unread_count_by_room diff --git a/synapse/server.py b/synapse/server.py index 88662c5b28..be83a59b88 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -34,6 +34,7 @@ from typing import ( Any, Awaitable, Callable, + Optional, TypeVar, cast, ) @@ -320,7 +321,7 @@ class HomeServer(metaclass=abc.ABCMeta): self, hostname: str, config: HomeServerConfig, - reactor: ISynapseReactor | None = None, + reactor: Optional[ISynapseReactor] = None, ): """ Args: @@ -353,7 +354,7 @@ class HomeServer(metaclass=abc.ABCMeta): self._module_web_resources_consumed = False # This attribute is set by the free function `refresh_certificate`. - self.tls_server_context_factory: IOpenSSLContextFactory | None = None + self.tls_server_context_factory: Optional[IOpenSSLContextFactory] = None self._is_shutdown = False self._async_shutdown_handlers: list[ShutdownInfo] = [] diff --git a/synapse/util/file_consumer.py b/synapse/util/file_consumer.py index 8d64684084..c473c524f6 100644 --- a/synapse/util/file_consumer.py +++ b/synapse/util/file_consumer.py @@ -19,7 +19,7 @@ # import queue -from typing import Any, BinaryIO, cast +from typing import Any, BinaryIO, Optional, Union, cast from twisted.internet import threads from twisted.internet.defer import Deferred @@ -50,7 +50,7 @@ class BackgroundFileConsumer: self._reactor: ISynapseReactor = reactor # Producer we're registered with - self._producer: IPushProducer | IPullProducer | None = None + self._producer: Optional[Union[IPushProducer, IPullProducer]] = None # True if PushProducer, false if PullProducer self.streaming = False @@ -72,7 +72,7 @@ class BackgroundFileConsumer: self._write_exception: Exception | None = None def registerProducer( - self, producer: IPushProducer | IPullProducer, streaming: bool + self, producer: Union[IPushProducer, IPullProducer], streaming: bool ) -> None: """Part of IConsumer interface diff --git a/tests/server.py b/tests/server.py index ce31a4162a..d17b2478e3 100644 --- a/tests/server.py +++ b/tests/server.py @@ -147,7 +147,7 @@ class FakeChannel: _reactor: MemoryReactorClock result: dict = attr.Factory(dict) _ip: str = "127.0.0.1" - _producer: IPullProducer | IPushProducer | None = None + _producer: Optional[Union[IPullProducer, IPushProducer]] = None resource_usage: ContextResourceUsage | None = None _request: Request | None = None @@ -248,7 +248,7 @@ class FakeChannel: # TODO This should ensure that the IProducer is an IPushProducer or # IPullProducer, unfortunately twisted.protocols.basic.FileSender does # implement those, but doesn't declare it. - self._producer = cast(IPushProducer | IPullProducer, producer) + self._producer = cast(Union[IPushProducer, IPullProducer], producer) self.producerStreaming = streaming def _produce() -> None: @@ -852,7 +852,7 @@ class FakeTransport: """Test reactor """ - _protocol: IProtocol | None = None + _protocol: Optional[IProtocol] = None """The Protocol which is producing data for this transport. Optional, but if set will get called back for connectionLost() notifications etc. """ @@ -871,7 +871,7 @@ class FakeTransport: disconnected = False connected = True buffer: bytes = b"" - producer: IPushProducer | None = None + producer: Optional[IPushProducer] = None autoflush: bool = True def getPeer(self) -> IPv4Address | IPv6Address: @@ -1073,7 +1073,7 @@ def setup_test_homeserver( cleanup_func: Callable[[Callable[[], Optional["Deferred[None]"]]], None], server_name: str = "test", config: HomeServerConfig | None = None, - reactor: ISynapseReactor | None = None, + reactor: Optional[ISynapseReactor] = None, homeserver_to_use: type[HomeServer] = TestHomeServer, db_txn_limit: int | None = None, **extra_homeserver_attributes: Any, diff --git a/tests/unittest.py b/tests/unittest.py index 7ea29364db..6022c750d0 100644 --- a/tests/unittest.py +++ b/tests/unittest.py @@ -37,6 +37,7 @@ from typing import ( Iterable, Mapping, NoReturn, + Optional, Protocol, TypeVar, ) @@ -636,7 +637,7 @@ class HomeserverTestCase(TestCase): self, server_name: str | None = None, config: JsonDict | None = None, - reactor: ISynapseReactor | None = None, + reactor: Optional[ISynapseReactor] = None, clock: Clock | None = None, **extra_homeserver_attributes: Any, ) -> HomeServer: