Migrate to cattrs for centralized model structuring#1994
Conversation
There was a problem hiding this comment.
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.pywith cattrs hooks (unknown key filtering, enums, unions, and custom containers). - Refactored
pyoverkiz/models.pyto remove most custom__init__converter logic and rely on cattrs structuring +__attrs_post_init__where needed. - Updated
pyoverkiz/client.pyand tests to useconverter.structure(...); addedcattrsas 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 (type → api_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.
2aaee1b to
1c7679b
Compare
There was a problem hiding this comment.
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.
…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.
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.
…nced functionality
… privacy and representation
…te their presence
…redundant id field
a17da1a to
569325c
Compare
|
@copilot resolve the merge conflicts in this pull request |
Co-authored-by: iMicknl <1424596+iMicknl@users.noreply.github.com>
Merge conflicts are resolved and pushed in commit |
tetienne
left a comment
There was a problem hiding this comment.
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.
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.
Summary
Alternative to #1991 — replaces per-model converter functions with a centralized
cattrsConverter.pyoverkiz/converter.pywith hooks for primitive unions, optional attrs classes, enums (withUnknownEnumMixin), and custom containers (States,CommandDefinitions)models.py— models become pure@define(kw_only=True)data classes with only type annotations and defaultsclient.pyto useconverter.structure()instead ofModel(**data)Comparison with #1991
_flexible_init,_to_list, etc.)converter.pycattrsBreaking Changes
ServerConfig.typerenamed toServerConfig.api_type.Gateway.connectivitytype changed toConnectivity | None(was non-optional).Gateway.idandPlace.idchanged from mutable fields to read-only properties.States.__getitem__now raisesKeyErrorinstead of returningNone— useStates.get()for the old behavior.CommandDefinitions.__getitem__now raisesKeyErrorinstead of returningNone— useCommandDefinitions.get()for the old behavior.Event.setup_oid— field name unchanged (cattrs handles thesetupOID→setup_oidconversion automatically).Execution.action_grouptype changed fromlist[Action]toActionGroup | None.ActionGroup.labeldefault simplified (no longer falls back to empty string).Test plan