objectstate 1.2.0__tar.gz → 1.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. {objectstate-1.2.0 → objectstate-1.3.1}/.github/workflows/publish.yml +11 -0
  2. {objectstate-1.2.0 → objectstate-1.3.1}/PKG-INFO +2 -2
  3. objectstate-1.3.1/docs/performance/canonical-context-type-resolution-20261005.json +45 -0
  4. {objectstate-1.2.0 → objectstate-1.3.1}/pyproject.toml +1 -1
  5. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/__init__.py +1 -1
  6. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/dual_axis_resolver.py +31 -33
  7. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/object_state.py +15 -55
  8. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/parameter_structure.py +15 -2
  9. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_construction_history.py +2 -1
  10. {objectstate-1.2.0 → objectstate-1.3.1}/.github/badges/coverage.svg +0 -0
  11. {objectstate-1.2.0 → objectstate-1.3.1}/.github/workflows/ci.yml +0 -0
  12. {objectstate-1.2.0 → objectstate-1.3.1}/.github/workflows/coverage-pages.yml +0 -0
  13. {objectstate-1.2.0 → objectstate-1.3.1}/.gitignore +0 -0
  14. {objectstate-1.2.0 → objectstate-1.3.1}/.readthedocs.yaml +0 -0
  15. {objectstate-1.2.0 → objectstate-1.3.1}/BADGE_SETUP.md +0 -0
  16. {objectstate-1.2.0 → objectstate-1.3.1}/LICENSE +0 -0
  17. {objectstate-1.2.0 → objectstate-1.3.1}/README.md +0 -0
  18. {objectstate-1.2.0 → objectstate-1.3.1}/docs/.gitignore +0 -0
  19. {objectstate-1.2.0 → objectstate-1.3.1}/docs/Makefile +0 -0
  20. {objectstate-1.2.0 → objectstate-1.3.1}/docs/api/modules.rst +0 -0
  21. {objectstate-1.2.0 → objectstate-1.3.1}/docs/architectural_fixes.rst +0 -0
  22. {objectstate-1.2.0 → objectstate-1.3.1}/docs/architecture.rst +0 -0
  23. {objectstate-1.2.0 → objectstate-1.3.1}/docs/archive/DOCUMENTATION_AUDIT.md +0 -0
  24. {objectstate-1.2.0 → objectstate-1.3.1}/docs/archive/DOCUMENTATION_FIXES.md +0 -0
  25. {objectstate-1.2.0 → objectstate-1.3.1}/docs/archive/README.md +0 -0
  26. {objectstate-1.2.0 → objectstate-1.3.1}/docs/conf.py +0 -0
  27. {objectstate-1.2.0 → objectstate-1.3.1}/docs/examples/atomic_operations.rst +0 -0
  28. {objectstate-1.2.0 → objectstate-1.3.1}/docs/examples/basic.rst +0 -0
  29. {objectstate-1.2.0 → objectstate-1.3.1}/docs/examples/dual_axis.rst +0 -0
  30. {objectstate-1.2.0 → objectstate-1.3.1}/docs/examples/index.rst +0 -0
  31. {objectstate-1.2.0 → objectstate-1.3.1}/docs/examples/ui.rst +0 -0
  32. {objectstate-1.2.0 → objectstate-1.3.1}/docs/index.rst +0 -0
  33. {objectstate-1.2.0 → objectstate-1.3.1}/docs/make.bat +0 -0
  34. {objectstate-1.2.0 → objectstate-1.3.1}/docs/provenance.rst +0 -0
  35. {objectstate-1.2.0 → objectstate-1.3.1}/docs/quickstart.rst +0 -0
  36. {objectstate-1.2.0 → objectstate-1.3.1}/docs/state_management.rst +0 -0
  37. {objectstate-1.2.0 → objectstate-1.3.1}/docs/state_propagation.rst +0 -0
  38. {objectstate-1.2.0 → objectstate-1.3.1}/docs/undo_redo.rst +0 -0
  39. {objectstate-1.2.0 → objectstate-1.3.1}/paper.bib +0 -0
  40. {objectstate-1.2.0 → objectstate-1.3.1}/paper.md +0 -0
  41. {objectstate-1.2.0 → objectstate-1.3.1}/plans/n_dimensional_inheritance.md +0 -0
  42. {objectstate-1.2.0 → objectstate-1.3.1}/scripts/check_ruff_added_lines.py +0 -0
  43. {objectstate-1.2.0 → objectstate-1.3.1}/scripts/release.py +0 -0
  44. {objectstate-1.2.0 → objectstate-1.3.1}/scripts/update_and_release.py +0 -0
  45. {objectstate-1.2.0 → objectstate-1.3.1}/scripts/update_version.py +0 -0
  46. {objectstate-1.2.0 → objectstate-1.3.1}/scripts/verify_release_ready.py +0 -0
  47. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/collection_containers.py +0 -0
  48. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/config.py +0 -0
  49. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/construction_binding.py +0 -0
  50. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/context_manager.py +0 -0
  51. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/docs/configuration_framework.rst +0 -0
  52. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/docs/context_system.rst +0 -0
  53. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/docs/object_state_lifecycle.rst +0 -0
  54. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/edit_session.py +0 -0
  55. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/field_access.py +0 -0
  56. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/global_config.py +0 -0
  57. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/history_migration.py +0 -0
  58. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/lazy_factory.py +0 -0
  59. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/live_context_resolver.py +0 -0
  60. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/object_state_metadata.py +0 -0
  61. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/object_state_registry.py +0 -0
  62. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/parameter_owner.py +0 -0
  63. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/parametric_axes.py +0 -0
  64. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/placeholder.py +0 -0
  65. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/reified_generics.py +0 -0
  66. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/snapshot_model.py +0 -0
  67. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/subfield_semantics.py +0 -0
  68. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/time_travel_profile.py +0 -0
  69. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/token_cache.py +0 -0
  70. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/transaction_checkpoint.py +0 -0
  71. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/ui_visibility.py +0 -0
  72. {objectstate-1.2.0 → objectstate-1.3.1}/src/objectstate/value_semantics.py +0 -0
  73. {objectstate-1.2.0 → objectstate-1.3.1}/tests/__init__.py +0 -0
  74. {objectstate-1.2.0 → objectstate-1.3.1}/tests/conftest.py +0 -0
  75. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_callable_baseline.py +0 -0
  76. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_config.py +0 -0
  77. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_context_manager.py +0 -0
  78. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_field_access.py +0 -0
  79. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_global_config.py +0 -0
  80. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_history_persistence.py +0 -0
  81. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_integration.py +0 -0
  82. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_lazy_factory.py +0 -0
  83. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_object_state_metadata_contracts.py +0 -0
  84. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_object_state_restore.py +0 -0
  85. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_object_state_update_instance.py +0 -0
  86. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_parameter_owner.py +0 -0
  87. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_parametric_axes.py +0 -0
  88. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_placeholder.py +0 -0
  89. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_registry_subscription.py +0 -0
  90. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_reified_generics.py +0 -0
  91. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_resolved_change_notifications.py +0 -0
  92. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_subfield_semantics.py +0 -0
  93. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_timeline_projection.py +0 -0
  94. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_transaction_checkpoint.py +0 -0
  95. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_ui_visible_field_projection.py +0 -0
  96. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_undo_redo_cascade.py +0 -0
  97. {objectstate-1.2.0 → objectstate-1.3.1}/tests/test_value_semantics.py +0 -0
@@ -4,6 +4,12 @@ on:
4
4
  push:
5
5
  tags:
6
6
  - 'v*'
7
+ workflow_dispatch:
8
+ inputs:
9
+ release_tag:
10
+ description: Existing release tag to publish
11
+ required: true
12
+ type: string
7
13
 
8
14
  permissions:
9
15
  contents: write
@@ -13,6 +19,8 @@ jobs:
13
19
  build-and-publish:
14
20
  name: Build and publish to PyPI
15
21
  runs-on: ubuntu-latest
22
+ env:
23
+ RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
16
24
  environment:
17
25
  name: pypi
18
26
  url: https://pypi.org/p/objectstate
@@ -20,6 +28,8 @@ jobs:
20
28
  steps:
21
29
  - name: Checkout code
22
30
  uses: actions/checkout@v4
31
+ with:
32
+ ref: refs/tags/${{ env.RELEASE_TAG }}
23
33
 
24
34
  - name: Set up Python
25
35
  uses: actions/setup-python@v5
@@ -42,6 +52,7 @@ jobs:
42
52
  - name: Create GitHub Release
43
53
  uses: softprops/action-gh-release@v2
44
54
  with:
55
+ tag_name: ${{ env.RELEASE_TAG }}
45
56
  files: dist/*
46
57
  generate_release_notes: true
47
58
  env:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: objectstate
3
- Version: 1.2.0
3
+ Version: 1.3.1
4
4
  Summary: Generic lazy dataclass configuration framework with dual-axis inheritance and contextvars-based resolution
5
5
  Project-URL: Homepage, https://github.com/OpenHCSDev/objectstate
6
6
  Project-URL: Documentation, https://objectstate.readthedocs.io
@@ -21,7 +21,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
21
  Classifier: Topic :: Utilities
22
22
  Requires-Python: >=3.11
23
23
  Requires-Dist: dill>=0.4.0
24
- Requires-Dist: python-introspect>=0.1.6
24
+ Requires-Dist: python-introspect<0.3,>=0.2.0
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: black==26.5.1; extra == 'dev'
27
27
  Requires-Dist: mypy==2.3.0; extra == 'dev'
@@ -0,0 +1,45 @@
1
+ {
2
+ "status": "PASS_PRIVATE_SAVED_CONSUMER_ONLY",
3
+ "baseline": "Installed ObjectState 1.3.0 from merged 7db151907a52a95743fbeae5a64220b28bc56690, PythonIntrospect 0.2.0",
4
+ "change": "resolve_with_provenance queries the existing canonical type-keyed maps admitted by extract_all_configs instead of scanning every value and reconstructing each owner type.",
5
+ "controls": {
6
+ "passed": 27,
7
+ "command": "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python -m pytest -q -o addopts= -p no:cacheprovider tests/test_context_manager.py tests/test_object_state_restore.py tests/test_callable_baseline.py tests/test_placeholder.py",
8
+ "laws": "Canonical map admission, duplicate/invalid declarations, MRO/None/provenance, real constructors/default factories, callable identity, detached mutable alias graphs"
9
+ },
10
+ "one_pair": [
11
+ {
12
+ "case": "Illumination Example3",
13
+ "first_job": true,
14
+ "steps": 10,
15
+ "baseline_seconds": 0.08565246313810349,
16
+ "candidate_seconds": 0.0676199309527874,
17
+ "saved_seconds": 0.018032532185316086
18
+ },
19
+ {
20
+ "case": "Original 3D monolayer",
21
+ "first_job": false,
22
+ "steps": 31,
23
+ "baseline_seconds": 0.2247260957956314,
24
+ "candidate_seconds": 0.19058690778911114,
25
+ "saved_seconds": 0.03413918800652027
26
+ }
27
+ ],
28
+ "parity": "Exact typed configuration/steps, provenance, canonical source and ancestor scopes PASS",
29
+ "guards": "Before/after source, actual eight installed dependency trees and wheel metadata, four native ABIs, authored input sources and helper hashes PASS; all owned processes terminal",
30
+ "preserved": "Context constructors/merges and extraction epochs, selected live field reads, same-type scope precedence, MRO class-first precedence, None inheritance, AttributeError behavior, fallback provenance and direct-owner default factories",
31
+ "limits": [
32
+ "ONE saved-owner pair, not a whole compiler, ordinary runtime or total speedup measurement.",
33
+ "Normal full Registry READY, document parsing, orchestrator initialization, graph/provider admission and execution were outside the clocks.",
34
+ "3D follows Illumination in each process; it is not an independent cold first-job observation.",
35
+ "This generalized cleanup is not the dominant performance route; the first-case 18 ms saving does not close the remaining total target gap.",
36
+ "Arbitrary corruption of the admitted map/type keys or in-operation instance __class__ rebinding was not established equivalent. Mutable field values remain live.",
37
+ "The public resolver accepting manually supplied maps is unchanged. No new cache, value snapshot or installed dependency mutation."
38
+ ],
39
+ "custody": {
40
+ "private_packet": "20261005/canonical-context-type-resolution-v1",
41
+ "baseline_resolver_sha256": "17fcda62912050bdcdd018bee2fca5f81c2f4940c5f27f2af3dc19b7691e1fb0",
42
+ "candidate_resolver_sha256": "e8e939bfb97ad2d07cb0f4893e43229fc30926274e7bdc7a4491264feeaeae97",
43
+ "version_change": "1.3.1 is a separate patch release; immutable 1.3.0 tags and installed wheel are untouched."
44
+ }
45
+ }
@@ -25,7 +25,7 @@ keywords = ["configuration", "dataclass", "lazy", "inheritance", "contextvars",
25
25
 
26
26
  dependencies = [
27
27
  "dill>=0.4.0",
28
- "python-introspect>=0.1.6",
28
+ "python-introspect>=0.2.0,<0.3",
29
29
  ]
30
30
 
31
31
  [project.optional-dependencies]
@@ -216,7 +216,7 @@ __all__ = [
216
216
  'ObjectStateEditSession',
217
217
  ]
218
218
 
219
- __version__ = "1.2.0"
219
+ __version__ = "1.3.1"
220
220
  __author__ = 'OpenHCS Team'
221
221
  __description__ = 'Generic configuration framework for lazy dataclass resolution'
222
222
 
@@ -324,24 +324,23 @@ def resolve_with_provenance(container_type: type, field_name: str) -> Tuple[Any,
324
324
  if _debug and field_name == 'well_filter':
325
325
  logger.debug(f"🔍 Phase 1 - Layer scope={scope_id!r}, checking same-type only (inner to outer)")
326
326
 
327
- for config_instance in layer_configs.values():
328
- instance_base = _normalize_to_base(type(config_instance))
329
- if _debug and field_name in ('well_filter', 'enabled') and instance_base == container_base:
330
- logger.debug(f"🔍 FOUND same-type config: {instance_base.__name__} @ scope={scope_id}")
331
- if instance_base == container_base: # Same-type only, no MRO
332
- try:
333
- value = object.__getattribute__(config_instance, field_name)
334
- if _debug and field_name in ('well_filter', 'enabled'):
335
- logger.debug(f"🔍 {container_base.__name__}.{field_name} @ scope={scope_id} = {value!r} (from object.__getattribute__)")
336
- if value is not None:
337
- # Found concrete value in hierarchy - return immediately
338
- if _debug and field_name in ('well_filter', 'enabled'):
339
- logger.debug(f"🔍 FOUND concrete value in hierarchy at scope={scope_id!r}, returning {value!r}")
340
- return value, scope_id, container_base
341
- # Don't set fallback here - let Phase 2 walk MRO to find
342
- # the highest type that defines this field
343
- except AttributeError:
344
- continue
327
+ # extract_all_configs admits one instance per canonical owner type.
328
+ config_instance = layer_configs.get(container_base)
329
+ if config_instance is None:
330
+ continue
331
+ try:
332
+ value = object.__getattribute__(config_instance, field_name)
333
+ if _debug and field_name in ('well_filter', 'enabled'):
334
+ logger.debug(f"🔍 {container_base.__name__}.{field_name} @ scope={scope_id} = {value!r} (from object.__getattribute__)")
335
+ if value is not None:
336
+ # Found concrete value in hierarchy - return immediately
337
+ if _debug and field_name in ('well_filter', 'enabled'):
338
+ logger.debug(f"🔍 FOUND concrete value in hierarchy at scope={scope_id!r}, returning {value!r}")
339
+ return value, scope_id, container_base
340
+ # Don't set fallback here - let Phase 2 walk MRO to find
341
+ # the highest type that defines this field
342
+ except AttributeError:
343
+ continue
345
344
 
346
345
  # PHASE 2: MRO fallback - no concrete value in hierarchy, try MRO inheritance
347
346
  #
@@ -367,25 +366,24 @@ def resolve_with_provenance(container_type: type, field_name: str) -> Tuple[Any,
367
366
 
368
367
  # Walk scopes from inner→outer for this MRO type
369
368
  for scope_id, layer_configs in reversed(all_layer_configs):
370
- for config_instance in layer_configs.values():
371
- instance_base = _normalize_to_base(type(config_instance))
372
- if instance_base != mro_type:
373
- continue
369
+ config_instance = layer_configs.get(mro_type)
370
+ if config_instance is None:
371
+ continue
374
372
 
375
- saw_type_anywhere = True
376
- last_scope_for_type = scope_id # update as we walk inner→outer (last = outermost)
373
+ saw_type_anywhere = True
374
+ last_scope_for_type = scope_id # update as we walk inner→outer (last = outermost)
377
375
 
378
- try:
379
- value = object.__getattribute__(config_instance, field_name)
380
- except AttributeError:
381
- continue
376
+ try:
377
+ value = object.__getattribute__(config_instance, field_name)
378
+ except AttributeError:
379
+ continue
382
380
 
383
- if _debug and field_name == 'well_filter':
384
- logger.debug(f"🔍 MRO: {mro_type.__name__}.{field_name} @ {scope_id!r} = {value!r}")
381
+ if _debug and field_name == 'well_filter':
382
+ logger.debug(f"🔍 MRO: {mro_type.__name__}.{field_name} @ {scope_id!r} = {value!r}")
385
383
 
386
- if value is not None:
387
- # Found MRO-inherited value
388
- return value, scope_id, mro_type
384
+ if value is not None:
385
+ # Found MRO-inherited value
386
+ return value, scope_id, mro_type
389
387
 
390
388
  # No non-None found for this MRO type across any scope.
391
389
  # Record fallback for the FIRST MRO type that exists anywhere.
@@ -529,7 +529,7 @@ class ObjectState:
529
529
  Dictionary mapping dotted parameter paths to their descriptions (value may be None).
530
530
  E.g., {'well_filter_config.well_filter': 'Filter wells by...'}
531
531
  """
532
- return dict(self._parameter_descriptions)
532
+ return self._parameter_structure.descriptions
533
533
 
534
534
  def type_for_path(self, field_path: str) -> ParameterOwner:
535
535
  """Return the ObjectState-recorded type for a dotted field path.
@@ -769,27 +769,13 @@ class ObjectState:
769
769
 
770
770
  Returns dict mapping param_name -> info object with .param_type, .default_value, and .description attributes.
771
771
  """
772
- from types import SimpleNamespace
773
-
774
- exclude_params = exclude_params or []
775
- result = {}
776
-
777
772
  # NOTE: python_introspect is a required dependency; fail loud if missing.
778
773
  # UnifiedParameterAnalyzer is responsible for correctness guarantees:
779
774
  # - defaults come from type/signature (never from instance)
780
775
  # - current instance values are accessed in a lazy-safe way (if needed)
781
776
  from python_introspect import UnifiedParameterAnalyzer
782
777
 
783
- ua_info = UnifiedParameterAnalyzer.analyze(obj, exclude_params=exclude_params)
784
- for name, info in ua_info.items():
785
- if name in exclude_params:
786
- continue
787
- result[name] = SimpleNamespace(
788
- param_type=getattr(info, "param_type", Any),
789
- default_value=getattr(info, "default_value", None),
790
- description=getattr(info, "description", None),
791
- )
792
- return result
778
+ return UnifiedParameterAnalyzer.analyze(obj, exclude_params=exclude_params)
793
779
 
794
780
  @staticmethod
795
781
  def _get_nested_dataclass_type(param_type: Any) -> type | None:
@@ -1219,7 +1205,10 @@ class ObjectState:
1219
1205
  raw_value = self.parameters.get(path.value, MISSING)
1220
1206
  resolved_value = self.get_resolved_value(path.value)
1221
1207
  saved_resolved_value = self.get_saved_resolved_value(path.value)
1222
- signature_default_value = self._signature_defaults.get(path.value, MISSING)
1208
+ declaration = self._parameter_structure.declarations.get(path.value)
1209
+ signature_default_value = (
1210
+ declaration.default_value if declaration is not None else MISSING
1211
+ )
1223
1212
 
1224
1213
  return build_subfield_semantic_index(
1225
1214
  owner_field_path=path,
@@ -1557,22 +1546,6 @@ class ObjectState:
1557
1546
  ) -> None:
1558
1547
  self._parameter_structure.direct_paths = value
1559
1548
 
1560
- @property
1561
- def _signature_defaults(self) -> dict[str, Any]:
1562
- return self._parameter_structure.defaults
1563
-
1564
- @_signature_defaults.setter
1565
- def _signature_defaults(self, value: dict[str, Any]) -> None:
1566
- self._parameter_structure.defaults = value
1567
-
1568
- @property
1569
- def _parameter_descriptions(self) -> dict[str, str | None]:
1570
- return self._parameter_structure.descriptions
1571
-
1572
- @_parameter_descriptions.setter
1573
- def _parameter_descriptions(self, value: dict[str, str | None]) -> None:
1574
- self._parameter_structure.descriptions = value
1575
-
1576
1549
  @property
1577
1550
  def _exclude_param_names(self) -> list[str]:
1578
1551
  return self._parameter_structure.exclusions
@@ -1893,16 +1866,17 @@ class ObjectState:
1893
1866
 
1894
1867
  # Use signature defaults (CLASS defaults), not instance values
1895
1868
  # This ensures reset goes back to None for lazy fields, not saved concrete values
1896
- default_value = self._signature_defaults.get(param_name)
1869
+ declaration = self._parameter_structure.declarations.get(param_name)
1870
+ default_value = declaration.default_value if declaration is not None else None
1897
1871
  self.update_parameter(param_name, default_value)
1898
1872
 
1899
1873
  def signature_default(self, param_name: str) -> Any:
1900
1874
  """Return the signature default recorded for one flat parameter path."""
1901
- if param_name not in self._signature_defaults:
1875
+ if param_name not in self._parameter_structure.declarations:
1902
1876
  raise KeyError(
1903
1877
  f"No signature default recorded for parameter {param_name!r}."
1904
1878
  )
1905
- return self._signature_defaults[param_name]
1879
+ return self._parameter_structure.declarations[param_name].default_value
1906
1880
 
1907
1881
  def get_current_values(self) -> dict[str, Any]:
1908
1882
  """
@@ -2014,14 +1988,13 @@ class ObjectState:
2014
1988
  """Compute signature-diff set from parameters vs defaults.
2015
1989
 
2016
1990
  Any field that differs from its signature default is included.
2017
- Nested dataclass container fields are implicitly excluded since
2018
- they don't have entries in _signature_defaults (only leaf fields do).
1991
+ Defaults come from the same declarations used for reset, including
1992
+ declared nested containers.
2019
1993
  """
2020
1994
  result = set()
2021
1995
  for k, v in self.parameters.items():
2022
- if k in self._signature_defaults:
2023
- # Direct dict key access - no special behavior to avoid
2024
- sig_default = self._signature_defaults[k]
1996
+ if k in self._parameter_structure.declarations:
1997
+ sig_default = self._parameter_structure.declarations[k].default_value
2025
1998
  is_diff = v != sig_default
2026
1999
  if is_diff:
2027
2000
  result.add(k)
@@ -2577,10 +2550,7 @@ class ObjectState:
2577
2550
  except AttributeError:
2578
2551
  current_value = info.default_value
2579
2552
 
2580
- # Store description entry for the dotted path. Even if the specific
2581
- # parameter has no description, ensure the dotted key exists so
2582
- # callers can rely on presence of the full path (value may be None).
2583
- structure.descriptions[dotted_path] = getattr(info, 'description', None)
2553
+ structure.declarations[dotted_path] = info
2584
2554
 
2585
2555
  # Check if this is a nested dataclass
2586
2556
  # First try from type annotation, then fall back to checking actual value
@@ -2599,13 +2569,6 @@ class ObjectState:
2599
2569
  # Store the nested dataclass instance in parameters (needed for UI rendering)
2600
2570
  parameters[dotted_path] = current_value
2601
2571
 
2602
- # Container resets follow the same declared-default authority as
2603
- # leaf resets. Lazy dataclass fields retain their intentional
2604
- # ``None`` default, while callable parameters with concrete
2605
- # dataclass defaults can be restored after an authored override
2606
- # is removed.
2607
- structure.defaults[dotted_path] = info.default_value
2608
-
2609
2572
  # Recurse into nested dataclass for child fields
2610
2573
  cls._extract_all_parameters_flat(
2611
2574
  current_value, parameters, structure, prefix=dotted_path, exclude_params=[]
@@ -2615,9 +2578,6 @@ class ObjectState:
2615
2578
  parameters[dotted_path] = current_value
2616
2579
  # Store the owner target that has this field.
2617
2580
  structure.owner_paths[dotted_path] = owner_target
2618
- # Store signature default for reset functionality (flattened)
2619
- # info.default_value is now guaranteed to be the CLASS signature default
2620
- structure.defaults[dotted_path] = info.default_value
2621
2581
 
2622
2582
  def _replace_parameter_structure(
2623
2583
  self,
@@ -6,6 +6,8 @@ import copy
6
6
  from dataclasses import dataclass, field, fields, is_dataclass, replace
7
7
  from typing import Any
8
8
 
9
+ from python_introspect import ParameterInfo
10
+
9
11
  from objectstate.field_access import DottedFieldPath
10
12
  from objectstate.parameter_owner import ParameterOwner
11
13
 
@@ -18,11 +20,22 @@ class ParameterStructure:
18
20
  direct_paths: dict[DottedFieldPath, tuple[DottedFieldPath, ...]] = field(
19
21
  default_factory=dict
20
22
  )
21
- defaults: dict[str, Any] = field(default_factory=dict)
22
- descriptions: dict[str, str | None] = field(default_factory=dict)
23
+ declarations: dict[str, ParameterInfo] = field(default_factory=dict)
23
24
  exclusions: list[str] = field(default_factory=list)
24
25
  excluded_values: dict[str, Any] = field(default_factory=dict)
25
26
 
27
+ @property
28
+ def defaults(self) -> dict[str, Any]:
29
+ """Derive editor reset values from admitted parameter declarations."""
30
+
31
+ return {path: info.default_value for path, info in self.declarations.items()}
32
+
33
+ @property
34
+ def descriptions(self) -> dict[str, str | None]:
35
+ """Request presentation help only when a consumer reads it."""
36
+
37
+ return {path: info.description for path, info in self.declarations.items()}
38
+
26
39
  def is_container_parameter(
27
40
  self, name: str, value: Any, parameters: dict[str, Any]
28
41
  ) -> bool:
@@ -148,7 +148,8 @@ def test_same_scope_callable_owner_and_schema_restore_without_replacing_state():
148
148
  assert ObjectStateRegistry.get_by_scope("callable") is state
149
149
  assert state.object_instance is function
150
150
  assert set(state.parameters) == parameters
151
- assert set(state._signature_defaults) == parameters
151
+ for name in parameters:
152
+ state.signature_default(name)
152
153
  assert set(state._path_to_type) == parameters
153
154
 
154
155
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes