Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.25] - 2026-09-29

### Added
- **`SerializerMixin` stores classes as references.** A class value is saved
as `{'__type__': 'module.Class'}` and resolved through the allowlist;
previously it became `None` with no error. A class, an enum or a tuple can
now be a dict key.
- `frozenset` round-trips (`__frozenset__`). Set items are now written in
sorted order, so equal sets give equal bytes. The 0.2.24 `__set__` payload
shape is unchanged.
- `register_class_alias(old_path, new_path)` loads old payloads after a class
moves or is renamed. Aliases may chain; cycles are refused; the allowlist
check applies to the new path.
- `_schema_version` / `_migrate(data, version)` on serializable classes. A
payload older than the class is migrated on load; a newer one raises
`SchemaVersionError`. Version 0 is not written, so existing payloads are
byte-identical.
- A missing module or class raises `MissingClassError` naming the path.
Both new errors subclass `ValueError`.

### Changed
- `PropagationMixin` no longer calls `_set_parent` on a class value after a
load.

## [0.2.24] - 2026-09-18

### Added
Expand Down
19 changes: 14 additions & 5 deletions steer_core/Mixins/Propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ def wrapper(self, new_value):
return decorator


def _has_parent(value: Any) -> bool:
"""Return True if ``value`` is an instance that can take a parent reference.

A class stored as a value (a type reference) also has ``_set_parent``, as an
unbound function. It must not be called.
"""
return hasattr(value, '_set_parent') and not isinstance(value, type)


class PropagationMixin:
"""
Mixin providing update propagation through a hierarchical object tree.
Expand Down Expand Up @@ -320,21 +329,21 @@ def _set_parent_on_value(self, value: Any, attr_name: Optional[str] = None) -> N
attr_name: The attribute name on this object that holds the value.
Used for setter-based propagation.
"""
if hasattr(value, '_set_parent'):
if _has_parent(value):
value._set_parent(self, attr_name)
elif isinstance(value, (list, tuple)):
elif isinstance(value, (list, tuple, set, frozenset)):
# For collections, items don't have a direct setter path
for item in value:
if hasattr(item, '_set_parent'):
if _has_parent(item):
item._set_parent(self, attr_name)
elif isinstance(value, dict):
# Check both keys AND values - materials can be dict keys
# For dict items, they don't have a direct setter path
for item in value.keys():
if hasattr(item, '_set_parent'):
if _has_parent(item):
item._set_parent(self, attr_name)
for item in value.values():
if hasattr(item, '_set_parent'):
if _has_parent(item):
item._set_parent(self, attr_name)

def __deepcopy__(self, memo):
Expand Down
Loading
Loading