Skip to content

Commit

Permalink
Drop support for Python 3.7.
Browse files Browse the repository at this point in the history
  • Loading branch information
aaugustin committed Apr 2, 2023
1 parent fe1879f commit 1bf7342
Show file tree
Hide file tree
Showing 17 changed files with 32 additions and 121 deletions.
4 changes: 0 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,15 @@ jobs:
strategy:
matrix:
python:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "pypy-3.7"
- "pypy-3.8"
- "pypy-3.9"
is_main:
- ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
exclude:
- python: "pypy-3.7"
is_main: false
- python: "pypy-3.8"
is_main: false
- python: "pypy-3.9"
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.7.
websockets requires Python ≥ 3.8.

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

11.1
12.0
----

*In development*

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

.. admonition:: websockets 12.0 requires Python ≥ 3.8.
:class: tip

websockets 11.0 is the last version supporting Python 3.7.


11.0
----

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.7"
requires-python = ">=3.8"
license = { text = "BSD-3-Clause" }
authors = [
{ name = "Aymeric Augustin", email = "aymeric.augustin@m4x.org" },
Expand All @@ -19,7 +19,6 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand Down
8 changes: 1 addition & 7 deletions src/websockets/datastructures.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import sys
from typing import (
Any,
Dict,
Expand All @@ -9,17 +8,12 @@
List,
Mapping,
MutableMapping,
Protocol,
Tuple,
Union,
)


if sys.version_info[:2] >= (3, 8):
from typing import Protocol
else: # pragma: no cover
Protocol = object # mypy will report errors on Python 3.7.


__all__ = ["Headers", "HeadersLike", "MultipleValuesError"]


Expand Down
8 changes: 0 additions & 8 deletions src/websockets/legacy/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,6 @@ async def read_http_response(self) -> Tuple[int, Headers]:
"""
try:
status_code, reason, headers = await read_response(self.reader)
# Remove this branch when dropping support for Python < 3.8
# because CancelledError no longer inherits Exception.
except asyncio.CancelledError: # pragma: no cover
raise
except Exception as exc:
raise InvalidMessage("did not receive a valid HTTP response") from exc

Expand Down Expand Up @@ -601,10 +597,6 @@ async def __aiter__(self) -> AsyncIterator[WebSocketClientProtocol]:
try:
async with self as protocol:
yield protocol
# Remove this branch when dropping support for Python < 3.8
# because CancelledError no longer inherits Exception.
except asyncio.CancelledError: # pragma: no cover
raise
except Exception:
# Add a random initial delay between 0 and 5 seconds.
# See 7.2.3. Recovering from Abnormal Closure in RFC 6544.
Expand Down
23 changes: 1 addition & 22 deletions src/websockets/legacy/compatibility.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,9 @@
from __future__ import annotations

import asyncio
import sys
from typing import Any, Dict


__all__ = ["asyncio_timeout", "loop_if_py_lt_38"]


if sys.version_info[:2] >= (3, 8):

def loop_if_py_lt_38(loop: asyncio.AbstractEventLoop) -> Dict[str, Any]:
"""
Helper for the removal of the loop argument in Python 3.10.
"""
return {}

else:

def loop_if_py_lt_38(loop: asyncio.AbstractEventLoop) -> Dict[str, Any]:
"""
Helper for the removal of the loop argument in Python 3.10.
"""
return {"loop": loop}
__all__ = ["asyncio_timeout"]


if sys.version_info[:2] >= (3, 11):
Expand Down
17 changes: 4 additions & 13 deletions src/websockets/legacy/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
)
from ..protocol import State
from ..typing import Data, LoggerLike, Subprotocol
from .compatibility import asyncio_timeout, loop_if_py_lt_38
from .compatibility import asyncio_timeout
from .framing import Frame


Expand Down Expand Up @@ -244,7 +244,7 @@ def __init__(
self._paused = False
self._drain_waiter: Optional[asyncio.Future[None]] = None

self._drain_lock = asyncio.Lock(**loop_if_py_lt_38(loop))
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.
Expand Down Expand Up @@ -339,7 +339,7 @@ async def _drain(self) -> None: # pragma: no cover
# write(...); yield from drain()
# in a loop would never call connection_lost(), so it
# would not see an error when the socket is closed.
await asyncio.sleep(0, **loop_if_py_lt_38(self.loop))
await asyncio.sleep(0)
await self._drain_helper()

def connection_open(self) -> None:
Expand Down Expand Up @@ -551,7 +551,6 @@ async def recv(self) -> Data:
await asyncio.wait(
[pop_message_waiter, self.transfer_data_task],
return_when=asyncio.FIRST_COMPLETED,
**loop_if_py_lt_38(self.loop),
)
finally:
self._pop_message_waiter = None
Expand Down Expand Up @@ -1247,10 +1246,7 @@ async def keepalive_ping(self) -> None:

try:
while True:
await asyncio.sleep(
self.ping_interval,
**loop_if_py_lt_38(self.loop),
)
await asyncio.sleep(self.ping_interval)

# ping() raises CancelledError if the connection is closed,
# when close_connection() cancels self.keepalive_ping_task.
Expand All @@ -1272,11 +1268,6 @@ async def keepalive_ping(self) -> None:
self.fail_connection(1011, "keepalive ping timeout")
break

# Remove this branch when dropping support for Python < 3.8
# because CancelledError no longer inherits Exception.
except asyncio.CancelledError:
raise

except ConnectionClosed:
pass

Expand Down
16 changes: 4 additions & 12 deletions src/websockets/legacy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from ..http import USER_AGENT
from ..protocol import State
from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol
from .compatibility import asyncio_timeout, loop_if_py_lt_38
from .compatibility import asyncio_timeout
from .handshake import build_response, check_request
from .http import read_request
from .protocol import WebSocketCommonProtocol
Expand Down Expand Up @@ -170,10 +170,6 @@ async def handler(self) -> None:
available_subprotocols=self.available_subprotocols,
extra_headers=self.extra_headers,
)
# Remove this branch when dropping support for Python < 3.8
# because CancelledError no longer inherits Exception.
except asyncio.CancelledError: # pragma: no cover
raise
except asyncio.TimeoutError: # pragma: no cover
raise
except ConnectionError:
Expand Down Expand Up @@ -770,7 +766,7 @@ async def _close(self, close_connections: bool) -> None:

# Wait until all accepted connections reach connection_made() and call
# register(). See https://bugs.python.org/issue34852 for details.
await asyncio.sleep(0, **loop_if_py_lt_38(self.get_loop()))
await asyncio.sleep(0)

if close_connections:
# Close OPEN connections with status code 1001. Since the server was
Expand All @@ -784,18 +780,14 @@ async def _close(self, close_connections: bool) -> None:
]
# asyncio.wait doesn't accept an empty first argument.
if close_tasks:
await asyncio.wait(
close_tasks,
**loop_if_py_lt_38(self.get_loop()),
)
await asyncio.wait(close_tasks)

# Wait until all connection handlers are complete.

# asyncio.wait doesn't accept an empty first argument.
if self.websockets:
await asyncio.wait(
[websocket.handler_task for websocket in self.websockets],
**loop_if_py_lt_38(self.get_loop()),
[websocket.handler_task for websocket in self.websockets]
)

# Tell wait_closed() to return.
Expand Down
21 changes: 0 additions & 21 deletions src/websockets/sync/compatibility.py

This file was deleted.

5 changes: 2 additions & 3 deletions src/websockets/sync/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from ..protocol import CONNECTING, OPEN, Event
from ..server import ServerProtocol
from ..typing import LoggerLike, Origin, Subprotocol
from .compatibility import socket_create_server
from .connection import Connection
from .utils import Deadline

Expand Down Expand Up @@ -397,9 +396,9 @@ def handler(websocket):
if unix:
if path is None:
raise TypeError("missing path argument")
sock = socket_create_server(path, family=socket.AF_UNIX)
sock = socket.create_server(path, family=socket.AF_UNIX)
else:
sock = socket_create_server((host, port))
sock = socket.create_server((host, port))
else:
if path is not None:
raise TypeError("path and sock arguments are incompatible")
Expand Down
6 changes: 3 additions & 3 deletions src/websockets/version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import importlib.metadata


__all__ = ["tag", "version", "commit"]

Expand All @@ -18,7 +20,7 @@

released = False

tag = version = commit = "11.1"
tag = version = commit = "12.0"


if not released: # pragma: no cover
Expand Down Expand Up @@ -56,8 +58,6 @@ def get_version(tag: str) -> str:

# Read version from package metadata if it is installed.
try:
import importlib.metadata # move up when dropping Python 3.7

return importlib.metadata.version("websockets")
except ImportError:
pass
Expand Down
3 changes: 1 addition & 2 deletions tests/legacy/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
OP_TEXT,
Close,
)
from websockets.legacy.compatibility import loop_if_py_lt_38
from websockets.legacy.framing import Frame
from websockets.legacy.protocol import WebSocketCommonProtocol, broadcast
from websockets.protocol import State
Expand Down Expand Up @@ -117,7 +116,7 @@ def make_drain_slow(self, delay=MS):
original_drain = self.protocol._drain

async def delayed_drain():
await asyncio.sleep(delay, **loop_if_py_lt_38(self.loop))
await asyncio.sleep(delay)
await original_drain()

self.protocol._drain = delayed_drain
Expand Down
3 changes: 2 additions & 1 deletion tests/legacy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class AsyncioTestCase(unittest.TestCase):
"""
Base class for tests that sets up an isolated event loop for each test.
Replace with IsolatedAsyncioTestCase when dropping Python < 3.8.
IsolatedAsyncioTestCase was introduced in Python 3.8 for similar purposes
but isn't a drop-in replacement.
"""

Expand Down
16 changes: 0 additions & 16 deletions tests/sync/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import contextlib
import ssl
import sys
import warnings

from websockets.sync.client import *
from websockets.sync.server import WebSocketServer
Expand All @@ -19,20 +17,6 @@
CLIENT_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
CLIENT_CONTEXT.load_verify_locations(CERTIFICATE)

# Work around https://github.com/openssl/openssl/issues/7967

# This bug causes connect() to hang in tests for the client. Including this
# workaround acknowledges that the issue could happen outside of the test suite.

# It shouldn't happen too often, or else OpenSSL 1.1.1 would be unusable. If it
# happens, we can look for a library-level fix, but it won't be easy.

if sys.version_info[:2] < (3, 8): # pragma: no cover
# ssl.OP_NO_TLSv1_3 was introduced and deprecated on Python 3.7.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
CLIENT_CONTEXT.options |= ssl.OP_NO_TLSv1_3


@contextlib.contextmanager
def run_client(wsuri_or_server, secure=None, resource_name="/", **kwargs):
Expand Down
4 changes: 1 addition & 3 deletions tests/sync/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import contextlib
import ssl
import sys
import threading

from websockets.sync.server import *
Expand All @@ -19,8 +18,7 @@
# It shouldn't happen too often, or else OpenSSL 1.1.1 would be unusable. If it
# happens, we can look for a library-level fix, but it won't be easy.

if sys.version_info[:2] >= (3, 8): # pragma: no cover
SERVER_CONTEXT.num_tickets = 0
SERVER_CONTEXT.num_tickets = 0


def crash(ws):
Expand Down

0 comments on commit 1bf7342

Please sign in to comment.