From 11807483411ead76a91b520c53e3526b318dd7ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 30 Oct 2026 18:37:22 -1000 Subject: [PATCH] feat: drop Python 3.10 support --- .claude/skills/pr-workflow/SKILL.md | 4 +- .github/workflows/ci.yml | 17 ++---- .pre-commit-config.yaml | 2 +- CLAUDE.md | 10 ++-- README.rst | 4 +- poetry.lock | 87 +---------------------------- pyproject.toml | 8 +-- src/zeroconf/_core.py | 3 +- src/zeroconf/_services/browser.py | 3 +- src/zeroconf/_utils/asyncio.py | 3 +- src/zeroconf/asyncio.py | 5 +- tests/conftest.py | 4 +- 12 files changed, 31 insertions(+), 119 deletions(-) diff --git a/.claude/skills/pr-workflow/SKILL.md b/.claude/skills/pr-workflow/SKILL.md index 72bbef708..9971653c8 100644 --- a/.claude/skills/pr-workflow/SKILL.md +++ b/.claude/skills/pr-workflow/SKILL.md @@ -117,8 +117,8 @@ CI runs three jobs: the PR title against Conventional Commits. If it fails, fix the title in the GitHub UI or with `gh pr edit --title "..."`; the workflow re-runs on the edit, no push needed. -- `test` — the full pytest matrix across CPython 3.10–3.14, - 3.14t (free-threaded), and PyPy 3.10, on Linux + macOS + +- `test` — the full pytest matrix across CPython 3.11–3.15, + 3.14t and 3.15t (free-threaded), and PyPy 3.11, on Linux + macOS + Windows. The free-threaded entry is the canary for unguarded shared-state bugs; failures there are often genuine even when the GIL-enabled rows pass. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 087ce2daf..2d745993f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,6 @@ jobs: fail-fast: false matrix: python-version: - - "3.10" - "3.11" - "3.12" - "3.13" @@ -80,7 +79,7 @@ jobs: - "3.14t" - "3.15" - "3.15t" - - "pypy-3.10" + - "pypy-3.11" os: - ubuntu-latest - macos-latest @@ -94,9 +93,9 @@ jobs: - os: windows-latest extension: use_cython - os: windows-latest - python-version: "pypy-3.10" + python-version: "pypy-3.11" - os: macos-latest - python-version: "pypy-3.10" + python-version: "pypy-3.11" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 @@ -270,10 +269,6 @@ jobs: musl: "musllinux" # qemu is slow, make a single # runner per Python version - - os: ubuntu-24.04-arm - qemu: armv7l - musl: "musllinux" - pyver: cp310 - os: ubuntu-24.04-arm qemu: armv7l musl: "musllinux" @@ -296,10 +291,6 @@ jobs: pyver: cp314t # qemu is slow, make a single # runner per Python version - - os: ubuntu-24.04-arm - qemu: armv7l - musl: "" - pyver: cp310 - os: ubuntu-24.04-arm qemu: armv7l musl: "" @@ -365,7 +356,7 @@ jobs: uses: pypa/cibuildwheel@e090b81e30c4d855ea63bf4b6e59204c09a101ae # v4.2.1 # to supply options, put them in 'env', like: env: - CIBW_SKIP: cp38-* cp39-* pp38-* pp39-* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} + CIBW_SKIP: cp38-* cp39-* cp310-* pp38-* pp39-* pp310-* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} CIBW_BEFORE_ALL_LINUX: apt install -y gcc || yum install -y gcc || apk add gcc CIBW_ARCHS_MACOS: arm64 REQUIRE_CYTHON: 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c836bf8c..47fd3ab36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: rev: v3.21.2 hooks: - id: pyupgrade - args: [--py310-plus] + args: [--py311-plus] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.8 hooks: diff --git a/CLAUDE.md b/CLAUDE.md index e1e73c591..21774fa59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,13 +77,13 @@ path. The authoritative list of cythonized modules lives in and `zeroconf/asyncio.py` re-export. - **Line length**: 110 (ruff `line-length = 110`). - `requires-python = ">=3.10"`, `target-version = "py310"` for - ruff; pyupgrade runs `--py310-plus`. + `requires-python = ">=3.11"`, `target-version = "py311"` for + ruff; pyupgrade runs `--py311-plus`. - **Imports**: ruff/isort sorted, `profile = "black"`, `known_first_party = ["zeroconf", "tests"]`. Prefer `from __future__ import annotations` so modern type syntax - works on 3.10. + works on 3.11. - **Generated `.c` files are not lint-targets.** `*.c` files next to each cythonized module are Cython output — never hand- @@ -135,8 +135,8 @@ CodSpeed benchmarks live under `tests/benchmarks/` and run in CI through `CodSpeedHQ/action`. Ad-hoc microbenchmarks for manual profiling live under `bench/` — those don't run in CI. -The CI matrix includes CPython 3.10 – 3.14, the free-threaded -3.14t build, and PyPy 3.10. Don't add anything that breaks +The CI matrix includes CPython 3.11 – 3.15, the free-threaded +3.14t and 3.15t builds, and PyPy 3.11. Don't add anything that breaks on the free-threaded build (no module-level mutable globals mutated from multiple threads without locks; no `PyDict_Next`-style escape hatches in Cython). diff --git a/README.rst b/README.rst index 941de2b1a..740819cc3 100644 --- a/README.rst +++ b/README.rst @@ -41,8 +41,8 @@ Compared to some other Zeroconf/Bonjour/Avahi Python packages, python-zeroconf: Python compatibility -------------------- -* CPython 3.10+ -* PyPy 3.10+ +* CPython 3.11+ +* PyPy 3.11+ Versioning ---------- diff --git a/poetry.lock b/poetry.lock index b8fdddfaf..0964b8027 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -27,19 +27,6 @@ files = [ [package.extras] dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - [[package]] name = "blockbuster" version = "1.5.29" @@ -280,9 +267,6 @@ files = [ {file = "coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] @@ -347,22 +331,6 @@ files = [ {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, -] - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "forbiddenfruit" version = "0.1.4" @@ -468,7 +436,6 @@ files = [ ] [package.dependencies] -exceptiongroup = {version = ">=1.0.0", markers = "python_full_version < \"3.11.0\""} sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] @@ -723,12 +690,10 @@ files = [ [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} iniconfig = ">=1.0.1" packaging = ">=22" pluggy = ">=1.5,<2" pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] @@ -746,7 +711,6 @@ files = [ ] [package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} pytest = ">=8.4,<10" typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} @@ -873,7 +837,6 @@ files = [ [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -953,7 +916,6 @@ sphinxcontrib-htmlhelp = ">=2.0.6" sphinxcontrib-jsmath = ">=1.0.1" sphinxcontrib-qthelp = ">=1.0.6" sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] @@ -1095,49 +1057,6 @@ lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] -[[package]] -name = "tomli" -version = "2.2.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev", "docs"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -1171,5 +1090,5 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [metadata] lock-version = "2.1" -python-versions = "^3.10" -content-hash = "cfed127ae8c83177c03d7e36c43c22874ccea76624911841e2006857ffe63969" +python-versions = "^3.11" +content-hash = "3847560bc6f5e463e1457c0adcdae9941997dc8edf5c16e15a6bc772d66b1f55" diff --git a/pyproject.toml b/pyproject.toml index 2ded77420..33db4eb93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ authors = [ maintainers = [ { name = "J. Nick Koston", email = "nick@koston.org" }, ] -requires-python = ">=3.10" +requires-python = ">=3.11" [project.urls] "Repository" = "https://github.com/python-zeroconf/python-zeroconf" @@ -80,7 +80,7 @@ match = "(?!(master|release-0.x)$)" prerelease = true [tool.poetry.dependencies] -python = "^3.10" +python = "^3.11" ifaddr = ">=0.1.7" [tool.poetry.group.dev.dependencies] @@ -99,7 +99,7 @@ sphinx = "^7.4.7 || ^8.1.3" sphinx-rtd-theme = "^3.1.0" [tool.ruff] -target-version = "py310" +target-version = "py311" line-length = 110 [tool.ruff.lint] @@ -117,10 +117,10 @@ ignore = [ "PGH004", # too many to fix "PGH003", # too many to fix "SIM110", # this is slower - "PYI034", # enable when we drop Py3.10 "PYI032", # breaks Cython "PYI041", # breaks Cython "PERF401", # Cython: closures inside cpdef functions not yet supported + "ASYNC109", # timeout params are public API ] select = [ "ASYNC", # async rules diff --git a/src/zeroconf/_core.py b/src/zeroconf/_core.py index 11ccc4f60..6ca8a25a8 100644 --- a/src/zeroconf/_core.py +++ b/src/zeroconf/_core.py @@ -14,6 +14,7 @@ import threading from collections.abc import Awaitable from types import TracebackType +from typing import Self from ._cache import DNSCache from ._dns import DNSQuestion, DNSQuestionType @@ -212,7 +213,7 @@ def __init__( self.start() - def __enter__(self) -> Zeroconf: + def __enter__(self) -> Self: return self def __exit__( # pylint: disable=useless-return diff --git a/src/zeroconf/_services/browser.py b/src/zeroconf/_services/browser.py index a3a0c840a..1c42c75e2 100644 --- a/src/zeroconf/_services/browser.py +++ b/src/zeroconf/_services/browser.py @@ -21,6 +21,7 @@ from typing import ( TYPE_CHECKING, Any, + Self, cast, ) @@ -762,7 +763,7 @@ def __init__( getattr(self, "native_id", self.ident), ) - def __enter__(self) -> ServiceBrowser: + def __enter__(self) -> Self: return self def __exit__( # pylint: disable=useless-return diff --git a/src/zeroconf/_utils/asyncio.py b/src/zeroconf/_utils/asyncio.py index c13a5558c..dd69a5e71 100644 --- a/src/zeroconf/_utils/asyncio.py +++ b/src/zeroconf/_utils/asyncio.py @@ -10,7 +10,6 @@ import asyncio import concurrent.futures import contextlib -import sys from collections.abc import Awaitable, Coroutine from typing import Any @@ -58,7 +57,7 @@ async def wait_future_or_timeout(future: asyncio.Future[bool | None], timeout: f try: await future except asyncio.CancelledError: - if sys.version_info >= (3, 11) and (task := asyncio.current_task()) and task.cancelling(): + if (task := asyncio.current_task()) and task.cancelling(): raise finally: handle.cancel() diff --git a/src/zeroconf/asyncio.py b/src/zeroconf/asyncio.py index b7a9f5e7e..73b4f894c 100644 --- a/src/zeroconf/asyncio.py +++ b/src/zeroconf/asyncio.py @@ -11,6 +11,7 @@ import contextlib from collections.abc import Awaitable, Callable from types import TracebackType # used in type hints +from typing import Self from ._core import Zeroconf from ._dns import DNSQuestionType @@ -48,7 +49,7 @@ def __init__( super().__init__(zeroconf, type_, handlers, listener, addr, port, delay, question_type) self._async_start() - async def __aenter__(self) -> AsyncServiceBrowser: + async def __aenter__(self) -> Self: return self async def __aexit__( @@ -138,7 +139,7 @@ def __init__( ) self.async_browsers: dict[ServiceListener, AsyncServiceBrowser] = {} - async def __aenter__(self) -> AsyncZeroconf: + async def __aenter__(self) -> Self: return self async def __aexit__( diff --git a/tests/conftest.py b/tests/conftest.py index 57ffb1346..a1adf9206 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ from zeroconf.asyncio import AsyncZeroconf # The dev dependency group only installs hypothesis on CPython (the CI -# matrix pins pypy-3.10 and hypothesis ships no pp310 wheels), and a +# matrix pins pypy-3.11 and hypothesis ships no pp311 wheels), and a # non-dev install has no hypothesis at all. collect_ignore: list[str] = [] if importlib.util.find_spec("hypothesis") is None: @@ -45,7 +45,7 @@ # `test_async_service_registration*` and `test_async_tasks` entries # share a single root cause: `Zeroconf.async_close()` -> ... -> # `ServiceBrowser.cancel()` calls `Thread.join()` to drain the -# dedicated browser thread, and on Python 3.10-3.12 the thread is +# dedicated browser thread, and on Python 3.11-3.12 the thread is # still alive when the join happens. `test_use_asyncio_false_*` is # by design (sync bootstrap when `use_asyncio=False` is requested from # inside a running loop); `test_run_coro_with_timeout` exercises the