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
19 changes: 5 additions & 14 deletions src/objectstate/object_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2600,20 +2600,11 @@ def _extract_all_parameters_flat(
# First try from type annotation, then fall back to checking actual value
nested_type = self._get_nested_dataclass_type(info.param_type)

# A registered lazy runtime value owns its raw reconstruction type.
# The enclosing annotation commonly names the concrete base config,
# but reconstructing unresolved ``None`` sentinels through that base
# would run concrete validation before inheritance resolution.
if current_value is not None:
value_type = type(current_value)
if is_dataclass(value_type):
from objectstate.lazy_factory import get_base_type_for_lazy

if (
nested_type is None
or get_base_type_for_lazy(value_type) is not None
):
nested_type = value_type
# The actual dataclass owns both its flattened fields and their
# reconstruction, including concrete nominal subtypes and lazy
# overlays. An annotation supplies the owner only without a value.
if current_value is not None and is_dataclass(type(current_value)):
nested_type = type(current_value)

if nested_type is not None and current_value is not None:
# Store the nested config type reference at this path
Expand Down
39 changes: 39 additions & 0 deletions tests/test_object_state_update_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,45 @@ class ConfigWithNestedTopology:
nested: NestedTopology = field(default_factory=NestedTopology)


@dataclass(frozen=True)
class SpatialConfig:
width: int = 12


@dataclass(frozen=True)
class VolumeConfig(SpatialConfig):
depth: int = 60


@dataclass
class ConfigWithSpatialDomain:
spatial: SpatialConfig = field(default_factory=SpatialConfig)


def test_nested_nominal_subtype_survives_raw_live_and_saved_reconstruction():
state = ObjectState(ConfigWithSpatialDomain(VolumeConfig()), scope_id="volume")
assert state.to_object().spatial == VolumeConfig()
assert state.to_saved_resolved_object().spatial == VolumeConfig()
assert state.to_resolved_object().spatial == VolumeConfig()

state.update_parameter("spatial.depth", 30)
assert state.to_object().spatial == VolumeConfig(depth=30)
assert state.to_resolved_object().spatial == VolumeConfig(depth=30)
assert state.to_saved_resolved_object().spatial == VolumeConfig(depth=60)


def test_nested_nominal_owner_changes_rebuild_parameter_topology():
state = ObjectState(ConfigWithSpatialDomain(), scope_id="spatial")
state.update_object_instance(ConfigWithSpatialDomain(VolumeConfig(width=24)))
assert state.to_saved_resolved_object().spatial == VolumeConfig(width=24)
assert "spatial.depth" in state.parameters

state.update_object_instance(ConfigWithSpatialDomain(SpatialConfig(width=18)))
assert state.to_object().spatial == SpatialConfig(width=18)
assert state.to_saved_resolved_object().spatial == SpatialConfig(width=18)
assert "spatial.depth" not in state.parameters


class DelegatedHost:
__objectstate_delegate__ = "config"

Expand Down
Loading