diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a89d4de..c40314f0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,17 +57,17 @@ jobs: strategy: matrix: python: - - "3.10" - "3.11" - "3.12" - "3.13" - "3.14" - - "3.14t" - - "pypy-3.10" + - "3.15" + - "3.15t" + - "pypy-3.11" is_main: - ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} exclude: - - python: "pypy-3.10" + - python: "pypy-3.11" is_main: false steps: - name: Check out repository diff --git a/docs/faq/common.rst b/docs/faq/common.rst index 1ee0062a..aecf674e 100644 --- a/docs/faq/common.rst +++ b/docs/faq/common.rst @@ -81,15 +81,11 @@ you can adjust it with the ``ping_timeout`` argument. How do I set a timeout on :meth:`~Connection.recv`? --------------------------------------------------- -On Python ≥ 3.11, use :func:`asyncio.timeout`:: +Use :func:`asyncio.timeout`:: async with asyncio.timeout(timeout=10): message = await websocket.recv() -On older versions of Python, use :func:`asyncio.wait_for`:: - - message = await asyncio.wait_for(websocket.recv(), timeout=10) - This technique works for most APIs. When it doesn't, for example with asynchronous context managers, websockets provides an ``open_timeout`` argument. diff --git a/docs/intro/index.rst b/docs/intro/index.rst index 6b783f90..e27072ff 100644 --- a/docs/intro/index.rst +++ b/docs/intro/index.rst @@ -6,7 +6,7 @@ Getting started Requirements ------------ -websockets requires Python ≥ 3.10. +websockets requires Python ≥ 3.11. .. admonition:: Use the most recent Python release :class: tip diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index a75faff9..c1459755 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -35,6 +35,11 @@ notice. Backwards-incompatible changes .............................. +.. admonition:: websockets 17.0 requires Python ≥ 3.11. + :class: tip + + websockets 16.1 is the last version supporting Python 3.10. + .. admonition:: In the :mod:`threading` implementation, the ``socket`` argument is renamed to ``sock``. :class: note @@ -46,6 +51,11 @@ Backwards-incompatible changes :class:`~sync.server.Server` is also renamed. If you're passing it as a keyword argument, you must change your code. +New features +............ + +* Validated compatibility with Python 3.15. + .. _16.1: 16.1 diff --git a/pyproject.toml b/pyproject.toml index 2e7cd3b8..e403685c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "websockets" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -requires-python = ">=3.10" +requires-python = ">=3.11" license = "BSD-3-Clause" authors = [ { name = "Aymeric Augustin", email = "aymeric.augustin@m4x.org" }, @@ -18,11 +18,11 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", ] dynamic = ["version", "readme"] @@ -57,8 +57,6 @@ core = "ctrace" omit = [ # */websockets matches src/websockets and .tox/**/site-packages/websockets "*/websockets/__main__.py", - "*/websockets/asyncio/async_timeout.py", - "*/websockets/asyncio/compatibility.py", "tests/maxi_cov.py", ] diff --git a/src/websockets/asyncio/async_timeout.py b/src/websockets/asyncio/async_timeout.py deleted file mode 100644 index 6ffa8996..00000000 --- a/src/websockets/asyncio/async_timeout.py +++ /dev/null @@ -1,282 +0,0 @@ -# From https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py -# Licensed under the Apache License (Apache-2.0) - -import asyncio -import enum -import sys -import warnings -from types import TracebackType -from typing import Optional, Type - - -if sys.version_info >= (3, 11): - from typing import final -else: - # From https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py - # Licensed under the Python Software Foundation License (PSF-2.0) - - # @final exists in 3.8+, but we backport it for all versions - # before 3.11 to keep support for the __final__ attribute. - # See https://bugs.python.org/issue46342 - def final(f): - """This decorator can be used to indicate to type checkers that - the decorated method cannot be overridden, and decorated class - cannot be subclassed. For example: - - class Base: - @final - def done(self) -> None: - ... - class Sub(Base): - def done(self) -> None: # Error reported by type checker - ... - @final - class Leaf: - ... - class Other(Leaf): # Error reported by type checker - ... - - There is no runtime checking of these properties. The decorator - sets the ``__final__`` attribute to ``True`` on the decorated object - to allow runtime introspection. - """ - try: - f.__final__ = True - except (AttributeError, TypeError): - # Skip the attribute silently if it is not writable. - # AttributeError happens if the object has __slots__ or a - # read-only property, TypeError if it's a builtin class. - pass - return f - - # End https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py - - -if sys.version_info >= (3, 11): - - def _uncancel_task(task: "asyncio.Task[object]") -> None: - task.uncancel() - -else: - - def _uncancel_task(task: "asyncio.Task[object]") -> None: - pass - - -__version__ = "4.0.3" - - -__all__ = ("timeout", "timeout_at", "Timeout") - - -def timeout(delay: Optional[float]) -> "Timeout": - """timeout context manager. - - Useful in cases when you want to apply timeout logic around block - of code or in cases when asyncio.wait_for is not suitable. For example: - - >>> async with timeout(0.001): - ... async with aiohttp.get('https://github.com') as r: - ... await r.text() - - - delay - value in seconds or None to disable timeout logic - """ - loop = asyncio.get_running_loop() - if delay is not None: - deadline = loop.time() + delay # type: Optional[float] - else: - deadline = None - return Timeout(deadline, loop) - - -def timeout_at(deadline: Optional[float]) -> "Timeout": - """Schedule the timeout at absolute time. - - deadline argument points on the time in the same clock system - as loop.time(). - - Please note: it is not POSIX time but a time with - undefined starting base, e.g. the time of the system power on. - - >>> async with timeout_at(loop.time() + 10): - ... async with aiohttp.get('https://github.com') as r: - ... await r.text() - - - """ - loop = asyncio.get_running_loop() - return Timeout(deadline, loop) - - -class _State(enum.Enum): - INIT = "INIT" - ENTER = "ENTER" - TIMEOUT = "TIMEOUT" - EXIT = "EXIT" - - -@final -class Timeout: - # Internal class, please don't instantiate it directly - # Use timeout() and timeout_at() public factories instead. - # - # Implementation note: `async with timeout()` is preferred - # over `with timeout()`. - # While technically the Timeout class implementation - # doesn't need to be async at all, - # the `async with` statement explicitly points that - # the context manager should be used from async function context. - # - # This design allows to avoid many silly misusages. - # - # TimeoutError is raised immediately when scheduled - # if the deadline is passed. - # The purpose is to time out as soon as possible - # without waiting for the next await expression. - - __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler", "_task") - - def __init__( - self, deadline: Optional[float], loop: asyncio.AbstractEventLoop - ) -> None: - self._loop = loop - self._state = _State.INIT - - self._task: Optional["asyncio.Task[object]"] = None - self._timeout_handler = None # type: Optional[asyncio.Handle] - if deadline is None: - self._deadline = None # type: Optional[float] - else: - self.update(deadline) - - def __enter__(self) -> "Timeout": - warnings.warn( - "with timeout() is deprecated, use async with timeout() instead", - DeprecationWarning, - stacklevel=2, - ) - self._do_enter() - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> Optional[bool]: - self._do_exit(exc_type) - return None - - async def __aenter__(self) -> "Timeout": - self._do_enter() - return self - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> Optional[bool]: - self._do_exit(exc_type) - return None - - @property - def expired(self) -> bool: - """Is timeout expired during execution?""" - return self._state == _State.TIMEOUT - - @property - def deadline(self) -> Optional[float]: - return self._deadline - - def reject(self) -> None: - """Reject scheduled timeout if any.""" - # cancel is maybe better name but - # task.cancel() raises CancelledError in asyncio world. - if self._state not in (_State.INIT, _State.ENTER): - raise RuntimeError(f"invalid state {self._state.value}") - self._reject() - - def _reject(self) -> None: - self._task = None - if self._timeout_handler is not None: - self._timeout_handler.cancel() - self._timeout_handler = None - - def shift(self, delay: float) -> None: - """Advance timeout on delay seconds. - - The delay can be negative. - - Raise RuntimeError if shift is called when deadline is not scheduled - """ - deadline = self._deadline - if deadline is None: - raise RuntimeError("cannot shift timeout if deadline is not scheduled") - self.update(deadline + delay) - - def update(self, deadline: float) -> None: - """Set deadline to absolute value. - - deadline argument points on the time in the same clock system - as loop.time(). - - If new deadline is in the past the timeout is raised immediately. - - Please note: it is not POSIX time but a time with - undefined starting base, e.g. the time of the system power on. - """ - if self._state == _State.EXIT: - raise RuntimeError("cannot reschedule after exit from context manager") - if self._state == _State.TIMEOUT: - raise RuntimeError("cannot reschedule expired timeout") - if self._timeout_handler is not None: - self._timeout_handler.cancel() - self._deadline = deadline - if self._state != _State.INIT: - self._reschedule() - - def _reschedule(self) -> None: - assert self._state == _State.ENTER - deadline = self._deadline - if deadline is None: - return - - now = self._loop.time() - if self._timeout_handler is not None: - self._timeout_handler.cancel() - - self._task = asyncio.current_task() - if deadline <= now: - self._timeout_handler = self._loop.call_soon(self._on_timeout) - else: - self._timeout_handler = self._loop.call_at(deadline, self._on_timeout) - - def _do_enter(self) -> None: - if self._state != _State.INIT: - raise RuntimeError(f"invalid state {self._state.value}") - self._state = _State.ENTER - self._reschedule() - - def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None: - if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: - assert self._task is not None - _uncancel_task(self._task) - self._timeout_handler = None - self._task = None - raise asyncio.TimeoutError - # timeout has not expired - self._state = _State.EXIT - self._reject() - return None - - def _on_timeout(self) -> None: - assert self._task is not None - self._task.cancel() - self._state = _State.TIMEOUT - # drop the reference early - self._timeout_handler = None - - -# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py diff --git a/src/websockets/asyncio/client.py b/src/websockets/asyncio/client.py index b41bed86..562ed18c 100644 --- a/src/websockets/asyncio/client.py +++ b/src/websockets/asyncio/client.py @@ -30,7 +30,6 @@ from ..streams import StreamReader from ..typing import LoggerLike, Origin, Subprotocol from ..uri import WebSocketURI, parse_uri -from .compatibility import TimeoutError, asyncio_timeout from .connection import Connection @@ -159,8 +158,7 @@ def process_exception(exc: Exception) -> Exception | None: """ # This catches python-socks' ProxyConnectionError and ProxyTimeoutError. - # Remove asyncio.TimeoutError when dropping Python < 3.11. - if isinstance(exc, (OSError, TimeoutError, asyncio.TimeoutError)): + if isinstance(exc, (OSError, TimeoutError)): return None if isinstance(exc, InvalidMessage) and isinstance(exc.__cause__, EOFError): return None @@ -550,7 +548,7 @@ def __await__(self) -> Generator[Any, None, ClientConnection]: async def __await_impl__(self) -> ClientConnection: try: - async with asyncio_timeout(self.open_timeout): + async with asyncio.timeout(self.open_timeout): for _ in range(MAX_REDIRECTS): self.connection = await self.create_connection() try: @@ -591,10 +589,6 @@ async def __await_impl__(self) -> ClientConnection: # Re-raise exception with an informative error message. raise TimeoutError("timed out during opening handshake") from exc - # ... = yield from connect(...) - remove when dropping Python < 3.11 - - __iter__ = __await__ - # async with connect(...) as ...: ... async def __aenter__(self) -> ClientConnection: diff --git a/src/websockets/asyncio/compatibility.py b/src/websockets/asyncio/compatibility.py deleted file mode 100644 index e1700006..00000000 --- a/src/websockets/asyncio/compatibility.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import annotations - -import sys - - -__all__ = ["TimeoutError", "aiter", "anext", "asyncio_timeout", "asyncio_timeout_at"] - - -if sys.version_info[:2] >= (3, 11): - TimeoutError = TimeoutError - aiter = aiter - anext = anext - from asyncio import ( - timeout as asyncio_timeout, # noqa: F401 - timeout_at as asyncio_timeout_at, # noqa: F401 - ) - -else: # Python < 3.11 - from asyncio import TimeoutError - - def aiter(async_iterable): - return type(async_iterable).__aiter__(async_iterable) - - async def anext(async_iterator): - return await type(async_iterator).__anext__(async_iterator) - - from .async_timeout import ( - timeout as asyncio_timeout, # noqa: F401 - timeout_at as asyncio_timeout_at, # noqa: F401 - ) diff --git a/src/websockets/asyncio/connection.py b/src/websockets/asyncio/connection.py index 636b4838..4c21ace7 100644 --- a/src/websockets/asyncio/connection.py +++ b/src/websockets/asyncio/connection.py @@ -6,7 +6,6 @@ import logging import random import struct -import sys import traceback import uuid from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Iterable, Mapping @@ -23,13 +22,6 @@ from ..http11 import Request, Response from ..protocol import CLOSED, OPEN, Event, Protocol, State from ..typing import BytesLike, Data, DataLike, LoggerLike, Subprotocol -from .compatibility import ( - TimeoutError, - aiter, - anext, - asyncio_timeout, - asyncio_timeout_at, -) from .messages import Assembler @@ -831,7 +823,7 @@ async def keepalive(self) -> None: if self.ping_timeout is not None: try: - async with asyncio_timeout(self.ping_timeout): + async with asyncio.timeout(self.ping_timeout): # connection_lost cancels keepalive immediately # after setting a ConnectionClosed exception on # pong_received. A CancelledError is raised here, @@ -945,7 +937,7 @@ async def send_context( # elapses, close the socket to terminate the connection. if wait_for_close: try: - async with asyncio_timeout_at(self.close_deadline): + async with asyncio.timeout_at(self.close_deadline): await asyncio.shield(self.connection_lost_waiter) except TimeoutError: # There's no risk of overwriting another error because @@ -1191,9 +1183,9 @@ def broadcast( errors on connections where the closing handshake is in progress. :func:`broadcast` ignores failures to write the message on some connections. - It continues writing to other connections. On Python 3.11 and above, you may - set ``raise_exceptions`` to :obj:`True` to record failures and raise all - exceptions in a :pep:`654` :exc:`ExceptionGroup`. + It continues writing to other connections. You may set ``raise_exceptions`` + to :obj:`True` to record failures and raise all exceptions in a :pep:`654` + :exc:`ExceptionGroup`. While :func:`broadcast` makes more sense for servers, it works identically with clients, if you have a use case for opening connections to many servers @@ -1218,8 +1210,6 @@ def broadcast( raise TypeError("data must be str or bytes") if raise_exceptions: - if sys.version_info[:2] < (3, 11): # pragma: no cover - raise ValueError("raise_exceptions requires at least Python 3.11") exceptions: list[Exception] = [] for connection in connections: diff --git a/src/websockets/asyncio/server.py b/src/websockets/asyncio/server.py index ef9bd807..8fc928af 100644 --- a/src/websockets/asyncio/server.py +++ b/src/websockets/asyncio/server.py @@ -24,7 +24,6 @@ from ..protocol import CONNECTING, OPEN, Event from ..server import ServerProtocol from ..typing import LoggerLike, Origin, StatusLike, Subprotocol -from .compatibility import asyncio_timeout from .connection import Connection, broadcast @@ -351,7 +350,7 @@ async def conn_handler(self, connection: ServerConnection) -> None: """ try: - async with asyncio_timeout(self.open_timeout): + async with asyncio.timeout(self.open_timeout): try: await connection.handshake( self.process_request, @@ -454,11 +453,6 @@ async def _close( # Stop accepting new connections. self.server.close() - # Wait until all accepted connections reach connection_made() and call - # register(). See https://github.com/python/cpython/issues/79033 for - # details. This workaround can be removed when dropping Python < 3.11. - await asyncio.sleep(0) - # After server.close(), handshake() closes OPENING connections with an # HTTP 503 error. @@ -848,10 +842,6 @@ async def __await_impl__(self) -> Server: self.server.wrap(server) return self.server - # ... = yield from serve(...) - remove when dropping Python < 3.11 - - __iter__ = __await__ - def unix_serve( handler: Callable[[ServerConnection], Awaitable[None]], diff --git a/src/websockets/cli.py b/src/websockets/cli.py index 6af1152c..dede1da8 100644 --- a/src/websockets/cli.py +++ b/src/websockets/cli.py @@ -193,8 +193,4 @@ def main(argv: list[str] | None = None) -> None: except ImportError: # readline isn't available on all platforms pass - # Remove the try/except block when dropping Python < 3.11. - try: - asyncio.run(interactive_client(args.uri)) - except KeyboardInterrupt: # pragma: no cover - pass + asyncio.run(interactive_client(args.uri)) diff --git a/src/websockets/legacy/client.py b/src/websockets/legacy/client.py index 980079aa..1e9539da 100644 --- a/src/websockets/legacy/client.py +++ b/src/websockets/legacy/client.py @@ -12,7 +12,6 @@ from types import TracebackType from typing import Any, Callable, cast -from ..asyncio.compatibility import asyncio_timeout from ..datastructures import Headers, HeadersLike from ..exceptions import ( InvalidHeader, @@ -656,7 +655,7 @@ def __await__(self) -> Generator[Any, None, WebSocketClientProtocol]: return self.__await_impl__().__await__() async def __await_impl__(self) -> WebSocketClientProtocol: - async with asyncio_timeout(self.open_timeout): + async with asyncio.timeout(self.open_timeout): for _redirects in range(self.MAX_REDIRECTS_ALLOWED): _transport, protocol = await self._create_connection() try: @@ -682,10 +681,6 @@ async def __await_impl__(self) -> WebSocketClientProtocol: else: raise SecurityError("too many redirects") - # ... = yield from connect(...) - remove when dropping Python < 3.11 - - __iter__ = __await__ - connect = Connect diff --git a/src/websockets/legacy/protocol.py b/src/websockets/legacy/protocol.py index ab4ec6bc..d46ce8d4 100644 --- a/src/websockets/legacy/protocol.py +++ b/src/websockets/legacy/protocol.py @@ -7,7 +7,6 @@ import random import ssl import struct -import sys import time import traceback import uuid @@ -15,7 +14,6 @@ from collections.abc import AsyncIterable, AsyncIterator, Awaitable, Iterable, Mapping from typing import Any, Callable, Deque, cast -from ..asyncio.compatibility import asyncio_timeout from ..datastructures import Headers from ..exceptions import ( ConnectionClosed, @@ -746,7 +744,7 @@ async def close( """ try: - async with asyncio_timeout(self.close_timeout): + async with asyncio.timeout(self.close_timeout): await self.write_close_frame(Close(code, reason)) except asyncio.TimeoutError: # If the close frame cannot be sent because the send buffers @@ -754,7 +752,7 @@ async def close( # Fail the connection to shut down faster. self.fail_connection() - # If no close frame is received within the timeout, asyncio_timeout() + # If no close frame is received within the timeout, asyncio.timeout() # cancels the data transfer task and raises TimeoutError. # If close() is called multiple times concurrently and one of these @@ -764,7 +762,7 @@ async def close( try: # If close() is canceled during the wait, self.transfer_data_task # is canceled before the timeout elapses. - async with asyncio_timeout(self.close_timeout): + async with asyncio.timeout(self.close_timeout): await self.transfer_data_task except (asyncio.TimeoutError, asyncio.CancelledError): pass @@ -1234,7 +1232,7 @@ async def keepalive_ping(self) -> None: if self.ping_timeout is not None: try: - async with asyncio_timeout(self.ping_timeout): + async with asyncio.timeout(self.ping_timeout): # Raises CancelledError if the connection is closed, # when close_connection() cancels keepalive_ping(). # Raises ConnectionClosed if the connection is lost, @@ -1350,7 +1348,7 @@ async def wait_for_connection_lost(self) -> bool: """ if not self.connection_lost_waiter.done(): try: - async with asyncio_timeout(self.close_timeout): + async with asyncio.timeout(self.close_timeout): await asyncio.shield(self.connection_lost_waiter) except asyncio.TimeoutError: pass @@ -1573,9 +1571,9 @@ def broadcast( errors on connections where the closing handshake is in progress. :func:`broadcast` ignores failures to write the message on some connections. - It continues writing to other connections. On Python 3.11 and above, you may - set ``raise_exceptions`` to :obj:`True` to record failures and raise all - exceptions in a :pep:`654` :exc:`ExceptionGroup`. + It continues writing to other connections. You may set ``raise_exceptions`` + to :obj:`True` to record failures and raise all exceptions in a :pep:`654` + :exc:`ExceptionGroup`. While :func:`broadcast` makes more sense for servers, it works identically with clients, if you have a use case for opening connections to many servers @@ -1594,8 +1592,6 @@ def broadcast( raise TypeError("data must be str or bytes-like") if raise_exceptions: - if sys.version_info[:2] < (3, 11): # pragma: no cover - raise ValueError("raise_exceptions requires at least Python 3.11") exceptions = [] opcode, data = prepare_data(message) diff --git a/src/websockets/legacy/server.py b/src/websockets/legacy/server.py index d2a69c71..cb22ba38 100644 --- a/src/websockets/legacy/server.py +++ b/src/websockets/legacy/server.py @@ -12,7 +12,6 @@ from types import TracebackType from typing import Any, Callable, cast -from ..asyncio.compatibility import asyncio_timeout from ..datastructures import Headers, HeadersLike, MultipleValuesError from ..exceptions import ( InvalidHandshake, @@ -157,7 +156,7 @@ async def handler(self) -> None: """ try: try: - async with asyncio_timeout(self.open_timeout): + async with asyncio.timeout(self.open_timeout): await self.handshake( origins=self.origins, available_extensions=self.available_extensions, @@ -760,11 +759,6 @@ async def _close(self, close_connections: bool) -> None: # Stop accepting new connections. self.server.close() - # Wait until all accepted connections reach connection_made() and call - # register(). See https://github.com/python/cpython/issues/79033 for - # details. This workaround can be removed when dropping Python < 3.11. - await asyncio.sleep(0) - if close_connections: # Close OPEN connections with close code 1001. After server.close(), # handshake() closes OPENING connections with an HTTP 503 error. @@ -1124,10 +1118,6 @@ async def __await_impl__(self) -> WebSocketServer: self.ws_server.wrap(server) return self.ws_server - # yield from serve(...) - remove when dropping Python < 3.11 - - __iter__ = __await__ - serve = Serve diff --git a/src/websockets/trio/connection.py b/src/websockets/trio/connection.py index 62edd210..b55058eb 100644 --- a/src/websockets/trio/connection.py +++ b/src/websockets/trio/connection.py @@ -12,11 +12,6 @@ import trio import trio.abc -from ..asyncio.compatibility import ( - TimeoutError, - aiter, - anext, -) from ..exceptions import ( ConcurrencyError, ConnectionClosed, diff --git a/src/websockets/typing.py b/src/websockets/typing.py index 69b1a8d3..57e5d868 100644 --- a/src/websockets/typing.py +++ b/src/websockets/typing.py @@ -2,7 +2,7 @@ import http import logging -from typing import TYPE_CHECKING, Any, NewType, Sequence +from typing import Any, NewType, Sequence __all__ = [ @@ -33,12 +33,8 @@ DataLike = str | bytes | bytearray | memoryview """Types accepted where :class:`Data` is expected.""" -if TYPE_CHECKING: - LoggerLike = logging.Logger | logging.LoggerAdapter[Any] - """Types accepted where a :class:`~logging.Logger` is expected.""" -else: # remove this branch when dropping support for Python < 3.11 - LoggerLike = logging.Logger | logging.LoggerAdapter - """Types accepted where a :class:`~logging.Logger` is expected.""" +LoggerLike = logging.Logger | logging.LoggerAdapter[Any] +"""Types accepted where a :class:`~logging.Logger` is expected.""" StatusLike = http.HTTPStatus | int diff --git a/tests/asyncio/test_client.py b/tests/asyncio/test_client.py index fc392c84..fcd3d09d 100644 --- a/tests/asyncio/test_client.py +++ b/tests/asyncio/test_client.py @@ -10,7 +10,6 @@ from unittest.mock import patch from websockets.asyncio.client import * -from websockets.asyncio.compatibility import TimeoutError from websockets.asyncio.server import serve, unix_serve from websockets.client import backoff from websockets.exceptions import ( diff --git a/tests/asyncio/test_connection.py b/tests/asyncio/test_connection.py index 76941a13..1527b539 100644 --- a/tests/asyncio/test_connection.py +++ b/tests/asyncio/test_connection.py @@ -3,12 +3,10 @@ import itertools import logging import socket -import sys import unittest import uuid from unittest.mock import Mock, patch -from websockets.asyncio.compatibility import TimeoutError, aiter, anext, asyncio_timeout from websockets.asyncio.connection import * from websockets.asyncio.connection import broadcast from websockets.exceptions import ( @@ -959,7 +957,7 @@ async def test_acknowledge_ping(self): async with self.drop_frames_rcvd(): # drop automatic response to ping pong_received = await self.connection.ping("this") await self.remote_connection.pong("this") - async with asyncio_timeout(MS): + async with asyncio.timeout(MS): await pong_received async def test_acknowledge_canceled_ping(self): @@ -977,7 +975,7 @@ async def test_acknowledge_ping_non_matching_pong(self): pong_received = await self.connection.ping("this") await self.remote_connection.pong("that") with self.assertRaises(TimeoutError): - async with asyncio_timeout(MS): + async with asyncio.timeout(MS): await pong_received async def test_acknowledge_previous_ping(self): @@ -986,7 +984,7 @@ async def test_acknowledge_previous_ping(self): pong_received = await self.connection.ping("this") await self.connection.ping("that") await self.remote_connection.pong("that") - async with asyncio_timeout(MS): + async with asyncio.timeout(MS): await pong_received async def test_acknowledge_previous_canceled_ping(self): @@ -996,7 +994,7 @@ async def test_acknowledge_previous_canceled_ping(self): pong_received_2 = await self.connection.ping("that") pong_received.cancel() await self.remote_connection.pong("that") - async with asyncio_timeout(MS): + async with asyncio.timeout(MS): await pong_received_2 with self.assertRaises(asyncio.CancelledError): await pong_received @@ -1022,7 +1020,7 @@ async def test_ping_duplicate_payload(self): ) await self.remote_connection.pong("idem") - async with asyncio_timeout(MS): + async with asyncio.timeout(MS): await pong_received await self.connection.ping("idem") # doesn't raise an exception @@ -1313,10 +1311,6 @@ async def test_broadcast_text(self): broadcast([self.connection], "😀") await self.assertFrameSent(Frame(Opcode.TEXT, "😀".encode())) - @unittest.skipIf( - sys.version_info[:2] < (3, 11), - "raise_exceptions requires Python 3.11+", - ) async def test_broadcast_text_reports_no_errors(self): """broadcast broadcasts a text message without raising exceptions.""" broadcast([self.connection], "😀", raise_exceptions=True) @@ -1327,10 +1321,6 @@ async def test_broadcast_binary(self): broadcast([self.connection], b"\x01\x02\xfe\xff") await self.assertFrameSent(Frame(Opcode.BINARY, b"\x01\x02\xfe\xff")) - @unittest.skipIf( - sys.version_info[:2] < (3, 11), - "raise_exceptions requires Python 3.11+", - ) async def test_broadcast_binary_reports_no_errors(self): """broadcast broadcasts a binary message without raising exceptions.""" broadcast([self.connection], b"\x01\x02\xfe\xff", raise_exceptions=True) @@ -1406,10 +1396,6 @@ async def fragments(): gate.set() await send_task - @unittest.skipIf( - sys.version_info[:2] < (3, 11), - "raise_exceptions requires Python 3.11+", - ) async def test_broadcast_reports_connection_with_send_blocked(self): """broadcast raises exceptions for connections blocked in send.""" gate = asyncio.Event() @@ -1449,10 +1435,6 @@ async def test_broadcast_skips_connection_failing_to_send(self): ], ) - @unittest.skipIf( - sys.version_info[:2] < (3, 11), - "raise_exceptions requires Python 3.11+", - ) async def test_broadcast_reports_connection_failing_to_send(self): """broadcast raises exceptions for connections failing to send.""" # Inject a fault by shutting down the transport for writing. diff --git a/tests/asyncio/test_messages.py b/tests/asyncio/test_messages.py index c862090a..435f93d6 100644 --- a/tests/asyncio/test_messages.py +++ b/tests/asyncio/test_messages.py @@ -3,7 +3,6 @@ import unittest import unittest.mock -from websockets.asyncio.compatibility import aiter, anext from websockets.asyncio.messages import * from websockets.asyncio.messages import SimpleQueue from websockets.exceptions import ConcurrencyError diff --git a/tests/asyncio/test_server.py b/tests/asyncio/test_server.py index 00dcb301..2ff61683 100644 --- a/tests/asyncio/test_server.py +++ b/tests/asyncio/test_server.py @@ -7,7 +7,6 @@ import unittest from websockets.asyncio.client import connect, unix_connect -from websockets.asyncio.compatibility import TimeoutError, asyncio_timeout from websockets.asyncio.server import * from websockets.exceptions import ( ConnectionClosedError, @@ -532,12 +531,12 @@ async def test_close_server_keeps_connections_open(self): # The server waits for the client to close the connection. with self.assertRaises(TimeoutError): - async with asyncio_timeout(MS): + async with asyncio.timeout(MS): await server.wait_closed() # Once the client closes the connection, the server terminates. await client.close() - async with asyncio_timeout(MS): + async with asyncio.timeout(MS): await server.wait_closed() async def test_close_server_keeps_handlers_running(self): @@ -551,11 +550,11 @@ async def test_close_server_keeps_handlers_running(self): # The server waits for the connection handler to terminate. with self.assertRaises(TimeoutError): - async with asyncio_timeout(2 * MS): + async with asyncio.timeout(2 * MS): await server.wait_closed() # Set a large timeout here, else the test becomes flaky. - async with asyncio_timeout(5 * MS): + async with asyncio.timeout(5 * MS): await server.wait_closed() diff --git a/tests/legacy/test_client_server.py b/tests/legacy/test_client_server.py index 6761620d..fdf1d74a 100644 --- a/tests/legacy/test_client_server.py +++ b/tests/legacy/test_client_server.py @@ -8,14 +8,12 @@ import re import socket import ssl -import sys import unittest import urllib.error import urllib.request import warnings from unittest.mock import patch -from websockets.asyncio.compatibility import asyncio_timeout from websockets.datastructures import Headers from websockets.exceptions import ( ConnectionClosed, @@ -1145,7 +1143,7 @@ def test_client_connect_canceled_during_handshake(self): async def cancelled_client(): start_client = connect(get_server_uri(self.server), sock=sock) - async with asyncio_timeout(5 * MS): + async with asyncio.timeout(5 * MS): await start_client with self.assertRaises(asyncio.TimeoutError): @@ -1368,43 +1366,6 @@ def test_checking_lack_of_origin_succeeds_backwards_compatibility(self): self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!") -@unittest.skipIf( - sys.version_info[:2] >= (3, 11), "asyncio.coroutine has been removed in Python 3.11" -) -class YieldFromTests(ClientServerTestsMixin, AsyncioTestCase): # pragma: no cover - @with_server() - def test_client(self): - # @asyncio.coroutine is deprecated on Python ≥ 3.8 - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - @asyncio.coroutine - def run_client(): - # Yield from connect. - client = yield from connect(get_server_uri(self.server)) - self.assertEqual(client.state, State.OPEN) - yield from client.close() - self.assertEqual(client.state, State.CLOSED) - - self.loop.run_until_complete(run_client()) - - def test_server(self): - # @asyncio.coroutine is deprecated on Python ≥ 3.8 - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - @asyncio.coroutine - def run_server(): - # Yield from serve. - server = yield from serve(default_handler, "localhost", 0) - self.assertTrue(server.sockets) - server.close() - yield from server.wait_closed() - self.assertFalse(server.sockets) - - self.loop.run_until_complete(run_server()) - - class AsyncAwaitTests(ClientServerTestsMixin, AsyncioTestCase): @with_server() def test_client(self): diff --git a/tests/legacy/test_protocol.py b/tests/legacy/test_protocol.py index 48d2f58e..75806c68 100644 --- a/tests/legacy/test_protocol.py +++ b/tests/legacy/test_protocol.py @@ -1,7 +1,6 @@ import asyncio import contextlib import logging -import sys import unittest import unittest.mock import warnings @@ -1462,10 +1461,6 @@ def test_broadcast_text(self): broadcast([self.protocol], "café") self.assertOneFrameSent(True, OP_TEXT, "café".encode()) - @unittest.skipIf( - sys.version_info[:2] < (3, 11), - "raise_exceptions requires Python 3.11+", - ) def test_broadcast_text_reports_no_errors(self): broadcast([self.protocol], "café", raise_exceptions=True) self.assertOneFrameSent(True, OP_TEXT, "café".encode()) @@ -1474,10 +1469,6 @@ def test_broadcast_binary(self): broadcast([self.protocol], b"tea") self.assertOneFrameSent(True, OP_BINARY, b"tea") - @unittest.skipIf( - sys.version_info[:2] < (3, 11), - "raise_exceptions requires Python 3.11+", - ) def test_broadcast_binary_reports_no_errors(self): broadcast([self.protocol], b"tea", raise_exceptions=True) self.assertOneFrameSent(True, OP_BINARY, b"tea") @@ -1527,10 +1518,6 @@ def test_broadcast_skips_connection_sending_fragmented_text(self): ["skipped broadcast: sending a fragmented message"], ) - @unittest.skipIf( - sys.version_info[:2] < (3, 11), - "raise_exceptions requires Python 3.11+", - ) def test_broadcast_reports_connection_sending_fragmented_text(self): self.make_drain_slow() self.loop.create_task(self.protocol.send(["ca", "fé"])) @@ -1557,10 +1544,6 @@ def test_broadcast_skips_connection_failing_to_send(self): ["skipped broadcast: failed to write message: RuntimeError: BOOM"], ) - @unittest.skipIf( - sys.version_info[:2] < (3, 11), - "raise_exceptions requires Python 3.11+", - ) def test_broadcast_reports_connection_failing_to_send(self): # Configure mock to raise an exception when writing to the network. self.protocol.transport.write.side_effect = RuntimeError("BOOM") diff --git a/tests/maxi_cov.py b/tests/maxi_cov.py index 8ccef7d3..9e640f2c 100755 --- a/tests/maxi_cov.py +++ b/tests/maxi_cov.py @@ -53,8 +53,6 @@ def get_mapping(src_dir="src"): if "legacy" not in os.path.dirname(src_file) and os.path.basename(src_file) != "__init__.py" and os.path.basename(src_file) != "__main__.py" - and os.path.basename(src_file) != "async_timeout.py" - and os.path.basename(src_file) != "compatibility.py" ] test_files = [ test_file @@ -100,10 +98,6 @@ def get_ignored_files(src_dir="src"): "*/websockets/__main__.py", # There is nothing to test on type declarations. "*/websockets/typing.py", - # We don't test compatibility modules with previous versions of Python - # or websockets (import locations). - "*/websockets/asyncio/async_timeout.py", - "*/websockets/asyncio/compatibility.py", # This approach isn't applicable to the test suite of the legacy # implementation, due to the huge test_client_server test module. "*/websockets/legacy/*", diff --git a/tests/proxy.py b/tests/proxy.py index 92dda6ac..d5839ed0 100644 --- a/tests/proxy.py +++ b/tests/proxy.py @@ -89,7 +89,6 @@ async def run_proxy(cls): mode=[cls.proxy_mode], # Don't intercept connections, but record them. ignore_hosts=["^localhost:", "^127.0.0.1:", "^::1:"], - # This option requires mitmproxy 11.0.0, which requires Python 3.11. show_ignored_hosts=True, ) cls.proxy_master = master = Master(options) diff --git a/tests/trio/test_connection.py b/tests/trio/test_connection.py index 3f74b0fc..bc767c67 100644 --- a/tests/trio/test_connection.py +++ b/tests/trio/test_connection.py @@ -6,7 +6,6 @@ import trio.testing -from websockets.asyncio.compatibility import TimeoutError, aiter, anext from websockets.exceptions import ( ConcurrencyError, ConnectionClosedError, diff --git a/tests/trio/test_messages.py b/tests/trio/test_messages.py index 6f1c3bed..7fd94ac1 100644 --- a/tests/trio/test_messages.py +++ b/tests/trio/test_messages.py @@ -5,7 +5,6 @@ import trio.testing -from websockets.asyncio.compatibility import aiter, anext from websockets.exceptions import ConcurrencyError from websockets.frames import OP_BINARY, OP_CONT, OP_TEXT, Frame from websockets.trio.messages import * diff --git a/tox.ini b/tox.ini index 5a3ffd73..c6204eee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,12 @@ [tox] env_list = - py310 py311 py312 py313 py314 + py315 coverage + maxi_cov ruff mypy @@ -15,8 +16,8 @@ commands = pass_env = WEBSOCKETS_* deps = - py311,py312,py313,py314,coverage,maxi_cov: mitmproxy - py311,py312,py313,py314,coverage,maxi_cov: python-socks[asyncio] + py311,py312,py313,py314,py315,coverage,maxi_cov: mitmproxy + py311,py312,py313,py314,py315,coverage,maxi_cov: python-socks[asyncio] trio werkzeug