diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fb9b06f..5e8d1ef0 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@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@ad3126e916f78f00edff4ed0317cf185271ccc2d # 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@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@a26af69be951a213d495a4c3e4e4022e16d87065 # 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@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*' }} 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@d3f86a106a0bac45b974a628896c90dbdf5c8093 # 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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 265f703e..8ad48a33 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.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.9.9 + rev: v0.11.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -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/CHANGELOG.md b/CHANGELOG.md index f28b0022..d8a3d4cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,75 @@ # 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 + +- 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 + +- 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 + +- 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 + +- 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/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/poetry.lock b/poetry.lock index e0d48822..13e12b6e 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"}, @@ -13,27 +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 ; 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" -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]] @@ -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"}, @@ -229,80 +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"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cython" @@ -310,6 +319,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 +393,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 +405,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 +421,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 +436,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 +448,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"}, @@ -440,25 +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)"] +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 +485,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"}, @@ -474,13 +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] @@ -495,6 +515,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 +540,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 +611,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 +623,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 +635,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 +651,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 +663,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 +678,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"}, @@ -669,17 +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)"] @@ -691,6 +721,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"}, @@ -719,13 +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] @@ -741,6 +773,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 +788,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 +810,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 +826,24 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "setuptools" -version = "75.8.2" +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-75.8.2-py3-none-any.whl", hash = "sha256:558e47c15f1811c1fa7adbd0096669bf76c1d3f433f58324df69f3f5ecac4e8f"}, - {file = "setuptools-75.8.2.tar.gz", hash = "sha256:4880473a969e5f23f2a2be3646b2dfd84af9028716d398e46192f84bc36900d2"}, + {file = "setuptools-80.0.0-py3-none-any.whl", hash = "sha256:a38f898dcd6e5380f4da4381a87ec90bd0a7eec23d204a5552e80ee3cab6bd27"}, + {file = "setuptools-80.0.0.tar.gz", hash = "sha256:c40a5b3729d58dd749c0f08f1a07d134fb8a0a3d7f87dc33e7c5e1f762138650"}, ] [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" @@ -815,6 +851,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 +863,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 +900,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 +920,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 +937,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 +954,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 +971,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 +986,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 +1001,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 +1018,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 +1035,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 +1070,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 +1078,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 +1091,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 +1109,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 = "972988da838067a7f2d12b8212ce54ba946cb38a4f63576a520dd1ed40ac3e9b" diff --git a/pyproject.toml b/pyproject.toml index 4e056cd7..d47a1966 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "zeroconf" -version = "0.146.1" +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" @@ -75,9 +75,9 @@ 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,<76.0.0" +setuptools = ">=65.6.3,<81.0.0" pytest-timeout = "^2.1.0" pytest-codspeed = "^3.1.0" @@ -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/__init__.py b/src/zeroconf/__init__.py index b915b8d7..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.1" +__version__ = "0.147.0" __license__ = "LGPL" 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/_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, 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/_services/info.py b/src/zeroconf/_services/info.py index 9cd8df16..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: @@ -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/src/zeroconf/_utils/net.py b/src/zeroconf/_utils/net.py index c2312e01..e67edf78 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 ip6_to_address_and_index(adapters: list[ifaddr.Adapter], ip: str) -> tuple[tuple[str, int, int], int]: +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: 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: @@ -147,15 +168,25 @@ 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: + 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}" @@ -198,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( @@ -244,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) @@ -370,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])) @@ -389,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 @@ -401,32 +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: - respond_socket = new_socket( - port=0, - ip_version=ip_version, - apple_p2p=apple_p2p, - bind_addr=i[0] if isinstance(i, tuple) else (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/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/services/test_browser.py b/tests/services/test_browser.py index 986df64e..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() @@ -899,7 +898,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) @@ -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/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() 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_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/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, diff --git a/tests/test_updates.py b/tests/test_updates.py index a057486c..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() @@ -81,7 +80,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_net.py b/tests/utils/test_net.py index f763b655..7de10661 100644 --- a/tests/utils/test_net.py +++ b/tests/utils/test_net.py @@ -6,23 +6,25 @@ import socket import sys import unittest -from unittest.mock import MagicMock, Mock, patch +import warnings +from unittest.mock import MagicMock, Mock, call, 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 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" @@ -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) == 3 + 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) @@ -127,11 +163,10 @@ 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) 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), @@ -147,100 +182,109 @@ 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) -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.""" - 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 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: - 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(): @@ -298,3 +342,94 @@ 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_interfaces_all_unicast(): + """Test create_sockets with unicast.""" + + 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",), + ) + + +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