[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/python-zeroconf/python-zeroconf/master/CLAUDE.md [Back]  [Original]

# 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](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 PR title, lowercase subject.** PRs are
  squash-merged, so the **PR title** becomes the commit on
  `master` and is the only string that has to parse as a
  Conventional Commit. The repo enforces this via the `pr-title`
  CI job in `ci.yml` using `amannn/action-semantic-pull-request`.
  Accepted types: `feat`, `fix`, `chore`, `ci`, `docs`,
  `refactor`, `test`, `perf`, `build`, etc. The subject (text
  after `type(scope):`) must start lowercase (enforced by
  `subjectPattern: ^(?![A-Z]).+$`). Per-commit messages on the
  PR branch are **not** linted; they get collapsed at squash-
  merge. `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

```bash
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][gh-report], not public issues or pull
requests. The policy is spelled out in [SECURITY.md](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.

[gh-report]: https://github.com/python-zeroconf/python-zeroconf/security/advisories/new

## 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 PR title that violates Conventional
  Commits.** The `pr-title` 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.

Web Proxy Viewer  |  New URL  |  Original Page