zeroconf 0.147.2__tar.gz → 0.147.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zeroconf-0.147.2 → zeroconf-0.147.4}/CHANGELOG.md +67 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/PKG-INFO +6 -5
- {zeroconf-0.147.2 → zeroconf-0.147.4}/pyproject.toml +34 -31
- {zeroconf-0.147.2 → zeroconf-0.147.4}/setup.py +2 -2
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/__init__.py +1 -1
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_core.py +10 -1
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/browser.py +11 -1
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/__init__.py +1 -4
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/conftest.py +19 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/services/test_browser.py +5 -10
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/services/test_info.py +4 -4
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_asyncio.py +13 -10
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_core.py +37 -3
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_dns.py +2 -2
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_exceptions.py +1 -1
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/utils/test_asyncio.py +9 -3
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/utils/test_net.py +1 -1
- {zeroconf-0.147.2 → zeroconf-0.147.4}/COPYING +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/README.rst +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/build_ext.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/docs/Makefile +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/docs/_ext/zeroconfautodocfix.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/docs/api.rst +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/docs/conf.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/docs/index.rst +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_cache.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_cache.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_dns.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_dns.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_engine.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_exceptions.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/__init__.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/answers.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/answers.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/multicast_outgoing_queue.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/multicast_outgoing_queue.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/query_handler.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/query_handler.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/record_manager.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_handlers/record_manager.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_history.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_history.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_listener.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_listener.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_logger.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_protocol/__init__.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_protocol/incoming.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_protocol/incoming.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_protocol/outgoing.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_protocol/outgoing.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_record_update.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_record_update.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/__init__.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/__init__.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/browser.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/info.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/info.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/registry.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/registry.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_services/types.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_transport.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_updates.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_updates.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_utils/__init__.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_utils/asyncio.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_utils/ipaddress.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_utils/ipaddress.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_utils/name.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_utils/net.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_utils/time.pxd +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/_utils/time.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/asyncio.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/const.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/src/zeroconf/py.typed +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/benchmarks/__init__.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/benchmarks/helpers.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/benchmarks/test_cache.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/benchmarks/test_incoming.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/benchmarks/test_outgoing.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/benchmarks/test_send.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/benchmarks/test_txt_properties.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/services/__init__.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/services/test_registry.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/services/test_types.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_cache.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_circular_imports.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_engine.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_handlers.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_history.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_init.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_listener.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_logger.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_protocol.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_services.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/test_updates.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/utils/__init__.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/utils/test_ipaddress.py +0 -0
- {zeroconf-0.147.2 → zeroconf-0.147.4}/tests/utils/test_name.py +0 -0
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
<!-- version list -->
|
|
4
|
+
|
|
5
|
+
## v0.147.4 (2026-05-16)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- **core**: Close owned event loop on Zeroconf.close() to stop FD leak
|
|
10
|
+
([#1685](https://github.com/python-zeroconf/python-zeroconf/pull/1685),
|
|
11
|
+
[`2f78370`](https://github.com/python-zeroconf/python-zeroconf/commit/2f78370c75d1082afe3191b7447aebfff1206657))
|
|
12
|
+
|
|
13
|
+
### Build System
|
|
14
|
+
|
|
15
|
+
- Adjust actions checkout ref parameter on release
|
|
16
|
+
([#1669](https://github.com/python-zeroconf/python-zeroconf/pull/1669),
|
|
17
|
+
[`bc8ec8d`](https://github.com/python-zeroconf/python-zeroconf/commit/bc8ec8d59d875522f75901644d423d30d803a030))
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
|
|
21
|
+
- Add CLAUDE.md orientation file and pr-workflow skill
|
|
22
|
+
([#1672](https://github.com/python-zeroconf/python-zeroconf/pull/1672),
|
|
23
|
+
[`8f8b4d6`](https://github.com/python-zeroconf/python-zeroconf/commit/8f8b4d6526729906337f4562c7b391745bb878af))
|
|
24
|
+
|
|
25
|
+
- Add Cython gotchas section to CLAUDE.md
|
|
26
|
+
([#1679](https://github.com/python-zeroconf/python-zeroconf/pull/1679),
|
|
27
|
+
[`5cfb09d`](https://github.com/python-zeroconf/python-zeroconf/commit/5cfb09d89395cb507d436c45fc38edb9e44b94c8))
|
|
28
|
+
|
|
29
|
+
- Add SECURITY.md with private vulnerability reporting policy
|
|
30
|
+
([#1675](https://github.com/python-zeroconf/python-zeroconf/pull/1675),
|
|
31
|
+
[`13f9048`](https://github.com/python-zeroconf/python-zeroconf/commit/13f9048f0f9786ce18e89daef04073847735a006))
|
|
32
|
+
|
|
33
|
+
### Testing
|
|
34
|
+
|
|
35
|
+
- Add quick_timing fixture and apply to register-heavy tests
|
|
36
|
+
([#1678](https://github.com/python-zeroconf/python-zeroconf/pull/1678),
|
|
37
|
+
[`d5e1f01`](https://github.com/python-zeroconf/python-zeroconf/commit/d5e1f01bb336ea19d982ce7d99f191723d3f18af))
|
|
38
|
+
|
|
39
|
+
- Fix flaky test_run_coro_with_timeout
|
|
40
|
+
([#1683](https://github.com/python-zeroconf/python-zeroconf/pull/1683),
|
|
41
|
+
[`277f80d`](https://github.com/python-zeroconf/python-zeroconf/commit/277f80da2c0fea5b256f981bd3f425906f6b7be6))
|
|
42
|
+
|
|
43
|
+
- Pass timeout=0 explicitly in test_event_loop_blocked
|
|
44
|
+
([#1676](https://github.com/python-zeroconf/python-zeroconf/pull/1676),
|
|
45
|
+
[`1b31ed5`](https://github.com/python-zeroconf/python-zeroconf/commit/1b31ed5fed9db1608255799701cd6f32b494952f))
|
|
46
|
+
|
|
47
|
+
- Pass timeout=200 to ServiceInfo-request timeout tests
|
|
48
|
+
([#1677](https://github.com/python-zeroconf/python-zeroconf/pull/1677),
|
|
49
|
+
[`01ef6ff`](https://github.com/python-zeroconf/python-zeroconf/commit/01ef6ffd9ff442b3cfb37d2793e0ca6ad5148832))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## v0.148.0 (2025-10-05)
|
|
53
|
+
|
|
54
|
+
### Features
|
|
55
|
+
|
|
56
|
+
- Trigger semantic releases for 0.x branch
|
|
57
|
+
([#1626](https://github.com/python-zeroconf/python-zeroconf/pull/1626),
|
|
58
|
+
[`812a2b3`](https://github.com/python-zeroconf/python-zeroconf/commit/812a2b3ff4370593a7a0c3ad67389c76c434aa9b))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## v0.147.3 (2025-10-04)
|
|
62
|
+
|
|
63
|
+
### Bug Fixes
|
|
64
|
+
|
|
65
|
+
- Update poetry to v2 ([#1623](https://github.com/python-zeroconf/python-zeroconf/pull/1623),
|
|
66
|
+
[`2c3c296`](https://github.com/python-zeroconf/python-zeroconf/commit/2c3c29655bd365213a7e0a4360b8dd860d833470))
|
|
67
|
+
|
|
3
68
|
|
|
4
69
|
## v0.147.2 (2025-09-05)
|
|
5
70
|
|
|
@@ -1976,3 +2041,5 @@ Include documentation and test files in source distributions, in order to make t
|
|
|
1976
2041
|
|
|
1977
2042
|
|
|
1978
2043
|
## v0.15.1 (2014-07-10)
|
|
2044
|
+
|
|
2045
|
+
- Initial Release
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: zeroconf
|
|
3
|
-
Version: 0.147.
|
|
3
|
+
Version: 0.147.4
|
|
4
4
|
Summary: A pure python implementation of multicast DNS service discovery
|
|
5
|
-
License: LGPL-2.1-or-later
|
|
5
|
+
License-Expression: LGPL-2.1-or-later
|
|
6
|
+
License-File: COPYING
|
|
6
7
|
Author: Paul Scott-Murphy
|
|
7
|
-
Requires-Python: >=3.9
|
|
8
|
+
Requires-Python: >=3.9
|
|
8
9
|
Classifier: Development Status :: 5 - Production/Stable
|
|
9
10
|
Classifier: Intended Audience :: Developers
|
|
10
11
|
Classifier: Intended Audience :: System Administrators
|
|
11
|
-
Classifier: License :: OSI Approved
|
|
12
12
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
13
13
|
Classifier: Operating System :: POSIX
|
|
14
14
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
22
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
22
23
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
23
24
|
Classifier: Topic :: Software Development :: Libraries
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
[
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ['setuptools>=77.0', 'Cython>=3.0.8', "poetry-core>=2.1.0"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
2
6
|
name = "zeroconf"
|
|
3
|
-
version = "0.147.
|
|
4
|
-
description = "A pure python implementation of multicast DNS service discovery"
|
|
5
|
-
authors = ["Paul Scott-Murphy", "William McBrine", "Jakub Stasiak", "J. Nick Koston"]
|
|
7
|
+
version = "0.147.4"
|
|
6
8
|
license = "LGPL-2.1-or-later"
|
|
9
|
+
description = "A pure python implementation of multicast DNS service discovery"
|
|
7
10
|
readme = "README.rst"
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Paul Scott-Murphy" },
|
|
13
|
+
{ name = "William McBrine" },
|
|
14
|
+
{ name = "Jakub Stasiak" },
|
|
15
|
+
{ name = "J. Nick Koston" },
|
|
16
|
+
]
|
|
17
|
+
requires-python = ">=3.9"
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
"Repository" = "https://github.com/python-zeroconf/python-zeroconf"
|
|
21
|
+
"Documentation" = "https://python-zeroconf.readthedocs.io"
|
|
22
|
+
"Bug Tracker" = "https://github.com/python-zeroconf/python-zeroconf/issues"
|
|
23
|
+
"Changelog" = "https://github.com/python-zeroconf/python-zeroconf/blob/master/CHANGELOG.md"
|
|
24
|
+
|
|
25
|
+
[tool.poetry]
|
|
10
26
|
classifiers=[
|
|
11
27
|
'Development Status :: 5 - Production/Stable',
|
|
12
28
|
'Intended Audience :: Developers',
|
|
@@ -15,11 +31,6 @@ classifiers=[
|
|
|
15
31
|
'Operating System :: POSIX :: Linux',
|
|
16
32
|
'Operating System :: MacOS :: MacOS X',
|
|
17
33
|
'Topic :: Software Development :: Libraries',
|
|
18
|
-
'Programming Language :: Python :: 3.9',
|
|
19
|
-
'Programming Language :: Python :: 3.10',
|
|
20
|
-
'Programming Language :: Python :: 3.11',
|
|
21
|
-
'Programming Language :: Python :: 3.12',
|
|
22
|
-
'Programming Language :: Python :: 3.13',
|
|
23
34
|
'Programming Language :: Python :: Implementation :: CPython',
|
|
24
35
|
'Programming Language :: Python :: Implementation :: PyPy',
|
|
25
36
|
]
|
|
@@ -35,22 +46,19 @@ include = [
|
|
|
35
46
|
# Make sure we don't package temporary C files generated by the build process
|
|
36
47
|
exclude = [ "**/*.c" ]
|
|
37
48
|
|
|
38
|
-
[tool.poetry.urls]
|
|
39
|
-
"Bug Tracker" = "https://github.com/python-zeroconf/python-zeroconf/issues"
|
|
40
|
-
"Changelog" = "https://github.com/python-zeroconf/python-zeroconf/blob/master/CHANGELOG.md"
|
|
41
|
-
|
|
42
49
|
[tool.poetry.build]
|
|
43
50
|
generate-setup-file = true
|
|
44
51
|
script = "build_ext.py"
|
|
45
52
|
|
|
46
53
|
[tool.semantic_release]
|
|
47
54
|
branch = "master"
|
|
48
|
-
version_toml = ["pyproject.toml:
|
|
55
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
49
56
|
version_variables = [
|
|
50
57
|
"src/zeroconf/__init__.py:__version__"
|
|
51
58
|
]
|
|
52
59
|
build_command = "pip install poetry && poetry build"
|
|
53
60
|
tag_format = "{version}"
|
|
61
|
+
allow_zero_version = true
|
|
54
62
|
|
|
55
63
|
[tool.semantic_release.changelog]
|
|
56
64
|
exclude_commit_patterns = [
|
|
@@ -64,8 +72,11 @@ keep_trailing_newline = true
|
|
|
64
72
|
[tool.semantic_release.branches.master]
|
|
65
73
|
match = "master"
|
|
66
74
|
|
|
75
|
+
[tool.semantic_release.branches."release-0.x"]
|
|
76
|
+
match = "release-0.x"
|
|
77
|
+
|
|
67
78
|
[tool.semantic_release.branches.noop]
|
|
68
|
-
match = "(?!master$)"
|
|
79
|
+
match = "(?!(master|release-0.x)$)"
|
|
69
80
|
prerelease = true
|
|
70
81
|
|
|
71
82
|
[tool.poetry.dependencies]
|
|
@@ -74,16 +85,16 @@ ifaddr = ">=0.1.7"
|
|
|
74
85
|
|
|
75
86
|
[tool.poetry.group.dev.dependencies]
|
|
76
87
|
pytest = ">=7.2,<9.0"
|
|
77
|
-
pytest-cov = ">=4,<
|
|
78
|
-
pytest-asyncio = ">=0.20.3,<1.
|
|
79
|
-
cython = "^3.
|
|
80
|
-
setuptools = ">=65.6.3,<
|
|
88
|
+
pytest-cov = ">=4,<8"
|
|
89
|
+
pytest-asyncio = ">=0.20.3,<1.3.0"
|
|
90
|
+
cython = "^3.2.4"
|
|
91
|
+
setuptools = ">=65.6.3,<83.0.0"
|
|
81
92
|
pytest-timeout = "^2.1.0"
|
|
82
|
-
pytest-codspeed = ">=
|
|
93
|
+
pytest-codspeed = ">=4.5.0,<5.0"
|
|
83
94
|
|
|
84
95
|
[tool.poetry.group.docs.dependencies]
|
|
85
96
|
sphinx = "^7.4.7 || ^8.1.3"
|
|
86
|
-
sphinx-rtd-theme = "^3.0
|
|
97
|
+
sphinx-rtd-theme = "^3.1.0"
|
|
87
98
|
|
|
88
99
|
[tool.ruff]
|
|
89
100
|
target-version = "py39"
|
|
@@ -159,12 +170,9 @@ select = [
|
|
|
159
170
|
"FLY002", # too many to fix right now
|
|
160
171
|
"PT018", # too many to fix right now
|
|
161
172
|
"PLR0124", # too many to fix right now
|
|
162
|
-
"SIM202" , # too many to fix right now
|
|
163
173
|
"PT012" , # too many to fix right now
|
|
164
174
|
"TID252", # too many to fix right now
|
|
165
175
|
"PLR0913", # skip this one
|
|
166
|
-
"SIM102" , # too many to fix right now
|
|
167
|
-
"SIM108", # too many to fix right now
|
|
168
176
|
"T201", # too many to fix right now
|
|
169
177
|
"PT004", # nice to have
|
|
170
178
|
]
|
|
@@ -266,11 +274,6 @@ allow_untyped_defs = true
|
|
|
266
274
|
module = "bench.*"
|
|
267
275
|
ignore_errors = true
|
|
268
276
|
|
|
269
|
-
[build-system]
|
|
270
|
-
# 1.5.2 required for https://github.com/python-poetry/poetry/issues/7505
|
|
271
|
-
requires = ['setuptools>=65.4.1', 'wheel', 'Cython>=3.0.8', "poetry-core>=1.5.2"]
|
|
272
|
-
build-backend = "poetry.core.masonry.api"
|
|
273
|
-
|
|
274
277
|
[tool.codespell]
|
|
275
278
|
ignore-words-list = ["additionals", "HASS"]
|
|
276
279
|
|
|
@@ -19,7 +19,7 @@ install_requires = \
|
|
|
19
19
|
|
|
20
20
|
setup_kwargs = {
|
|
21
21
|
'name': 'zeroconf',
|
|
22
|
-
'version': '0.147.
|
|
22
|
+
'version': '0.147.4',
|
|
23
23
|
'description': 'A pure python implementation of multicast DNS service discovery',
|
|
24
24
|
'long_description': 'python-zeroconf\n===============\n\n.. image:: https://github.com/python-zeroconf/python-zeroconf/workflows/CI/badge.svg\n :target: https://github.com/python-zeroconf/python-zeroconf?query=workflow%3ACI+branch%3Amaster\n\n.. image:: https://img.shields.io/pypi/v/zeroconf.svg\n :target: https://pypi.python.org/pypi/zeroconf\n\n.. image:: https://codecov.io/gh/python-zeroconf/python-zeroconf/branch/master/graph/badge.svg\n :target: https://codecov.io/gh/python-zeroconf/python-zeroconf\n\n.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json\n :target: https://codspeed.io/python-zeroconf/python-zeroconf\n :alt: Codspeed.io status for python-zeroconf\n\n.. image:: https://readthedocs.org/projects/python-zeroconf/badge/?version=latest\n :target: https://python-zeroconf.readthedocs.io/en/latest/?badge=latest\n :alt: Documentation Status\n\n`Documentation <https://python-zeroconf.readthedocs.io/en/latest/>`_.\n\nThis is fork of pyzeroconf, Multicast DNS Service Discovery for Python,\noriginally by Paul Scott-Murphy (https://github.com/paulsm/pyzeroconf),\nmodified by William McBrine (https://github.com/wmcbrine/pyzeroconf).\n\nThe original William McBrine\'s fork note::\n\n This fork is used in all of my TiVo-related projects: HME for Python\n (and therefore HME/VLC), Network Remote, Remote Proxy, and pyTivo.\n Before this, I was tracking the changes for zeroconf.py in three\n separate repos. I figured I should have an authoritative source.\n\n Although I make changes based on my experience with TiVos, I expect that\n they\'re generally applicable. This version also includes patches found\n on the now-defunct (?) Launchpad repo of pyzeroconf, and elsewhere\n around the net -- not always well-documented, sorry.\n\nCompatible with:\n\n* Bonjour\n* Avahi\n\nCompared to some other Zeroconf/Bonjour/Avahi Python packages, python-zeroconf:\n\n* isn\'t tied to Bonjour or Avahi\n* doesn\'t use D-Bus\n* doesn\'t force you to use particular event loop or Twisted (asyncio is used under the hood but not required)\n* is pip-installable\n* has PyPI distribution\n* has an optional cython extension for performance (pure python is supported as well)\n\nPython compatibility\n--------------------\n\n* CPython 3.9+\n* PyPy 3.9+\n\nVersioning\n----------\n\nThis project uses semantic versioning.\n\nStatus\n------\n\nThis project is actively maintained.\n\nTraffic Reduction\n-----------------\n\nBefore version 0.32, most traffic reduction techniques described in https://datatracker.ietf.org/doc/html/rfc6762#section-7\nwhere not implemented which could lead to excessive network traffic. It is highly recommended that version 0.32 or later\nis used if this is a concern.\n\nIPv6 support\n------------\n\nIPv6 support is relatively new and currently limited, specifically:\n\n* `InterfaceChoice.All` is an alias for `InterfaceChoice.Default` on non-POSIX\n systems.\n* Dual-stack IPv6 sockets are used, which may not be supported everywhere (some\n BSD variants do not have them).\n* Listening on localhost (`::1`) does not work. Help with understanding why is\n appreciated.\n\nHow to get python-zeroconf?\n===========================\n\n* PyPI page https://pypi.org/project/zeroconf/\n* GitHub project https://github.com/python-zeroconf/python-zeroconf\n\nThe easiest way to install python-zeroconf is using pip::\n\n pip install zeroconf\n\n\n\nHow do I use it?\n================\n\nHere\'s an example of browsing for a service:\n\n.. code-block:: python\n\n from zeroconf import ServiceBrowser, ServiceListener, Zeroconf\n\n\n class MyListener(ServiceListener):\n\n def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} updated")\n\n def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n print(f"Service {name} removed")\n\n def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:\n info = zc.get_service_info(type_, name)\n print(f"Service {name} added, service info: {info}")\n\n\n zeroconf = Zeroconf()\n listener = MyListener()\n browser = ServiceBrowser(zeroconf, "_http._tcp.local.", listener)\n try:\n input("Press enter to exit...\\n\\n")\n finally:\n zeroconf.close()\n\n.. note::\n\n Discovery and service registration use *all* available network interfaces by default.\n If you want to customize that you need to specify ``interfaces`` argument when\n constructing ``Zeroconf`` object (see the code for details).\n\nIf you don\'t know the name of the service you need to browse for, try:\n\n.. code-block:: python\n\n from zeroconf import ZeroconfServiceTypes\n print(\'\\n\'.join(ZeroconfServiceTypes.find()))\n\nSee examples directory for more.\n\nChangelog\n=========\n\n`Changelog <CHANGELOG.md>`_\n\nLicense\n=======\n\nLGPL, see COPYING file for details.\n',
|
|
25
25
|
'author': 'Paul Scott-Murphy',
|
|
@@ -31,7 +31,7 @@ setup_kwargs = {
|
|
|
31
31
|
'packages': packages,
|
|
32
32
|
'package_data': package_data,
|
|
33
33
|
'install_requires': install_requires,
|
|
34
|
-
'python_requires': '>=3.9
|
|
34
|
+
'python_requires': '>=3.9',
|
|
35
35
|
}
|
|
36
36
|
from build_ext import *
|
|
37
37
|
build(setup_kwargs)
|
|
@@ -88,7 +88,7 @@ from ._utils.time import ( # noqa # import needed for backwards compat
|
|
|
88
88
|
|
|
89
89
|
__author__ = "Paul Scott-Murphy, William McBrine"
|
|
90
90
|
__maintainer__ = "Jakub Stasiak <jakub@stasiak.at>"
|
|
91
|
-
__version__ = "0.147.
|
|
91
|
+
__version__ = "0.147.4"
|
|
92
92
|
__license__ = "LGPL"
|
|
93
93
|
|
|
94
94
|
|
|
@@ -676,13 +676,22 @@ class Zeroconf(QuietLogger):
|
|
|
676
676
|
|
|
677
677
|
def _shutdown_threads(self) -> None:
|
|
678
678
|
"""Shutdown any threads."""
|
|
679
|
+
assert self.loop is not None
|
|
680
|
+
if self.loop.is_closed():
|
|
681
|
+
# close() is documented as idempotent — a second call after the
|
|
682
|
+
# loop has been torn down must be a no-op rather than raising.
|
|
683
|
+
return
|
|
679
684
|
self.notify_all()
|
|
680
685
|
if not self._loop_thread:
|
|
681
686
|
return
|
|
682
|
-
assert self.loop is not None
|
|
683
687
|
shutdown_loop(self.loop)
|
|
684
688
|
self._loop_thread.join()
|
|
685
689
|
self._loop_thread = None
|
|
690
|
+
# The loop's selector (epoll FD on Linux) and self-pipe sockets stay
|
|
691
|
+
# open until loop.close() is called. We own this loop because
|
|
692
|
+
# _start_thread() created it, so close it here to avoid leaking
|
|
693
|
+
# those file descriptors across Zeroconf() construct/close cycles.
|
|
694
|
+
self.loop.close()
|
|
686
695
|
|
|
687
696
|
def close(self) -> None:
|
|
688
697
|
"""Ends the background threads, and prevent this instance from
|
|
@@ -23,6 +23,7 @@ USA
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
25
|
import asyncio
|
|
26
|
+
import contextlib
|
|
26
27
|
import heapq
|
|
27
28
|
import queue
|
|
28
29
|
import random
|
|
@@ -793,7 +794,16 @@ class ServiceBrowser(_ServiceBrowserBase, threading.Thread):
|
|
|
793
794
|
"""Cancel the browser."""
|
|
794
795
|
assert self.zc.loop is not None
|
|
795
796
|
self.queue.put(None)
|
|
796
|
-
|
|
797
|
+
# While the loop is running, _async_cancel stops the query scheduler
|
|
798
|
+
# and cancels the query-sender task — that is the normal cleanup
|
|
799
|
+
# path. Skip scheduling solely because the loop is closed: a closed
|
|
800
|
+
# loop rejects call_soon_threadsafe with RuntimeError. The
|
|
801
|
+
# is_closed() check narrows the common case (loop already closed by
|
|
802
|
+
# Zeroconf.close()) without paying for raise/catch; suppress covers
|
|
803
|
+
# the residual is_closed() -> call_soon_threadsafe race window.
|
|
804
|
+
with contextlib.suppress(RuntimeError):
|
|
805
|
+
if not self.zc.loop.is_closed():
|
|
806
|
+
self.zc.loop.call_soon_threadsafe(self._async_cancel)
|
|
797
807
|
self.join()
|
|
798
808
|
|
|
799
809
|
def run(self) -> None:
|
|
@@ -95,10 +95,7 @@ def time_changed_millis(millis: float | None = None) -> None:
|
|
|
95
95
|
"""Call all scheduled events for a time."""
|
|
96
96
|
loop = asyncio.get_running_loop()
|
|
97
97
|
loop_time = loop.time()
|
|
98
|
-
if millis is not None
|
|
99
|
-
mock_seconds_into_future = millis / 1000
|
|
100
|
-
else:
|
|
101
|
-
mock_seconds_into_future = loop_time
|
|
98
|
+
mock_seconds_into_future = millis / 1000 if millis is not None else loop_time
|
|
102
99
|
|
|
103
100
|
with mock.patch("time.monotonic", return_value=mock_seconds_into_future):
|
|
104
101
|
for task in list(loop._scheduled): # type: ignore[attr-defined]
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import threading
|
|
6
|
+
from collections.abc import Generator
|
|
6
7
|
from unittest.mock import patch
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
@@ -40,3 +41,21 @@ def disable_duplicate_packet_suppression():
|
|
|
40
41
|
"""
|
|
41
42
|
with patch.object(const, "_DUPLICATE_PACKET_SUPPRESSION_INTERVAL", 0):
|
|
42
43
|
yield
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def quick_timing() -> Generator[None]:
|
|
48
|
+
"""Shorten the probe/announce/goodbye intervals for tests on loopback.
|
|
49
|
+
|
|
50
|
+
The production values (_CHECK_TIME=500ms, _REGISTER_TIME=225ms,
|
|
51
|
+
_UNREGISTER_TIME=125ms) exist for RFC 6762 interop on real
|
|
52
|
+
networks. Tests on 127.0.0.1 do not need them and pay 1-2s per
|
|
53
|
+
register/unregister cycle without this fixture. Opt in by adding
|
|
54
|
+
`quick_timing` to a test's argument list.
|
|
55
|
+
"""
|
|
56
|
+
with (
|
|
57
|
+
patch.object(_core, "_CHECK_TIME", 10),
|
|
58
|
+
patch.object(_core, "_REGISTER_TIME", 10),
|
|
59
|
+
patch.object(_core, "_UNREGISTER_TIME", 10),
|
|
60
|
+
):
|
|
61
|
+
yield
|
|
@@ -227,10 +227,7 @@ class TestServiceBrowser(unittest.TestCase):
|
|
|
227
227
|
generated = r.DNSOutgoing(const._FLAGS_QR_RESPONSE)
|
|
228
228
|
assert generated.is_response() is True
|
|
229
229
|
|
|
230
|
-
if service_state_change == r.ServiceStateChange.Removed
|
|
231
|
-
ttl = 0
|
|
232
|
-
else:
|
|
233
|
-
ttl = 120
|
|
230
|
+
ttl = 0 if service_state_change == r.ServiceStateChange.Removed else 120
|
|
234
231
|
|
|
235
232
|
generated.add_answer_at_time(
|
|
236
233
|
r.DNSText(
|
|
@@ -1575,9 +1572,8 @@ async def test_close_zeroconf_without_browser_before_start_up_queries():
|
|
|
1575
1572
|
registration_name = f"xxxyyy.{type_}"
|
|
1576
1573
|
|
|
1577
1574
|
def on_service_state_change(zeroconf, service_type, state_change, name):
|
|
1578
|
-
if name == registration_name:
|
|
1579
|
-
|
|
1580
|
-
service_added.set()
|
|
1575
|
+
if name == registration_name and state_change is ServiceStateChange.Added:
|
|
1576
|
+
service_added.set()
|
|
1581
1577
|
|
|
1582
1578
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
1583
1579
|
zeroconf_browser = aiozc.zeroconf
|
|
@@ -1644,9 +1640,8 @@ async def test_close_zeroconf_without_browser_after_start_up_queries():
|
|
|
1644
1640
|
registration_name = f"xxxyyy.{type_}"
|
|
1645
1641
|
|
|
1646
1642
|
def on_service_state_change(zeroconf, service_type, state_change, name):
|
|
1647
|
-
if name == registration_name:
|
|
1648
|
-
|
|
1649
|
-
service_added.set()
|
|
1643
|
+
if name == registration_name and state_change is ServiceStateChange.Added:
|
|
1644
|
+
service_added.set()
|
|
1650
1645
|
|
|
1651
1646
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
1652
1647
|
zeroconf_browser = aiozc.zeroconf
|
|
@@ -1050,12 +1050,12 @@ def test_request_timeout():
|
|
|
1050
1050
|
"""Test that the timeout does not throw an exception and finishes close to the actual timeout."""
|
|
1051
1051
|
zeroconf = r.Zeroconf(interfaces=["127.0.0.1"])
|
|
1052
1052
|
start_time = r.current_time_millis()
|
|
1053
|
-
assert zeroconf.get_service_info("_notfound.local.", "notthere._notfound.local.") is None
|
|
1053
|
+
assert zeroconf.get_service_info("_notfound.local.", "notthere._notfound.local.", timeout=200) is None
|
|
1054
1054
|
end_time = r.current_time_millis()
|
|
1055
1055
|
zeroconf.close()
|
|
1056
|
-
#
|
|
1056
|
+
# 200ms for the timeout passed above
|
|
1057
1057
|
# 1000ms for loaded systems + schedule overhead
|
|
1058
|
-
assert (end_time - start_time) <
|
|
1058
|
+
assert (end_time - start_time) < 200 + 1000
|
|
1059
1059
|
|
|
1060
1060
|
|
|
1061
1061
|
@pytest.mark.asyncio
|
|
@@ -1888,7 +1888,7 @@ async def test_unicast_flag_if_requested() -> None:
|
|
|
1888
1888
|
# patch the zeroconf send
|
|
1889
1889
|
with patch.object(aiozc.zeroconf, "async_send", async_send):
|
|
1890
1890
|
await aiozc.async_get_service_info(
|
|
1891
|
-
f"willnotbefound.{type_}", type_, question_type=r.DNSQuestionType.QU
|
|
1891
|
+
f"willnotbefound.{type_}", type_, timeout=200, question_type=r.DNSQuestionType.QU
|
|
1892
1892
|
)
|
|
1893
1893
|
|
|
1894
1894
|
await aiozc.async_close()
|
|
@@ -260,7 +260,7 @@ async def test_async_service_registration_with_server_missing() -> None:
|
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
@pytest.mark.asyncio
|
|
263
|
-
async def test_async_service_registration_same_server_different_ports() -> None:
|
|
263
|
+
async def test_async_service_registration_same_server_different_ports(quick_timing: None) -> None:
|
|
264
264
|
"""Test registering services with the same server with different srv records."""
|
|
265
265
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
266
266
|
type_ = "_test1-srvc-type._tcp.local."
|
|
@@ -327,7 +327,7 @@ async def test_async_service_registration_same_server_different_ports() -> None:
|
|
|
327
327
|
|
|
328
328
|
|
|
329
329
|
@pytest.mark.asyncio
|
|
330
|
-
async def test_async_service_registration_same_server_same_ports() -> None:
|
|
330
|
+
async def test_async_service_registration_same_server_same_ports(quick_timing: None) -> None:
|
|
331
331
|
"""Test registering services with the same server with the exact same srv record."""
|
|
332
332
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
333
333
|
type_ = "_test1-srvc-type._tcp.local."
|
|
@@ -468,7 +468,7 @@ async def test_async_service_registration_name_does_not_match_type() -> None:
|
|
|
468
468
|
|
|
469
469
|
|
|
470
470
|
@pytest.mark.asyncio
|
|
471
|
-
async def test_async_service_registration_name_strict_check() -> None:
|
|
471
|
+
async def test_async_service_registration_name_strict_check(quick_timing: None) -> None:
|
|
472
472
|
"""Test registering services throws when the name does not comply."""
|
|
473
473
|
zc = Zeroconf(interfaces=["127.0.0.1"])
|
|
474
474
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
@@ -824,7 +824,7 @@ async def test_service_browser_cancel_async_context_manager():
|
|
|
824
824
|
|
|
825
825
|
|
|
826
826
|
@pytest.mark.asyncio
|
|
827
|
-
async def test_async_unregister_all_services() -> None:
|
|
827
|
+
async def test_async_unregister_all_services(quick_timing: None) -> None:
|
|
828
828
|
"""Test unregistering all services."""
|
|
829
829
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
830
830
|
type_ = "_test1-srvc-type._tcp.local."
|
|
@@ -870,8 +870,8 @@ async def test_async_unregister_all_services() -> None:
|
|
|
870
870
|
_clear_cache(aiozc.zeroconf)
|
|
871
871
|
|
|
872
872
|
tasks = []
|
|
873
|
-
tasks.append(aiozc.async_get_service_info(type_, registration_name))
|
|
874
|
-
tasks.append(aiozc.async_get_service_info(type_, registration_name2))
|
|
873
|
+
tasks.append(aiozc.async_get_service_info(type_, registration_name, timeout=200))
|
|
874
|
+
tasks.append(aiozc.async_get_service_info(type_, registration_name2, timeout=200))
|
|
875
875
|
results = await asyncio.gather(*tasks)
|
|
876
876
|
assert results[0] is None
|
|
877
877
|
assert results[1] is None
|
|
@@ -883,7 +883,7 @@ async def test_async_unregister_all_services() -> None:
|
|
|
883
883
|
|
|
884
884
|
|
|
885
885
|
@pytest.mark.asyncio
|
|
886
|
-
async def test_async_zeroconf_service_types():
|
|
886
|
+
async def test_async_zeroconf_service_types(quick_timing: None) -> None:
|
|
887
887
|
type_ = "_test-srvc-type._tcp.local."
|
|
888
888
|
name = "xxxyyy"
|
|
889
889
|
registration_name = f"{name}.{type_}"
|
|
@@ -1276,12 +1276,15 @@ async def test_async_request_timeout():
|
|
|
1276
1276
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
1277
1277
|
await aiozc.zeroconf.async_wait_for_start()
|
|
1278
1278
|
start_time = current_time_millis()
|
|
1279
|
-
assert
|
|
1279
|
+
assert (
|
|
1280
|
+
await aiozc.async_get_service_info("_notfound.local.", "notthere._notfound.local.", timeout=200)
|
|
1281
|
+
is None
|
|
1282
|
+
)
|
|
1280
1283
|
end_time = current_time_millis()
|
|
1281
1284
|
await aiozc.async_close()
|
|
1282
|
-
#
|
|
1285
|
+
# 200ms for the timeout passed above
|
|
1283
1286
|
# 1000ms for loaded systems + schedule overhead
|
|
1284
|
-
assert (end_time - start_time) <
|
|
1287
|
+
assert (end_time - start_time) < 200 + 1000
|
|
1285
1288
|
|
|
1286
1289
|
|
|
1287
1290
|
@pytest.mark.asyncio
|
|
@@ -12,6 +12,7 @@ import time
|
|
|
12
12
|
import unittest
|
|
13
13
|
import unittest.mock
|
|
14
14
|
import warnings
|
|
15
|
+
from pathlib import Path
|
|
15
16
|
from typing import cast
|
|
16
17
|
from unittest.mock import AsyncMock, Mock, patch
|
|
17
18
|
|
|
@@ -83,6 +84,40 @@ class Framework(unittest.TestCase):
|
|
|
83
84
|
rv.close()
|
|
84
85
|
rv.close()
|
|
85
86
|
|
|
87
|
+
def test_close_releases_owned_event_loop(self):
|
|
88
|
+
"""Closing a Zeroconf that started its own loop thread closes that loop.
|
|
89
|
+
|
|
90
|
+
Regression test for issue #1589 — without loop.close(), the selector
|
|
91
|
+
(epoll on Linux) and its self-pipe sockets stay open across each
|
|
92
|
+
Zeroconf construct/close cycle and the process eventually exhausts
|
|
93
|
+
its FD limit.
|
|
94
|
+
"""
|
|
95
|
+
rv = r.Zeroconf(interfaces=["127.0.0.1"])
|
|
96
|
+
loop = rv.loop
|
|
97
|
+
assert loop is not None
|
|
98
|
+
assert loop.is_running()
|
|
99
|
+
rv.close()
|
|
100
|
+
assert loop.is_closed()
|
|
101
|
+
|
|
102
|
+
@unittest.skipUnless(sys.platform.startswith("linux"), "Requires /proc/<pid>/fd")
|
|
103
|
+
@unittest.skipUnless(Path(f"/proc/{os.getpid()}/fd").is_dir(), "/proc/<pid>/fd not available")
|
|
104
|
+
def test_close_does_not_leak_file_descriptors(self):
|
|
105
|
+
"""Tight loops of Zeroconf()/close() do not leak FDs (issue #1589)."""
|
|
106
|
+
fd_dir = Path(f"/proc/{os.getpid()}/fd")
|
|
107
|
+
|
|
108
|
+
def _fd_count() -> int:
|
|
109
|
+
return sum(1 for _ in fd_dir.iterdir())
|
|
110
|
+
|
|
111
|
+
# Warm-up cycle so any one-shot import-time FDs land before measuring.
|
|
112
|
+
r.Zeroconf(interfaces=["127.0.0.1"]).close()
|
|
113
|
+
baseline = _fd_count()
|
|
114
|
+
for _ in range(10):
|
|
115
|
+
r.Zeroconf(interfaces=["127.0.0.1"]).close()
|
|
116
|
+
# Allow tiny slack for unrelated FDs the test harness may open
|
|
117
|
+
# (e.g. coverage), but reject the per-cycle linear growth pattern
|
|
118
|
+
# the bug produced (~3 FDs per cycle, so >=30 over 10 cycles).
|
|
119
|
+
assert _fd_count() - baseline < 10
|
|
120
|
+
|
|
86
121
|
@unittest.skipIf(not has_working_ipv6(), "Requires IPv6")
|
|
87
122
|
@unittest.skipIf(os.environ.get("SKIP_IPV6"), "IPv6 tests disabled")
|
|
88
123
|
def test_launch_and_close_v4_v6(self):
|
|
@@ -721,7 +756,7 @@ async def test_multiple_sync_instances_stared_from_async_close():
|
|
|
721
756
|
await asyncio.sleep(0)
|
|
722
757
|
|
|
723
758
|
|
|
724
|
-
def test_shutdown_while_register_in_process():
|
|
759
|
+
def test_shutdown_while_register_in_process(quick_timing: None) -> None:
|
|
725
760
|
"""Test we can shutdown while registering a service in another thread."""
|
|
726
761
|
|
|
727
762
|
# instantiate a zeroconf instance
|
|
@@ -753,11 +788,10 @@ def test_shutdown_while_register_in_process():
|
|
|
753
788
|
|
|
754
789
|
|
|
755
790
|
@pytest.mark.asyncio
|
|
756
|
-
@patch("zeroconf._core._STARTUP_TIMEOUT", 0)
|
|
757
791
|
@patch("zeroconf._core.AsyncEngine._async_setup", new_callable=AsyncMock)
|
|
758
792
|
async def test_event_loop_blocked(mock_start):
|
|
759
793
|
"""Test we raise NotRunningException when waiting for startup that times out."""
|
|
760
794
|
aiozc = AsyncZeroconf(interfaces=["127.0.0.1"])
|
|
761
795
|
with pytest.raises(NotRunningException):
|
|
762
|
-
await aiozc.zeroconf.async_wait_for_start()
|
|
796
|
+
await aiozc.zeroconf.async_wait_for_start(timeout=0)
|
|
763
797
|
assert aiozc.zeroconf.started is False
|
|
@@ -76,7 +76,7 @@ class TestDunder(unittest.TestCase):
|
|
|
76
76
|
def test_dns_question_repr(self):
|
|
77
77
|
question = r.DNSQuestion("irrelevant", const._TYPE_SRV, const._CLASS_IN | const._CLASS_UNIQUE)
|
|
78
78
|
repr(question)
|
|
79
|
-
assert
|
|
79
|
+
assert (question != question) is False
|
|
80
80
|
|
|
81
81
|
def test_dns_service_repr(self):
|
|
82
82
|
service = r.DNSService(
|
|
@@ -112,7 +112,7 @@ class TestDunder(unittest.TestCase):
|
|
|
112
112
|
addresses=[socket.inet_aton("10.0.1.2")],
|
|
113
113
|
)
|
|
114
114
|
|
|
115
|
-
assert
|
|
115
|
+
assert (info != info) is False
|
|
116
116
|
repr(info)
|
|
117
117
|
|
|
118
118
|
def test_service_info_text_properties_not_given(self):
|
|
@@ -123,19 +123,25 @@ async def test_run_coro_with_timeout() -> None:
|
|
|
123
123
|
"""Test running a coroutine with a timeout raises EventLoopBlocked."""
|
|
124
124
|
loop = asyncio.get_event_loop()
|
|
125
125
|
task: asyncio.Task | None = None
|
|
126
|
+
task_created = asyncio.Event()
|
|
126
127
|
|
|
127
128
|
async def _saved_sleep_task():
|
|
128
129
|
nonlocal task
|
|
129
|
-
task = asyncio.create_task(asyncio.sleep(
|
|
130
|
-
|
|
130
|
+
task = asyncio.create_task(asyncio.sleep(1))
|
|
131
|
+
task_created.set()
|
|
131
132
|
await task
|
|
132
133
|
|
|
133
134
|
def _run_in_loop():
|
|
134
|
-
aioutils.run_coro_with_timeout(_saved_sleep_task(), loop,
|
|
135
|
+
aioutils.run_coro_with_timeout(_saved_sleep_task(), loop, 50)
|
|
135
136
|
|
|
136
137
|
with pytest.raises(EventLoopBlocked), patch.object(aioutils, "_LOADED_SYSTEM_TIMEOUT", 0.0):
|
|
137
138
|
await loop.run_in_executor(None, _run_in_loop)
|
|
138
139
|
|
|
140
|
+
# The outer .result() can raise EventLoopBlocked before the loop
|
|
141
|
+
# has scheduled the coroutine — wait until the inner task is
|
|
142
|
+
# created before asserting on it. Use an asyncio.Event so the
|
|
143
|
+
# wait yields back to the loop instead of blocking it.
|
|
144
|
+
await asyncio.wait_for(task_created.wait(), 1.0)
|
|
139
145
|
assert task is not None
|
|
140
146
|
# ensure the thread is shutdown
|
|
141
147
|
task.cancel()
|
|
@@ -357,7 +357,7 @@ def test_create_sockets_interfaces_all_unicast():
|
|
|
357
357
|
mock_socket = Mock(spec=socket.socket)
|
|
358
358
|
mock_new_socket.return_value = mock_socket
|
|
359
359
|
|
|
360
|
-
listen_socket,
|
|
360
|
+
listen_socket, _respond_sockets = r.create_sockets(
|
|
361
361
|
interfaces=r.InterfaceChoice.All, unicast=True, ip_version=r.IPVersion.All
|
|
362
362
|
)
|
|
363
363
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|