diff --git a/.verification/lazy-field-read/delivery.md b/.verification/lazy-field-read/delivery.md new file mode 100644 index 0000000..35b7edc --- /dev/null +++ b/.verification/lazy-field-read/delivery.md @@ -0,0 +1,3 @@ +FieldProxy follows ParameterStructure's existing admitted paths instead of re-reading dataclass annotations during each attribute access. Containers remain proxies; only leaves resolve through ObjectState. get_resolved_value uses the same extracted child topology instead of scanning every resolved key. No extra value cache. + +Native application succeeded. One direct native ObjectState control read a nested leaf, edited it through update_parameter, then read its updated value. First attempt omitted set_base_config_type and failed during setup; corrected application initialization passed. No suite, profile, or speed measurement. Toad must consume state.fields instead of reconstructing an object. diff --git a/.verification/lazy-field-read/fix.plan.json b/.verification/lazy-field-read/fix.plan.json new file mode 100644 index 0000000..e890868 --- /dev/null +++ b/.verification/lazy-field-read/fix.plan.json @@ -0,0 +1,31 @@ +{ + "recipes": [ + { + "recipe_id": "read-extracted-field-topology", + "operations": [ + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P192--objectstate/src/objectstate/object_state.py", + "target_qualname": "FieldProxy.__getattr__", + "replacements": [ + { + "old_source": " def __getattr__(self, name: str) -> Any:\n \"\"\"Get field value or nested FieldProxy.\n\n Args:\n name: Field name to access\n\n Returns:\n FieldProxy for nested dataclass fields, or resolved value for leaf fields\n \"\"\"\n new_path = f'{self._path}.{name}' if self._path else name\n\n # Get field info from the type\n if not is_dataclass(self._type):\n type_name = getattr(self._type, '__name__', str(self._type))\n raise AttributeError(f\"{type_name} is not a dataclass\")\n\n field_info = None\n for f in dataclass_fields(self._type):\n if f.name == name:\n field_info = f\n break\n\n if field_info is None:\n type_name = getattr(self._type, '__name__', str(self._type))\n raise AttributeError(f\"{type_name} has no field '{name}'\")\n\n # Check if field is a nested dataclass\n field_type = field_info.type\n\n # Handle Optional[DataclassType]\n from typing import Union, get_args, get_origin\n origin = get_origin(field_type)\n if origin is Union:\n args = get_args(field_type)\n if len(args) == 2 and type(None) in args:\n inner_type = next(arg for arg in args if arg is not type(None))\n if is_dataclass(inner_type) and isinstance(inner_type, type):\n return FieldProxy(self._state, new_path, inner_type)\n\n # Handle direct dataclass type\n if isinstance(field_type, type) and is_dataclass(field_type):\n return FieldProxy(self._state, new_path, field_type)\n\n # Leaf field - get resolved value\n return self._state.get_resolved_value(new_path)", + "new_source": " def __getattr__(self, name: str) -> Any:\n \"\"\"Follow the state's extracted paths; resolve only the requested leaf.\"\"\"\n new_path = f'{self._path}.{name}' if self._path else name\n if new_path not in self._state.parameters:\n raise AttributeError(f\"ObjectState has no field {new_path!r}\")\n structure = self._state.parameter_structure\n if structure.direct_paths.get(DottedFieldPath(new_path)):\n return FieldProxy(self._state, new_path, structure.owner_paths[new_path])\n return self._state.get_resolved_value(new_path)" + } + ] + }, + { + "operation": "patch_target", + "file_path": "/home/ts/wt/codex-live--P192--objectstate/src/objectstate/object_state.py", + "target_qualname": "ObjectState.get_resolved_value", + "replacements": [ + { + "old_source": "prefix = f\"{param_name}.\"\n has_subfields = any(key.startswith(prefix) for key in self._live_resolved.keys())", + "new_source": "has_subfields = bool(self.parameter_structure.direct_paths.get(DottedFieldPath(param_name)))" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/objectstate/object_state.py b/src/objectstate/object_state.py index c00efe7..f0885d7 100644 --- a/src/objectstate/object_state.py +++ b/src/objectstate/object_state.py @@ -67,49 +67,13 @@ def __init__(self, state: 'ObjectState', path: str, field_type: type): object.__setattr__(self, '_type', field_type) def __getattr__(self, name: str) -> Any: - """Get field value or nested FieldProxy. - - Args: - name: Field name to access - - Returns: - FieldProxy for nested dataclass fields, or resolved value for leaf fields - """ + """Follow the state's extracted paths; resolve only the requested leaf.""" new_path = f'{self._path}.{name}' if self._path else name - - # Get field info from the type - if not is_dataclass(self._type): - type_name = getattr(self._type, '__name__', str(self._type)) - raise AttributeError(f"{type_name} is not a dataclass") - - field_info = None - for f in dataclass_fields(self._type): - if f.name == name: - field_info = f - break - - if field_info is None: - type_name = getattr(self._type, '__name__', str(self._type)) - raise AttributeError(f"{type_name} has no field '{name}'") - - # Check if field is a nested dataclass - field_type = field_info.type - - # Handle Optional[DataclassType] - from typing import Union, get_args, get_origin - origin = get_origin(field_type) - if origin is Union: - args = get_args(field_type) - if len(args) == 2 and type(None) in args: - inner_type = next(arg for arg in args if arg is not type(None)) - if is_dataclass(inner_type) and isinstance(inner_type, type): - return FieldProxy(self._state, new_path, inner_type) - - # Handle direct dataclass type - if isinstance(field_type, type) and is_dataclass(field_type): - return FieldProxy(self._state, new_path, field_type) - - # Leaf field - get resolved value + if new_path not in self._state.parameters: + raise AttributeError(f"ObjectState has no field {new_path!r}") + structure = self._state.parameter_structure + if structure.direct_paths.get(DottedFieldPath(new_path)): + return FieldProxy(self._state, new_path, structure.owner_paths[new_path]) return self._state.get_resolved_value(new_path) def __setattr__(self, name: str, value: Any) -> None: @@ -1198,8 +1162,7 @@ def get_resolved_value(self, param_name: str) -> Any: assert self._live_resolved is not None # Guaranteed by _ensure_live_resolved # Check if this is a container/dataclass field (has subfields in _live_resolved) - prefix = f"{param_name}." - has_subfields = any(key.startswith(prefix) for key in self._live_resolved.keys()) + has_subfields = bool(self.parameter_structure.direct_paths.get(DottedFieldPath(param_name))) if has_subfields: # This is a container field - reconstruct's dataclass from live resolved values