Skip to content

Migrate to cattrs for centralized model structuring#1994

Merged
iMicknl merged 33 commits intov2/mainfrom
v2/cattrs-models
Apr 24, 2026
Merged

Migrate to cattrs for centralized model structuring#1994
iMicknl merged 33 commits intov2/mainfrom
v2/cattrs-models

Conversation

@iMicknl
Copy link
Copy Markdown
Owner

@iMicknl iMicknl commented Apr 17, 2026

Summary

Alternative to #1991 — replaces per-model converter functions with a centralized cattrs Converter.

  • Adds pyoverkiz/converter.py with hooks for primitive unions, optional attrs classes, enums (with UnknownEnumMixin), and custom containers (States, CommandDefinitions)
  • Strips all converter infrastructure from models.py — models become pure @define(kw_only=True) data classes with only type annotations and defaults
  • Updates client.py to use converter.structure() instead of Model(**data)
  • Updates tests to use the converter for API-like dict structuring

Comparison with #1991

#1991 (model-simplifications) This PR (cattrs)
Converter location Per-model (_flexible_init, _to_list, etc.) Centralized converter.py
Model classes Still have some converter logic Pure data classes
Dependencies None new Adds cattrs
Forward references Eliminated by class reordering Not needed — cattrs resolves types
Splitting models later Requires careful import ordering No ordering constraints

Breaking Changes

  • ServerConfig.type renamed to ServerConfig.api_type.
  • Gateway.connectivity type changed to Connectivity | None (was non-optional).
  • Gateway.id and Place.id changed from mutable fields to read-only properties.
  • States.__getitem__ now raises KeyError instead of returning None — use States.get() for the old behavior.
  • CommandDefinitions.__getitem__ now raises KeyError instead of returning None — use CommandDefinitions.get() for the old behavior.
  • Event.setup_oid — field name unchanged (cattrs handles the setupOIDsetup_oid conversion automatically).
  • Execution.action_group type changed from list[Action] to ActionGroup | None.
  • ActionGroup.label default simplified (no longer falls back to empty string).

Test plan

@iMicknl iMicknl requested a review from tetienne as a code owner April 17, 2026 15:03
Copilot AI review requested due to automatic review settings April 17, 2026 15:03
@iMicknl iMicknl added enhancement New feature or request breaking v2 labels Apr 17, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates Overkiz API response structuring from per-model init/converter logic to a centralized cattrs converter, making pyoverkiz.models primarily plain attrs data classes and updating the client/tests to structure API-like dicts through the shared converter.

Changes:

  • Added a centralized pyoverkiz/converter.py with cattrs hooks (unknown key filtering, enums, unions, and custom containers).
  • Refactored pyoverkiz/models.py to remove most custom __init__ converter logic and rely on cattrs structuring + __attrs_post_init__ where needed.
  • Updated pyoverkiz/client.py and tests to use converter.structure(...); added cattrs as a dependency.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pyoverkiz/converter.py Introduces centralized cattrs converter and structuring hooks.
pyoverkiz/models.py Simplifies models/containers to attrs classes and post-init computed fields.
pyoverkiz/client.py Switches API parsing to converter.structure() across endpoints.
pyoverkiz/auth/factory.py Updates ServerConfig field rename usage (typeapi_type).
tests/test_models.py Updates model tests to structure API-like dicts via converter; adds container behavior tests.
tests/test_ui_profile.py Updates UI profile tests to use structured models and converter.
tests/test_client.py Updates tests for ServerConfig.api_type and Action kw-only initialization.
pyproject.toml Adds cattrs dependency.
uv.lock Locks cattrs and updates dependency graph accordingly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyoverkiz/models.py
Comment thread pyoverkiz/models.py
Comment thread pyoverkiz/models.py Outdated
Comment thread pyoverkiz/converter.py Outdated
Comment thread pyoverkiz/converter.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyoverkiz/models.py
Comment thread pyoverkiz/converter.py
iMicknl added 19 commits April 19, 2026 20:29
…ation defaults

- Replace manual __init__ methods with attrs converters/factories across
  all model classes, using _flexible_init decorator to handle unknown API keys.
- Rename ServerConfig.type to api_type to avoid shadowing the builtin.
  Update all references in client.py, auth/factory.py, and tests.
- Fix Location.__init__ bug where field() objects were used as parameter
  defaults instead of None (now fixed by removing manual __init__).
__getitem__ now raises KeyError on missing keys (standard Python mapping
protocol). get() returns None for missing keys. __contains__ uses explicit
name comparison instead of delegating to __getitem__.
Replace Any types with concrete types based on actual API payloads:
metadata -> str, deleted_raw_devices_count -> int, protocol_type -> int.
Both containers used O(n) linear scans for __contains__, __getitem__,
get, and select. These are called on hot paths during Home Assistant
polling cycles across many devices.
…init

Replace manual __init__ with declarative attrs fields and __attrs_post_init__.
The ui_class and widget fields are now public, resolved at construction time
from either the API kwargs or the definition fallback.
## Summary
- ActionGroup `label` field now defaults to `""` via a `_to_str`
converter instead of post-init fixup
- Simplifies `__attrs_post_init__` to only handle id/oid resolution

Depends on #1991 (model simplifications).

## Breaking changes
- `ActionGroup.label` type changed from `str | None` to `str` (always a
string, `None` input is converted to `""`)

## Test plan
- [ ] ActionGroup tests pass with label=None, label="", and
label="value"
The API returns `setupOID` which decamelizes to `setup_oid`, but the
field was named `setupoid`. This caused `_flexible_init` to silently
drop the value, making `Event.setup_oid` always None.
action_group uses _to_optional which can return None, but the type
annotation was non-optional. Also add start_time, execution_type and
execution_sub_type which are present in the API response but were
silently dropped by _flexible_init.
The field uses _to_optional converter which returns None for None input,
but the type annotation was non-optional Connectivity.
Definition.commands uses an inline lambda converter instead. This
function was defined but never referenced anywhere.
Reorder class definitions so dependencies are defined before dependents,
allowing converter helpers to reference actual classes instead of string
names. Setup is moved to the bottom since it references nearly every
other model.

The only remaining string forward reference is Place.sub_places which
is self-referential and unavoidable.

Also adds section comments to group models by domain: state/command
primitives, device/definition, execution/action groups, infrastructure,
configuration, and UI profiles.
Replace per-model converter functions (_flexible_init, _to_list,
_to_optional, _to_optional_enum) with a single cattrs Converter that
handles all dict-to-model structuring. Models become pure data classes
with only type annotations and defaults.

- Add cattrs dependency
- Add pyoverkiz/converter.py with hooks for primitive unions, optional
  attrs classes, enums, and container types (States, CommandDefinitions)
- Strip all converter infrastructure from models.py
- Update client.py to use converter.structure() instead of Model(**data)
- Update tests to use converter for API-like dict structuring
cattrs 26.1.0 natively handles Optional[AttrsClass] unions, extra
dict keys, and enum structuring — remove the custom hooks that
duplicated this built-in behavior.
iMicknl added 7 commits April 19, 2026 20:29
The _is_primitive_union hook was matching unions like FailureType | None and
ExecutionState | None, causing those fields to pass through as raw values
instead of being structured into enum instances. Exclude pure Optional[Enum]
unions so the enum hook handles them correctly.
@iMicknl
Copy link
Copy Markdown
Owner Author

iMicknl commented Apr 21, 2026

@copilot resolve the merge conflicts in this pull request

Co-authored-by: iMicknl <1424596+iMicknl@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 21, 2026

@copilot resolve the merge conflicts in this pull request

Merge conflicts are resolved and pushed in commit cb656fb. I merged v2/main into this branch, reconciled conflicts in pyoverkiz/client.py, pyoverkiz/models.py, and tests, and verified with lint/type checks plus full test suite (all passing).

Copy link
Copy Markdown
Collaborator

@tetienne tetienne left a comment

Choose a reason for hiding this comment

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

Love the direction here — converter.py at 64 lines vs the distributed _flexible_init + per-field converter approach is a much cleaner separation of concerns. The models are now pure data classes, which is exactly the right shape. Enum hook correctly routes through UnknownEnumMixin._missing_, and the _is_primitive_union logic handles the StateType passthrough cleanly (and the Optional[Enum] fix in 6701da1 is correct). One small blocker to fix, and a few things worth discussing.

Comment thread pyoverkiz/converter.py
Comment thread pyoverkiz/converter.py
Comment thread pyproject.toml Outdated
Comment thread pyoverkiz/client.py
iMicknl added 6 commits April 24, 2026 11:05
Symmetric with _structure_states — if the API returns
"commands": null for a device, iterating over None raises TypeError.
GenConverter in strict mode would raise on unknown API keys;
Converter silently drops them for forward-compatibility.
The hooks used (register_structure_hook, register_structure_hook_func)
have been stable since well before 23.2. A broader floor reduces
friction with Home Assistant's dependency tree.
All ProtocolType fields are single-word lowercase, so decamelize
is unnecessary. The comment prevents a future "consistency" fix
from silently breaking things.
…tions

Return False for non-str inputs instead of delegating to dict
membership, which would raise TypeError for unhashable types.
Prevents silently corrupting the index by storing a State
under a different name than its own.
@iMicknl iMicknl merged commit c1e56fc into v2/main Apr 24, 2026
8 checks passed
@iMicknl iMicknl deleted the v2/cattrs-models branch April 24, 2026 12:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking enhancement New feature or request v2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants