From 6c02b1fe6e1a6b86194e7e90e0039c058ff4f8e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:21:23 +0000 Subject: [PATCH 01/46] chore(pre-commit.ci): pre-commit autoupdate (#1534) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0c966743..f0d1bec6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ ci: repos: - repo: https://github.com/commitizen-tools/commitizen - rev: v4.2.1 + rev: v4.2.2 hooks: - id: commitizen stages: [commit-msg] @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.6 + rev: v0.9.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From a06df79660093e7a59bc88c125c459aa7ec5df1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Feb 2025 23:21:18 -0600 Subject: [PATCH 02/46] chore(ci): bump python-semantic-release/python-semantic-release from 9.17.0 to 9.21.0 in the github-actions group (#1535) chore(ci): bump python-semantic-release/python-semantic-release Bumps the github-actions group with 1 update: [python-semantic-release/python-semantic-release](https://github.com/python-semantic-release/python-semantic-release). Updates `python-semantic-release/python-semantic-release` from 9.17.0 to 9.21.0 - [Release notes](https://github.com/python-semantic-release/python-semantic-release/releases) - [Changelog](https://github.com/python-semantic-release/python-semantic-release/blob/master/CHANGELOG.rst) - [Commits](https://github.com/python-semantic-release/python-semantic-release/compare/v9.17.0...v9.21.0) --- updated-dependencies: - dependency-name: python-semantic-release/python-semantic-release dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 578fe76a..5a1a1720 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,14 +134,14 @@ jobs: # Do a dry run of PSR - name: Test release - uses: python-semantic-release/python-semantic-release@v9.17.0 + uses: python-semantic-release/python-semantic-release@v9.21.0 if: github.ref_name != 'master' with: root_options: --noop # On main branch: actual PSR + upload to PyPI & GitHub - name: Release - uses: python-semantic-release/python-semantic-release@v9.17.0 + uses: python-semantic-release/python-semantic-release@v9.21.0 id: release if: github.ref_name == 'master' with: From c2ac47e2df6d8ee92d51fc7a72d1d18f88e0132f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 22:21:43 -0700 Subject: [PATCH 03/46] chore(deps-dev): bump pytest from 8.3.4 to 8.3.5 (#1536) --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9caaac17..6b66bcd1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -647,13 +647,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.3.4" +version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [package.dependencies] From 558c0607fd1bf4a2ecc9bb5d84bcd8824c7fc922 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 2 Mar 2025 22:22:02 -0700 Subject: [PATCH 04/46] chore(deps-dev): bump setuptools from 75.8.0 to 75.8.2 (#1537) --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6b66bcd1..e0d48822 100644 --- a/poetry.lock +++ b/poetry.lock @@ -791,13 +791,13 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "75.8.0" +version = "75.8.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, - {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, + {file = "setuptools-75.8.2-py3-none-any.whl", hash = "sha256:558e47c15f1811c1fa7adbd0096669bf76c1d3f433f58324df69f3f5ecac4e8f"}, + {file = "setuptools-75.8.2.tar.gz", hash = "sha256:4880473a969e5f23f2a2be3646b2dfd84af9028716d398e46192f84bc36900d2"}, ] [package.extras] From 25454648221ab659b48b58f7cfb2f6199fadea1c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 14:00:53 -1000 Subject: [PATCH 05/46] chore(pre-commit.ci): pre-commit autoupdate (#1538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/commitizen-tools/commitizen: v4.2.2 → v4.4.1](https://github.com/commitizen-tools/commitizen/compare/v4.2.2...v4.4.1) - [github.com/astral-sh/ruff-pre-commit: v0.9.7 → v0.9.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.9.7...v0.9.9) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0d1bec6..265f703e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ ci: repos: - repo: https://github.com/commitizen-tools/commitizen - rev: v4.2.2 + rev: v4.4.1 hooks: - id: commitizen stages: [commit-msg] @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.7 + rev: v0.9.9 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 7d0768f7622e3af9e7cdb7335bb2ddbbe493b4bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Mar 2025 14:01:17 -1000 Subject: [PATCH 06/46] chore: update process to get relase tag from PSR in release workflow (#1539) fixes #1201 --- .github/workflows/ci.yml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a1a1720..457b4e1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,6 +125,7 @@ jobs: contents: write outputs: released: ${{ steps.release.outputs.released }} + newest_release_tag: ${{ steps.release.outputs.tag }} steps: - uses: actions/checkout@v4 @@ -261,22 +262,13 @@ jobs: echo "CIBW_BUILD=${{ matrix.pyver }}*" >> $GITHUB_ENV fi - - name: Install python-semantic-release - run: pipx install python-semantic-release==7.34.6 - - - name: Get Release Tag - id: release_tag - shell: bash - run: | - echo "::set-output name=newest_release_tag::$(semantic-release print-version --current)" - - uses: actions/checkout@v4 with: - ref: "${{ steps.release_tag.outputs.newest_release_tag }}" + ref: ${{ needs.release.outputs.newest_release_tag }} fetch-depth: 0 - name: Build wheels ${{ matrix.musl }} (${{ matrix.qemu }}) - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v2.23.0 # to supply options, put them in 'env', like: env: CIBW_SKIP: cp36-* cp37-* pp36-* pp37-* pp38-* cp38-* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} From dea233c1e0e80584263090727ce07648755964af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Mar 2025 15:36:34 -1000 Subject: [PATCH 07/46] feat: reduce size of wheels (#1540) feat: reduce size of binaries --- build_ext.py | 57 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/build_ext.py b/build_ext.py index e91f6350..7faa607f 100644 --- a/build_ext.py +++ b/build_ext.py @@ -5,8 +5,44 @@ from distutils.command.build_ext import build_ext from typing import Any +try: + from setuptools import Extension +except ImportError: + from distutils.core import Extension + _LOGGER = logging.getLogger(__name__) +TO_CYTHONIZE = [ + "src/zeroconf/_dns.py", + "src/zeroconf/_cache.py", + "src/zeroconf/_history.py", + "src/zeroconf/_record_update.py", + "src/zeroconf/_listener.py", + "src/zeroconf/_protocol/incoming.py", + "src/zeroconf/_protocol/outgoing.py", + "src/zeroconf/_handlers/answers.py", + "src/zeroconf/_handlers/record_manager.py", + "src/zeroconf/_handlers/multicast_outgoing_queue.py", + "src/zeroconf/_handlers/query_handler.py", + "src/zeroconf/_services/__init__.py", + "src/zeroconf/_services/browser.py", + "src/zeroconf/_services/info.py", + "src/zeroconf/_services/registry.py", + "src/zeroconf/_updates.py", + "src/zeroconf/_utils/ipaddress.py", + "src/zeroconf/_utils/time.py", +] + +EXTENSIONS = [ + Extension( + ext.removeprefix("src/").removesuffix(".py").replace("/", "."), + [ext], + language="c", + extra_compile_args=["-O3", "-g0"], + ) + for ext in TO_CYTHONIZE +] + class BuildExt(build_ext): def build_extensions(self) -> None: @@ -25,26 +61,7 @@ def build(setup_kwargs: Any) -> None: setup_kwargs.update( { "ext_modules": cythonize( - [ - "src/zeroconf/_dns.py", - "src/zeroconf/_cache.py", - "src/zeroconf/_history.py", - "src/zeroconf/_record_update.py", - "src/zeroconf/_listener.py", - "src/zeroconf/_protocol/incoming.py", - "src/zeroconf/_protocol/outgoing.py", - "src/zeroconf/_handlers/answers.py", - "src/zeroconf/_handlers/record_manager.py", - "src/zeroconf/_handlers/multicast_outgoing_queue.py", - "src/zeroconf/_handlers/query_handler.py", - "src/zeroconf/_services/__init__.py", - "src/zeroconf/_services/browser.py", - "src/zeroconf/_services/info.py", - "src/zeroconf/_services/registry.py", - "src/zeroconf/_updates.py", - "src/zeroconf/_utils/ipaddress.py", - "src/zeroconf/_utils/time.py", - ], + EXTENSIONS, compiler_directives={"language_level": "3"}, # Python 3 ), "cmdclass": {"build_ext": BuildExt}, From c9ef9ee527767d3e2fae0dd0d7df1b5ed156ea26 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 5 Mar 2025 01:44:48 +0000 Subject: [PATCH 08/46] 0.146.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- src/zeroconf/__init__.py | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e770a88f..580dffb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # CHANGELOG +## v0.146.0 (2025-03-05) + +### Features + +- Reduce size of wheels ([#1540](https://github.com/python-zeroconf/python-zeroconf/pull/1540), + [`dea233c`](https://github.com/python-zeroconf/python-zeroconf/commit/dea233c1e0e80584263090727ce07648755964af)) + +feat: reduce size of binaries + + ## v0.145.1 (2025-02-18) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index e6aa0efe..2b94783e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zeroconf" -version = "0.145.1" +version = "0.146.0" description = "A pure python implementation of multicast DNS service discovery" authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"] license = "LGPL-2.1-or-later" diff --git a/src/zeroconf/__init__.py b/src/zeroconf/__init__.py index d2235d5c..68e7213d 100644 --- a/src/zeroconf/__init__.py +++ b/src/zeroconf/__init__.py @@ -88,7 +88,7 @@ __author__ = "Paul Scott-Murphy, William McBrine" __maintainer__ = "Jakub Stasiak " -__version__ = "0.145.1" +__version__ = "0.146.0" __license__ = "LGPL" From fa65cc8791a6f4c53bc29088cb60b83f420b1ae6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 4 Mar 2025 17:06:33 -1000 Subject: [PATCH 09/46] fix: use trusted publishing for uploading wheels (#1541) --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 457b4e1d..2fb9b06f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -284,19 +284,19 @@ jobs: needs: [build_wheels] runs-on: ubuntu-latest environment: release + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - uses: actions/download-artifact@v4 with: # unpacks default artifact into dist/ # if `name: artifact` is omitted, the action will create extra parent dir - pattern: wheels-* path: dist + pattern: wheels-* merge-multiple: true - - uses: pypa/gh-action-pypi-publish@v1.12.4 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} + - uses: + pypa/gh-action-pypi-publish@v1.12.4 # To test: repository_url: https://test.pypi.org/legacy/ From ea6905b1a0e6122e1baa0cbb39db1cb91ec0f310 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 5 Mar 2025 03:16:07 +0000 Subject: [PATCH 10/46] 0.146.1 Automatically generated by python-semantic-release --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- src/zeroconf/__init__.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 580dffb0..f28b0022 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # CHANGELOG +## v0.146.1 (2025-03-05) + +### Bug Fixes + +- Use trusted publishing for uploading wheels + ([#1541](https://github.com/python-zeroconf/python-zeroconf/pull/1541), + [`fa65cc8`](https://github.com/python-zeroconf/python-zeroconf/commit/fa65cc8791a6f4c53bc29088cb60b83f420b1ae6)) + + ## v0.146.0 (2025-03-05) ### Features diff --git a/pyproject.toml b/pyproject.toml index 2b94783e..4e056cd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zeroconf" -version = "0.146.0" +version = "0.146.1" description = "A pure python implementation of multicast DNS service discovery" authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"] license = "LGPL-2.1-or-later" diff --git a/src/zeroconf/__init__.py b/src/zeroconf/__init__.py index 68e7213d..b915b8d7 100644 --- a/src/zeroconf/__init__.py +++ b/src/zeroconf/__init__.py @@ -88,7 +88,7 @@ __author__ = "Paul Scott-Murphy, William McBrine" __maintainer__ = "Jakub Stasiak " -__version__ = "0.146.0" +__version__ = "0.146.1" __license__ = "LGPL" From 080462ed7bfd49311010a3c06d600e77bcc5fb8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 19:08:36 -1000 Subject: [PATCH 11/46] chore(deps-dev): bump setuptools from 75.8.2 to 76.0.0 (#1543) --- poetry.lock | 83 ++++++++++++++++++++++++++++++++++++++++---------- pyproject.toml | 2 +- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index e0d48822..75863563 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -6,6 +6,7 @@ version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, @@ -17,6 +18,7 @@ version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, @@ -31,6 +33,7 @@ version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, @@ -42,6 +45,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -121,6 +125,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -222,6 +227,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev", "docs"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -233,6 +240,7 @@ version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, @@ -302,7 +310,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cython" @@ -310,6 +318,7 @@ version = "3.0.12" description = "The Cython compiler for writing C extensions in the Python language." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["dev"] files = [ {file = "Cython-3.0.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba67eee9413b66dd9fbacd33f0bc2e028a2a120991d77b5fd4b19d0b1e4039b9"}, {file = "Cython-3.0.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee2717e5b5f7d966d0c6e27d2efe3698c357aa4d61bb3201997c7a4f9fe485a"}, @@ -383,6 +392,7 @@ version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -394,6 +404,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -408,6 +420,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -422,6 +435,7 @@ version = "0.2.0" description = "Cross-platform network interface and IP address enumeration library" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748"}, {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, @@ -433,6 +447,7 @@ version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["docs"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -444,6 +459,8 @@ version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] +markers = "python_version < \"3.10\"" files = [ {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, @@ -453,12 +470,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -467,6 +484,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -478,6 +496,7 @@ version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, @@ -495,6 +514,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -519,6 +539,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -589,6 +610,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -600,6 +622,7 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -611,6 +634,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -626,6 +650,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -637,6 +662,7 @@ version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -651,6 +677,7 @@ version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, @@ -673,6 +700,7 @@ version = "0.25.3" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"}, {file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"}, @@ -691,6 +719,7 @@ version = "3.2.0" description = "Pytest plugin to create CodSpeed benchmarks" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34"}, {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c"}, @@ -723,6 +752,7 @@ version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, @@ -741,6 +771,7 @@ version = "2.3.1" description = "pytest plugin to abort hanging tests" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"}, {file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"}, @@ -755,6 +786,7 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -776,6 +808,7 @@ version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, @@ -791,23 +824,24 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "75.8.2" +version = "76.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "setuptools-75.8.2-py3-none-any.whl", hash = "sha256:558e47c15f1811c1fa7adbd0096669bf76c1d3f433f58324df69f3f5ecac4e8f"}, - {file = "setuptools-75.8.2.tar.gz", hash = "sha256:4880473a969e5f23f2a2be3646b2dfd84af9028716d398e46192f84bc36900d2"}, + {file = "setuptools-76.0.0-py3-none-any.whl", hash = "sha256:199466a166ff664970d0ee145839f5582cb9bca7a0a3a2e795b6a9cb2308e9c6"}, + {file = "setuptools-76.0.0.tar.gz", hash = "sha256:43b4ee60e10b0d0ee98ad11918e114c70701bc6051662a9a675a0496c1a158f4"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "snowballstemmer" @@ -815,6 +849,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -826,6 +861,7 @@ version = "7.4.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, @@ -862,6 +898,7 @@ version = "3.0.2" description = "Read the Docs theme for Sphinx" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, @@ -881,6 +918,7 @@ version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -897,6 +935,7 @@ version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -913,6 +952,7 @@ version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -929,6 +969,7 @@ version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = false python-versions = ">=2.7" +groups = ["docs"] files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, @@ -943,6 +984,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["docs"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -957,6 +999,7 @@ version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -973,6 +1016,7 @@ version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -989,6 +1033,7 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] 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"}, @@ -1023,6 +1068,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version < \"3.11\""} [[package]] name = "typing-extensions" @@ -1030,6 +1076,8 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -1041,13 +1089,14 @@ version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1058,20 +1107,22 @@ version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" +groups = ["dev", "docs"] +markers = "python_version < \"3.10\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.9" -content-hash = "ea903296f015035c594eb8cce08d4dedc716074e33644033938dfdb5f047d72e" +content-hash = "f866b539caf6f0140faba8aa19f4e1fae2013a48fc3346747f104dfe62ef290b" diff --git a/pyproject.toml b/pyproject.toml index 4e056cd7..9d38dc55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ pytest = ">=7.2,<9.0" pytest-cov = ">=4,<7" pytest-asyncio = ">=0.20.3,<0.26.0" cython = "^3.0.5" -setuptools = ">=65.6.3,<76.0.0" +setuptools = ">=65.6.3,<77.0.0" pytest-timeout = "^2.1.0" pytest-codspeed = "^3.1.0" From 89e3cbd4ad7ceab07925bfeb8814d3d1163d810f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 06:47:31 -1000 Subject: [PATCH 12/46] chore(pre-commit.ci): pre-commit autoupdate (#1544) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 265f703e..633a2c35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.9.10 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 019c641dc22c1fb30f7764525ab9777eaa98b388 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Mar 2025 12:40:28 -1000 Subject: [PATCH 13/46] chore: upgrade to ruff 0.1.0 (#1547) --- .pre-commit-config.yaml | 2 +- pyproject.toml | 9 +--- src/zeroconf/_services/browser.py | 2 +- tests/__init__.py | 1 - tests/benchmarks/test_cache.py | 1 - tests/benchmarks/test_incoming.py | 1 - tests/benchmarks/test_outgoing.py | 1 - tests/benchmarks/test_send.py | 3 +- tests/benchmarks/test_txt_properties.py | 1 - tests/conftest.py | 5 +-- tests/services/test_browser.py | 19 ++++---- tests/services/test_info.py | 33 +++++++------- tests/test_asyncio.py | 59 ++++++++++++------------- tests/test_cache.py | 7 ++- tests/test_circular_imports.py | 2 +- tests/test_core.py | 7 ++- tests/test_dns.py | 1 - tests/test_engine.py | 5 +-- tests/test_handlers.py | 33 +++++++------- tests/test_protocol.py | 1 - tests/test_services.py | 1 - tests/test_updates.py | 1 - tests/utils/test_asyncio.py | 9 ++-- tests/utils/test_name.py | 1 - tests/utils/test_net.py | 1 - 25 files changed, 89 insertions(+), 117 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 633a2c35..a38eaca6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.10 + rev: v0.1.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/pyproject.toml b/pyproject.toml index 9d38dc55..9c92f362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,22 +94,18 @@ ignore = [ "S101", # use of assert "S104", # S104 Possible binding to all interfaces "PLR0912", # too many to fix right now - "TC001", # too many to fix right now "TID252", # skip "PLR0913", # too late to make changes here "PLR0911", # would be breaking change "TRY003", # too many to fix "SLF001", # design choice - "TC003", # too many to fix "PLR2004" , # too many to fix "PGH004", # too many to fix "PGH003", # too many to fix "SIM110", # this is slower - "FURB136", # this is slower for Cython "PYI034", # enable when we drop Py3.10 "PYI032", # breaks Cython "PYI041", # breaks Cython - "FURB188", # usually slower "PERF401", # Cython: closures inside cpdef functions not yet supported ] select = [ @@ -124,7 +120,6 @@ select = [ "I", # isort "RUF", # ruff specific "FLY", # flynt - "FURB", # refurb "G", # flake8-logging-format , "PERF", # Perflint "PGH", # pygrep-hooks @@ -140,7 +135,6 @@ select = [ "SLOT", # flake8-slots "T100", # Trace found: {name} used "T20", # flake8-print - "TC", # flake8-type-checking "TID", # Tidy imports "TRY", # tryceratops ] @@ -171,9 +165,8 @@ select = [ "PLR0913", # skip this one "SIM102" , # too many to fix right now "SIM108", # too many to fix right now - "TC003", # too many to fix right now - "TC002", # too many to fix right now "T201", # too many to fix right now + "PT004", # nice to have ] "bench/**/*" = [ "T201", # intended diff --git a/src/zeroconf/_services/browser.py b/src/zeroconf/_services/browser.py index ab8c050d..6bf3f0f4 100644 --- a/src/zeroconf/_services/browser.py +++ b/src/zeroconf/_services/browser.py @@ -278,7 +278,7 @@ def generate_service_query( if not qu_question and question_history.suppresses(question, now_millis, known_answers): log.debug("Asking %s was suppressed by the question history", question) continue - if TYPE_CHECKING: + if TYPE_CHECKING: # noqa: SIM108 pointer_known_answers = cast(set[DNSPointer], known_answers) else: pointer_known_answers = known_answers diff --git a/tests/__init__.py b/tests/__init__.py index a70cca60..3df09819 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -29,7 +29,6 @@ from unittest import mock import ifaddr - from zeroconf import DNSIncoming, DNSQuestion, DNSRecord, Zeroconf from zeroconf._history import QuestionHistory diff --git a/tests/benchmarks/test_cache.py b/tests/benchmarks/test_cache.py index 7813f679..e32abda0 100644 --- a/tests/benchmarks/test_cache.py +++ b/tests/benchmarks/test_cache.py @@ -1,7 +1,6 @@ from __future__ import annotations from pytest_codspeed import BenchmarkFixture - from zeroconf import DNSCache, DNSPointer, current_time_millis from zeroconf.const import _CLASS_IN, _TYPE_PTR diff --git a/tests/benchmarks/test_incoming.py b/tests/benchmarks/test_incoming.py index 6d31e51e..672e5c78 100644 --- a/tests/benchmarks/test_incoming.py +++ b/tests/benchmarks/test_incoming.py @@ -5,7 +5,6 @@ import socket from pytest_codspeed import BenchmarkFixture - from zeroconf import ( DNSAddress, DNSIncoming, diff --git a/tests/benchmarks/test_outgoing.py b/tests/benchmarks/test_outgoing.py index a8db4d6f..cc2f3f42 100644 --- a/tests/benchmarks/test_outgoing.py +++ b/tests/benchmarks/test_outgoing.py @@ -3,7 +3,6 @@ from __future__ import annotations from pytest_codspeed import BenchmarkFixture - from zeroconf._protocol.outgoing import State from .helpers import generate_packets diff --git a/tests/benchmarks/test_send.py b/tests/benchmarks/test_send.py index 596662a2..d931b48b 100644 --- a/tests/benchmarks/test_send.py +++ b/tests/benchmarks/test_send.py @@ -4,13 +4,12 @@ import pytest from pytest_codspeed import BenchmarkFixture - from zeroconf.asyncio import AsyncZeroconf from .helpers import generate_packets -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_sending_packets(benchmark: BenchmarkFixture) -> None: """Benchmark sending packets.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) diff --git a/tests/benchmarks/test_txt_properties.py b/tests/benchmarks/test_txt_properties.py index 72afa0b6..b7b0e767 100644 --- a/tests/benchmarks/test_txt_properties.py +++ b/tests/benchmarks/test_txt_properties.py @@ -1,7 +1,6 @@ from __future__ import annotations from pytest_codspeed import BenchmarkFixture - from zeroconf import ServiceInfo info = ServiceInfo( diff --git a/tests/conftest.py b/tests/conftest.py index 531c810b..3d891ec4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,6 @@ from unittest.mock import patch import pytest - from zeroconf import _core, const from zeroconf._handlers import query_handler @@ -20,7 +19,7 @@ def verify_threads_ended(): assert not threads -@pytest.fixture +@pytest.fixture() def run_isolated(): """Change the mDNS port to run the test in isolation.""" with ( @@ -31,7 +30,7 @@ def run_isolated(): yield -@pytest.fixture +@pytest.fixture() def disable_duplicate_packet_suppression(): """Disable duplicate packet suppress. diff --git a/tests/services/test_browser.py b/tests/services/test_browser.py index 986df64e..f5237365 100644 --- a/tests/services/test_browser.py +++ b/tests/services/test_browser.py @@ -14,7 +14,6 @@ from unittest.mock import patch import pytest - import zeroconf as r import zeroconf._services.browser as _services_browser from zeroconf import ( @@ -556,7 +555,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name): zeroconf_browser.close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_asking_default_is_asking_qm_questions_after_the_first_qu(): """Verify the service browser's first questions are QU and refresh queries are QM.""" service_added = asyncio.Event() @@ -658,7 +657,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_ttl_refresh_cancelled_rescue_query(): """Verify seeing a name again cancels the rescue query.""" service_added = asyncio.Event() @@ -768,7 +767,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_asking_qm_questions(): """Verify explicitly asking QM questions.""" type_ = "_quservice._tcp.local." @@ -807,7 +806,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_asking_qu_questions(): """Verify the service browser can ask QU questions.""" type_ = "_quservice._tcp.local." @@ -1139,7 +1138,7 @@ def test_group_ptr_queries_with_known_answers(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_generate_service_query_suppress_duplicate_questions(): """Generate a service query for sending with zeroconf.send.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1192,7 +1191,7 @@ async def test_generate_service_query_suppress_duplicate_questions(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_query_scheduler(): delay = const._BROWSER_TIME types_ = {"_hap._tcp.local.", "_http._tcp.local."} @@ -1285,7 +1284,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_query_scheduler_rescue_records(): delay = const._BROWSER_TIME types_ = {"_hap._tcp.local.", "_http._tcp.local."} @@ -1580,7 +1579,7 @@ def test_scheduled_ptr_query_dunder_methods(): assert query75 >= other # type: ignore[operator] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_close_zeroconf_without_browser_before_start_up_queries(): """Test that we stop sending startup queries if zeroconf is closed out from under the browser.""" service_added = asyncio.Event() @@ -1648,7 +1647,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await browser.async_cancel() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_close_zeroconf_without_browser_after_start_up_queries(): """Test that we stop sending rescue queries if zeroconf is closed out from under the browser.""" service_added = asyncio.Event() diff --git a/tests/services/test_info.py b/tests/services/test_info.py index 3d4c5302..8b912bea 100644 --- a/tests/services/test_info.py +++ b/tests/services/test_info.py @@ -14,7 +14,6 @@ from unittest.mock import patch import pytest - import zeroconf as r from zeroconf import DNSAddress, RecordUpdate, const from zeroconf._services import info @@ -828,7 +827,7 @@ def test_scoped_addresses_from_cache(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_multiple_a_addresses_newest_address_first(): """Test that info.addresses returns the newest seen address first.""" type_ = "_http._tcp.local." @@ -848,7 +847,7 @@ async def test_multiple_a_addresses_newest_address_first(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_invalid_a_addresses(caplog): type_ = "_http._tcp.local." registration_name = f"multiarec.{type_}" @@ -1057,7 +1056,7 @@ def test_request_timeout(): assert (end_time - start_time) < 3000 + 1000 -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_we_try_four_times_with_random_delay(): """Verify we try four times even with the random delay.""" type_ = "_typethatisnothere._tcp.local." @@ -1080,7 +1079,7 @@ def async_send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT): assert request_count == 4 -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_release_wait_when_new_recorded_added(): """Test that async_request returns as soon as new matching records are added to the cache.""" type_ = "_http._tcp.local." @@ -1145,7 +1144,7 @@ async def test_release_wait_when_new_recorded_added(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_port_changes_are_seen(): """Test that port changes are seen by async_request.""" type_ = "_http._tcp.local." @@ -1228,7 +1227,7 @@ async def test_port_changes_are_seen(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_port_changes_are_seen_with_directed_request(): """Test that port changes are seen by async_request with a directed request.""" type_ = "_http._tcp.local." @@ -1311,7 +1310,7 @@ async def test_port_changes_are_seen_with_directed_request(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_ipv4_changes_are_seen(): """Test that ipv4 changes are seen by async_request.""" type_ = "_http._tcp.local." @@ -1399,7 +1398,7 @@ async def test_ipv4_changes_are_seen(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_ipv6_changes_are_seen(): """Test that ipv6 changes are seen by async_request.""" type_ = "_http._tcp.local." @@ -1494,7 +1493,7 @@ async def test_ipv6_changes_are_seen(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_bad_ip_addresses_ignored_in_cache(): """Test that bad ip address in the cache are ignored async_request.""" type_ = "_http._tcp.local." @@ -1548,7 +1547,7 @@ async def test_bad_ip_addresses_ignored_in_cache(): assert info.addresses_by_version(IPVersion.V4Only) == [b"\x7f\x00\x00\x01"] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_service_name_change_as_seen_has_ip_in_cache(): """Test that service name changes are seen by async_request when the ip is in the cache.""" type_ = "_http._tcp.local." @@ -1630,7 +1629,7 @@ async def test_service_name_change_as_seen_has_ip_in_cache(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_service_name_change_as_seen_ip_not_in_cache(): """Test that service name changes are seen by async_request when the ip is not in the cache.""" type_ = "_http._tcp.local." @@ -1712,7 +1711,7 @@ async def test_service_name_change_as_seen_ip_not_in_cache(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() @patch.object(info, "_LISTENER_TIME", 10000000) async def test_release_wait_when_new_recorded_added_concurrency(): """Test that concurrent async_request returns as soon as new matching records are added to the cache.""" @@ -1784,7 +1783,7 @@ async def test_release_wait_when_new_recorded_added_concurrency(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_service_info_nsec_records(): """Test we can generate nsec records from ServiceInfo.""" type_ = "_http._tcp.local." @@ -1799,7 +1798,7 @@ async def test_service_info_nsec_records(): assert nsec_record.rdtypes == [const._TYPE_A, const._TYPE_AAAA] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_address_resolver(): """Test that the address resolver works.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1823,7 +1822,7 @@ async def test_address_resolver(): assert resolver.addresses == [b"\x7f\x00\x00\x01"] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_address_resolver_ipv4(): """Test that the IPv4 address resolver works.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1847,7 +1846,7 @@ async def test_address_resolver_ipv4(): assert resolver.addresses == [b"\x7f\x00\x00\x01"] -@pytest.mark.asyncio +@pytest.mark.asyncio() @unittest.skipIf(not has_working_ipv6(), "Requires IPv6") @unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled") async def test_address_resolver_ipv6(): diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 40ecf816..e3102507 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -11,7 +11,6 @@ from unittest.mock import ANY, call, patch import pytest - import zeroconf._services.browser as _services_browser from zeroconf import ( DNSAddress, @@ -79,14 +78,14 @@ def verify_threads_ended(): assert not threads -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_basic_usage() -> None: """Test we can create and close the instance.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_close_twice() -> None: """Test we can close twice.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -94,7 +93,7 @@ async def test_async_close_twice() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_with_sync_passed_in() -> None: """Test we can create and close the instance when passing in a sync Zeroconf.""" zc = Zeroconf(interfaces=["127.0.0.1"]) @@ -103,7 +102,7 @@ async def test_async_with_sync_passed_in() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_with_sync_passed_in_closed_in_async() -> None: """Test caller closes the sync version in async.""" zc = Zeroconf(interfaces=["127.0.0.1"]) @@ -113,7 +112,7 @@ async def test_async_with_sync_passed_in_closed_in_async() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_sync_within_event_loop_executor() -> None: """Test sync version still works from an executor within an event loop.""" @@ -125,7 +124,7 @@ def sync_code(): await asyncio.get_event_loop().run_in_executor(None, sync_code) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_service_registration() -> None: """Test registering services broadcasts the registration by default.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -192,7 +191,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_service_registration_with_server_missing() -> None: """Test registering a service with the server not specified. @@ -259,7 +258,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_service_registration_same_server_different_ports() -> None: """Test registering services with the same server with different srv records.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -326,7 +325,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_service_registration_same_server_same_ports() -> None: """Test registering services with the same server with the exact same srv record.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -393,7 +392,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_service_registration_name_conflict() -> None: """Test registering services throws on name conflict.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -441,7 +440,7 @@ async def test_async_service_registration_name_conflict() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_service_registration_name_does_not_match_type() -> None: """Test registering services throws when the name does not match the type.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -467,7 +466,7 @@ async def test_async_service_registration_name_does_not_match_type() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_service_registration_name_strict_check() -> None: """Test registering services throws when the name does not comply.""" zc = Zeroconf(interfaces=["127.0.0.1"]) @@ -502,7 +501,7 @@ async def test_async_service_registration_name_strict_check() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_tasks() -> None: """Test awaiting broadcast tasks""" @@ -568,7 +567,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_wait_unblocks_on_update() -> None: """Test async_wait will unblock on update.""" @@ -604,7 +603,7 @@ async def test_async_wait_unblocks_on_update() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_service_info_async_request() -> None: """Test registering services broadcasts and query with AsyncServceInfo.async_request.""" if not has_working_ipv6() or os.environ.get("SKIP_IPV6"): @@ -713,7 +712,7 @@ async def test_service_info_async_request() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_service_browser() -> None: """Test AsyncServiceBrowser.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -773,7 +772,7 @@ def update_service(self, aiozc: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_context_manager() -> None: """Test using an async context manager.""" type_ = "_test10-sr-type._tcp.local." @@ -797,7 +796,7 @@ async def test_async_context_manager() -> None: assert aiosinfo is not None -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_service_browser_cancel_async_context_manager(): """Test we can cancel an AsyncServiceBrowser with it being used as an async context manager.""" @@ -823,7 +822,7 @@ class MyServiceListener(ServiceListener): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_unregister_all_services() -> None: """Test unregistering all services.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -882,7 +881,7 @@ async def test_async_unregister_all_services() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_zeroconf_service_types(): type_ = "_test-srvc-type._tcp.local." name = "xxxyyy" @@ -916,7 +915,7 @@ async def test_async_zeroconf_service_types(): await zeroconf_registrar.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_guard_against_running_serviceinfo_request_event_loop() -> None: """Test that running ServiceInfo.request from the event loop throws.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -927,7 +926,7 @@ async def test_guard_against_running_serviceinfo_request_event_loop() -> None: await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_service_browser_instantiation_generates_add_events_from_cache(): """Test that the ServiceBrowser will generate Add events with the existing cache when starting.""" @@ -976,7 +975,7 @@ def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-de await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_integration(): service_added = asyncio.Event() service_removed = asyncio.Event() @@ -1124,7 +1123,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu(): """Verify the service info first question is QU and subsequent ones are QM questions.""" type_ = "_quservice._tcp.local." @@ -1178,7 +1177,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_service_browser_ignores_unrelated_updates(): """Test that the ServiceBrowser ignores unrelated updates.""" @@ -1275,7 +1274,7 @@ def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-de await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_request_timeout(): """Test that the timeout does not throw an exception and finishes close to the actual timeout.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1289,7 +1288,7 @@ async def test_async_request_timeout(): assert (end_time - start_time) < 3000 + 1000 -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_request_non_running_instance(): """Test that the async_request throws when zeroconf is not running.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1298,7 +1297,7 @@ async def test_async_request_non_running_instance(): await aiozc.async_get_service_info("_notfound.local.", "notthere._notfound.local.") -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_legacy_unicast_response(run_isolated): """Verify legacy unicast responses include questions and correct id.""" type_ = "_mservice._tcp.local." @@ -1339,7 +1338,7 @@ async def test_legacy_unicast_response(run_isolated): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_update_with_uppercase_names(run_isolated): """Test an ip update from a shelly which uses uppercase names.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) diff --git a/tests/test_cache.py b/tests/test_cache.py index 9d55435d..5bd6a869 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -7,7 +7,6 @@ from heapq import heapify, heappop import pytest - import zeroconf as r from zeroconf import const @@ -364,7 +363,7 @@ def test_async_get_unique_returns_newest_record(): assert record is record2 -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_cache_heap_cleanup() -> None: """Test that the heap gets cleaned up when there are many old expirations.""" cache = r.DNSCache() @@ -416,7 +415,7 @@ async def test_cache_heap_cleanup() -> None: assert not cache.async_entries_with_name(name), cache._expire_heap -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_cache_heap_multi_name_cleanup() -> None: """Test cleanup with multiple names.""" cache = r.DNSCache() @@ -452,7 +451,7 @@ async def test_cache_heap_multi_name_cleanup() -> None: assert not cache.async_entries_with_name(name), cache._expire_heap -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_cache_heap_pops_order() -> None: """Test cache heap is popped in order.""" cache = r.DNSCache() diff --git a/tests/test_circular_imports.py b/tests/test_circular_imports.py index 74ed1f12..79d58ae1 100644 --- a/tests/test_circular_imports.py +++ b/tests/test_circular_imports.py @@ -8,7 +8,7 @@ import pytest -@pytest.mark.asyncio +@pytest.mark.asyncio() @pytest.mark.timeout(30) # cloud can take > 9s @pytest.mark.parametrize( "module", diff --git a/tests/test_core.py b/tests/test_core.py index fcfdf424..1dfb9806 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,7 +15,6 @@ from unittest.mock import AsyncMock, Mock, patch import pytest - import zeroconf as r from zeroconf import NotRunningException, Zeroconf, const, current_time_millis from zeroconf._listener import AsyncListener, _WrappedTransport @@ -665,7 +664,7 @@ def test_tc_bit_defers_last_response_missing(): zc.close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_open_close_twice_from_async() -> None: """Test we can close twice from a coroutine when using Zeroconf. @@ -685,7 +684,7 @@ async def test_open_close_twice_from_async() -> None: await asyncio.sleep(0) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_multiple_sync_instances_stared_from_async_close(): """Test we can shutdown multiple sync instances from async.""" @@ -741,7 +740,7 @@ def _background_register(): bgthread.join() -@pytest.mark.asyncio +@pytest.mark.asyncio() @patch("zeroconf._core._STARTUP_TIMEOUT", 0) @patch("zeroconf._core.AsyncEngine._async_setup", new_callable=AsyncMock) async def test_event_loop_blocked(mock_start): diff --git a/tests/test_dns.py b/tests/test_dns.py index 246c8dcf..5928338c 100644 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -8,7 +8,6 @@ import unittest.mock import pytest - import zeroconf as r from zeroconf import DNSHinfo, DNSText, ServiceInfo, const, current_time_millis from zeroconf._dns import DNSRRSet diff --git a/tests/test_engine.py b/tests/test_engine.py index b7a94c86..5f244804 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -8,7 +8,6 @@ from unittest.mock import patch import pytest - import zeroconf as r from zeroconf import _engine, const from zeroconf.asyncio import AsyncZeroconf @@ -30,7 +29,7 @@ def teardown_module(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_reaper(): with patch.object(_engine, "_CACHE_CLEANUP_INTERVAL", 0.01): aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -65,7 +64,7 @@ async def test_reaper(): assert record_with_1s_ttl not in entries -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_reaper_aborts_when_done(): """Ensure cache cleanup stops when zeroconf is done.""" with patch.object(_engine, "_CACHE_CLEANUP_INTERVAL", 0.01): diff --git a/tests/test_handlers.py b/tests/test_handlers.py index ffa4ff88..58f8ecb1 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -13,7 +13,6 @@ from unittest.mock import patch import pytest - import zeroconf as r from zeroconf import ServiceInfo, Zeroconf, const, current_time_millis from zeroconf._handlers.multicast_outgoing_queue import ( @@ -493,7 +492,7 @@ def test_unicast_response(): zc.close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_probe_answered_immediately(): """Verify probes are responded to immediately.""" # instantiate a zeroconf instance @@ -544,7 +543,7 @@ async def test_probe_answered_immediately(): zc.close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_probe_answered_immediately_with_uppercase_name(): """Verify probes are responded to immediately with an uppercase name.""" # instantiate a zeroconf instance @@ -1092,7 +1091,7 @@ def test_enumeration_query_with_no_registered_services(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_qu_response_only_sends_additionals_if_sends_answer(): """Test that a QU response does not send additionals unless it sends the answer as well.""" # instantiate a zeroconf instance @@ -1258,7 +1257,7 @@ async def test_qu_response_only_sends_additionals_if_sends_answer(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_cache_flush_bit(): """Test that the cache flush bit sets the TTL to one for matching records.""" # instantiate a zeroconf instance @@ -1361,7 +1360,7 @@ async def test_cache_flush_bit(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_record_update_manager_add_listener_callsback_existing_records(): """Test that the RecordUpdateManager will callback existing records.""" @@ -1415,7 +1414,7 @@ def async_update_records(self, zc: Zeroconf, now: float, records: list[r.RecordU await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_questions_query_handler_populates_the_question_history_from_qm_questions(): aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) zc = aiozc.zeroconf @@ -1461,7 +1460,7 @@ async def test_questions_query_handler_populates_the_question_history_from_qm_qu await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_questions_query_handler_does_not_put_qu_questions_in_history(): aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) zc = aiozc.zeroconf @@ -1504,7 +1503,7 @@ async def test_questions_query_handler_does_not_put_qu_questions_in_history(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_guard_against_low_ptr_ttl(): """Ensure we enforce a min for PTR record ttls to avoid excessive refresh queries from ServiceBrowsers. @@ -1555,7 +1554,7 @@ async def test_guard_against_low_ptr_ttl(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_duplicate_goodbye_answers_in_packet(): """Ensure we do not throw an exception when there are duplicate goodbye records in a packet.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1587,7 +1586,7 @@ async def test_duplicate_goodbye_answers_in_packet(): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_response_aggregation_timings(run_isolated): """Verify multicast responses are aggregated.""" type_ = "_mservice._tcp.local." @@ -1709,7 +1708,7 @@ async def test_response_aggregation_timings(run_isolated): await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_response_aggregation_timings_multiple(run_isolated, disable_duplicate_packet_suppression): """Verify multicast responses that are aggregated do not take longer than 620ms to send. @@ -1791,7 +1790,7 @@ async def test_response_aggregation_timings_multiple(run_isolated, disable_dupli assert info2.dns_pointer() in incoming.answers() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_response_aggregation_random_delay(): """Verify the random delay for outgoing multicast will coalesce into a single group @@ -1899,7 +1898,7 @@ async def test_response_aggregation_random_delay(): assert info5.dns_pointer() in outgoing_queue.queue[1].answers -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_future_answers_are_removed_on_send(): """Verify any future answers scheduled to be sent are removed when we send.""" type_ = "_mservice._tcp.local." @@ -1963,7 +1962,7 @@ async def test_future_answers_are_removed_on_send(): assert info2.dns_pointer() in outgoing_queue.queue[0].answers -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_add_listener_warns_when_not_using_record_update_listener(caplog): """Log when a listener is added that is not using RecordUpdateListener as a base class.""" @@ -1988,7 +1987,7 @@ def async_update_records(self, zc: Zeroconf, now: float, records: list[r.RecordU await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_updates_iteration_safe(): """Ensure we can safely iterate over the async_updates.""" @@ -2032,7 +2031,7 @@ def async_update_records(self, zc: Zeroconf, now: float, records: list[r.RecordU await aiozc.async_close() -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_updates_complete_iteration_safe(): """Ensure we can safely iterate over the async_updates_complete.""" diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 08d7e600..78fed0e0 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -11,7 +11,6 @@ from typing import cast import pytest - import zeroconf as r from zeroconf import DNSHinfo, DNSIncoming, DNSText, const, current_time_millis diff --git a/tests/test_services.py b/tests/test_services.py index 7d7c3fc7..d192c652 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -11,7 +11,6 @@ from typing import Any import pytest - import zeroconf as r from zeroconf import Zeroconf from zeroconf._services.info import ServiceInfo diff --git a/tests/test_updates.py b/tests/test_updates.py index a057486c..376082e7 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -7,7 +7,6 @@ import time import pytest - import zeroconf as r from zeroconf import Zeroconf, const from zeroconf._record_update import RecordUpdate diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py index 7989a82c..4d2ee0ec 100644 --- a/tests/utils/test_asyncio.py +++ b/tests/utils/test_asyncio.py @@ -10,14 +10,13 @@ from unittest.mock import patch import pytest - from zeroconf import EventLoopBlocked from zeroconf._engine import _CLOSE_TIMEOUT from zeroconf._utils import asyncio as aioutils from zeroconf.const import _LOADED_SYSTEM_TIMEOUT -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_async_get_all_tasks() -> None: """Test we can get all tasks in the event loop. @@ -33,7 +32,7 @@ async def test_async_get_all_tasks() -> None: await aioutils._async_get_all_tasks(loop) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_get_running_loop_from_async() -> None: """Test we can get the event loop.""" assert isinstance(aioutils.get_running_loop(), asyncio.AbstractEventLoop) @@ -44,7 +43,7 @@ def test_get_running_loop_no_loop() -> None: assert aioutils.get_running_loop() is None -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_wait_future_or_timeout_times_out() -> None: """Test wait_future_or_timeout will timeout.""" loop = asyncio.get_running_loop() @@ -118,7 +117,7 @@ def test_cumulative_timeouts_less_than_close_plus_buffer(): ) < 1 + _CLOSE_TIMEOUT + _LOADED_SYSTEM_TIMEOUT -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_run_coro_with_timeout() -> None: """Test running a coroutine with a timeout raises EventLoopBlocked.""" loop = asyncio.get_event_loop() diff --git a/tests/utils/test_name.py b/tests/utils/test_name.py index 1feb7713..3b70c7d4 100644 --- a/tests/utils/test_name.py +++ b/tests/utils/test_name.py @@ -5,7 +5,6 @@ import socket import pytest - from zeroconf import BadTypeInNameException from zeroconf._services.info import ServiceInfo, instance_name_from_service_info from zeroconf._utils import name as nameutils diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index f763b655..17ff6196 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -10,7 +10,6 @@ import ifaddr import pytest - import zeroconf as r from zeroconf._utils import net as netutils From 806e3678c0a6552f9b2f43d38eb673d509006d51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Mar 2025 13:11:53 -1000 Subject: [PATCH 14/46] chore: update deps (#1548) dependabot is still having issues so lets do this manually for now - Updating certifi (2024.12.14 -> 2025.1.31) - Updating jinja2 (3.1.5 -> 3.1.6) - Updating babel (2.16.0 -> 2.17.0) - Updating coverage (7.6.10 -> 7.6.12) - Updating pytest (8.3.4 -> 8.3.5) - Updating cython (3.0.11 -> 3.0.12) - Updating setuptools (75.8.0 -> 76.0.0) --- poetry.lock | 177 ++++++++++++++++++++++++++-------------------------- 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/poetry.lock b/poetry.lock index 75863563..8c4713e8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -14,29 +14,29 @@ files = [ [[package]] name = "babel" -version = "2.16.0" +version = "2.17.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, - {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] -dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["docs"] files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] @@ -236,81 +236,82 @@ files = [ [[package]] name = "coverage" -version = "7.6.10" +version = "7.6.12" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, - {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, - {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, - {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, - {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, - {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, - {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, - {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, - {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, - {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, - {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, - {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, - {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, - {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, - {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, - {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, - {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, - {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, - {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, - {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, - {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, - {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, - {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, - {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, - {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, - {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, - {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, - {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, - {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, - {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, - {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, - {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, - {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, + {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, + {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, + {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, + {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, + {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, + {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, + {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, + {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, + {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, + {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, + {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, + {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, + {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, + {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, + {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, + {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, + {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, + {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, + {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, + {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, + {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, + {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, + {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, + {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, + {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, + {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, + {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, + {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, + {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, + {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, + {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, + {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, + {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, ] [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\""] +toml = ["tomli"] [[package]] name = "cython" @@ -455,27 +456,27 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.6.1" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev", "docs"] markers = "python_version < \"3.10\"" files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -492,14 +493,14 @@ files = [ [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["docs"] files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -835,13 +836,13 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "snowballstemmer" @@ -1096,7 +1097,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1115,11 +1116,11 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [metadata] From 5915e5b417be7443e98e869f4fc9ba1ae68414d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 11:01:55 -1000 Subject: [PATCH 15/46] chore(pre-commit.ci): pre-commit autoupdate (#1549) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(pre-commit.ci): pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.0 → v0.11.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.0...v0.11.0) * chore(pre-commit.ci): auto fixes * chore: fix violations --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- build_ext.py | 2 +- src/zeroconf/_services/browser.py | 2 +- tests/__init__.py | 1 + tests/benchmarks/test_cache.py | 1 + tests/benchmarks/test_incoming.py | 1 + tests/benchmarks/test_outgoing.py | 1 + tests/benchmarks/test_send.py | 3 +- tests/benchmarks/test_txt_properties.py | 1 + tests/conftest.py | 5 ++- tests/services/test_browser.py | 21 ++++----- tests/services/test_info.py | 33 +++++++------- tests/test_asyncio.py | 59 +++++++++++++------------ tests/test_cache.py | 7 +-- tests/test_circular_imports.py | 2 +- tests/test_core.py | 7 +-- tests/test_dns.py | 1 + tests/test_engine.py | 5 ++- tests/test_handlers.py | 33 +++++++------- tests/test_protocol.py | 1 + tests/test_services.py | 1 + tests/test_updates.py | 3 +- tests/utils/test_asyncio.py | 9 ++-- tests/utils/test_name.py | 1 + tests/utils/test_net.py | 1 + 25 files changed, 112 insertions(+), 91 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a38eaca6..5d03fcde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.0 + rev: v0.11.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/build_ext.py b/build_ext.py index 7faa607f..ff088f83 100644 --- a/build_ext.py +++ b/build_ext.py @@ -53,7 +53,7 @@ def build_extensions(self) -> None: def build(setup_kwargs: Any) -> None: - if os.environ.get("SKIP_CYTHON", False): + if os.environ.get("SKIP_CYTHON"): return try: from Cython.Build import cythonize diff --git a/src/zeroconf/_services/browser.py b/src/zeroconf/_services/browser.py index 6bf3f0f4..ab8c050d 100644 --- a/src/zeroconf/_services/browser.py +++ b/src/zeroconf/_services/browser.py @@ -278,7 +278,7 @@ def generate_service_query( if not qu_question and question_history.suppresses(question, now_millis, known_answers): log.debug("Asking %s was suppressed by the question history", question) continue - if TYPE_CHECKING: # noqa: SIM108 + if TYPE_CHECKING: pointer_known_answers = cast(set[DNSPointer], known_answers) else: pointer_known_answers = known_answers diff --git a/tests/__init__.py b/tests/__init__.py index 3df09819..a70cca60 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -29,6 +29,7 @@ from unittest import mock import ifaddr + from zeroconf import DNSIncoming, DNSQuestion, DNSRecord, Zeroconf from zeroconf._history import QuestionHistory diff --git a/tests/benchmarks/test_cache.py b/tests/benchmarks/test_cache.py index e32abda0..7813f679 100644 --- a/tests/benchmarks/test_cache.py +++ b/tests/benchmarks/test_cache.py @@ -1,6 +1,7 @@ from __future__ import annotations from pytest_codspeed import BenchmarkFixture + from zeroconf import DNSCache, DNSPointer, current_time_millis from zeroconf.const import _CLASS_IN, _TYPE_PTR diff --git a/tests/benchmarks/test_incoming.py b/tests/benchmarks/test_incoming.py index 672e5c78..6d31e51e 100644 --- a/tests/benchmarks/test_incoming.py +++ b/tests/benchmarks/test_incoming.py @@ -5,6 +5,7 @@ import socket from pytest_codspeed import BenchmarkFixture + from zeroconf import ( DNSAddress, DNSIncoming, diff --git a/tests/benchmarks/test_outgoing.py b/tests/benchmarks/test_outgoing.py index cc2f3f42..a8db4d6f 100644 --- a/tests/benchmarks/test_outgoing.py +++ b/tests/benchmarks/test_outgoing.py @@ -3,6 +3,7 @@ from __future__ import annotations from pytest_codspeed import BenchmarkFixture + from zeroconf._protocol.outgoing import State from .helpers import generate_packets diff --git a/tests/benchmarks/test_send.py b/tests/benchmarks/test_send.py index d931b48b..596662a2 100644 --- a/tests/benchmarks/test_send.py +++ b/tests/benchmarks/test_send.py @@ -4,12 +4,13 @@ import pytest from pytest_codspeed import BenchmarkFixture + from zeroconf.asyncio import AsyncZeroconf from .helpers import generate_packets -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_sending_packets(benchmark: BenchmarkFixture) -> None: """Benchmark sending packets.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) diff --git a/tests/benchmarks/test_txt_properties.py b/tests/benchmarks/test_txt_properties.py index b7b0e767..72afa0b6 100644 --- a/tests/benchmarks/test_txt_properties.py +++ b/tests/benchmarks/test_txt_properties.py @@ -1,6 +1,7 @@ from __future__ import annotations from pytest_codspeed import BenchmarkFixture + from zeroconf import ServiceInfo info = ServiceInfo( diff --git a/tests/conftest.py b/tests/conftest.py index 3d891ec4..531c810b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from unittest.mock import patch import pytest + from zeroconf import _core, const from zeroconf._handlers import query_handler @@ -19,7 +20,7 @@ def verify_threads_ended(): assert not threads -@pytest.fixture() +@pytest.fixture def run_isolated(): """Change the mDNS port to run the test in isolation.""" with ( @@ -30,7 +31,7 @@ def run_isolated(): yield -@pytest.fixture() +@pytest.fixture def disable_duplicate_packet_suppression(): """Disable duplicate packet suppress. diff --git a/tests/services/test_browser.py b/tests/services/test_browser.py index f5237365..d57568f4 100644 --- a/tests/services/test_browser.py +++ b/tests/services/test_browser.py @@ -14,6 +14,7 @@ from unittest.mock import patch import pytest + import zeroconf as r import zeroconf._services.browser as _services_browser from zeroconf import ( @@ -555,7 +556,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name): zeroconf_browser.close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_asking_default_is_asking_qm_questions_after_the_first_qu(): """Verify the service browser's first questions are QU and refresh queries are QM.""" service_added = asyncio.Event() @@ -657,7 +658,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_ttl_refresh_cancelled_rescue_query(): """Verify seeing a name again cancels the rescue query.""" service_added = asyncio.Event() @@ -767,7 +768,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_asking_qm_questions(): """Verify explicitly asking QM questions.""" type_ = "_quservice._tcp.local." @@ -806,7 +807,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_asking_qu_questions(): """Verify the service browser can ask QU questions.""" type_ = "_quservice._tcp.local." @@ -898,7 +899,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name): browser.cancel() - assert len(updates) + assert updates assert len([isinstance(update, r.DNSPointer) and update.name == type_ for update in updates]) >= 1 zc.remove_listener(listener) @@ -1138,7 +1139,7 @@ def test_group_ptr_queries_with_known_answers(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_generate_service_query_suppress_duplicate_questions(): """Generate a service query for sending with zeroconf.send.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1191,7 +1192,7 @@ async def test_generate_service_query_suppress_duplicate_questions(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_query_scheduler(): delay = const._BROWSER_TIME types_ = {"_hap._tcp.local.", "_http._tcp.local."} @@ -1284,7 +1285,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_query_scheduler_rescue_records(): delay = const._BROWSER_TIME types_ = {"_hap._tcp.local.", "_http._tcp.local."} @@ -1579,7 +1580,7 @@ def test_scheduled_ptr_query_dunder_methods(): assert query75 >= other # type: ignore[operator] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_close_zeroconf_without_browser_before_start_up_queries(): """Test that we stop sending startup queries if zeroconf is closed out from under the browser.""" service_added = asyncio.Event() @@ -1647,7 +1648,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await browser.async_cancel() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_close_zeroconf_without_browser_after_start_up_queries(): """Test that we stop sending rescue queries if zeroconf is closed out from under the browser.""" service_added = asyncio.Event() diff --git a/tests/services/test_info.py b/tests/services/test_info.py index 8b912bea..3d4c5302 100644 --- a/tests/services/test_info.py +++ b/tests/services/test_info.py @@ -14,6 +14,7 @@ from unittest.mock import patch import pytest + import zeroconf as r from zeroconf import DNSAddress, RecordUpdate, const from zeroconf._services import info @@ -827,7 +828,7 @@ def test_scoped_addresses_from_cache(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_multiple_a_addresses_newest_address_first(): """Test that info.addresses returns the newest seen address first.""" type_ = "_http._tcp.local." @@ -847,7 +848,7 @@ async def test_multiple_a_addresses_newest_address_first(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_invalid_a_addresses(caplog): type_ = "_http._tcp.local." registration_name = f"multiarec.{type_}" @@ -1056,7 +1057,7 @@ def test_request_timeout(): assert (end_time - start_time) < 3000 + 1000 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_we_try_four_times_with_random_delay(): """Verify we try four times even with the random delay.""" type_ = "_typethatisnothere._tcp.local." @@ -1079,7 +1080,7 @@ def async_send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT): assert request_count == 4 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_release_wait_when_new_recorded_added(): """Test that async_request returns as soon as new matching records are added to the cache.""" type_ = "_http._tcp.local." @@ -1144,7 +1145,7 @@ async def test_release_wait_when_new_recorded_added(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_port_changes_are_seen(): """Test that port changes are seen by async_request.""" type_ = "_http._tcp.local." @@ -1227,7 +1228,7 @@ async def test_port_changes_are_seen(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_port_changes_are_seen_with_directed_request(): """Test that port changes are seen by async_request with a directed request.""" type_ = "_http._tcp.local." @@ -1310,7 +1311,7 @@ async def test_port_changes_are_seen_with_directed_request(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_ipv4_changes_are_seen(): """Test that ipv4 changes are seen by async_request.""" type_ = "_http._tcp.local." @@ -1398,7 +1399,7 @@ async def test_ipv4_changes_are_seen(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_ipv6_changes_are_seen(): """Test that ipv6 changes are seen by async_request.""" type_ = "_http._tcp.local." @@ -1493,7 +1494,7 @@ async def test_ipv6_changes_are_seen(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_bad_ip_addresses_ignored_in_cache(): """Test that bad ip address in the cache are ignored async_request.""" type_ = "_http._tcp.local." @@ -1547,7 +1548,7 @@ async def test_bad_ip_addresses_ignored_in_cache(): assert info.addresses_by_version(IPVersion.V4Only) == [b"\x7f\x00\x00\x01"] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_service_name_change_as_seen_has_ip_in_cache(): """Test that service name changes are seen by async_request when the ip is in the cache.""" type_ = "_http._tcp.local." @@ -1629,7 +1630,7 @@ async def test_service_name_change_as_seen_has_ip_in_cache(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_service_name_change_as_seen_ip_not_in_cache(): """Test that service name changes are seen by async_request when the ip is not in the cache.""" type_ = "_http._tcp.local." @@ -1711,7 +1712,7 @@ async def test_service_name_change_as_seen_ip_not_in_cache(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio @patch.object(info, "_LISTENER_TIME", 10000000) async def test_release_wait_when_new_recorded_added_concurrency(): """Test that concurrent async_request returns as soon as new matching records are added to the cache.""" @@ -1783,7 +1784,7 @@ async def test_release_wait_when_new_recorded_added_concurrency(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_service_info_nsec_records(): """Test we can generate nsec records from ServiceInfo.""" type_ = "_http._tcp.local." @@ -1798,7 +1799,7 @@ async def test_service_info_nsec_records(): assert nsec_record.rdtypes == [const._TYPE_A, const._TYPE_AAAA] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_address_resolver(): """Test that the address resolver works.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1822,7 +1823,7 @@ async def test_address_resolver(): assert resolver.addresses == [b"\x7f\x00\x00\x01"] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_address_resolver_ipv4(): """Test that the IPv4 address resolver works.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1846,7 +1847,7 @@ async def test_address_resolver_ipv4(): assert resolver.addresses == [b"\x7f\x00\x00\x01"] -@pytest.mark.asyncio() +@pytest.mark.asyncio @unittest.skipIf(not has_working_ipv6(), "Requires IPv6") @unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled") async def test_address_resolver_ipv6(): diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index e3102507..40ecf816 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -11,6 +11,7 @@ from unittest.mock import ANY, call, patch import pytest + import zeroconf._services.browser as _services_browser from zeroconf import ( DNSAddress, @@ -78,14 +79,14 @@ def verify_threads_ended(): assert not threads -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_basic_usage() -> None: """Test we can create and close the instance.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_close_twice() -> None: """Test we can close twice.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -93,7 +94,7 @@ async def test_async_close_twice() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_with_sync_passed_in() -> None: """Test we can create and close the instance when passing in a sync Zeroconf.""" zc = Zeroconf(interfaces=["127.0.0.1"]) @@ -102,7 +103,7 @@ async def test_async_with_sync_passed_in() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_with_sync_passed_in_closed_in_async() -> None: """Test caller closes the sync version in async.""" zc = Zeroconf(interfaces=["127.0.0.1"]) @@ -112,7 +113,7 @@ async def test_async_with_sync_passed_in_closed_in_async() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_sync_within_event_loop_executor() -> None: """Test sync version still works from an executor within an event loop.""" @@ -124,7 +125,7 @@ def sync_code(): await asyncio.get_event_loop().run_in_executor(None, sync_code) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_service_registration() -> None: """Test registering services broadcasts the registration by default.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -191,7 +192,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_service_registration_with_server_missing() -> None: """Test registering a service with the server not specified. @@ -258,7 +259,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_service_registration_same_server_different_ports() -> None: """Test registering services with the same server with different srv records.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -325,7 +326,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_service_registration_same_server_same_ports() -> None: """Test registering services with the same server with the exact same srv record.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -392,7 +393,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_service_registration_name_conflict() -> None: """Test registering services throws on name conflict.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -440,7 +441,7 @@ async def test_async_service_registration_name_conflict() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_service_registration_name_does_not_match_type() -> None: """Test registering services throws when the name does not match the type.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -466,7 +467,7 @@ async def test_async_service_registration_name_does_not_match_type() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_service_registration_name_strict_check() -> None: """Test registering services throws when the name does not comply.""" zc = Zeroconf(interfaces=["127.0.0.1"]) @@ -501,7 +502,7 @@ async def test_async_service_registration_name_strict_check() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_tasks() -> None: """Test awaiting broadcast tasks""" @@ -567,7 +568,7 @@ def update_service(self, zeroconf: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_wait_unblocks_on_update() -> None: """Test async_wait will unblock on update.""" @@ -603,7 +604,7 @@ async def test_async_wait_unblocks_on_update() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_service_info_async_request() -> None: """Test registering services broadcasts and query with AsyncServceInfo.async_request.""" if not has_working_ipv6() or os.environ.get("SKIP_IPV6"): @@ -712,7 +713,7 @@ async def test_service_info_async_request() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_service_browser() -> None: """Test AsyncServiceBrowser.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -772,7 +773,7 @@ def update_service(self, aiozc: Zeroconf, type: str, name: str) -> None: ] -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_context_manager() -> None: """Test using an async context manager.""" type_ = "_test10-sr-type._tcp.local." @@ -796,7 +797,7 @@ async def test_async_context_manager() -> None: assert aiosinfo is not None -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_service_browser_cancel_async_context_manager(): """Test we can cancel an AsyncServiceBrowser with it being used as an async context manager.""" @@ -822,7 +823,7 @@ class MyServiceListener(ServiceListener): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_unregister_all_services() -> None: """Test unregistering all services.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -881,7 +882,7 @@ async def test_async_unregister_all_services() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_zeroconf_service_types(): type_ = "_test-srvc-type._tcp.local." name = "xxxyyy" @@ -915,7 +916,7 @@ async def test_async_zeroconf_service_types(): await zeroconf_registrar.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_guard_against_running_serviceinfo_request_event_loop() -> None: """Test that running ServiceInfo.request from the event loop throws.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -926,7 +927,7 @@ async def test_guard_against_running_serviceinfo_request_event_loop() -> None: await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_service_browser_instantiation_generates_add_events_from_cache(): """Test that the ServiceBrowser will generate Add events with the existing cache when starting.""" @@ -975,7 +976,7 @@ def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-de await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_integration(): service_added = asyncio.Event() service_removed = asyncio.Event() @@ -1123,7 +1124,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT, v6_flow_scope=()): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_info_asking_default_is_asking_qm_questions_after_the_first_qu(): """Verify the service info first question is QU and subsequent ones are QM questions.""" type_ = "_quservice._tcp.local." @@ -1177,7 +1178,7 @@ def send(out, addr=const._MDNS_ADDR, port=const._MDNS_PORT): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_service_browser_ignores_unrelated_updates(): """Test that the ServiceBrowser ignores unrelated updates.""" @@ -1274,7 +1275,7 @@ def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-de await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_request_timeout(): """Test that the timeout does not throw an exception and finishes close to the actual timeout.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1288,7 +1289,7 @@ async def test_async_request_timeout(): assert (end_time - start_time) < 3000 + 1000 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_request_non_running_instance(): """Test that the async_request throws when zeroconf is not running.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1297,7 +1298,7 @@ async def test_async_request_non_running_instance(): await aiozc.async_get_service_info("_notfound.local.", "notthere._notfound.local.") -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_legacy_unicast_response(run_isolated): """Verify legacy unicast responses include questions and correct id.""" type_ = "_mservice._tcp.local." @@ -1338,7 +1339,7 @@ async def test_legacy_unicast_response(run_isolated): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_update_with_uppercase_names(run_isolated): """Test an ip update from a shelly which uses uppercase names.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) diff --git a/tests/test_cache.py b/tests/test_cache.py index 5bd6a869..9d55435d 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -7,6 +7,7 @@ from heapq import heapify, heappop import pytest + import zeroconf as r from zeroconf import const @@ -363,7 +364,7 @@ def test_async_get_unique_returns_newest_record(): assert record is record2 -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_cache_heap_cleanup() -> None: """Test that the heap gets cleaned up when there are many old expirations.""" cache = r.DNSCache() @@ -415,7 +416,7 @@ async def test_cache_heap_cleanup() -> None: assert not cache.async_entries_with_name(name), cache._expire_heap -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_cache_heap_multi_name_cleanup() -> None: """Test cleanup with multiple names.""" cache = r.DNSCache() @@ -451,7 +452,7 @@ async def test_cache_heap_multi_name_cleanup() -> None: assert not cache.async_entries_with_name(name), cache._expire_heap -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_cache_heap_pops_order() -> None: """Test cache heap is popped in order.""" cache = r.DNSCache() diff --git a/tests/test_circular_imports.py b/tests/test_circular_imports.py index 79d58ae1..74ed1f12 100644 --- a/tests/test_circular_imports.py +++ b/tests/test_circular_imports.py @@ -8,7 +8,7 @@ import pytest -@pytest.mark.asyncio() +@pytest.mark.asyncio @pytest.mark.timeout(30) # cloud can take > 9s @pytest.mark.parametrize( "module", diff --git a/tests/test_core.py b/tests/test_core.py index 1dfb9806..fcfdf424 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -15,6 +15,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest + import zeroconf as r from zeroconf import NotRunningException, Zeroconf, const, current_time_millis from zeroconf._listener import AsyncListener, _WrappedTransport @@ -664,7 +665,7 @@ def test_tc_bit_defers_last_response_missing(): zc.close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_open_close_twice_from_async() -> None: """Test we can close twice from a coroutine when using Zeroconf. @@ -684,7 +685,7 @@ async def test_open_close_twice_from_async() -> None: await asyncio.sleep(0) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_multiple_sync_instances_stared_from_async_close(): """Test we can shutdown multiple sync instances from async.""" @@ -740,7 +741,7 @@ def _background_register(): bgthread.join() -@pytest.mark.asyncio() +@pytest.mark.asyncio @patch("zeroconf._core._STARTUP_TIMEOUT", 0) @patch("zeroconf._core.AsyncEngine._async_setup", new_callable=AsyncMock) async def test_event_loop_blocked(mock_start): diff --git a/tests/test_dns.py b/tests/test_dns.py index 5928338c..246c8dcf 100644 --- a/tests/test_dns.py +++ b/tests/test_dns.py @@ -8,6 +8,7 @@ import unittest.mock import pytest + import zeroconf as r from zeroconf import DNSHinfo, DNSText, ServiceInfo, const, current_time_millis from zeroconf._dns import DNSRRSet diff --git a/tests/test_engine.py b/tests/test_engine.py index 5f244804..b7a94c86 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -8,6 +8,7 @@ from unittest.mock import patch import pytest + import zeroconf as r from zeroconf import _engine, const from zeroconf.asyncio import AsyncZeroconf @@ -29,7 +30,7 @@ def teardown_module(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_reaper(): with patch.object(_engine, "_CACHE_CLEANUP_INTERVAL", 0.01): aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -64,7 +65,7 @@ async def test_reaper(): assert record_with_1s_ttl not in entries -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_reaper_aborts_when_done(): """Ensure cache cleanup stops when zeroconf is done.""" with patch.object(_engine, "_CACHE_CLEANUP_INTERVAL", 0.01): diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 58f8ecb1..ffa4ff88 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -13,6 +13,7 @@ from unittest.mock import patch import pytest + import zeroconf as r from zeroconf import ServiceInfo, Zeroconf, const, current_time_millis from zeroconf._handlers.multicast_outgoing_queue import ( @@ -492,7 +493,7 @@ def test_unicast_response(): zc.close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_probe_answered_immediately(): """Verify probes are responded to immediately.""" # instantiate a zeroconf instance @@ -543,7 +544,7 @@ async def test_probe_answered_immediately(): zc.close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_probe_answered_immediately_with_uppercase_name(): """Verify probes are responded to immediately with an uppercase name.""" # instantiate a zeroconf instance @@ -1091,7 +1092,7 @@ def test_enumeration_query_with_no_registered_services(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_qu_response_only_sends_additionals_if_sends_answer(): """Test that a QU response does not send additionals unless it sends the answer as well.""" # instantiate a zeroconf instance @@ -1257,7 +1258,7 @@ async def test_qu_response_only_sends_additionals_if_sends_answer(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_cache_flush_bit(): """Test that the cache flush bit sets the TTL to one for matching records.""" # instantiate a zeroconf instance @@ -1360,7 +1361,7 @@ async def test_cache_flush_bit(): # This test uses asyncio because it needs to access the cache directly # which is not threadsafe -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_record_update_manager_add_listener_callsback_existing_records(): """Test that the RecordUpdateManager will callback existing records.""" @@ -1414,7 +1415,7 @@ def async_update_records(self, zc: Zeroconf, now: float, records: list[r.RecordU await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_questions_query_handler_populates_the_question_history_from_qm_questions(): aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) zc = aiozc.zeroconf @@ -1460,7 +1461,7 @@ async def test_questions_query_handler_populates_the_question_history_from_qm_qu await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_questions_query_handler_does_not_put_qu_questions_in_history(): aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) zc = aiozc.zeroconf @@ -1503,7 +1504,7 @@ async def test_questions_query_handler_does_not_put_qu_questions_in_history(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_guard_against_low_ptr_ttl(): """Ensure we enforce a min for PTR record ttls to avoid excessive refresh queries from ServiceBrowsers. @@ -1554,7 +1555,7 @@ async def test_guard_against_low_ptr_ttl(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_duplicate_goodbye_answers_in_packet(): """Ensure we do not throw an exception when there are duplicate goodbye records in a packet.""" aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) @@ -1586,7 +1587,7 @@ async def test_duplicate_goodbye_answers_in_packet(): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_response_aggregation_timings(run_isolated): """Verify multicast responses are aggregated.""" type_ = "_mservice._tcp.local." @@ -1708,7 +1709,7 @@ async def test_response_aggregation_timings(run_isolated): await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_response_aggregation_timings_multiple(run_isolated, disable_duplicate_packet_suppression): """Verify multicast responses that are aggregated do not take longer than 620ms to send. @@ -1790,7 +1791,7 @@ async def test_response_aggregation_timings_multiple(run_isolated, disable_dupli assert info2.dns_pointer() in incoming.answers() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_response_aggregation_random_delay(): """Verify the random delay for outgoing multicast will coalesce into a single group @@ -1898,7 +1899,7 @@ async def test_response_aggregation_random_delay(): assert info5.dns_pointer() in outgoing_queue.queue[1].answers -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_future_answers_are_removed_on_send(): """Verify any future answers scheduled to be sent are removed when we send.""" type_ = "_mservice._tcp.local." @@ -1962,7 +1963,7 @@ async def test_future_answers_are_removed_on_send(): assert info2.dns_pointer() in outgoing_queue.queue[0].answers -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_add_listener_warns_when_not_using_record_update_listener(caplog): """Log when a listener is added that is not using RecordUpdateListener as a base class.""" @@ -1987,7 +1988,7 @@ def async_update_records(self, zc: Zeroconf, now: float, records: list[r.RecordU await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_updates_iteration_safe(): """Ensure we can safely iterate over the async_updates.""" @@ -2031,7 +2032,7 @@ def async_update_records(self, zc: Zeroconf, now: float, records: list[r.RecordU await aiozc.async_close() -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_updates_complete_iteration_safe(): """Ensure we can safely iterate over the async_updates_complete.""" diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 78fed0e0..08d7e600 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -11,6 +11,7 @@ from typing import cast import pytest + import zeroconf as r from zeroconf import DNSHinfo, DNSIncoming, DNSText, const, current_time_millis diff --git a/tests/test_services.py b/tests/test_services.py index d192c652..7d7c3fc7 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -11,6 +11,7 @@ from typing import Any import pytest + import zeroconf as r from zeroconf import Zeroconf from zeroconf._services.info import ServiceInfo diff --git a/tests/test_updates.py b/tests/test_updates.py index 376082e7..ec1296f7 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -7,6 +7,7 @@ import time import pytest + import zeroconf as r from zeroconf import Zeroconf, const from zeroconf._record_update import RecordUpdate @@ -80,7 +81,7 @@ def on_service_state_change(zeroconf, service_type, state_change, name): browser.cancel() - assert len(updates) + assert updates assert len([isinstance(update, r.DNSPointer) and update.name == type_ for update in updates]) >= 1 zc.remove_listener(listener) diff --git a/tests/utils/test_asyncio.py b/tests/utils/test_asyncio.py index 4d2ee0ec..7989a82c 100644 --- a/tests/utils/test_asyncio.py +++ b/tests/utils/test_asyncio.py @@ -10,13 +10,14 @@ from unittest.mock import patch import pytest + from zeroconf import EventLoopBlocked from zeroconf._engine import _CLOSE_TIMEOUT from zeroconf._utils import asyncio as aioutils from zeroconf.const import _LOADED_SYSTEM_TIMEOUT -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_async_get_all_tasks() -> None: """Test we can get all tasks in the event loop. @@ -32,7 +33,7 @@ async def test_async_get_all_tasks() -> None: await aioutils._async_get_all_tasks(loop) -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_get_running_loop_from_async() -> None: """Test we can get the event loop.""" assert isinstance(aioutils.get_running_loop(), asyncio.AbstractEventLoop) @@ -43,7 +44,7 @@ def test_get_running_loop_no_loop() -> None: assert aioutils.get_running_loop() is None -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_wait_future_or_timeout_times_out() -> None: """Test wait_future_or_timeout will timeout.""" loop = asyncio.get_running_loop() @@ -117,7 +118,7 @@ def test_cumulative_timeouts_less_than_close_plus_buffer(): ) < 1 + _CLOSE_TIMEOUT + _LOADED_SYSTEM_TIMEOUT -@pytest.mark.asyncio() +@pytest.mark.asyncio async def test_run_coro_with_timeout() -> None: """Test running a coroutine with a timeout raises EventLoopBlocked.""" loop = asyncio.get_event_loop() diff --git a/tests/utils/test_name.py b/tests/utils/test_name.py index 3b70c7d4..1feb7713 100644 --- a/tests/utils/test_name.py +++ b/tests/utils/test_name.py @@ -5,6 +5,7 @@ import socket import pytest + from zeroconf import BadTypeInNameException from zeroconf._services.info import ServiceInfo, instance_name_from_service_info from zeroconf._utils import name as nameutils diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index 17ff6196..f763b655 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -10,6 +10,7 @@ import ifaddr import pytest + import zeroconf as r from zeroconf._utils import net as netutils From 33bf0e4ef2e3468b7c9df1a53709ea0d9e35f32c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 19:13:09 -1000 Subject: [PATCH 16/46] chore(deps-dev): bump setuptools from 76.0.0 to 77.0.3 (#1550) --- poetry.lock | 32 ++++++++++++++++---------------- pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index 8c4713e8..ec600ab6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -25,7 +25,7 @@ files = [ ] [package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] +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 = "certifi" @@ -311,7 +311,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cython" @@ -471,12 +471,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -825,24 +825,24 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "76.0.0" +version = "77.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "setuptools-76.0.0-py3-none-any.whl", hash = "sha256:199466a166ff664970d0ee145839f5582cb9bca7a0a3a2e795b6a9cb2308e9c6"}, - {file = "setuptools-76.0.0.tar.gz", hash = "sha256:43b4ee60e10b0d0ee98ad11918e114c70701bc6051662a9a675a0496c1a158f4"}, + {file = "setuptools-77.0.3-py3-none-any.whl", hash = "sha256:67122e78221da5cf550ddd04cf8742c8fe12094483749a792d56cd669d6cf58c"}, + {file = "setuptools-77.0.3.tar.gz", hash = "sha256:583b361c8da8de57403743e756609670de6fb2345920e36dc5c2d914c319c945"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "snowballstemmer" @@ -1097,7 +1097,7 @@ files = [ ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1116,14 +1116,14 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "f866b539caf6f0140faba8aa19f4e1fae2013a48fc3346747f104dfe62ef290b" +content-hash = "6185b531e93844e1dbd399c197c9376fc7d2efa2cbff6bdb7585484dc6dbfb86" diff --git a/pyproject.toml b/pyproject.toml index 9c92f362..198605f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ pytest = ">=7.2,<9.0" pytest-cov = ">=4,<7" pytest-asyncio = ">=0.20.3,<0.26.0" cython = "^3.0.5" -setuptools = ">=65.6.3,<77.0.0" +setuptools = ">=65.6.3,<78.0.0" pytest-timeout = "^2.1.0" pytest-codspeed = "^3.1.0" From f05b0127774ac69db5a6a7ba02ecdf57e46b4f9b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Mar 2025 10:02:29 -1000 Subject: [PATCH 17/46] chore(pre-commit.ci): pre-commit autoupdate (#1551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.0 → v0.11.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.0...v0.11.2) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5d03fcde..cf19bfa2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.0 + rev: v0.11.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 741b6ef3639334cb558b16ce568b33bf308e6688 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:10:38 -1000 Subject: [PATCH 18/46] chore(deps-dev): bump setuptools from 77.0.3 to 78.1.0 (#1552) --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index ec600ab6..b16f7e87 100644 --- a/poetry.lock +++ b/poetry.lock @@ -825,14 +825,14 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "77.0.3" +version = "78.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "setuptools-77.0.3-py3-none-any.whl", hash = "sha256:67122e78221da5cf550ddd04cf8742c8fe12094483749a792d56cd669d6cf58c"}, - {file = "setuptools-77.0.3.tar.gz", hash = "sha256:583b361c8da8de57403743e756609670de6fb2345920e36dc5c2d914c319c945"}, + {file = "setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8"}, + {file = "setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54"}, ] [package.extras] @@ -1126,4 +1126,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "6185b531e93844e1dbd399c197c9376fc7d2efa2cbff6bdb7585484dc6dbfb86" +content-hash = "94e87573380ca1c563c3af5fbd6363399a4c333c9f697c1b2191835714d1ffaa" diff --git a/pyproject.toml b/pyproject.toml index 198605f9..608b849b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ pytest = ">=7.2,<9.0" pytest-cov = ">=4,<7" pytest-asyncio = ">=0.20.3,<0.26.0" cython = "^3.0.5" -setuptools = ">=65.6.3,<78.0.0" +setuptools = ">=65.6.3,<79.0.0" pytest-timeout = "^2.1.0" pytest-codspeed = "^3.1.0" From 0fe79d7a53789719225509bce8c124950aed6237 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:21:52 -1000 Subject: [PATCH 19/46] chore(deps-dev): bump pytest-asyncio from 0.25.3 to 0.26.0 (#1553) Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.25.3 to 0.26.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.25.3...v0.26.0) --- updated-dependencies: - dependency-name: pytest-asyncio dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 9 +++++---- pyproject.toml | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index b16f7e87..845974d6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -697,18 +697,19 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.25.3" +version = "0.26.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"}, - {file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"}, + {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, + {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, ] [package.dependencies] pytest = ">=8.2,<9" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.10\""} [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] @@ -1126,4 +1127,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "94e87573380ca1c563c3af5fbd6363399a4c333c9f697c1b2191835714d1ffaa" +content-hash = "e3c96e694e9c149b96323081d51675d7a9d5ad8243f4338ff149e643a65417cb" diff --git a/pyproject.toml b/pyproject.toml index 608b849b..569fe977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ ifaddr = ">=0.1.7" [tool.poetry.group.dev.dependencies] pytest = ">=7.2,<9.0" pytest-cov = ">=4,<7" -pytest-asyncio = ">=0.20.3,<0.26.0" +pytest-asyncio = ">=0.20.3,<0.27.0" cython = "^3.0.5" setuptools = ">=65.6.3,<79.0.0" pytest-timeout = "^2.1.0" From 34043735e13ba254cb5e31e03b6d672447ba6e57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Mar 2025 19:29:47 -1000 Subject: [PATCH 20/46] chore: pin GitHub actions to SHAs to mitigate supply chain attacks (#1554) * chore: pin GitHub actions to SHAs to mitigate supply chain attacks * chore: pin GitHub actions to SHAs to mitigate supply chain attacks --- .github/workflows/ci.yml | 52 +++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fb9b06f..b61e5e45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,11 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 with: python-version: "3.12" - - uses: pre-commit/action@v3.0.1 + - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 # Make sure commit messages follow the conventional commits convention: # https://www.conventionalcommits.org @@ -26,10 +26,10 @@ jobs: name: Lint Commit Messages runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 - - uses: wagoid/commitlint-github-action@v6 + - uses: wagoid/commitlint-github-action@b948419dd99f3fd78a6548d48f94e3df7f6bf3ed # v6 test: strategy: @@ -65,11 +65,11 @@ jobs: python-version: "pypy-3.10" runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Install poetry run: pipx install poetry - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 with: python-version: ${{ matrix.python-version }} cache: "poetry" @@ -87,25 +87,25 @@ jobs: - name: Test with Pytest run: poetry run pytest --durations=20 --timeout=60 -v --cov=zeroconf --cov-branch --cov-report xml --cov-report html --cov-report term-missing tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5 with: token: ${{ secrets.CODECOV_TOKEN }} benchmark: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup Python 3.13 - uses: actions/setup-python@v5 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 with: python-version: 3.13 - - uses: snok/install-poetry@v1.4.1 + - uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 - name: Install Dependencies run: | REQUIRE_CYTHON=1 poetry install --only=main,dev shell: bash - name: Run benchmarks - uses: CodSpeedHQ/action@v3 + uses: CodSpeedHQ/action@0010eb0ca6e89b80c88e8edaaa07cfe5f3e6664d # v3 with: token: ${{ secrets.CODSPEED_TOKEN }} run: poetry run pytest --no-cov -vvvvv --codspeed tests/benchmarks @@ -128,32 +128,32 @@ jobs: newest_release_tag: ${{ steps.release.outputs.tag }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 ref: ${{ github.head_ref || github.ref_name }} # Do a dry run of PSR - name: Test release - uses: python-semantic-release/python-semantic-release@v9.21.0 + uses: python-semantic-release/python-semantic-release@26bb37cfab71a5a372e3db0f48a6eac57519a4a6 # v9.21.0 if: github.ref_name != 'master' with: root_options: --noop # On main branch: actual PSR + upload to PyPI & GitHub - name: Release - uses: python-semantic-release/python-semantic-release@v9.21.0 + uses: python-semantic-release/python-semantic-release@26bb37cfab71a5a372e3db0f48a6eac57519a4a6 # v9.21.0 id: release if: github.ref_name == 'master' with: github_token: ${{ secrets.GITHUB_TOKEN }} - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 if: steps.release.outputs.released == 'true' - name: Publish package distributions to GitHub Releases - uses: python-semantic-release/upload-to-gh-release@main + uses: python-semantic-release/upload-to-gh-release@0a92b5d7ebfc15a84f9801ebd1bf706343d43711 # main if: steps.release.outputs.released == 'true' with: github_token: ${{ secrets.GITHUB_TOKEN }} @@ -228,18 +228,18 @@ jobs: pyver: cp313 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: fetch-depth: 0 ref: "master" # Used to host cibuildwheel - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 with: python-version: "3.12" - name: Set up QEMU if: ${{ matrix.qemu }} - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 with: platforms: all # This should be temporary @@ -262,20 +262,20 @@ jobs: echo "CIBW_BUILD=${{ matrix.pyver }}*" >> $GITHUB_ENV fi - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: ref: ${{ needs.release.outputs.newest_release_tag }} fetch-depth: 0 - name: Build wheels ${{ matrix.musl }} (${{ matrix.qemu }}) - uses: pypa/cibuildwheel@v2.23.0 + uses: pypa/cibuildwheel@6cccd09a31908ffd175b012fb8bf4e1dbda3bc6c # v2.23.0 # to supply options, put them in 'env', like: env: CIBW_SKIP: cp36-* cp37-* pp36-* pp37-* pp38-* cp38-* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} CIBW_BEFORE_ALL_LINUX: apt install -y gcc || yum install -y gcc || apk add gcc REQUIRE_CYTHON: 1 - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: path: ./wheelhouse/*.whl name: wheels-${{ matrix.os }}-${{ matrix.musl }}-${{ matrix.qemu }}-${{ matrix.pyver }} @@ -288,7 +288,7 @@ jobs: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 with: # unpacks default artifact into dist/ # if `name: artifact` is omitted, the action will create extra parent dir @@ -297,6 +297,4 @@ jobs: merge-multiple: true - uses: - pypa/gh-action-pypi-publish@v1.12.4 - - # To test: repository_url: https://test.pypi.org/legacy/ + pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 From 54eb3830dc794d78b8419153f8233713e1dff840 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Mar 2025 19:19:24 -1000 Subject: [PATCH 21/46] chore(ci): bump pypa/cibuildwheel from 2.23.0 to 2.23.2 in the github-actions group (#1556) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b61e5e45..ffe20f82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -268,7 +268,7 @@ jobs: fetch-depth: 0 - name: Build wheels ${{ matrix.musl }} (${{ matrix.qemu }}) - uses: pypa/cibuildwheel@6cccd09a31908ffd175b012fb8bf4e1dbda3bc6c # v2.23.0 + uses: pypa/cibuildwheel@d04cacbc9866d432033b1d09142936e6a0e2121a # v2.23.2 # to supply options, put them in 'env', like: env: CIBW_SKIP: cp36-* cp37-* pp36-* pp37-* pp38-* cp38-* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} From b757ddf98d7d04c366281a4281a449c5c2cb897d Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 1 Apr 2025 21:11:16 +0200 Subject: [PATCH 22/46] fix: create listener socket with specific IP version (#1557) * fix: create listener socket with specific IP version Create listener sockets when using unicast with specific IP version as well, just like in `new_respond_socket()`. * chore(tests): add unit test for socket creation with unicast addressing --- src/zeroconf/_utils/net.py | 5 +++-- tests/utils/test_net.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/zeroconf/_utils/net.py b/src/zeroconf/_utils/net.py index c2312e01..b4f3ef77 100644 --- a/src/zeroconf/_utils/net.py +++ b/src/zeroconf/_utils/net.py @@ -421,11 +421,12 @@ def create_sockets( else: respond_socket = None else: + is_v6 = isinstance(i, tuple) respond_socket = new_socket( port=0, - ip_version=ip_version, + ip_version=IPVersion.V6Only if is_v6 else IPVersion.V4Only, apple_p2p=apple_p2p, - bind_addr=i[0] if isinstance(i, tuple) else (i,), + bind_addr=cast(tuple[tuple[str, int, int], int], i)[0] if is_v6 else (cast(str, i),), ) if respond_socket is not None: diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index f763b655..ad8648de 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -298,3 +298,35 @@ def test_new_respond_socket_new_socket_returns_none(): """Test new_respond_socket returns None if new_socket returns None.""" with patch.object(netutils, "new_socket", return_value=None): assert netutils.new_respond_socket(("0.0.0.0", 0)) is None # type: ignore[arg-type] + + +def test_create_sockets(): + """Test create_sockets with unicast and IPv4.""" + + with ( + patch("zeroconf._utils.net.new_socket") as mock_new_socket, + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + ): + mock_socket = Mock(spec=socket.socket) + mock_new_socket.return_value = mock_socket + + listen_socket, respond_sockets = r.create_sockets( + interfaces=r.InterfaceChoice.All, unicast=True, ip_version=r.IPVersion.All + ) + + assert listen_socket is None + mock_new_socket.assert_any_call( + port=0, + ip_version=r.IPVersion.V6Only, + apple_p2p=False, + bind_addr=("2001:db8::", 1, 1), + ) + mock_new_socket.assert_any_call( + port=0, + ip_version=r.IPVersion.V4Only, + apple_p2p=False, + bind_addr=("192.168.1.5",), + ) From 94620b084addfff6d7b73dd5d7ed69c1a213415e Mon Sep 17 00:00:00 2001 From: semantic-release Date: Tue, 1 Apr 2025 19:20:01 +0000 Subject: [PATCH 23/46] 0.146.2 Automatically generated by python-semantic-release --- CHANGELOG.md | 16 ++++++++++++++++ pyproject.toml | 2 +- src/zeroconf/__init__.py | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f28b0022..0ffa0f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # CHANGELOG +## v0.146.2 (2025-04-01) + +### Bug Fixes + +- Create listener socket with specific IP version + ([#1557](https://github.com/python-zeroconf/python-zeroconf/pull/1557), + [`b757ddf`](https://github.com/python-zeroconf/python-zeroconf/commit/b757ddf98d7d04c366281a4281a449c5c2cb897d)) + +* fix: create listener socket with specific IP version + +Create listener sockets when using unicast with specific IP version as well, just like in + `new_respond_socket()`. + +* chore(tests): add unit test for socket creation with unicast addressing + + ## v0.146.1 (2025-03-05) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index 569fe977..b2850113 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zeroconf" -version = "0.146.1" +version = "0.146.2" description = "A pure python implementation of multicast DNS service discovery" authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"] license = "LGPL-2.1-or-later" diff --git a/src/zeroconf/__init__.py b/src/zeroconf/__init__.py index b915b8d7..01496e22 100644 --- a/src/zeroconf/__init__.py +++ b/src/zeroconf/__init__.py @@ -88,7 +88,7 @@ __author__ = "Paul Scott-Murphy, William McBrine" __maintainer__ = "Jakub Stasiak " -__version__ = "0.146.1" +__version__ = "0.146.2" __license__ = "LGPL" From bd643a227bc4d6a949d558850ad1431bc2940d74 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 2 Apr 2025 19:41:16 +0200 Subject: [PATCH 24/46] fix: correctly override question type flag for requests (#1558) * fix: correctly override question type flag for requests Currently even when setting the explicit question type flag, the implementation ignores it for subsequent queries. This commit ensures that all queries respect the explicit question type flag. * chore(tests): add test for explicit question type flag Add unit test to validate that the explicit question type flag is set correctly in outgoing requests. --- src/zeroconf/_services/info.py | 2 +- tests/services/test_info.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/zeroconf/_services/info.py b/src/zeroconf/_services/info.py index 9cd8df16..fff9e125 100644 --- a/src/zeroconf/_services/info.py +++ b/src/zeroconf/_services/info.py @@ -859,7 +859,7 @@ async def async_request( if last <= now: return False if next_ <= now: - this_question_type = question_type or QU_QUESTION if first_request else QM_QUESTION + this_question_type = question_type or (QU_QUESTION if first_request else QM_QUESTION) out = self._generate_request_query(zc, now, this_question_type) first_request = False if out.questions: diff --git a/tests/services/test_info.py b/tests/services/test_info.py index 3d4c5302..660b56d2 100644 --- a/tests/services/test_info.py +++ b/tests/services/test_info.py @@ -17,6 +17,7 @@ import zeroconf as r from zeroconf import DNSAddress, RecordUpdate, const +from zeroconf._protocol.outgoing import DNSOutgoing from zeroconf._services import info from zeroconf._services.info import ServiceInfo from zeroconf._utils.net import IPVersion @@ -1871,3 +1872,23 @@ async def test_address_resolver_ipv6(): aiozc.zeroconf.async_send(outgoing) assert await resolve_task assert resolver.ip_addresses_by_version(IPVersion.All) == [ip_address("fe80::52e:c2f2:bc5f:e9c6")] + + +@pytest.mark.asyncio +async def test_unicast_flag_if_requested() -> None: + """Verify we try four times even with the random delay.""" + type_ = "_typethatisnothere._tcp.local." + aiozc = AsyncZeroconf(interfaces=["127.0.0.1"]) + + def async_send(out: DNSOutgoing, addr: str | None = None, port: int = const._MDNS_PORT) -> None: + """Sends an outgoing packet.""" + for question in out.questions: + assert question.unicast + + # patch the zeroconf send + with patch.object(aiozc.zeroconf, "async_send", async_send): + await aiozc.async_get_service_info( + f"willnotbefound.{type_}", type_, question_type=r.DNSQuestionType.QU + ) + + await aiozc.async_close() From 16c257c0ca2772a024c6e9920df2375436bfc73c Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 2 Apr 2025 17:51:12 +0000 Subject: [PATCH 25/46] 0.146.3 Automatically generated by python-semantic-release --- CHANGELOG.md | 19 +++++++++++++++++++ pyproject.toml | 2 +- src/zeroconf/__init__.py | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ffa0f63..ccb6bdd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # CHANGELOG +## v0.146.3 (2025-04-02) + +### Bug Fixes + +- Correctly override question type flag for requests + ([#1558](https://github.com/python-zeroconf/python-zeroconf/pull/1558), + [`bd643a2`](https://github.com/python-zeroconf/python-zeroconf/commit/bd643a227bc4d6a949d558850ad1431bc2940d74)) + +* fix: correctly override question type flag for requests + +Currently even when setting the explicit question type flag, the implementation ignores it for + subsequent queries. This commit ensures that all queries respect the explicit question type flag. + +* chore(tests): add test for explicit question type flag + +Add unit test to validate that the explicit question type flag is set correctly in outgoing + requests. + + ## v0.146.2 (2025-04-01) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index b2850113..7e21a38f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zeroconf" -version = "0.146.2" +version = "0.146.3" description = "A pure python implementation of multicast DNS service discovery" authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"] license = "LGPL-2.1-or-later" diff --git a/src/zeroconf/__init__.py b/src/zeroconf/__init__.py index 01496e22..c266c318 100644 --- a/src/zeroconf/__init__.py +++ b/src/zeroconf/__init__.py @@ -88,7 +88,7 @@ __author__ = "Paul Scott-Murphy, William McBrine" __maintainer__ = "Jakub Stasiak " -__version__ = "0.146.2" +__version__ = "0.146.3" __license__ = "LGPL" From b044d2af9c3d357a49c010380f49471e92684f7e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:33:10 -1000 Subject: [PATCH 26/46] chore(pre-commit.ci): pre-commit autoupdate (#1555) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(pre-commit.ci): pre-commit autoupdate updates: - [github.com/PyCQA/flake8: 7.1.2 → 7.2.0](https://github.com/PyCQA/flake8/compare/7.1.2...7.2.0) * chore: remove useless nonlocal statements --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- tests/services/test_browser.py | 13 ------------- tests/test_asyncio.py | 9 --------- tests/test_updates.py | 1 - tests/utils/test_net.py | 1 - 5 files changed, 1 insertion(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf19bfa2..985d54b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: hooks: - id: codespell - repo: https://github.com/PyCQA/flake8 - rev: 7.1.2 + rev: 7.2.0 hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy diff --git a/tests/services/test_browser.py b/tests/services/test_browser.py index d57568f4..e9135bb6 100644 --- a/tests/services/test_browser.py +++ b/tests/services/test_browser.py @@ -866,7 +866,6 @@ class LegacyRecordUpdateListener(r.RecordUpdateListener): """A RecordUpdateListener that does not implement update_records.""" def update_record(self, zc: Zeroconf, now: float, record: r.DNSRecord) -> None: - nonlocal updates updates.append(record) listener = LegacyRecordUpdateListener() @@ -923,7 +922,6 @@ def test_service_browser_is_aware_of_port_changes(): # dummy service callback def on_service_state_change(zeroconf, service_type, state_change, name): """Dummy callback.""" - nonlocal callbacks if name == registration_name: callbacks.append((service_type, state_change, name)) @@ -985,17 +983,14 @@ def test_service_browser_listeners_update_service(): class MyServiceListener(r.ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) @@ -1050,12 +1045,10 @@ def test_service_browser_listeners_no_update_service(): class MyServiceListener(r.ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) @@ -1374,17 +1367,14 @@ def test_service_browser_matching(): class MyServiceListener(r.ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) @@ -1465,17 +1455,14 @@ def test_service_browser_expire_callbacks(): class MyServiceListener(r.ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) diff --git a/tests/test_asyncio.py b/tests/test_asyncio.py index 40ecf816..b6e124aa 100644 --- a/tests/test_asyncio.py +++ b/tests/test_asyncio.py @@ -940,17 +940,14 @@ async def test_service_browser_instantiation_generates_add_events_from_cache(): class MyServiceListener(ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) @@ -1191,17 +1188,14 @@ async def test_service_browser_ignores_unrelated_updates(): class MyServiceListener(ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks if name == registration_name: callbacks.append(("update", type_, name)) @@ -1349,15 +1343,12 @@ async def test_update_with_uppercase_names(run_isolated): class MyServiceListener(ServiceListener): def add_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks callbacks.append(("add", type_, name)) def remove_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks callbacks.append(("remove", type_, name)) def update_service(self, zc, type_, name) -> None: # type: ignore[no-untyped-def] - nonlocal callbacks callbacks.append(("update", type_, name)) listener = MyServiceListener() diff --git a/tests/test_updates.py b/tests/test_updates.py index ec1296f7..d8b16083 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -48,7 +48,6 @@ class LegacyRecordUpdateListener(r.RecordUpdateListener): """A RecordUpdateListener that does not implement update_records.""" def update_record(self, zc: Zeroconf, now: float, record: r.DNSRecord) -> None: - nonlocal updates updates.append(record) listener = LegacyRecordUpdateListener() diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index ad8648de..6bdafb37 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -127,7 +127,6 @@ def test_disable_ipv6_only_or_raise(): errors_logged = [] def _log_error(*args): - nonlocal errors_logged errors_logged.append(args) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) From f89a90e610094b721ec536f9b0ddee41592838fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Apr 2025 18:43:34 -1000 Subject: [PATCH 27/46] chore(deps-dev): bump pytest-cov from 6.0.0 to 6.1.1 (#1560) --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 845974d6..367374ed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -750,14 +750,14 @@ test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"] [[package]] name = "pytest-cov" -version = "6.0.0" +version = "6.1.1" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, + {file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"}, + {file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"}, ] [package.dependencies] From 389a8a2724d3f6d328fee0bef38d7addc29d19c4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 08:47:19 -1000 Subject: [PATCH 28/46] chore(pre-commit.ci): pre-commit autoupdate (#1561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/commitizen-tools/commitizen: v4.4.1 → v4.5.0](https://github.com/commitizen-tools/commitizen/compare/v4.4.1...v4.5.0) - [github.com/astral-sh/ruff-pre-commit: v0.11.2 → v0.11.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.2...v0.11.4) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 985d54b6..1faee010 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ ci: repos: - repo: https://github.com/commitizen-tools/commitizen - rev: v4.4.1 + rev: v4.5.0 hooks: - id: commitizen stages: [commit-msg] @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.2 + rev: v0.11.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 83594887521507cf77bfc0a397becabaaab287c2 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 14 Apr 2025 11:33:33 +0200 Subject: [PATCH 29/46] fix: avoid loading adapter list twice (#1564) --- src/zeroconf/_utils/net.py | 40 +++++++++++++++++++++++++++++--------- tests/utils/test_net.py | 40 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/src/zeroconf/_utils/net.py b/src/zeroconf/_utils/net.py index b4f3ef77..e687ab60 100644 --- a/src/zeroconf/_utils/net.py +++ b/src/zeroconf/_utils/net.py @@ -28,7 +28,8 @@ import socket import struct import sys -from collections.abc import Sequence +import warnings +from collections.abc import Iterable, Sequence from typing import Any, Union, cast import ifaddr @@ -73,19 +74,39 @@ def _encode_address(address: str) -> bytes: return socket.inet_pton(address_family, address) -def get_all_addresses() -> list[str]: - return list({addr.ip for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv4}) # type: ignore[misc] +def get_all_addresses_ipv4(adapters: Iterable[ifaddr.Adapter]) -> list[str]: + return list({addr.ip for iface in adapters for addr in iface.ips if addr.is_IPv4}) # type: ignore[misc] -def get_all_addresses_v6() -> list[tuple[tuple[str, int, int], int]]: +def get_all_addresses_ipv6(adapters: Iterable[ifaddr.Adapter]) -> list[tuple[tuple[str, int, int], int]]: # IPv6 multicast uses positive indexes for interfaces # TODO: What about multi-address interfaces? return list( - {(addr.ip, iface.index) for iface in ifaddr.get_adapters() for addr in iface.ips if addr.is_IPv6} # type: ignore[misc] + {(addr.ip, iface.index) for iface in adapters for addr in iface.ips if addr.is_IPv6} # type: ignore[misc] + ) + + +def get_all_addresses() -> list[str]: + warnings.warn( + "get_all_addresses is deprecated, and will be removed in a future version. Use ifaddr" + "directly instead to get a list of adapters.", + DeprecationWarning, + stacklevel=2, + ) + return get_all_addresses_ipv4(ifaddr.get_adapters()) + + +def get_all_addresses_v6() -> list[tuple[tuple[str, int, int], int]]: + warnings.warn( + "get_all_addresses_v6 is deprecated, and will be removed in a future version. Use ifaddr" + "directly instead to get a list of adapters.", + DeprecationWarning, + stacklevel=2, ) + return get_all_addresses_ipv6(ifaddr.get_adapters()) -def ip6_to_address_and_index(adapters: list[ifaddr.Adapter], ip: str) -> tuple[tuple[str, int, int], int]: +def ip6_to_address_and_index(adapters: Iterable[ifaddr.Adapter], ip: str) -> tuple[tuple[str, int, int], int]: if "%" in ip: ip = ip[: ip.index("%")] # Strip scope_id. ipaddr = ipaddress.ip_address(ip) @@ -102,7 +123,7 @@ def ip6_to_address_and_index(adapters: list[ifaddr.Adapter], ip: str) -> tuple[t raise RuntimeError(f"No adapter found for IP address {ip}") -def interface_index_to_ip6_address(adapters: list[ifaddr.Adapter], index: int) -> tuple[str, int, int]: +def interface_index_to_ip6_address(adapters: Iterable[ifaddr.Adapter], index: int) -> tuple[str, int, int]: for adapter in adapters: if adapter.index == index: for adapter_ip in adapter.ips: @@ -152,10 +173,11 @@ def normalize_interface_choice( if ip_version != IPVersion.V6Only: result.append("0.0.0.0") elif choice is InterfaceChoice.All: + adapters = ifaddr.get_adapters() if ip_version != IPVersion.V4Only: - result.extend(get_all_addresses_v6()) + result.extend(get_all_addresses_ipv6(adapters)) if ip_version != IPVersion.V6Only: - result.extend(get_all_addresses()) + result.extend(get_all_addresses_ipv4(adapters)) if not result: raise RuntimeError( f"No interfaces to listen on, check that any interfaces have IP version {ip_version}" diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index 6bdafb37..eff2befd 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -6,12 +6,14 @@ import socket import sys import unittest +import warnings from unittest.mock import MagicMock, Mock, patch import ifaddr import pytest import zeroconf as r +from zeroconf import get_all_addresses, get_all_addresses_v6 from zeroconf._utils import net as netutils @@ -35,6 +37,40 @@ def _generate_mock_adapters(): return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] +def test_get_all_addresses() -> None: + """Test public get_all_addresses API.""" + with ( + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + warnings.catch_warnings(record=True) as warned, + ): + addresses = get_all_addresses() + assert isinstance(addresses, list) + assert len(addresses) == 3 + assert len(warned) == 1 + first_warning = warned[0] + assert "get_all_addresses is deprecated" in str(first_warning.message) + + +def test_get_all_addresses_v6() -> None: + """Test public get_all_addresses_v6 API.""" + with ( + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ), + warnings.catch_warnings(record=True) as warned, + ): + addresses = get_all_addresses_v6() + assert isinstance(addresses, list) + assert len(addresses) == 1 + assert len(warned) == 1 + first_warning = warned[0] + assert "get_all_addresses_v6 is deprecated" in str(first_warning.message) + + def test_ip6_to_address_and_index(): """Test we can extract from mocked adapters.""" adapters = _generate_mock_adapters() @@ -84,8 +120,8 @@ def test_ip6_addresses_to_indexes(): def test_normalize_interface_choice_errors(): """Test we generate exception on invalid input.""" with ( - patch("zeroconf._utils.net.get_all_addresses", return_value=[]), - patch("zeroconf._utils.net.get_all_addresses_v6", return_value=[]), + patch("zeroconf._utils.net.get_all_addresses_ipv4", return_value=[]), + patch("zeroconf._utils.net.get_all_addresses_ipv6", return_value=[]), pytest.raises(RuntimeError), ): netutils.normalize_interface_choice(r.InterfaceChoice.All) From 79016f12055272e700d0f1aca38e9bcd2f89aa3e Mon Sep 17 00:00:00 2001 From: semantic-release Date: Mon, 14 Apr 2025 09:45:11 +0000 Subject: [PATCH 30/46] 0.146.4 Automatically generated by python-semantic-release --- CHANGELOG.md | 9 +++++++++ pyproject.toml | 2 +- src/zeroconf/__init__.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccb6bdd7..3c56284d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ # CHANGELOG +## v0.146.4 (2025-04-14) + +### Bug Fixes + +- Avoid loading adapter list twice + ([#1564](https://github.com/python-zeroconf/python-zeroconf/pull/1564), + [`8359488`](https://github.com/python-zeroconf/python-zeroconf/commit/83594887521507cf77bfc0a397becabaaab287c2)) + + ## v0.146.3 (2025-04-02) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index 7e21a38f..e4de325f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zeroconf" -version = "0.146.3" +version = "0.146.4" description = "A pure python implementation of multicast DNS service discovery" authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"] license = "LGPL-2.1-or-later" diff --git a/src/zeroconf/__init__.py b/src/zeroconf/__init__.py index c266c318..89b622c2 100644 --- a/src/zeroconf/__init__.py +++ b/src/zeroconf/__init__.py @@ -88,7 +88,7 @@ __author__ = "Paul Scott-Murphy, William McBrine" __maintainer__ = "Jakub Stasiak " -__version__ = "0.146.3" +__version__ = "0.146.4" __license__ = "LGPL" From 77a6717e0f2185ff8da090b6442404bb3c8a9919 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 14 Apr 2025 11:54:26 +0200 Subject: [PATCH 31/46] chore(test): fix resource warnings in test_net.py (#1565) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/utils/test_net.py | 175 ++++++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 79 deletions(-) diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index eff2befd..2ed0c6f2 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -165,8 +165,8 @@ def test_disable_ipv6_only_or_raise(): def _log_error(*args): errors_logged.append(args) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) with ( + socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock, pytest.raises(OSError), patch.object(netutils.log, "error", _log_error), patch("socket.socket.setsockopt", side_effect=OSError), @@ -182,19 +182,21 @@ def _log_error(*args): @pytest.mark.skipif(not hasattr(socket, "SO_REUSEPORT"), reason="System does not have SO_REUSEPORT") def test_set_so_reuseport_if_available_is_present(): """Test that setting socket.SO_REUSEPORT only OSError errno.ENOPROTOOPT is trapped.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError): - netutils.set_so_reuseport_if_available(sock) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError): + netutils.set_so_reuseport_if_available(sock) - with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)): - netutils.set_so_reuseport_if_available(sock) + with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)): + netutils.set_so_reuseport_if_available(sock) @pytest.mark.skipif(hasattr(socket, "SO_REUSEPORT"), reason="System has SO_REUSEPORT") def test_set_so_reuseport_if_available_not_present(): """Test that we do not try to set SO_REUSEPORT if it is not present.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - with patch("socket.socket.setsockopt", side_effect=OSError): + with ( + socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock, + patch("socket.socket.setsockopt", side_effect=OSError), + ): netutils.set_so_reuseport_if_available(sock) @@ -202,80 +204,95 @@ def test_set_mdns_port_socket_options_for_ip_version(): """Test OSError with errno with EINVAL and bind address ''. from setsockopt IP_MULTICAST_TTL does not raise.""" - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - - # Should raise on EPERM always - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only) - - # Should raise on EINVAL always when bind address is not '' - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("127.0.0.1",), r.IPVersion.V4Only) - - # Should not raise on EINVAL when bind address is '' - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + # Should raise on EPERM always + with ( + pytest.raises(OSError), + patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)), + ): + netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only) + + # Should raise on EINVAL always when bind address is not '' + with ( + pytest.raises(OSError), + patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)), + ): + netutils.set_mdns_port_socket_options_for_ip_version(sock, ("127.0.0.1",), r.IPVersion.V4Only) + + # Should not raise on EINVAL when bind address is '' + with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): + netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only) def test_add_multicast_member(caplog: pytest.LogCaptureFixture) -> None: - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - interface = "127.0.0.1" - - # EPERM should always raise - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)): - netutils.add_multicast_member(sock, interface) - - # EADDRINUSE should return False - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EADDRINUSE, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # EADDRNOTAVAIL should return False - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EADDRNOTAVAIL, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # EINVAL should return False - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # ENOPROTOOPT should return False - with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # ENODEV should raise for ipv4 - with pytest.raises(OSError), patch("socket.socket.setsockopt", side_effect=OSError(errno.ENODEV, None)): - assert netutils.add_multicast_member(sock, interface) is False - - # ENODEV should return False for ipv6 - with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENODEV, None)): - assert netutils.add_multicast_member(sock, ("2001:db8::", 1, 1)) is False # type: ignore[arg-type] - - # No IPv6 support should return False for IPv6 - with patch("socket.inet_pton", side_effect=OSError()): - assert netutils.add_multicast_member(sock, ("2001:db8::", 1, 1)) is False # type: ignore[arg-type] - - # No error should return True - with patch("socket.socket.setsockopt"): - assert netutils.add_multicast_member(sock, interface) is True - - # Ran out of IGMP memberships is forgiving and logs about igmp_max_memberships on linux - caplog.clear() - with ( - patch.object(sys, "platform", "linux"), - patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOBUFS, "No buffer space available")), - ): - assert netutils.add_multicast_member(sock, interface) is False - assert "No buffer space available" in caplog.text - assert "net.ipv4.igmp_max_memberships" in caplog.text - - # Ran out of IGMP memberships is forgiving and logs - caplog.clear() - with ( - patch.object(sys, "platform", "darwin"), - patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOBUFS, "No buffer space available")), - ): - assert netutils.add_multicast_member(sock, interface) is False - assert "No buffer space available" in caplog.text - assert "net.ipv4.igmp_max_memberships" not in caplog.text + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + interface = "127.0.0.1" + + # EPERM should always raise + with ( + pytest.raises(OSError), + patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)), + ): + netutils.add_multicast_member(sock, interface) + + # EADDRINUSE should return False + with patch("socket.socket.setsockopt", side_effect=OSError(errno.EADDRINUSE, None)): + assert netutils.add_multicast_member(sock, interface) is False + + # EADDRNOTAVAIL should return False + with patch("socket.socket.setsockopt", side_effect=OSError(errno.EADDRNOTAVAIL, None)): + assert netutils.add_multicast_member(sock, interface) is False + + # EINVAL should return False + with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): + assert netutils.add_multicast_member(sock, interface) is False + + # ENOPROTOOPT should return False + with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENOPROTOOPT, None)): + assert netutils.add_multicast_member(sock, interface) is False + + # ENODEV should raise for ipv4 + with ( + pytest.raises(OSError), + patch("socket.socket.setsockopt", side_effect=OSError(errno.ENODEV, None)), + ): + assert netutils.add_multicast_member(sock, interface) is False + + # ENODEV should return False for ipv6 + with patch("socket.socket.setsockopt", side_effect=OSError(errno.ENODEV, None)): + assert netutils.add_multicast_member(sock, ("2001:db8::", 1, 1)) is False # type: ignore[arg-type] + + # No IPv6 support should return False for IPv6 + with patch("socket.inet_pton", side_effect=OSError()): + assert netutils.add_multicast_member(sock, ("2001:db8::", 1, 1)) is False # type: ignore[arg-type] + + # No error should return True + with patch("socket.socket.setsockopt"): + assert netutils.add_multicast_member(sock, interface) is True + + # Ran out of IGMP memberships is forgiving and logs about igmp_max_memberships on linux + caplog.clear() + with ( + patch.object(sys, "platform", "linux"), + patch( + "socket.socket.setsockopt", side_effect=OSError(errno.ENOBUFS, "No buffer space available") + ), + ): + assert netutils.add_multicast_member(sock, interface) is False + assert "No buffer space available" in caplog.text + assert "net.ipv4.igmp_max_memberships" in caplog.text + + # Ran out of IGMP memberships is forgiving and logs + caplog.clear() + with ( + patch.object(sys, "platform", "darwin"), + patch( + "socket.socket.setsockopt", side_effect=OSError(errno.ENOBUFS, "No buffer space available") + ), + ): + assert netutils.add_multicast_member(sock, interface) is False + assert "No buffer space available" in caplog.text + assert "net.ipv4.igmp_max_memberships" not in caplog.text def test_bind_raises_skips_address(): From cb2f5b15403df8d4f8abb6f7dcac6d867756fb9a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Apr 2025 07:42:27 -1000 Subject: [PATCH 32/46] chore(pre-commit.ci): pre-commit autoupdate (#1566) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1faee010..19efa1c7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ ci: repos: - repo: https://github.com/commitizen-tools/commitizen - rev: v4.5.0 + rev: v4.6.0 hooks: - id: commitizen stages: [commit-msg] @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.11.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From cc0f8350c30c82409b1a9bfecb19ff9b3368d6a7 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 14 Apr 2025 23:10:36 +0200 Subject: [PATCH 33/46] fix: address non-working socket configuration (#1563) Co-authored-by: J. Nick Koston --- src/zeroconf/_utils/net.py | 97 ++++++++++++++++++++++--------------- tests/test_core.py | 19 ++++++-- tests/utils/test_net.py | 99 +++++++++++++++++++++++++++++--------- 3 files changed, 148 insertions(+), 67 deletions(-) diff --git a/src/zeroconf/_utils/net.py b/src/zeroconf/_utils/net.py index e687ab60..e67edf78 100644 --- a/src/zeroconf/_utils/net.py +++ b/src/zeroconf/_utils/net.py @@ -168,8 +168,17 @@ def normalize_interface_choice( result: list[str | tuple[tuple[str, int, int], int]] = [] if choice is InterfaceChoice.Default: if ip_version != IPVersion.V4Only: - # IPv6 multicast uses interface 0 to mean the default - result.append((("", 0, 0), 0)) + # IPv6 multicast uses interface 0 to mean the default. However, + # the default interface can't be used for outgoing IPv6 multicast + # requests. In a way, interface choice default isn't really working + # with IPv6. Inform the user accordingly. + message = ( + "IPv6 multicast requests can't be sent using default interface. " + "Use V4Only, InterfaceChoice.All or an explicit list of interfaces." + ) + log.error(message) + warnings.warn(message, DeprecationWarning, stacklevel=2) + result.append((("::", 0, 0), 0)) if ip_version != IPVersion.V6Only: result.append("0.0.0.0") elif choice is InterfaceChoice.All: @@ -220,28 +229,33 @@ def set_so_reuseport_if_available(s: socket.socket) -> None: raise -def set_mdns_port_socket_options_for_ip_version( +def set_respond_socket_multicast_options( s: socket.socket, - bind_addr: tuple[str] | tuple[str, int, int], ip_version: IPVersion, ) -> None: - """Set ttl/hops and loop for mdns port.""" - if ip_version != IPVersion.V6Only: - ttl = struct.pack(b"B", 255) - loop = struct.pack(b"B", 1) + """Set ttl/hops and loop for mDNS respond socket.""" + if ip_version == IPVersion.V4Only: # OpenBSD needs the ttl and loop values for the IP_MULTICAST_TTL and # IP_MULTICAST_LOOP socket options as an unsigned char. - try: - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) - s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop) - except OSError as e: - if bind_addr[0] != "" or get_errno(e) != errno.EINVAL: # Fails to set on MacOS - raise - - if ip_version != IPVersion.V4Only: + ttl = struct.pack(b"B", 255) + loop = struct.pack(b"B", 1) + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) + s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, loop) + elif ip_version == IPVersion.V6Only: # However, char doesn't work here (at least on Linux) s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 255) s.setsockopt(_IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, True) + else: + # A shared sender socket is not really possible, especially with link-local + # multicast addresses (ff02::/16), the kernel needs to know which interface + # to use for routing. + # + # It seems that macOS even refuses to take IPv4 socket options if this is an + # AF_INET6 socket. + # + # In theory we could reconfigure the socket on each send, but that is not + # really practical for Python Zerconf. + raise RuntimeError("Dual-stack responder socket not supported") def new_socket( @@ -266,14 +280,12 @@ def new_socket( s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) set_so_reuseport_if_available(s) - if port == _MDNS_PORT: - set_mdns_port_socket_options_for_ip_version(s, bind_addr, ip_version) - if apple_p2p: # SO_RECV_ANYIF = 0x1104 # https://opensource.apple.com/source/xnu/xnu-4570.41.2/bsd/sys/socket.h s.setsockopt(socket.SOL_SOCKET, 0x1104, 1) + # Bind expects (address, port) for AF_INET and (address, port, flowinfo, scope_id) for AF_INET6 bind_tup = (bind_addr[0], port, *bind_addr[1:]) try: s.bind(bind_tup) @@ -392,15 +404,27 @@ def add_multicast_member( def new_respond_socket( interface: str | tuple[tuple[str, int, int], int], apple_p2p: bool = False, + unicast: bool = False, ) -> socket.socket | None: + """Create interface specific socket for responding to multicast queries.""" is_v6 = isinstance(interface, tuple) + + # For response sockets: + # - Bind explicitly to the interface address + # - Use ephemeral ports if in unicast mode + # - Create socket according to the interface IP type (IPv4 or IPv6) respond_socket = new_socket( + bind_addr=cast(tuple[tuple[str, int, int], int], interface)[0] if is_v6 else (cast(str, interface),), + port=0 if unicast else _MDNS_PORT, ip_version=(IPVersion.V6Only if is_v6 else IPVersion.V4Only), apple_p2p=apple_p2p, - bind_addr=cast(tuple[tuple[str, int, int], int], interface)[0] if is_v6 else (cast(str, interface),), ) + if unicast: + return respond_socket + if not respond_socket: return None + log.debug("Configuring socket %s with multicast interface %s", respond_socket, interface) if is_v6: iface_bin = struct.pack("@I", cast(int, interface[1])) @@ -411,6 +435,7 @@ def new_respond_socket( socket.IP_MULTICAST_IF, socket.inet_aton(cast(str, interface)), ) + set_respond_socket_multicast_options(respond_socket, IPVersion.V6Only if is_v6 else IPVersion.V4Only) return respond_socket @@ -423,33 +448,27 @@ def create_sockets( if unicast: listen_socket = None else: - listen_socket = new_socket(ip_version=ip_version, apple_p2p=apple_p2p, bind_addr=("",)) + listen_socket = new_socket(bind_addr=("",), ip_version=ip_version, apple_p2p=apple_p2p) normalized_interfaces = normalize_interface_choice(interfaces, ip_version) - # If we are using InterfaceChoice.Default we can use + # If we are using InterfaceChoice.Default with only IPv4 or only IPv6, we can use # a single socket to listen and respond. - if not unicast and interfaces is InterfaceChoice.Default: - for i in normalized_interfaces: - add_multicast_member(cast(socket.socket, listen_socket), i) + if not unicast and interfaces is InterfaceChoice.Default and ip_version != IPVersion.All: + for interface in normalized_interfaces: + add_multicast_member(cast(socket.socket, listen_socket), interface) + # Sent responder socket options to the dual-use listen socket + set_respond_socket_multicast_options(cast(socket.socket, listen_socket), ip_version) return listen_socket, [cast(socket.socket, listen_socket)] respond_sockets = [] - for i in normalized_interfaces: - if not unicast: - if add_multicast_member(cast(socket.socket, listen_socket), i): - respond_socket = new_respond_socket(i, apple_p2p=apple_p2p) - else: - respond_socket = None - else: - is_v6 = isinstance(i, tuple) - respond_socket = new_socket( - port=0, - ip_version=IPVersion.V6Only if is_v6 else IPVersion.V4Only, - apple_p2p=apple_p2p, - bind_addr=cast(tuple[tuple[str, int, int], int], i)[0] if is_v6 else (cast(str, i),), - ) + for interface in normalized_interfaces: + # Only create response socket if unicast or becoming multicast member was successful + if not unicast and not add_multicast_member(cast(socket.socket, listen_socket), interface): + continue + + respond_socket = new_respond_socket(interface, apple_p2p=apple_p2p, unicast=unicast) if respond_socket is not None: respond_sockets.append(respond_socket) diff --git a/tests/test_core.py b/tests/test_core.py index fcfdf424..8c53d207 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -11,6 +11,7 @@ import time import unittest import unittest.mock +import warnings from typing import cast from unittest.mock import AsyncMock, Mock, patch @@ -87,16 +88,26 @@ def test_close_multiple_times(self): def test_launch_and_close_v4_v6(self): rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.All) rv.close() - rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.All) - rv.close() + with warnings.catch_warnings(record=True) as warned: + rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.All) + rv.close() + first_warning = warned[0] + assert "IPv6 multicast requests can't be sent using default interface" in str( + first_warning.message + ) @unittest.skipIf(not has_working_ipv6(), "Requires IPv6") @unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled") def test_launch_and_close_v6_only(self): rv = r.Zeroconf(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.V6Only) rv.close() - rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.V6Only) - rv.close() + with warnings.catch_warnings(record=True) as warned: + rv = r.Zeroconf(interfaces=r.InterfaceChoice.Default, ip_version=r.IPVersion.V6Only) + rv.close() + first_warning = warned[0] + assert "IPv6 multicast requests can't be sent using default interface" in str( + first_warning.message + ) @unittest.skipIf(sys.platform == "darwin", reason="apple_p2p failure path not testable on mac") def test_launch_and_close_apple_p2p_not_mac(self): diff --git a/tests/utils/test_net.py b/tests/utils/test_net.py index 2ed0c6f2..7de10661 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -7,7 +7,7 @@ import sys import unittest import warnings -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, call, patch import ifaddr import pytest @@ -20,11 +20,11 @@ def _generate_mock_adapters(): mock_lo0 = Mock(spec=ifaddr.Adapter) mock_lo0.nice_name = "lo0" - mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")] + mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0"), ifaddr.IP(("::1", 0, 0), 128, "lo")] mock_lo0.index = 0 mock_eth0 = Mock(spec=ifaddr.Adapter) mock_eth0.nice_name = "eth0" - mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] + mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0"), ifaddr.IP(("fd00:db8::", 1, 1), 8, "eth0")] mock_eth0.index = 1 mock_eth1 = Mock(spec=ifaddr.Adapter) mock_eth1.nice_name = "eth1" @@ -65,7 +65,7 @@ def test_get_all_addresses_v6() -> None: ): addresses = get_all_addresses_v6() assert isinstance(addresses, list) - assert len(addresses) == 1 + assert len(addresses) == 3 assert len(warned) == 1 first_warning = warned[0] assert "get_all_addresses_v6 is deprecated" in str(first_warning.message) @@ -200,28 +200,20 @@ def test_set_so_reuseport_if_available_not_present(): netutils.set_so_reuseport_if_available(sock) -def test_set_mdns_port_socket_options_for_ip_version(): +def test_set_respond_socket_multicast_options(): """Test OSError with errno with EINVAL and bind address ''. from setsockopt IP_MULTICAST_TTL does not raise.""" - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: - # Should raise on EPERM always - with ( - pytest.raises(OSError), - patch("socket.socket.setsockopt", side_effect=OSError(errno.EPERM, None)), - ): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only) - - # Should raise on EINVAL always when bind address is not '' - with ( - pytest.raises(OSError), - patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)), - ): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("127.0.0.1",), r.IPVersion.V4Only) + # Should raise on EINVAL always + with ( + socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock, + pytest.raises(OSError), + patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)), + ): + netutils.set_respond_socket_multicast_options(sock, r.IPVersion.V4Only) - # Should not raise on EINVAL when bind address is '' - with patch("socket.socket.setsockopt", side_effect=OSError(errno.EINVAL, None)): - netutils.set_mdns_port_socket_options_for_ip_version(sock, ("",), r.IPVersion.V4Only) + with pytest.raises(RuntimeError): + netutils.set_respond_socket_multicast_options(sock, r.IPVersion.All) def test_add_multicast_member(caplog: pytest.LogCaptureFixture) -> None: @@ -352,8 +344,8 @@ def test_new_respond_socket_new_socket_returns_none(): assert netutils.new_respond_socket(("0.0.0.0", 0)) is None # type: ignore[arg-type] -def test_create_sockets(): - """Test create_sockets with unicast and IPv4.""" +def test_create_sockets_interfaces_all_unicast(): + """Test create_sockets with unicast.""" with ( patch("zeroconf._utils.net.new_socket") as mock_new_socket, @@ -382,3 +374,62 @@ def test_create_sockets(): apple_p2p=False, bind_addr=("192.168.1.5",), ) + + +def test_create_sockets_interfaces_all() -> None: + """Test create_sockets with all interfaces. + + Tests if a responder socket is created for every successful multicast + join. + """ + adapters = _generate_mock_adapters() + + # Additional IPv6 addresses usually fail to add membership + failure_interface = ("fd00:db8::", 1, 1) + + expected_calls = [] + for adapter in adapters: + for ip in adapter.ips: + if ip.ip == failure_interface: + continue + + if ip.is_IPv4: + bind_addr = (ip.ip,) + ip_version = r.IPVersion.V4Only + else: + bind_addr = ip.ip + ip_version = r.IPVersion.V6Only + + expected_calls.append( + call( + port=5353, + ip_version=ip_version, + apple_p2p=False, + bind_addr=bind_addr, + ) + ) + + def _patched_add_multicast_member(sock, interface): + return interface[0] != failure_interface + + with ( + patch("zeroconf._utils.net.new_socket") as mock_new_socket, + patch( + "zeroconf._utils.net.ifaddr.get_adapters", + return_value=adapters, + ), + patch("zeroconf._utils.net.add_multicast_member", side_effect=_patched_add_multicast_member), + ): + mock_socket = Mock(spec=socket.socket) + mock_new_socket.return_value = mock_socket + + r.create_sockets(interfaces=r.InterfaceChoice.All, ip_version=r.IPVersion.All) + + def call_to_tuple(c): + return (c.args, tuple(sorted(c.kwargs.items()))) + + # Exclude first new_socket call as this is the listen socket + actual_calls_set = {call_to_tuple(c) for c in mock_new_socket.call_args_list[1:]} + expected_calls_set = {call_to_tuple(c) for c in expected_calls} + + assert actual_calls_set == expected_calls_set From d2517387dccf8b55b71bbbc62919ded55c8359d2 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Mon, 14 Apr 2025 21:20:26 +0000 Subject: [PATCH 34/46] 0.146.5 Automatically generated by python-semantic-release --- CHANGELOG.md | 11 +++++++++++ pyproject.toml | 2 +- src/zeroconf/__init__.py | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c56284d..6d107aa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # CHANGELOG +## v0.146.5 (2025-04-14) + +### Bug Fixes + +- Address non-working socket configuration + ([#1563](https://github.com/python-zeroconf/python-zeroconf/pull/1563), + [`cc0f835`](https://github.com/python-zeroconf/python-zeroconf/commit/cc0f8350c30c82409b1a9bfecb19ff9b3368d6a7)) + +Co-authored-by: J. Nick Koston + + ## v0.146.4 (2025-04-14) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index e4de325f..189d9ddc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zeroconf" -version = "0.146.4" +version = "0.146.5" description = "A pure python implementation of multicast DNS service discovery" authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"] license = "LGPL-2.1-or-later" diff --git a/src/zeroconf/__init__.py b/src/zeroconf/__init__.py index 89b622c2..2449e835 100644 --- a/src/zeroconf/__init__.py +++ b/src/zeroconf/__init__.py @@ -88,7 +88,7 @@ __author__ = "Paul Scott-Murphy, William McBrine" __maintainer__ = "Jakub Stasiak " -__version__ = "0.146.4" +__version__ = "0.146.5" __license__ = "LGPL" From a11abc45fe2d9ebc5574092f9d4b3048ff3833fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 08:37:59 -1000 Subject: [PATCH 35/46] chore(pre-commit.ci): pre-commit autoupdate (#1570) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 19efa1c7..cc85aa90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.5 + rev: v0.11.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From b6d5aa36444cb30c87a17903021f041b4dbbe252 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Apr 2025 08:38:12 -1000 Subject: [PATCH 36/46] chore(deps-dev): bump setuptools from 78.1.0 to 79.0.0 (#1569) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 367374ed..6fa6edce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -826,14 +826,14 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "78.1.0" +version = "79.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8"}, - {file = "setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54"}, + {file = "setuptools-79.0.0-py3-none-any.whl", hash = "sha256:b9ab3a104bedb292323f53797b00864e10e434a3ab3906813a7169e4745b912a"}, + {file = "setuptools-79.0.0.tar.gz", hash = "sha256:9828422e7541213b0aacb6e10bbf9dd8febeaa45a48570e09b6d100e063fc9f9"}, ] [package.extras] @@ -1127,4 +1127,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "e3c96e694e9c149b96323081d51675d7a9d5ad8243f4338ff149e643a65417cb" +content-hash = "bcb9007a7aedbd388c0e4a757d21ccb2443fe58d07e8bc57493ee4d5f54eb998" diff --git a/pyproject.toml b/pyproject.toml index 189d9ddc..96c0aec0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ pytest = ">=7.2,<9.0" pytest-cov = ">=4,<7" pytest-asyncio = ">=0.20.3,<0.27.0" cython = "^3.0.5" -setuptools = ">=65.6.3,<79.0.0" +setuptools = ">=65.6.3,<80.0.0" pytest-timeout = "^2.1.0" pytest-codspeed = "^3.1.0" From 2874924c27d822fd6eaea12126e071b60effb6fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 27 Apr 2025 23:31:18 -0500 Subject: [PATCH 37/46] chore(deps-dev): bump setuptools from 79.0.0 to 80.0.0 (#1571) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6fa6edce..13e12b6e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -826,14 +826,14 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "79.0.0" +version = "80.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "setuptools-79.0.0-py3-none-any.whl", hash = "sha256:b9ab3a104bedb292323f53797b00864e10e434a3ab3906813a7169e4745b912a"}, - {file = "setuptools-79.0.0.tar.gz", hash = "sha256:9828422e7541213b0aacb6e10bbf9dd8febeaa45a48570e09b6d100e063fc9f9"}, + {file = "setuptools-80.0.0-py3-none-any.whl", hash = "sha256:a38f898dcd6e5380f4da4381a87ec90bd0a7eec23d204a5552e80ee3cab6bd27"}, + {file = "setuptools-80.0.0.tar.gz", hash = "sha256:c40a5b3729d58dd749c0f08f1a07d134fb8a0a3d7f87dc33e7c5e1f762138650"}, ] [package.extras] @@ -1127,4 +1127,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "bcb9007a7aedbd388c0e4a757d21ccb2443fe58d07e8bc57493ee4d5f54eb998" +content-hash = "972988da838067a7f2d12b8212ce54ba946cb38a4f63576a520dd1ed40ac3e9b" diff --git a/pyproject.toml b/pyproject.toml index 96c0aec0..a1390502 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ pytest = ">=7.2,<9.0" pytest-cov = ">=4,<7" pytest-asyncio = ">=0.20.3,<0.27.0" cython = "^3.0.5" -setuptools = ">=65.6.3,<80.0.0" +setuptools = ">=65.6.3,<81.0.0" pytest-timeout = "^2.1.0" pytest-codspeed = "^3.1.0" From cb54a65cd1b9a80bf0c19c3e274adf20703cd783 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 12:46:38 -0400 Subject: [PATCH 38/46] chore(pre-commit.ci): pre-commit autoupdate (#1572) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc85aa90..8ad48a33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: pyupgrade args: [--py39-plus] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.6 + rev: v0.11.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From f5c15e9bc412936a6fc943771ea0d66cba73e050 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 09:49:23 +0200 Subject: [PATCH 39/46] chore(ci): bump the github-actions group with 4 updates (#1573) --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffe20f82..5e8d1ef0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 @@ -69,7 +69,7 @@ jobs: - name: Install poetry run: pipx install poetry - name: Set up Python - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ matrix.python-version }} cache: "poetry" @@ -87,7 +87,7 @@ jobs: - name: Test with Pytest run: poetry run pytest --durations=20 --timeout=60 -v --cov=zeroconf --cov-branch --cov-report xml --cov-report html --cov-report term-missing tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5 + uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -96,7 +96,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Setup Python 3.13 - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: 3.13 - uses: snok/install-poetry@76e04a911780d5b312d89783f7b1cd627778900a # v1.4.1 @@ -234,7 +234,7 @@ jobs: ref: "master" # Used to host cibuildwheel - name: Set up Python - uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.12" - name: Set up QEMU @@ -268,7 +268,7 @@ jobs: fetch-depth: 0 - name: Build wheels ${{ matrix.musl }} (${{ matrix.qemu }}) - uses: pypa/cibuildwheel@d04cacbc9866d432033b1d09142936e6a0e2121a # v2.23.2 + uses: pypa/cibuildwheel@faf86a6ed7efa889faf6996aa23820831055001a # v2.23.3 # to supply options, put them in 'env', like: env: CIBW_SKIP: cp36-* cp37-* pp36-* pp37-* pp38-* cp38-* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }} @@ -288,7 +288,7 @@ jobs: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: # unpacks default artifact into dist/ # if `name: artifact` is omitted, the action will create extra parent dir From 02eef34ca5df803b05ff337a9103d7994458988d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sat, 3 May 2025 16:50:43 +0200 Subject: [PATCH 40/46] chore: some Cython 3.1.0rc1 build failures (#1574) --- src/zeroconf/_listener.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zeroconf/_listener.pxd b/src/zeroconf/_listener.pxd index 20084b47..4cbc5d00 100644 --- a/src/zeroconf/_listener.pxd +++ b/src/zeroconf/_listener.pxd @@ -50,7 +50,7 @@ cdef class AsyncListener: cpdef _respond_query( self, - object msg, + DNSIncoming msg, object addr, object port, object transport, From 66b673cb768eaa15581ea60a8de590382806937c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 10:03:16 -0500 Subject: [PATCH 41/46] chore: make zeroconf._services.info compatible with Cython 3.1 (#1576) --- src/zeroconf/_services/info.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/zeroconf/_services/info.py b/src/zeroconf/_services/info.py index fff9e125..9b38de9d 100644 --- a/src/zeroconf/_services/info.py +++ b/src/zeroconf/_services/info.py @@ -577,7 +577,7 @@ def _process_record_threadsafe(self, zc: Zeroconf, record: DNSRecord, now: float def dns_addresses( self, - override_ttl: int | None = None, + override_ttl: int_ | None = None, version: IPVersion = IPVersion.All, ) -> list[DNSAddress]: """Return matching DNSAddress from ServiceInfo.""" @@ -585,7 +585,7 @@ def dns_addresses( def _dns_addresses( self, - override_ttl: int | None, + override_ttl: int_ | None, version: IPVersion, ) -> list[DNSAddress]: """Return matching DNSAddress from ServiceInfo.""" @@ -611,11 +611,11 @@ def _dns_addresses( self._dns_address_cache = records return records - def dns_pointer(self, override_ttl: int | None = None) -> DNSPointer: + def dns_pointer(self, override_ttl: int_ | None = None) -> DNSPointer: """Return DNSPointer from ServiceInfo.""" return self._dns_pointer(override_ttl) - def _dns_pointer(self, override_ttl: int | None) -> DNSPointer: + def _dns_pointer(self, override_ttl: int_ | None) -> DNSPointer: """Return DNSPointer from ServiceInfo.""" cacheable = override_ttl is None if self._dns_pointer_cache is not None and cacheable: @@ -632,11 +632,11 @@ def _dns_pointer(self, override_ttl: int | None) -> DNSPointer: self._dns_pointer_cache = record return record - def dns_service(self, override_ttl: int | None = None) -> DNSService: + def dns_service(self, override_ttl: int_ | None = None) -> DNSService: """Return DNSService from ServiceInfo.""" return self._dns_service(override_ttl) - def _dns_service(self, override_ttl: int | None) -> DNSService: + def _dns_service(self, override_ttl: int_ | None) -> DNSService: """Return DNSService from ServiceInfo.""" cacheable = override_ttl is None if self._dns_service_cache is not None and cacheable: @@ -659,11 +659,11 @@ def _dns_service(self, override_ttl: int | None) -> DNSService: self._dns_service_cache = record return record - def dns_text(self, override_ttl: int | None = None) -> DNSText: + def dns_text(self, override_ttl: int_ | None = None) -> DNSText: """Return DNSText from ServiceInfo.""" return self._dns_text(override_ttl) - def _dns_text(self, override_ttl: int | None) -> DNSText: + def _dns_text(self, override_ttl: int_ | None) -> DNSText: """Return DNSText from ServiceInfo.""" cacheable = override_ttl is None if self._dns_text_cache is not None and cacheable: @@ -680,11 +680,11 @@ def _dns_text(self, override_ttl: int | None) -> DNSText: self._dns_text_cache = record return record - def dns_nsec(self, missing_types: list[int], override_ttl: int | None = None) -> DNSNsec: + def dns_nsec(self, missing_types: list[int], override_ttl: int_ | None = None) -> DNSNsec: """Return DNSNsec from ServiceInfo.""" return self._dns_nsec(missing_types, override_ttl) - def _dns_nsec(self, missing_types: list[int], override_ttl: int | None) -> DNSNsec: + def _dns_nsec(self, missing_types: list[int], override_ttl: int_ | None) -> DNSNsec: """Return DNSNsec from ServiceInfo.""" return DNSNsec( self._name, @@ -696,11 +696,11 @@ def _dns_nsec(self, missing_types: list[int], override_ttl: int | None) -> DNSNs 0.0, ) - def get_address_and_nsec_records(self, override_ttl: int | None = None) -> set[DNSRecord]: + def get_address_and_nsec_records(self, override_ttl: int_ | None = None) -> set[DNSRecord]: """Build a set of address records and NSEC records for non-present record types.""" return self._get_address_and_nsec_records(override_ttl) - def _get_address_and_nsec_records(self, override_ttl: int | None) -> set[DNSRecord]: + def _get_address_and_nsec_records(self, override_ttl: int_ | None) -> set[DNSRecord]: """Build a set of address records and NSEC records for non-present record types.""" cacheable = override_ttl is None if self._get_address_and_nsec_records_cache is not None and cacheable: From 5a72fd4ca0c10c9759341517c3bfb0fd0bf062c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 10:09:58 -0500 Subject: [PATCH 42/46] chore: migrate TTL to only accept int (#1577) --- src/zeroconf/_cache.pxd | 2 +- src/zeroconf/_cache.py | 2 +- src/zeroconf/_dns.pxd | 18 +++++++++--------- src/zeroconf/_dns.py | 23 +++++++++++------------ src/zeroconf/_handlers/record_manager.pxd | 2 +- src/zeroconf/_services/browser.py | 3 +-- src/zeroconf/const.py | 2 +- tests/test_protocol.py | 2 +- 8 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/zeroconf/_cache.pxd b/src/zeroconf/_cache.pxd index 273d46c3..05a40c0f 100644 --- a/src/zeroconf/_cache.pxd +++ b/src/zeroconf/_cache.pxd @@ -83,5 +83,5 @@ cdef class DNSCache: self, DNSRecord record, double now, - cython.float ttl + unsigned int ttl ) diff --git a/src/zeroconf/_cache.py b/src/zeroconf/_cache.py index c8e2686e..c7ca8472 100644 --- a/src/zeroconf/_cache.py +++ b/src/zeroconf/_cache.py @@ -317,7 +317,7 @@ def async_mark_unique_records_older_than_1s_to_expire( # Expire in 1s self._async_set_created_ttl(record, now, 1) - def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _float) -> None: + def _async_set_created_ttl(self, record: DNSRecord, now: _float, ttl: _int) -> None: """Set the created time and ttl of a record.""" # It would be better if we made a copy instead of mutating the record # in place, but records currently don't have a copy method. diff --git a/src/zeroconf/_dns.pxd b/src/zeroconf/_dns.pxd index 5ff98a8d..7ef1dbec 100644 --- a/src/zeroconf/_dns.pxd +++ b/src/zeroconf/_dns.pxd @@ -44,10 +44,10 @@ cdef class DNSQuestion(DNSEntry): cdef class DNSRecord(DNSEntry): - cdef public cython.float ttl + cdef public unsigned int ttl cdef public double created - cdef _fast_init_record(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, double created) + cdef _fast_init_record(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, double created) cdef bint _suppressed_by_answer(self, DNSRecord answer) @@ -66,7 +66,7 @@ cdef class DNSRecord(DNSEntry): cpdef bint is_recent(self, double now) - cdef _set_created_ttl(self, double now, cython.float ttl) + cdef _set_created_ttl(self, double now, unsigned int ttl) cdef class DNSAddress(DNSRecord): @@ -74,7 +74,7 @@ cdef class DNSAddress(DNSRecord): cdef public bytes address cdef public object scope_id - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, bytes address, object scope_id, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, bytes address, object scope_id, double created) cdef bint _eq(self, DNSAddress other) @@ -87,7 +87,7 @@ cdef class DNSHinfo(DNSRecord): cdef public str cpu cdef public str os - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, str cpu, str os, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, str cpu, str os, double created) cdef bint _eq(self, DNSHinfo other) @@ -99,7 +99,7 @@ cdef class DNSPointer(DNSRecord): cdef public str alias cdef public str alias_key - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, str alias, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, str alias, double created) cdef bint _eq(self, DNSPointer other) @@ -110,7 +110,7 @@ cdef class DNSText(DNSRecord): cdef public cython.int _hash cdef public bytes text - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, bytes text, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, bytes text, double created) cdef bint _eq(self, DNSText other) @@ -125,7 +125,7 @@ cdef class DNSService(DNSRecord): cdef public str server cdef public str server_key - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, cython.uint priority, cython.uint weight, cython.uint port, str server, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, cython.uint priority, cython.uint weight, cython.uint port, str server, double created) cdef bint _eq(self, DNSService other) @@ -137,7 +137,7 @@ cdef class DNSNsec(DNSRecord): cdef public str next_name cdef public cython.list rdtypes - cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, cython.float ttl, str next_name, cython.list rdtypes, double created) + cdef _fast_init(self, str name, cython.uint type_, cython.uint class_, unsigned int ttl, str next_name, cython.list rdtypes, double created) cdef bint _eq(self, DNSNsec other) diff --git a/src/zeroconf/_dns.py b/src/zeroconf/_dns.py index 591eb018..60df14b1 100644 --- a/src/zeroconf/_dns.py +++ b/src/zeroconf/_dns.py @@ -166,18 +166,17 @@ class DNSRecord(DNSEntry): __slots__ = ("created", "ttl") - # TODO: Switch to just int ttl def __init__( self, name: str, type_: int, class_: int, - ttl: float | int, + ttl: _int, created: float | None = None, ) -> None: self._fast_init_record(name, type_, class_, ttl, created or current_time_millis()) - def _fast_init_record(self, name: str, type_: _int, class_: _int, ttl: _float, created: _float) -> None: + def _fast_init_record(self, name: str, type_: _int, class_: _int, ttl: _int, created: _float) -> None: """Fast init for reuse.""" self._fast_init_entry(name, type_, class_) self.ttl = ttl @@ -227,7 +226,7 @@ def is_recent(self, now: _float) -> bool: """Returns true if the record more than one quarter of its TTL remaining.""" return self.created + (_RECENT_TIME_MS * self.ttl) > now - def _set_created_ttl(self, created: _float, ttl: float | int) -> None: + def _set_created_ttl(self, created: _float, ttl: _int) -> None: """Set the created and ttl of a record.""" # It would be better if we made a copy instead of mutating the record # in place, but records currently don't have a copy method. @@ -266,7 +265,7 @@ def _fast_init( name: str, type_: _int, class_: _int, - ttl: _float, + ttl: _int, address: bytes, scope_id: _int | None, created: _float, @@ -327,7 +326,7 @@ def __init__( self._fast_init(name, type_, class_, ttl, cpu, os, created or current_time_millis()) def _fast_init( - self, name: str, type_: _int, class_: _int, ttl: _float, cpu: str, os: str, created: _float + self, name: str, type_: _int, class_: _int, ttl: _int, cpu: str, os: str, created: _float ) -> None: """Fast init for reuse.""" self._fast_init_record(name, type_, class_, ttl, created) @@ -374,7 +373,7 @@ def __init__( self._fast_init(name, type_, class_, ttl, alias, created or current_time_millis()) def _fast_init( - self, name: str, type_: _int, class_: _int, ttl: _float, alias: str, created: _float + self, name: str, type_: _int, class_: _int, ttl: _int, alias: str, created: _float ) -> None: self._fast_init_record(name, type_, class_, ttl, created) self.alias = alias @@ -429,7 +428,7 @@ def __init__( self._fast_init(name, type_, class_, ttl, text, created or current_time_millis()) def _fast_init( - self, name: str, type_: _int, class_: _int, ttl: _float, text: bytes, created: _float + self, name: str, type_: _int, class_: _int, ttl: _int, text: bytes, created: _float ) -> None: self._fast_init_record(name, type_, class_, ttl, created) self.text = text @@ -468,7 +467,7 @@ def __init__( name: str, type_: int, class_: int, - ttl: float | int, + ttl: int, priority: int, weight: int, port: int, @@ -484,7 +483,7 @@ def _fast_init( name: str, type_: _int, class_: _int, - ttl: _float, + ttl: _int, priority: _int, weight: _int, port: _int, @@ -539,7 +538,7 @@ def __init__( name: str, type_: int, class_: int, - ttl: int | float, + ttl: _int, next_name: str, rdtypes: list[int], created: float | None = None, @@ -551,7 +550,7 @@ def _fast_init( name: str, type_: _int, class_: _int, - ttl: _float, + ttl: _int, next_name: str, rdtypes: list[_int], created: _float, diff --git a/src/zeroconf/_handlers/record_manager.pxd b/src/zeroconf/_handlers/record_manager.pxd index 37232b13..b9bde975 100644 --- a/src/zeroconf/_handlers/record_manager.pxd +++ b/src/zeroconf/_handlers/record_manager.pxd @@ -8,7 +8,7 @@ from .._updates cimport RecordUpdateListener from .._utils.time cimport current_time_millis from .._record_update cimport RecordUpdate -cdef cython.float _DNS_PTR_MIN_TTL +cdef unsigned int _DNS_PTR_MIN_TTL cdef cython.uint _TYPE_PTR cdef object _ADDRESS_RECORD_TYPES cdef bint TYPE_CHECKING diff --git a/src/zeroconf/_services/browser.py b/src/zeroconf/_services/browser.py index ab8c050d..1f60e8f9 100644 --- a/src/zeroconf/_services/browser.py +++ b/src/zeroconf/_services/browser.py @@ -394,9 +394,8 @@ def _schedule_ptr_refresh( refresh_time_millis: float_, ) -> None: """Schedule a query for a pointer.""" - ttl = int(pointer.ttl) if isinstance(pointer.ttl, float) else pointer.ttl scheduled_ptr_query = _ScheduledPTRQuery( - pointer.alias, pointer.name, ttl, expire_time_millis, refresh_time_millis + pointer.alias, pointer.name, pointer.ttl, expire_time_millis, refresh_time_millis ) self._schedule_ptr_query(scheduled_ptr_query) diff --git a/src/zeroconf/const.py b/src/zeroconf/const.py index 3b4b3abc..c3a62875 100644 --- a/src/zeroconf/const.py +++ b/src/zeroconf/const.py @@ -57,7 +57,7 @@ # ServiceBrowsers generating excessive queries refresh queries. # Apple uses a 15s minimum TTL, however we do not have the same # level of rate limit and safe guards so we use 1/4 of the recommended value -_DNS_PTR_MIN_TTL = _DNS_OTHER_TTL / 4 +_DNS_PTR_MIN_TTL = 1125 _DNS_PACKET_HEADER_LEN = 12 diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 08d7e600..edd87c2e 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -196,7 +196,7 @@ def test_suppress_answer(self): "testname2.local.", const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE, - const._DNS_HOST_TTL / 2, + int(const._DNS_HOST_TTL / 2), 0, 0, 80, From daaf8d6981c778fe4ba0a63371d9368cf217891a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 10:19:16 -0500 Subject: [PATCH 43/46] feat: Cython 3.1 support (#1578) From 1569383c6cf8ce8977427cfdaf5c7104ce52ab08 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 11:04:00 -0500 Subject: [PATCH 44/46] feat: cython 3.11 support (#1579) From 1d9c94a82d8da16b8f5355131e6167b69293da6c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 3 May 2025 11:12:27 -0500 Subject: [PATCH 45/46] feat: add cython 3.1 support (#1580) From 4cf513f69169b5992a73fe0bc431ec17f8f5040d Mon Sep 17 00:00:00 2001 From: semantic-release Date: Sat, 3 May 2025 16:22:31 +0000 Subject: [PATCH 46/46] 0.147.0 Automatically generated by python-semantic-release --- CHANGELOG.md | 14 ++++++++++++++ pyproject.toml | 2 +- src/zeroconf/__init__.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d107aa5..d8a3d4cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # CHANGELOG +## v0.147.0 (2025-05-03) + +### Features + +- Add cython 3.1 support ([#1580](https://github.com/python-zeroconf/python-zeroconf/pull/1580), + [`1d9c94a`](https://github.com/python-zeroconf/python-zeroconf/commit/1d9c94a82d8da16b8f5355131e6167b69293da6c)) + +- Cython 3.1 support ([#1578](https://github.com/python-zeroconf/python-zeroconf/pull/1578), + [`daaf8d6`](https://github.com/python-zeroconf/python-zeroconf/commit/daaf8d6981c778fe4ba0a63371d9368cf217891a)) + +- Cython 3.11 support ([#1579](https://github.com/python-zeroconf/python-zeroconf/pull/1579), + [`1569383`](https://github.com/python-zeroconf/python-zeroconf/commit/1569383c6cf8ce8977427cfdaf5c7104ce52ab08)) + + ## v0.146.5 (2025-04-14) ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index a1390502..d47a1966 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zeroconf" -version = "0.146.5" +version = "0.147.0" description = "A pure python implementation of multicast DNS service discovery" authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"] license = "LGPL-2.1-or-later" diff --git a/src/zeroconf/__init__.py b/src/zeroconf/__init__.py index 2449e835..439ffceb 100644 --- a/src/zeroconf/__init__.py +++ b/src/zeroconf/__init__.py @@ -88,7 +88,7 @@ __author__ = "Paul Scott-Murphy, William McBrine" __maintainer__ = "Jakub Stasiak " -__version__ = "0.146.5" +__version__ = "0.147.0" __license__ = "LGPL"