diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6ef2a7b5..11e0b290 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 diff --git a/benchmarks/run.py b/benchmarks/run.py index 8ffc0053..be72664a 100644 --- a/benchmarks/run.py +++ b/benchmarks/run.py @@ -754,9 +754,7 @@ def build_meta(args: argparse.Namespace) -> dict[str, Any]: """Metadata describing the code and machine the run was made on.""" return { "benchmark_version": 1, - "timestamp": datetime_module.datetime.now( - datetime_module.timezone.utc - ).isoformat(), + "timestamp": datetime_module.datetime.now(datetime_module.UTC).isoformat(), "commit": git_commit(), "label": args.label, "python": sys.version.split()[0], diff --git a/docs/source/intro/install.rst b/docs/source/intro/install.rst index 0267da94..b1e3b261 100644 --- a/docs/source/intro/install.rst +++ b/docs/source/intro/install.rst @@ -7,7 +7,7 @@ Installation Guide Before We Begin =============== -HAP-python requires Python 3.4+. +HAP-python requires Python 3.11+. This guide will cover the current version of Raspbian and Ubuntu LTS. It is somewhat safe to assume the process for newer versions of Ubuntu will work. diff --git a/pyhap/camera.py b/pyhap/camera.py index 9c7e42d6..ae9fa8b0 100644 --- a/pyhap/camera.py +++ b/pyhap/camera.py @@ -25,7 +25,6 @@ import logging import os import struct -import sys from types import MappingProxyType from typing import TYPE_CHECKING, Any from uuid import UUID @@ -50,11 +49,6 @@ from pyhap.camera_multi_tier import MultiTierController from pyhap.camera_webrtc import WebRTCController -if sys.version_info >= (3, 11): - from asyncio import timeout as async_timeout -else: - from async_timeout import timeout as async_timeout - SETUP_TYPES = { "SESSION_ID": b"\x01", "STATUS": b"\x02", @@ -2263,10 +2257,10 @@ async def stop_stream(self, session_info: dict[str, Any]) -> None: try: with contextlib.suppress(ProcessLookupError): ffmpeg_process.terminate() - async with async_timeout(2.0): + async with asyncio.timeout(2.0): _, stderr = await ffmpeg_process.communicate() logger.debug("Stream command stderr: %s", stderr) - except asyncio.TimeoutError: + except TimeoutError: logger.error( "Timeout while waiting for the stream process " "to terminate. Trying with kill." diff --git a/pyhap/camera_multi_tier.py b/pyhap/camera_multi_tier.py index c9f1f8e1..2cc21dde 100644 --- a/pyhap/camera_multi_tier.py +++ b/pyhap/camera_multi_tier.py @@ -748,7 +748,7 @@ async def handle_end(self, session: MultiTierSession) -> None: process.terminate() try: await asyncio.wait_for(process.wait(), 2.0) - except asyncio.TimeoutError: + except TimeoutError: logger.warning("[%s] Killing the stream process", session.id) with contextlib.suppress(ProcessLookupError): process.kill() diff --git a/pyhap/camera_stream.py b/pyhap/camera_stream.py index 73ce1bad..38099a2c 100644 --- a/pyhap/camera_stream.py +++ b/pyhap/camera_stream.py @@ -340,7 +340,7 @@ async def _release(self, session: _S, hooks: list[asyncio.Task[Any]]) -> bool: ) try: await asyncio.wait_for(self._release_hook(session), self.hook_timeout) - except asyncio.TimeoutError: + except TimeoutError: logger.warning("[%s] Releasing the session timed out", session.id) return False # The delegate may fail in many ways, the session is gone anyway. diff --git a/pyhap/camera_talkback.py b/pyhap/camera_talkback.py index a6c62310..fcfec43c 100644 --- a/pyhap/camera_talkback.py +++ b/pyhap/camera_talkback.py @@ -199,10 +199,7 @@ def _run(self) -> None: try: data, addr = self._sock.recvfrom(2048) except TimeoutError: - # socket.timeout aliases TimeoutError from Python 3.10 on, but - # is an OSError subclass on 3.9, which this project supports, - # so catching TimeoutError here would kill the receiver thread - # on the first (0.5s) poll timeout on Python 3.9. + # The 0.5 s poll timeout, check the stop event again. continue except OSError: break diff --git a/pyhap/hap_handler.py b/pyhap/hap_handler.py index b3a26bfb..7b04d91f 100644 --- a/pyhap/hap_handler.py +++ b/pyhap/hap_handler.py @@ -8,7 +8,6 @@ from http import HTTPStatus import inspect import logging -import sys from typing import TYPE_CHECKING, Any, cast from urllib.parse import ParseResult, parse_qs, urlparse import uuid @@ -33,11 +32,6 @@ from .state import State from .util import from_hap_json, to_hap_json -if sys.version_info >= (3, 11): - from asyncio import timeout as async_timeout -else: - from async_timeout import timeout as async_timeout - if TYPE_CHECKING: from .accessory_driver import AccessoryDriver @@ -105,7 +99,7 @@ class UnprivilegedRequestException(Exception): async def _run_with_timeout(coro: Awaitable[bytes], timeout: float) -> bytes: """Run a coroutine with a timeout.""" - async with async_timeout(timeout): + async with asyncio.timeout(timeout): return await coro diff --git a/pyhap/hds_server.py b/pyhap/hds_server.py index c059dd8c..785ee888 100644 --- a/pyhap/hds_server.py +++ b/pyhap/hds_server.py @@ -174,7 +174,7 @@ async def drain(self) -> None: """ try: await asyncio.wait_for(self._can_write.wait(), _DRAIN_TIMEOUT) - except asyncio.TimeoutError: + except TimeoutError: logger.info("Aborting HDS connection: peer stopped reading") self.abort() # connection_lost wakes us and cancels the recording streams, so a diff --git a/pyhap/util.py b/pyhap/util.py index ff1200fd..d540ced9 100644 --- a/pyhap/util.py +++ b/pyhap/util.py @@ -5,7 +5,6 @@ import inspect import random import socket -import sys from typing import Any, cast from uuid import UUID @@ -13,11 +12,6 @@ from .const import BASE_UUID -if sys.version_info >= (3, 11): - from asyncio import timeout as async_timeout -else: - from async_timeout import timeout as async_timeout - ALPHANUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" HEX_DIGITS = "0123456789ABCDEF" _BACKGROUND_TASKS: set[asyncio.Task[Any]] = set() @@ -147,9 +141,9 @@ async def event_wait(event: asyncio.Event, timeout: float) -> bool: :rtype: bool """ try: - async with async_timeout(timeout): + async with asyncio.timeout(timeout): await event.wait() - except asyncio.TimeoutError: + except TimeoutError: pass return event.is_set() diff --git a/pyproject.toml b/pyproject.toml index f5ce1f42..841aec15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,14 +8,13 @@ license = "Apache-2.0" description = "HomeKit Accessory Protocol implementation in python" readme = "README.md" authors = [{ name = "Ivan Kalchev" }] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Natural Language :: English", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -23,7 +22,6 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "async_timeout;python_version<'3.11'", "cryptography", "chacha20poly1305-reuseable", "orjson>=3.7.2", @@ -61,7 +59,7 @@ version = { attr = "pyhap.const.__version__" } [tool.ruff] line-length = 88 -target-version = "py310" +target-version = "py311" # Directories previously excluded from black. exclude = ["generated"] @@ -146,7 +144,7 @@ include = [ ] [tool.ty.environment] -python-version = "3.10" +python-version = "3.11" [tool.pytest.ini_options] testpaths = [ diff --git a/tests/test_camera.py b/tests/test_camera.py index 62bf32b8..dc3dec9b 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -742,7 +742,7 @@ async def test_stop_stream_kill_after_process_exited(mock_driver: Any) -> None: """Verify the kill fallback tolerates a process that exited meanwhile.""" acc = camera.Camera(_OPTIONS, mock_driver, "Camera") process = Mock() - process.communicate = AsyncMock(side_effect=asyncio.TimeoutError) + process.communicate = AsyncMock(side_effect=TimeoutError) process.kill.side_effect = ProcessLookupError process.wait = AsyncMock(return_value=0) session_info = { diff --git a/tests/test_hap_protocol.py b/tests/test_hap_protocol.py index 95de4e2e..22953be8 100644 --- a/tests/test_hap_protocol.py +++ b/tests/test_hap_protocol.py @@ -1199,7 +1199,7 @@ async def test_camera_snapshot_times_out(driver: AccessoryDriver) -> None: connections = {} def _get_snapshot(*_: Any) -> NoReturn: - raise asyncio.TimeoutError("timeout") + raise TimeoutError("timeout") acc = Accessory(driver, "TestAcc") # Camera-only snapshot method, attached dynamically so the diff --git a/tox.ini b/tox.ini index cabd6d4a..f245245d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,9 @@ [tox] -envlist = py310, py311, py312, py313, docs, lint, ty +envlist = py311, py312, py313, docs, lint, ty skip_missing_interpreters = True [gh-actions] python = - 3.10: py310 3.11: py311 3.12: py312 3.13: py313 @@ -64,8 +63,5 @@ extras = bench deps = -r{toxinidir}/requirements_test.txt - # ty type checks against python-version 3.10, where hap_handler - # imports async_timeout instead of asyncio.timeout. - async_timeout commands = ty check