Skip to content

Latest commit

 

History

History
283 lines (245 loc) · 14.8 KB

File metadata and controls

283 lines (245 loc) · 14.8 KB

Notes for LLM contributors

A short orientation file for an LLM working in this repo. Skim before making changes; keep edits consistent with what's described here. Read README.rst for the user-facing intro.

What this project is

python-zeroconf is a pure-Python implementation of multicast DNS service discovery (mDNS / DNS-SD, RFC 6762 + RFC 6763). It is the mDNS engine behind Home Assistant and a long list of other Python projects that need to announce or discover services on the local network. Public API is exported from the top-level zeroconf package; an async API lives at zeroconf.asyncio.

There is no external protocol owner — the on-the-wire format is the mDNS / DNS-SD RFCs. Behaviour changes that affect packet contents or timing should cite the relevant RFC section.

Hot paths (_cache, _dns, _history, _listener, _record_update, _updates, _protocol/{incoming,outgoing}, _handlers/*, _services/*, _utils/{ipaddress,time}) are Cythonized at build time for throughput. They keep working as pure Python — SKIP_CYTHON=1 disables the extension build — but production wheels ship compiled and CodSpeed benchmarks track that path. The authoritative list of cythonized modules lives in build_ext.py (TO_CYTHONIZE).

Code style

  • Docstrings: terse, default to single-line. A docstring is the function's contract, not its narrative. Almost every docstring should be one line — """Summary.""" — describing what the function does and what the caller can pass. Multi-line is the exception, only justified when there is non-obvious caller-visible behaviour the type signature and parameter names don't already convey.

    What does NOT belong in docstrings or comments:

    • Rationale / motivation / "why we used to do X" — that's the PR description and the commit message. Git already remembers.
    • Cross-references to issue numbers ("closes #N", "follow-up to #M") — the PR body carries those.
    • Restatement of the function body in prose. If the next line of the docstring is just describing what the next line of code does, delete the docstring line.
    • Test docstrings retelling the production-side story. A test docstring should name what the test pins, in one sentence — not re-explain the bug, the fix, or the surrounding flow.
  • Comments: same bar. Default to writing no comments. Add one only when the why is non-obvious: a hidden constraint, a subtle invariant, a workaround for a specific bug, behaviour that would surprise a reader. RFC citations are useful when the reason for a timing constant or framing decision is "the spec says so" — leave those in. If removing the comment wouldn't confuse a future reader, don't write it.

    Don't remove existing comments unless the code they describe is gone — the original author left them for a reason.

  • Don't pad commits, docstrings, or comments with cross- references to old codepaths or issue numbers unless there's a clear reason a future reader needs that link.

  • Method order: public API at the top, private helpers (_underscore_prefixed) at the bottom. Modules whose names start with _ (_cache, _dns, _handlers/, etc.) are internal; the supported surface is what zeroconf/__init__.py and zeroconf/asyncio.py re-export.

  • Line length: 110 (ruff line-length = 110). requires-python = ">=3.10", target-version = "py310" for ruff; pyupgrade runs --py310-plus.

  • Imports: ruff/isort sorted, profile = "black", known_first_party = ["zeroconf", "tests"]. Prefer from __future__ import annotations so modern type syntax works on 3.10.

  • Generated .c files are not lint-targets. *.c files next to each cythonized module are Cython output — never hand- edit them. They are excluded from sdist (exclude = ["**/*.c"] in pyproject.toml) and regenerated by the build.

Commit / PR conventions

  • Conventional Commits are enforced. CI runs commitlint with @commitlint/config-conventional, and pre-commit runs commitizen on the commit message. The header has no length cap (header-max-length = [0, "always", Infinity]), but the type prefix is required: feat:, fix:, chore:, ci:, docs:, refactor:, test:, perf:, build:, etc. semantic-release excludes chore* and ci* from the changelog, so use those prefixes for housekeeping and reserve feat/fix/perf for user-visible changes.
  • No Co-Authored-By trailers from automated agents. Project preference.
  • Imperative-mood subject after the type prefix ("fix: handle empty answer", not "fix: handled empty answer").
  • There is no .github/PULL_REQUEST_TEMPLATE.md in this repo — the PR body is free-form. The pr-workflow skill (under .claude/skills/pr-workflow/) walks through the conventions that do apply: conventional-commit subject, RFC citations for protocol-affecting changes, a test-plan section.
  • Pre-commit runs ruff (lint + format), mypy, flake8, codespell, cython-lint, and pyupgrade. Run pre-commit locally before pushing; the CI lint job is just pre-commit/action, so a green local pre-commit run = a green CI lint job.

Running tests

poetry run pytest --durations=20 --timeout=60 -v tests

…or make test, which runs the same command. Test discovery defaults from pyproject.toml already pass --cov=zeroconf and pythonpath = ["src"]. pytest-asyncio is used in the default per-test mode (no auto mode); async tests are marked explicitly with @pytest.mark.asyncio.

CodSpeed benchmarks live under tests/benchmarks/ and run in CI through CodSpeedHQ/action. Ad-hoc microbenchmarks for manual profiling live under bench/ — those don't run in CI.

The CI matrix includes CPython 3.10 – 3.14, the free-threaded 3.14t build, and PyPy 3.10. Don't add anything that breaks on the free-threaded build (no module-level mutable globals mutated from multiple threads without locks; no PyDict_Next-style escape hatches in Cython).

Build conventions

  • Cython is optional but expected in wheels. build_ext.py cythonizes every module listed in TO_CYTHONIZE. The build is driven by poetry-core (generate-setup-file = true, script = "build_ext.py"); BuildExt.build_extensions swallows build failures so source installs fall back to pure Python. SKIP_CYTHON=1 skips the Cython step entirely; REQUIRE_CYTHON=1 re-raises so a missing extension fails the build loudly (CI wheel builds use this).
  • Modules that get Cythonized ship a sibling .pxd for type declarations. When changing the signature of a Cythonized function — or adding a new attribute to a cdef class — update the .pxd in the same commit, or the extension will pick up a stale declaration and the in-tree .c will be regenerated with the wrong layout.
  • Adding a new module to TO_CYTHONIZE is a deliberate decision: the module must be hot enough to matter, must not rely on Python-only constructs that Cython refuses (the existing PERF401, PYI032, PYI041 ruff ignores exist because Cython rejects closures and PEP 604 unions in cpdef), and must stay free-threading-safe.
  • compiler_directives = {"language_level": "3"}. The build pipeline does not currently set freethreading_compatible, but the test matrix exercises 3.14t, so any new Cython module needs to keep working there.

Cython gotchas

Non-obvious traps in the .py + .pxd setup that work fine in pure-Python mode but break or silently misbehave in the shipped Cython wheels. Distilled from the patterns that already exist in this repo's .pxd files and from incidents in sibling Cython- accelerated projects.

  • cdef-typed module constants are not Python-importable. Declaring cdef unsigned int _ANSWER_STRATEGY_POINTER in query_handler.pxd makes Cython treat _ANSWER_STRATEGY_POINTER = 1 in query_handler.py as a C int assignment; the Python module dict never gets the binding. from zeroconf._handlers.query_handler import _ANSWER_STRATEGY_POINTER succeeds in pure-Python but raises ImportError under Cython. If you need the value visible from Python (e.g. a test wants to assert on it), define both names — a public ANSWER_STRATEGY_POINTER = 1 Python binding plus a cdef-typed _ANSWER_STRATEGY_POINTER = ANSWER_STRATEGY_POINTER alias for hot-path comparisons.

  • Match the existing unsigned int convention for length, TTL, type/class, and offset fields. _protocol/incoming.pxd, _cache.pxd, and _handlers/*.pxd already declare these as unsigned int end-to-end. Introducing a cdef int return that carries a value originally decoded into unsigned int flips sign for any value with bit 31 set — TTL is a 32-bit DNS field (RFC 1035 §3.2.1, interpreted as unsigned), so a large TTL passed back through a cdef int boundary becomes negative and trips < 0 sentinel branches. Stay with unsigned int across the whole call chain; if you need a real sentinel, return an explicit value (UINT_MAX, a dedicated constant) and check for it by equality.

  • Module-level Python int constants force PyLong_AsLong on every hot-path comparison. if record.type == _TYPE_PTR compiles to a Python attribute lookup + PyLong_AsLong per call when _TYPE_PTR is just a .py-level binding. The repo already follows the right pattern — _cache.pxd / record_manager.pxd declare cdef unsigned int _TYPE_PTR, _DNS_PTR_MIN_TTL, _MIN_SCHEDULED_RECORD_EXPIRATION, etc. When adding a new size / TTL / type constant from const.py to a cdef hot path in _protocol/, _cache, _handlers/, or _listener, add the cdef-typed alias to the corresponding .pxd at the same time.

  • Sign-compare warnings in generated C are real. gcc/ clang warns when comparing unsigned int with int because the signed value is implicitly converted to unsigned for the compare — a negative value becomes a huge positive. Match the signedness of compared operands in the .pxd (e.g. if the local is unsigned int, declare the constant as cdef unsigned int; if the local is int, declare it cdef int). The warning predicts the unsigned -> signed overflow class of bug.

  • CodSpeed regressions only show up in the Cython build. Pure-Python (SKIP_CYTHON=1) tests can pass while production wire-format hot paths regress. Trust the CodSpeed check on PRs that touch any file in TO_CYTHONIZE; rebuild in place with REQUIRE_CYTHON=1 poetry install --only=main,dev before pushing if perf-sensitive code changed.

Reporting security issues

Suspected security vulnerabilities go through GitHub's private vulnerability reporting, not public issues or pull requests. The policy is spelled out in SECURITY.md. If a user describes what sounds like a vulnerability in chat, point them at that route instead of opening a public issue, PR, or commit that names the bug class and the affected code path.

Useful entry points

Path What
src/zeroconf/__init__.py Public package — re-exports Zeroconf, ServiceBrowser, ServiceInfo, etc.
src/zeroconf/asyncio.py Async API: AsyncZeroconf, AsyncServiceBrowser, AsyncZeroconfServiceTypes
src/zeroconf/_core.py Zeroconf core — socket setup, send/recv loop, registration/probing
src/zeroconf/_engine.py Asyncio engine driving the listener
src/zeroconf/_listener.py Cython-accelerated packet listener
src/zeroconf/_cache.py DNS record cache (Cythonized)
src/zeroconf/_dns.py DNS record / question classes (Cythonized)
src/zeroconf/_history.py Outgoing-question history for known-answer suppression
src/zeroconf/_record_update.py Record-update dataclass passed to listeners
src/zeroconf/_protocol/ DNSIncoming / DNSOutgoing wire codec (Cythonized)
src/zeroconf/_handlers/ Query / answer / multicast queueing (Cythonized)
src/zeroconf/_services/ ServiceBrowser, ServiceInfo, ServiceRegistry, types
src/zeroconf/_updates.py RecordUpdateListener base class (Cythonized)
src/zeroconf/_utils/ ipaddress, time, net, name, asyncio helpers
src/zeroconf/const.py Timeouts, intervals, multicast group constants
src/zeroconf/_exceptions.py Public exception hierarchy
tests/ Pytest suite
tests/benchmarks/ CodSpeed benchmarks
bench/ Manual microbenchmarks (not run in CI)
build_ext.py TO_CYTHONIZE list + poetry-core build hook

Things not to do

  • Don't hand-edit the generated .c files next to Cythonized modules. They are build output; modify the .py (and .pxd) and let Cython regenerate.
  • Don't change a Cythonized module's cdef class layout or a cpdef/cdef signature without updating its .pxd — the extension build will silently pick up a stale declaration and the resulting wheel will crash at import time.
  • Don't add Co-Authored-By trailers from automated agents to commits in this repo.
  • Don't introduce a commit message that violates Conventional Commits. The commitlint job will fail the PR.
  • Don't tighten timings or constants in const.py without an RFC citation in the commit message. mDNS interop with Avahi / Bonjour / Windows hinges on those numbers.
  • Don't bypass BuildExt's exception swallowing in build_ext.py without thought. Pure-Python fallback is a feature for source installs on platforms without a compiler (and for the PyPy matrix entries, which never load the C extensions).
  • Don't break the free-threaded test matrix entry (3.14t). CPython 3.14t exercises this code without the GIL; module- level mutable state and unguarded cross-thread Cython attribute access will surface as flakiness there before anywhere else.