Skip to content

feat(typing): achieve full strict typing compliance across OWNd (mypy --strict) - #36

Merged
GreenGrassBlueOcean merged 4 commits into
OpenWebNet-HA:feat/session-is-openfrom
GreenGrassBlueOcean:feat/strict-typing
Sep 16, 2026
Merged

GreenGrassBlueOcean merged 4 commits into
OpenWebNet-HA:feat/session-is-openfrom
GreenGrassBlueOcean:feat/strict-typing

Conversation

@GreenGrassBlueOcean

@GreenGrassBlueOcean GreenGrassBlueOcean commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR completes the full strict typing migration (mypy --strict) across all modules of OWNd, satisfying Home Assistant Platinum quality scale standards (strict-typing) and providing complete type safety for library consumers (including MyHOME).

Note

Stacked on #35. This PR's base is feat/session-is-open (a mirror of #35's head, 54f61e9, on the org repo), so the diff and commit list here are the typing work only. It lands after #35, on its own: when #35 merges and that mirror branch is deleted, GitHub retargets this PR to master automatically. Nothing else is stacked on it.

Key Changes

  1. Strict Type Annotations Across All Modules:

    • OWNd/connection.py: Fully typed OWNSession, OWNEventSession, OWNDiscoverySession, and OWNGateway. Added type narrowing for HMAC/legacy authentication nonces, proper optionality handling on socket streams and callbacks, and precise return types.
    • OWNd/discovery.py: Annotated scan(), SSDP, and mDNS discovery packet parsing, interfaces, and dispatchers.
    • OWNd/__main__.py: Annotated CLI entry points and argument parsers.
    • OWNd/message.py:
      • Systematically annotated all 13 Event subclasses (OWNScenarioEvent, OWNLightingEvent, OWNAutomationEvent, OWNHeatingEvent, OWNAlarmEvent, OWNAuxEvent, OWNGatewayEvent, OWNCENEvent, OWNSceneEvent, OWNEnergyEvent, OWNDryContactEvent, OWNCENPlusEvent, OWNSoundEvent).
      • Systematically annotated all 10 Command subclasses (OWNStatusRequest, OWNLightingCommand, OWNAutomationCommand, OWNHeatingCommand, OWNAlarmCommand, OWNAVCommand, OWNGatewayCommand, OWNEnergyCommand, OWNSoundCommand, OWNDryContactCommand).
      • Replaced implicit dynamic attributes with explicit typed attributes, strongly typed where, what, dimension, and parameter dictionaries.
  2. Strict Mypy Configuration (pyproject.toml):

    • Enabled strict = true and warn_unused_configs = true.
    • Removed all 9 disabled error codes (arg-type, assignment, index, operator, return, return-value, truthy-function, union-attr, var-annotated).
    • Removed implicit_optional = true.
    • Configured clean module overrides for third-party libraries lacking type stubs (dateutil.*, defusedxml.*, serial_asyncio.*).
  3. Packaging & Verification:

    • Confirmed OWNd/py.typed is present and packaged in wheel and sdist distributions.
    • All 5 library standards passed via scripts/verify_library_standards.py.

Validation

  • mypy OWNd: 0 errors across all 10 source files.
  • mypy --strict OWNd: 0 errors across all 10 source files.
  • pytest tests: 627 passed (100% pass rate).
  • Test Coverage: 99.5% statement coverage.
  • ruff check: All checks passed.

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Interstellar0verdrive

Copy link
Copy Markdown
Member

Passing on this one: a strict-typing migration across the parser and the connection is outside what I can judge, and the risk in a change this size isn't a wrong annotation, it's behaviour that shifts by accident. That needs someone who can run the conformance corpus and read the diff as code. @xtimmy86x or @fedem95 would be the right eyes here, or @mvalla if he's willing, since the golden vectors are partly his.

I'll say the one thing I can: OWNd is what every installation runs, including the 0.7.x line my own house is on, so I'd want this to land on its own, after #35, rather than stacked under anything else.

@Interstellar0verdrive
Interstellar0verdrive removed their request for review September 16, 2026 12:21
@GreenGrassBlueOcean

GreenGrassBlueOcean commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

@Interstellar0verdrive — fair call, and the right one: the risk in a diff this size is behaviour that moves by accident, so let me put the evidence you'd want a reviewer to have in front of them, and then turn your request into something that runs on its own.

1. The diff is a third of what GitHub shows

connection.py on master has mixed CRLF/LF endings and these branches normalised it, so whole unchanged blocks render as rewritten. Real change, whitespace and line endings ignored:

as rendered actual
#35 +248 / −218 +38 / −8 (before today's fix)
#36 +691 / −615 +324 / −248

Both read as intended with whitespace hidden: #35 ?w=1, #36 ?w=1. I've left the history as is so @xtimmy86x's review hashes stay valid; #38 (draft, lands after this one) adds the .gitattributes and renormalises master so it cannot recur.

2. What #36 changes, read as code

Annotations, plus three kinds of guard on paths where the value could already not be None:

  • nonce is not None after is_nonce()nonce is set by the same regex match that makes is_nonce() true;
  • _where is not None in group / area — both are gated on is_group / is_area, which need a _where;
  • str(where) in the heating/energy/sound builders — accepts an int where a str was assumed; identical output for str input.

No control-flow change, no frame text change. pyproject.toml only tightens mypy. The one early return that looks behavioural — _validate_gateway_clock_values on dimension is None — isn't: the table lookup it guards already returned None for that key, so the function fell through to the same place.

3. The conformance corpus, run

Against the #35 + #36 stack, on a clean checkout of MyHOME v2-phase1-architecture:

  • golden corpus, 255 vectors (Legrand PDFs as judge, openwebnet4j as oracle): 255 / 255, same as on 2.0.0b6;
  • MyHOME's full suite (1599 tests, core 2026.9.2 / Python 3.14): 1599 / 1599;
  • OWNd's own suite: 630 / 630, mypy --strict clean.

The full suite is what found the two real problems, neither of them in #36: the retry flap @xtimmy86x reported on #35 (fixed in 54f61e9, three regression tests), and one MyHOME test that spied on close() by name (OpenWebNet-HA/MyHOME#372). It also found, on the MyHOME side, that the current v2-phase1-architecture head can't send a frame at all (OpenWebNet-HA/MyHOME#371) — unrelated to these PRs, but the same lesson: mocks agree with each other, only the real pairing tells the truth.

4. Your request, as CI

#37 adds two jobs to OWNd's CI, each with the PR's checkout installed in place of the release MyHOME pins: the golden corpus and smoke gates (about a minute), and MyHOME's full suite (about four). From then on "someone who can run the conformance corpus" is every PR, before anyone reads it.

5. Landing order and release risk

Agreed on both counts. #36 stays stacked on #35 only as GitHub sees it — #35 lands first and alone, #36 rebases and lands second, nothing else under either. On the 0.7.x line: MyHOME pins OWNd== exactly, so an OWNd release reaches no installation until a MyHOME release bumps the pin. A 2.0.0b7 cannot touch a house on 0.7.x, and a 2.0.0bN MyHOME is proven against it by #37 before that bump.

What would move this: @xtimmy86x, a second look at #35 with 54f61e9; @xtimmy86x / @fedem95 / @mvalla, a code read of #36 with ?w=1 and the three guard classes above as the checklist — the corpus has already done the frame-level part.

@xtimmy86x

Copy link
Copy Markdown
Contributor

Thanks for the typing work. I checked commit 9c122ec: CI is green, and my local comparison against the MyHOME corpus found no regressions across 253 checks covering 106 frames.

There is one typing issue I would like addressed before approving: OWNCommandSession.send() now accepts only str, although passing an OWNCommand object remains supported at runtime and is already used by MyHOME.

For example:

await session.send(OWNLightingCommand.switch_on("11"))

Mypy rejects this with:

Argument 1 to "send" has incompatible type "OWNLightingCommand"; expected "str"

Could you update send() and the related signatures to accept both raw strings and command objects, and add a small consumer typing check covering both cases?

This is an annotation compatibility issue rather than a runtime failure, but it matters for consumers now that the library exposes its types through py.typed. Once corrected, I’m happy to recheck for approval.

…pass

Review of 9c122ec (OpenWebNet-HA#36, @xtimmy86x): OWNCommandSession.send(),
_locked_send() and send_to_gateway() were annotated `message: str`,
while the wire path is `str(message).encode()` and MyHOME passes
OWNCommand builders and OWNMessage.parse() results. With py.typed
shipped, that made a working call a type error for every consumer:

    Argument 1 to "send" has incompatible type "OWNLightingCommand";
    expected "str"

The three signatures now take `str | OWNMessage`.

tests/typing/consumer_send.py is the consumer's side of the contract:
the calls MyHOME really makes (raw frame, builder, parsed command,
parsed message, one-shot helper), checked by `mypy --strict` in CI's
quality job and by tests/test_typing_consumer.py under pytest. It
fails five ways on the previous annotation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

@xtimmy86x — agreed, and thanks for checking with the corpus. Fixed in 0fe2938.

SignaturesOWNCommandSession.send(), _locked_send() and send_to_gateway() now take message: str | OWNMessage. OWNMessage rather than OWNCommand, because MyHOME also passes OWNMessage.parse() results (services.py, the raw-frame service), and the wire path is str(message).encode() for any of them. The docstring says so.

Consumer typing checktests/typing/consumer_send.py: five calls MyHOME really makes (raw frame, builder, is_status_request=True, parsed command, parsed message, and both forms through send_to_gateway). It is checked two ways:

  • CI's quality job now runs mypy OWNd tests/typing;
  • tests/test_typing_consumer.py runs the same under pytest via mypy.api (skips if mypy is absent), so a plain test run notices too.

On the previous annotation it fails with exactly your message, five times; on 0fe2938 mypy is clean on 11 files, 631 tests pass, ruff clean, and MyHOME's golden corpus still 255/255 against the branch.

Rebased nothing — this is one commit on top of 9c122ec, so your earlier comparison still stands.

xtimmy86x
xtimmy86x previously approved these changes Sep 16, 2026

@xtimmy86x xtimmy86x left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked commit 0fe2938: my previous concern is resolved. The updated str | OWNMessage signatures correctly cover raw frames and command objects, and the consumer typing checks are now included in CI.

All six checks are green, including strict mypy validation of the library and consumer examples. The follow-up changes do not alter the runtime send logic.

No remaining blockers from my review. Approved, thanks for addressing this.

@GreenGrassBlueOcean
GreenGrassBlueOcean dismissed xtimmy86x’s stale review September 16, 2026 13:27

The merge-base changed after approval.

@xtimmy86x
xtimmy86x removed the request for review from fedem95 September 16, 2026 13:27
xtimmy86x
xtimmy86x previously approved these changes Sep 16, 2026
@xtimmy86x

Copy link
Copy Markdown
Contributor

@GreenGrassBlueOcean I submitted the approval again, but GitHub still blocks the merge, marking it as stale because the merge base changed.

The branch is currently 4 commits ahead and 1 behind master following the merge of #35. Could you sync it with the current master and let CI complete? Once that is done, please request my review again and I’ll recheck and approve.

My technical assessment remains positive; this is about resolving the review/merge-base state.

@GreenGrassBlueOcean
GreenGrassBlueOcean dismissed xtimmy86x’s stale review September 16, 2026 13:34

The merge-base changed after approval.

@GreenGrassBlueOcean
GreenGrassBlueOcean changed the base branch from master to feat/session-is-open September 16, 2026 13:55
@GreenGrassBlueOcean
GreenGrassBlueOcean merged commit e80dff3 into OpenWebNet-HA:feat/session-is-open Sep 16, 2026
6 checks passed
@GreenGrassBlueOcean
GreenGrassBlueOcean deleted the feat/strict-typing branch September 16, 2026 13:56
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/OWNd that referenced this pull request Sep 16, 2026
…pass

Review of 9c122ec (OpenWebNet-HA#36, @xtimmy86x): OWNCommandSession.send(),
_locked_send() and send_to_gateway() were annotated `message: str`,
while the wire path is `str(message).encode()` and MyHOME passes
OWNCommand builders and OWNMessage.parse() results. With py.typed
shipped, that made a working call a type error for every consumer:

    Argument 1 to "send" has incompatible type "OWNLightingCommand";
    expected "str"

The three signatures now take `str | OWNMessage`.

tests/typing/consumer_send.py is the consumer's side of the contract:
the calls MyHOME really makes (raw frame, builder, parsed command,
parsed message, one-shot helper), checked by `mypy --strict` in CI's
quality job and by tests/test_typing_consumer.py under pytest. It
fails five ways on the previous annotation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/OWNd that referenced this pull request Sep 16, 2026
…e, test)

master stored five files with CRLF - .gitignore, LICENSE, __main__.py,
connection.py, message.py - next to an LF majority, and connection.py
mixed the two within the file. Any edit from a differently configured
editor then rewrote the whole file: OpenWebNet-HA#35 rendered as +248/-218 for a
+38/-8 change, OpenWebNet-HA#36 as +691/-615 for +324/-248, and a reviewer passed
on both for that reason.

`* text=auto eol=lf`, `git add --renormalize .`, and
tests/test_line_endings.py so a CR that bypasses the clean filter fails
the suite instead of the next reviewer. That test found one such path
straight away: validate_corpus.py rewrote tests/golden/corpus.json with
the platform newline on every run, so any Windows checkout that ran the
suite had a CRLF corpus.json waiting to be committed. It now writes LF.

Every byte other than line endings is unchanged: `git diff
--ignore-cr-at-eol` shows only the two new files and the one-line
newline argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@GreenGrassBlueOcean

Copy link
Copy Markdown
Contributor Author

For the record: this merged into feat/session-is-open, not master — the base I had switched it to was a mirror of #35's head, and #35 had already merged to master by then, so the mirror was a dead end. My mistake. The same four commits, byte-identical tree, are on #39 against master; review and approval stay here. The mirror branch goes away once #39 is in.

xtimmy86x added a commit that referenced this pull request Sep 16, 2026
feat(typing): strict typing across OWNd — re-land of #36 on master
GreenGrassBlueOcean added a commit to GreenGrassBlueOcean/OWNd that referenced this pull request Sep 16, 2026
…e, test)

master stored five files with CRLF - .gitignore, LICENSE, __main__.py,
connection.py, message.py - next to an LF majority, and connection.py
mixed the two within the file. Any edit from a differently configured
editor then rewrote the whole file: OpenWebNet-HA#35 rendered as +248/-218 for a
+38/-8 change, OpenWebNet-HA#36 as +691/-615 for +324/-248, and a reviewer passed
on both for that reason.

`* text=auto eol=lf`, `git add --renormalize .`, and
tests/test_line_endings.py so a CR that bypasses the clean filter fails
the suite instead of the next reviewer. That test found one such path
straight away: validate_corpus.py rewrote tests/golden/corpus.json with
the platform newline on every run, so any Windows checkout that ran the
suite had a CRLF corpus.json waiting to be committed. It now writes LF.

Every byte other than line endings is unchanged: `git diff
--ignore-cr-at-eol` shows only the two new files and the one-line
newline argument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants