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
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ jobs:
strategy:
matrix:
python:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
Expand Down
2 changes: 1 addition & 1 deletion docs/intro/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Getting started
Requirements
------------

websockets requires Python ≥ 3.9.
websockets requires Python ≥ 3.10.

.. admonition:: Use the most recent Python release
:class: tip
Expand Down
12 changes: 10 additions & 2 deletions docs/project/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ fixing regressions shortly after a release.
Only documented APIs are public. Undocumented, private APIs may change without
notice.

.. _15.1:
.. _16.0:

15.1
16.0
----

*In development*

Backwards-incompatible changes
..............................

.. admonition:: websockets 16.0 requires Python ≥ 3.10.
:class: tip

websockets 15.0 is the last version supporting Python 3.9.

Improvements
............

Expand Down
6 changes: 2 additions & 4 deletions docs/topics/logging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,7 @@ Here's how to include them in logs, assuming they're in the

async with serve(
...,
# Python < 3.10 requires passing None as the second argument.
logger=LoggerAdapter(logging.getLogger("websockets.server"), None),
logger=LoggerAdapter(logging.getLogger("websockets.server")),
):
...

Expand Down Expand Up @@ -176,8 +175,7 @@ a :class:`~logging.LoggerAdapter`::

async with serve(
...,
# Python < 3.10 requires passing None as the second argument.
logger=LoggerAdapter(logging.getLogger("websockets.server"), None),
logger=LoggerAdapter(logging.getLogger("websockets.server")),
):
...

Expand Down
2 changes: 1 addition & 1 deletion example/deployment/kubernetes/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.9-alpine
FROM python:3.13-alpine

RUN pip install websockets

Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.9"
requires-python = ">=3.10"
license = { text = "BSD-3-Clause" }
authors = [
{ name = "Aymeric Augustin", email = "[email protected]" },
Expand All @@ -19,7 +19,6 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down
5 changes: 2 additions & 3 deletions src/websockets/asyncio/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ 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.10
# ... = yield from connect(...) - remove when dropping Python < 3.11

__iter__ = __await__

Expand Down Expand Up @@ -629,8 +629,7 @@ async def __aiter__(self) -> AsyncIterator[ClientConnection]:
self.logger.info(
"connect failed; reconnecting in %.1f seconds: %s",
delay,
# Remove first argument when dropping Python 3.9.
traceback.format_exception_only(type(exc), exc)[0].strip(),
traceback.format_exception_only(exc)[0].strip(),
)
await asyncio.sleep(delay)
continue
Expand Down
6 changes: 1 addition & 5 deletions src/websockets/asyncio/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,11 +1224,7 @@ def broadcast(
else:
connection.logger.warning(
"skipped broadcast: failed to write message: %s",
traceback.format_exception_only(
# Remove first argument when dropping Python 3.9.
type(write_exception),
write_exception,
)[0].strip(),
traceback.format_exception_only(write_exception)[0].strip(),
)

if raise_exceptions and exceptions:
Expand Down
2 changes: 1 addition & 1 deletion src/websockets/asyncio/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,7 +834,7 @@ async def __await_impl__(self) -> Server:
self.server.wrap(server)
return self.server

# ... = yield from serve(...) - remove when dropping Python < 3.10
# ... = yield from serve(...) - remove when dropping Python < 3.11

__iter__ = __await__

Expand Down
12 changes: 4 additions & 8 deletions src/websockets/datastructures.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from collections.abc import Iterable, Iterator, Mapping, MutableMapping
from typing import Any, Protocol, Union
from typing import Any, Protocol


__all__ = [
Expand Down Expand Up @@ -171,13 +171,9 @@ def keys(self) -> Iterable[str]: ...
def __getitem__(self, key: str) -> str: ...


# Change to Headers | Mapping[str, str] | ... when dropping Python < 3.10.
HeadersLike = Union[
Headers,
Mapping[str, str],
Iterable[tuple[str, str]],
SupportsKeysAndGetItem,
]
HeadersLike = (
Headers | Mapping[str, str] | Iterable[tuple[str, str]] | SupportsKeysAndGetItem
)
"""
Types accepted where :class:`Headers` is expected.

Expand Down
4 changes: 2 additions & 2 deletions src/websockets/frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import secrets
import struct
from collections.abc import Generator, Sequence
from typing import Callable, Union
from typing import Callable

from .exceptions import PayloadTooBig, ProtocolError

Expand Down Expand Up @@ -140,7 +140,7 @@ class Frame:
"""

opcode: Opcode
data: Union[bytes, bytearray, memoryview]
data: bytes | bytearray | memoryview
fin: bool = True
rsv1: bool = False
rsv2: bool = False
Expand Down
8 changes: 3 additions & 5 deletions src/websockets/legacy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,16 +607,14 @@ async def __aiter__(self) -> AsyncIterator[WebSocketClientProtocol]:
self.logger.info(
"connect failed; reconnecting in %.1f seconds: %s",
initial_delay,
# Remove first argument when dropping Python 3.9.
traceback.format_exception_only(type(exc), exc)[0].strip(),
traceback.format_exception_only(exc)[0].strip(),
)
await asyncio.sleep(initial_delay)
else:
self.logger.info(
"connect failed again; retrying in %d seconds: %s",
int(backoff_delay),
# Remove first argument when dropping Python 3.9.
traceback.format_exception_only(type(exc), exc)[0].strip(),
traceback.format_exception_only(exc)[0].strip(),
)
await asyncio.sleep(int(backoff_delay))
# Increase delay with truncated exponential backoff.
Expand Down Expand Up @@ -673,7 +671,7 @@ async def __await_impl__(self) -> WebSocketClientProtocol:
else:
raise SecurityError("too many redirects")

# ... = yield from connect(...) - remove when dropping Python < 3.10
# ... = yield from connect(...) - remove when dropping Python < 3.11

__iter__ = __await__

Expand Down
16 changes: 3 additions & 13 deletions src/websockets/legacy/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,6 @@ def __init__(
self._paused = False
self._drain_waiter: asyncio.Future[None] | None = None

self._drain_lock = asyncio.Lock()

# This class implements the data transfer and closing handshake, which
# are shared between the client-side and the server-side.
# Subclasses implement the opening handshake and, on success, execute
Expand Down Expand Up @@ -1166,12 +1164,8 @@ def write_frame_sync(self, fin: bool, opcode: int, data: bytes) -> None:

async def drain(self) -> None:
try:
# drain() cannot be called concurrently by multiple coroutines.
# See https://github.com/python/cpython/issues/74116 for details.
# This workaround can be removed when dropping Python < 3.10.
async with self._drain_lock:
# Handle flow control automatically.
await self._drain()
# Handle flow control automatically.
await self._drain()
except ConnectionError:
# Terminate the connection if the socket died.
self.fail_connection()
Expand Down Expand Up @@ -1626,11 +1620,7 @@ def broadcast(
else:
websocket.logger.warning(
"skipped broadcast: failed to write message: %s",
traceback.format_exception_only(
# Remove first argument when dropping Python 3.9.
type(write_exception),
write_exception,
)[0].strip(),
traceback.format_exception_only(write_exception)[0].strip(),
)

if raise_exceptions and exceptions:
Expand Down
10 changes: 5 additions & 5 deletions src/websockets/legacy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import warnings
from collections.abc import Awaitable, Generator, Iterable, Sequence
from types import TracebackType
from typing import Any, Callable, Union, cast
from typing import Any, Callable, cast

from ..asyncio.compatibility import asyncio_timeout
from ..datastructures import Headers, HeadersLike, MultipleValuesError
Expand Down Expand Up @@ -48,8 +48,7 @@
]


# Change to HeadersLike | ... when dropping Python < 3.10.
HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]]
HeadersLikeOrCallable = HeadersLike | Callable[[str, Headers], HeadersLike]

HTTPResponse = tuple[StatusLike, HeadersLike, bytes]

Expand Down Expand Up @@ -706,7 +705,8 @@ def wrap(self, server: asyncio.base_events.Server) -> None:
self.logger.info("server listening on %s", name)

# Initialized here because we need a reference to the event loop.
# This should be moved back to __init__ when dropping Python < 3.10.
# This could be moved back to __init__ now that Python < 3.10 isn't
# supported anymore, but I'm not taking that risk in legacy code.
self.closed_waiter = server.get_loop().create_future()

def register(self, protocol: WebSocketServerProtocol) -> None:
Expand Down Expand Up @@ -1124,7 +1124,7 @@ async def __await_impl__(self) -> WebSocketServer:
self.ws_server.wrap(server)
return self.ws_server

# yield from serve(...) - remove when dropping Python < 3.10
# yield from serve(...) - remove when dropping Python < 3.11

__iter__ = __await__

Expand Down
4 changes: 1 addition & 3 deletions src/websockets/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import logging
import uuid
from collections.abc import Generator
from typing import Union

from .exceptions import (
ConnectionClosed,
Expand Down Expand Up @@ -39,8 +38,7 @@
"SEND_EOF",
]

# Change to Request | Response | Frame when dropping Python < 3.10.
Event = Union[Request, Response, Frame]
Event = Request | Response | Frame
"""Events that :meth:`~Protocol.events_received` may return."""


Expand Down
16 changes: 6 additions & 10 deletions src/websockets/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import http
import logging
from typing import TYPE_CHECKING, Any, NewType, Optional, Sequence, Union
from typing import TYPE_CHECKING, Any, NewType, Sequence


__all__ = [
Expand All @@ -18,8 +18,7 @@

# Public types used in the signature of public APIs

# Change to str | bytes when dropping Python < 3.10.
Data = Union[str, bytes]
Data = str | bytes
"""Types supported in a WebSocket message:
:class:`str` for a Text_ frame, :class:`bytes` for a Binary_.

Expand All @@ -29,17 +28,15 @@
"""


# Change to logging.Logger | ... when dropping Python < 3.10.
if TYPE_CHECKING:
LoggerLike = Union[logging.Logger, logging.LoggerAdapter[Any]]
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 = Union[logging.Logger, logging.LoggerAdapter]
LoggerLike = logging.Logger | logging.LoggerAdapter
"""Types accepted where a :class:`~logging.Logger` is expected."""


# Change to http.HTTPStatus | int when dropping Python < 3.10.
StatusLike = Union[http.HTTPStatus, int]
StatusLike = http.HTTPStatus | int
"""
Types accepted where an :class:`~http.HTTPStatus` is expected."""

Expand All @@ -55,8 +52,7 @@
ExtensionName = NewType("ExtensionName", str)
"""Name of a WebSocket extension."""

# Change to tuple[str, str | None] when dropping Python < 3.10.
ExtensionParameter = tuple[str, Optional[str]]
ExtensionParameter = tuple[str, str | None]
"""Parameter of a WebSocket extension."""


Expand Down
Loading