objectstate 1.3.1__tar.gz → 2.1.0__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 (105) hide show
  1. {objectstate-1.3.1 → objectstate-2.1.0}/.github/badges/coverage.svg +1 -1
  2. {objectstate-1.3.1 → objectstate-2.1.0}/PKG-INFO +24 -7
  3. {objectstate-1.3.1 → objectstate-2.1.0}/README.md +22 -6
  4. {objectstate-1.3.1 → objectstate-2.1.0}/docs/architectural_fixes.rst +3 -3
  5. {objectstate-1.3.1 → objectstate-2.1.0}/docs/architecture.rst +10 -10
  6. {objectstate-1.3.1 → objectstate-2.1.0}/docs/examples/dual_axis.rst +11 -11
  7. {objectstate-1.3.1 → objectstate-2.1.0}/docs/provenance.rst +12 -12
  8. {objectstate-1.3.1 → objectstate-2.1.0}/pyproject.toml +1 -0
  9. {objectstate-1.3.1 → objectstate-2.1.0}/scripts/verify_release_ready.py +3 -8
  10. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/__init__.py +5 -6
  11. objectstate-2.1.0/src/objectstate/codegen.py +137 -0
  12. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/context_manager.py +8 -63
  13. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/docs/configuration_framework.rst +1 -1
  14. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/dual_axis_resolver.py +10 -47
  15. objectstate-2.1.0/src/objectstate/inspection.py +162 -0
  16. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/lazy_factory.py +53 -7
  17. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/live_context_resolver.py +3 -2
  18. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/object_state.py +38 -64
  19. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/object_state_registry.py +35 -77
  20. objectstate-2.1.0/src/objectstate/scope_key.py +88 -0
  21. objectstate-2.1.0/tests/test_authored_paths.py +29 -0
  22. objectstate-2.1.0/tests/test_codegen.py +113 -0
  23. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_construction_history.py +0 -20
  24. objectstate-2.1.0/tests/test_inspection.py +51 -0
  25. objectstate-2.1.0/tests/test_release_readiness.py +17 -0
  26. objectstate-2.1.0/tests/test_scope_key.py +47 -0
  27. objectstate-1.3.1/src/objectstate/history_migration.py +0 -19
  28. {objectstate-1.3.1 → objectstate-2.1.0}/.github/workflows/ci.yml +0 -0
  29. {objectstate-1.3.1 → objectstate-2.1.0}/.github/workflows/coverage-pages.yml +0 -0
  30. {objectstate-1.3.1 → objectstate-2.1.0}/.github/workflows/publish.yml +0 -0
  31. {objectstate-1.3.1 → objectstate-2.1.0}/.gitignore +0 -0
  32. {objectstate-1.3.1 → objectstate-2.1.0}/.readthedocs.yaml +0 -0
  33. {objectstate-1.3.1 → objectstate-2.1.0}/BADGE_SETUP.md +0 -0
  34. {objectstate-1.3.1 → objectstate-2.1.0}/LICENSE +0 -0
  35. {objectstate-1.3.1 → objectstate-2.1.0}/docs/.gitignore +0 -0
  36. {objectstate-1.3.1 → objectstate-2.1.0}/docs/Makefile +0 -0
  37. {objectstate-1.3.1 → objectstate-2.1.0}/docs/api/modules.rst +0 -0
  38. {objectstate-1.3.1 → objectstate-2.1.0}/docs/archive/DOCUMENTATION_AUDIT.md +0 -0
  39. {objectstate-1.3.1 → objectstate-2.1.0}/docs/archive/DOCUMENTATION_FIXES.md +0 -0
  40. {objectstate-1.3.1 → objectstate-2.1.0}/docs/archive/README.md +0 -0
  41. {objectstate-1.3.1 → objectstate-2.1.0}/docs/conf.py +0 -0
  42. {objectstate-1.3.1 → objectstate-2.1.0}/docs/examples/atomic_operations.rst +0 -0
  43. {objectstate-1.3.1 → objectstate-2.1.0}/docs/examples/basic.rst +0 -0
  44. {objectstate-1.3.1 → objectstate-2.1.0}/docs/examples/index.rst +0 -0
  45. {objectstate-1.3.1 → objectstate-2.1.0}/docs/examples/ui.rst +0 -0
  46. {objectstate-1.3.1 → objectstate-2.1.0}/docs/index.rst +0 -0
  47. {objectstate-1.3.1 → objectstate-2.1.0}/docs/make.bat +0 -0
  48. {objectstate-1.3.1 → objectstate-2.1.0}/docs/performance/canonical-context-type-resolution-20261005.json +0 -0
  49. {objectstate-1.3.1 → objectstate-2.1.0}/docs/quickstart.rst +0 -0
  50. {objectstate-1.3.1 → objectstate-2.1.0}/docs/state_management.rst +0 -0
  51. {objectstate-1.3.1 → objectstate-2.1.0}/docs/state_propagation.rst +0 -0
  52. {objectstate-1.3.1 → objectstate-2.1.0}/docs/undo_redo.rst +0 -0
  53. {objectstate-1.3.1 → objectstate-2.1.0}/paper.bib +0 -0
  54. {objectstate-1.3.1 → objectstate-2.1.0}/paper.md +0 -0
  55. {objectstate-1.3.1 → objectstate-2.1.0}/plans/n_dimensional_inheritance.md +0 -0
  56. {objectstate-1.3.1 → objectstate-2.1.0}/scripts/check_ruff_added_lines.py +0 -0
  57. {objectstate-1.3.1 → objectstate-2.1.0}/scripts/release.py +0 -0
  58. {objectstate-1.3.1 → objectstate-2.1.0}/scripts/update_and_release.py +0 -0
  59. {objectstate-1.3.1 → objectstate-2.1.0}/scripts/update_version.py +0 -0
  60. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/collection_containers.py +0 -0
  61. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/config.py +0 -0
  62. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/construction_binding.py +0 -0
  63. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/docs/context_system.rst +0 -0
  64. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/docs/object_state_lifecycle.rst +0 -0
  65. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/edit_session.py +0 -0
  66. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/field_access.py +0 -0
  67. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/global_config.py +0 -0
  68. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/object_state_metadata.py +0 -0
  69. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/parameter_owner.py +0 -0
  70. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/parameter_structure.py +0 -0
  71. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/parametric_axes.py +0 -0
  72. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/placeholder.py +0 -0
  73. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/reified_generics.py +0 -0
  74. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/snapshot_model.py +0 -0
  75. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/subfield_semantics.py +0 -0
  76. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/time_travel_profile.py +0 -0
  77. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/token_cache.py +0 -0
  78. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/transaction_checkpoint.py +0 -0
  79. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/ui_visibility.py +0 -0
  80. {objectstate-1.3.1 → objectstate-2.1.0}/src/objectstate/value_semantics.py +0 -0
  81. {objectstate-1.3.1 → objectstate-2.1.0}/tests/__init__.py +0 -0
  82. {objectstate-1.3.1 → objectstate-2.1.0}/tests/conftest.py +0 -0
  83. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_callable_baseline.py +0 -0
  84. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_config.py +0 -0
  85. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_context_manager.py +0 -0
  86. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_field_access.py +0 -0
  87. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_global_config.py +0 -0
  88. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_history_persistence.py +0 -0
  89. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_integration.py +0 -0
  90. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_lazy_factory.py +0 -0
  91. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_object_state_metadata_contracts.py +0 -0
  92. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_object_state_restore.py +0 -0
  93. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_object_state_update_instance.py +0 -0
  94. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_parameter_owner.py +0 -0
  95. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_parametric_axes.py +0 -0
  96. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_placeholder.py +0 -0
  97. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_registry_subscription.py +0 -0
  98. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_reified_generics.py +0 -0
  99. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_resolved_change_notifications.py +0 -0
  100. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_subfield_semantics.py +0 -0
  101. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_timeline_projection.py +0 -0
  102. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_transaction_checkpoint.py +0 -0
  103. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_ui_visible_field_projection.py +0 -0
  104. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_undo_redo_cascade.py +0 -0
  105. {objectstate-1.3.1 → objectstate-2.1.0}/tests/test_value_semantics.py +0 -0
@@ -1 +1 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 74.21%"><title>coverage: 74.21%</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#ea7233"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="315" y="150" fill-opacity=".8" filter="url(#blur)" textLength="510">coverage</text><text x="315" y="150" fill-opacity=".3" textLength="510">coverage</text></g><text x="315" y="140" textLength="510">coverage</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="865" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">74.21%</text><text x="865" y="150" fill-opacity=".3" textLength="430">74.21%</text></g><text x="865" y="140" textLength="430">74.21%</text></g></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 74.96%"><title>coverage: 74.96%</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#ea7233"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="315" y="150" fill-opacity=".8" filter="url(#blur)" textLength="510">coverage</text><text x="315" y="150" fill-opacity=".3" textLength="510">coverage</text></g><text x="315" y="140" textLength="510">coverage</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="865" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">74.96%</text><text x="865" y="150" fill-opacity=".3" textLength="430">74.96%</text></g><text x="865" y="140" textLength="430">74.96%</text></g></g></svg>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: objectstate
3
- Version: 1.3.1
3
+ Version: 2.1.0
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,6 +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: pycodify<0.3,>=0.2.0
24
25
  Requires-Dist: python-introspect<0.3,>=0.2.0
25
26
  Provides-Extra: dev
26
27
  Requires-Dist: black==26.5.1; extra == 'dev'
@@ -136,6 +137,22 @@ ensure_global_config_context(GlobalPipelineConfig, global_config)
136
137
  pip install objectstate
137
138
  ```
138
139
 
140
+ ## Saving Configs as Python Source
141
+
142
+ `objectstate.codegen` registers a [pycodify](https://github.com/OpenHCSDev/pycodify)
143
+ formatter for lazy dataclasses. Source is written from raw field values, so an
144
+ inherited `None` stays inherited when the file is loaded back:
145
+
146
+ ```python
147
+ import objectstate.codegen # registers the formatter
148
+ from pycodify import Assignment, generate_python_source
149
+
150
+ source = generate_python_source(Assignment("config", lazy_config), clean_mode=True)
151
+ ```
152
+
153
+ Clean mode writes only constructor-set fields and concrete values; full mode
154
+ writes every field.
155
+
139
156
  ## ObjectState Registry
140
157
 
141
158
  ObjectState separates mutable working state from saved baseline, enabling dirty tracking and undo/redo:
@@ -290,19 +307,19 @@ class GlobalPipelineConfig:
290
307
  # Use the decorator on a new config class
291
308
  @global_pipeline_config
292
309
  @dataclass
293
- class WellFilterConfig:
294
- well_filter: str = None
310
+ class PartitionFilterConfig:
311
+ partition_filter: str = None
295
312
  mode: str = "include"
296
313
 
297
314
  # After module loading, GlobalPipelineConfig automatically has:
298
- # - well_filter_config: WellFilterConfig = WellFilterConfig()
299
- # And LazyWellFilterConfig is auto-created
315
+ # - partition_filter_config: PartitionFilterConfig = PartitionFilterConfig()
316
+ # And LazyPartitionFilterConfig is auto-created
300
317
  ```
301
318
 
302
319
  **How it works:**
303
320
  - Decorated classes are injected as fields into `GlobalPipelineConfig`
304
- - Field name is snake_case of class name (e.g., `WellFilterConfig` → `well_filter_config`)
305
- - Lazy version is automatically created (e.g., `LazyWellFilterConfig`)
321
+ - Field name is snake_case of class name (e.g., `PartitionFilterConfig` → `partition_filter_config`)
322
+ - Lazy version is automatically created (e.g., `LazyPartitionFilterConfig`)
306
323
  - Injection happens at end of module loading via `_inject_all_pending_fields()`
307
324
 
308
325
  This enables a clean, modular configuration structure where each component's config is automatically part of the global configuration.
@@ -100,6 +100,22 @@ ensure_global_config_context(GlobalPipelineConfig, global_config)
100
100
  pip install objectstate
101
101
  ```
102
102
 
103
+ ## Saving Configs as Python Source
104
+
105
+ `objectstate.codegen` registers a [pycodify](https://github.com/OpenHCSDev/pycodify)
106
+ formatter for lazy dataclasses. Source is written from raw field values, so an
107
+ inherited `None` stays inherited when the file is loaded back:
108
+
109
+ ```python
110
+ import objectstate.codegen # registers the formatter
111
+ from pycodify import Assignment, generate_python_source
112
+
113
+ source = generate_python_source(Assignment("config", lazy_config), clean_mode=True)
114
+ ```
115
+
116
+ Clean mode writes only constructor-set fields and concrete values; full mode
117
+ writes every field.
118
+
103
119
  ## ObjectState Registry
104
120
 
105
121
  ObjectState separates mutable working state from saved baseline, enabling dirty tracking and undo/redo:
@@ -254,19 +270,19 @@ class GlobalPipelineConfig:
254
270
  # Use the decorator on a new config class
255
271
  @global_pipeline_config
256
272
  @dataclass
257
- class WellFilterConfig:
258
- well_filter: str = None
273
+ class PartitionFilterConfig:
274
+ partition_filter: str = None
259
275
  mode: str = "include"
260
276
 
261
277
  # After module loading, GlobalPipelineConfig automatically has:
262
- # - well_filter_config: WellFilterConfig = WellFilterConfig()
263
- # And LazyWellFilterConfig is auto-created
278
+ # - partition_filter_config: PartitionFilterConfig = PartitionFilterConfig()
279
+ # And LazyPartitionFilterConfig is auto-created
264
280
  ```
265
281
 
266
282
  **How it works:**
267
283
  - Decorated classes are injected as fields into `GlobalPipelineConfig`
268
- - Field name is snake_case of class name (e.g., `WellFilterConfig` → `well_filter_config`)
269
- - Lazy version is automatically created (e.g., `LazyWellFilterConfig`)
284
+ - Field name is snake_case of class name (e.g., `PartitionFilterConfig` → `partition_filter_config`)
285
+ - Lazy version is automatically created (e.g., `LazyPartitionFilterConfig`)
270
286
  - Injection happens at end of module loading via `_inject_all_pending_fields()`
271
287
 
272
288
  This enables a clean, modular configuration structure where each component's config is automatically part of the global configuration.
@@ -65,8 +65,8 @@ Fix 2: Enable Real-Time MRO Inheritance
65
65
 
66
66
  **Problem:** During ``_recompute_invalid_fields()``, the code was using
67
67
  ``self.object_instance`` (original saved state) instead of reconstructing from current
68
- parameters. This broke real-time MRO inheritance—editing ``WellFilterConfig.well_filter``
69
- wouldn't update placeholders for inheriting fields like ``PathPlanningConfig.well_filter``.
68
+ parameters. This broke real-time MRO inheritance—editing ``PartitionFilterConfig.partition_filter``
69
+ wouldn't update placeholders for inheriting fields like ``PathPlanningConfig.partition_filter``.
70
70
 
71
71
  **Solution:** Reconstruct current object from parameters without delegate sync:
72
72
 
@@ -233,7 +233,7 @@ Testing & Validation
233
233
  These fixes were validated through:
234
234
 
235
235
  1. **Manual testing**: Save GlobalPipelineConfig → no crash, descendants clear dirty markers
236
- 2. **Real-time inheritance**: Edit WellFilterConfig → inheriting fields update immediately
236
+ 2. **Real-time inheritance**: Edit PartitionFilterConfig → inheriting fields update immediately
237
237
  3. **Legacy compatibility**: Old snapshots with ``saved_parameters=None`` load correctly
238
238
  4. **Multi-level inheritance**: Global → Pipeline → Step inheritance works correctly
239
239
 
@@ -348,20 +348,20 @@ When you use the generated decorator, the decorated class is automatically injec
348
348
 
349
349
  @global_pipeline_config
350
350
  @dataclass
351
- class WellFilterConfig:
352
- well_filter: str = None
351
+ class PartitionFilterConfig:
352
+ partition_filter: str = None
353
353
  mode: str = "include"
354
354
 
355
355
  # After module loading, GlobalPipelineConfig has:
356
- # - well_filter_config: WellFilterConfig = WellFilterConfig()
357
- # And LazyWellFilterConfig is auto-created
356
+ # - partition_filter_config: PartitionFilterConfig = PartitionFilterConfig()
357
+ # And LazyPartitionFilterConfig is auto-created
358
358
 
359
359
  **Injection process:**
360
360
 
361
361
  1. Decorated classes are registered for injection
362
362
  2. At module load completion, ``_inject_all_pending_fields()`` is called
363
- 3. Field name is snake_case of class name (``WellFilterConfig`` → ``well_filter_config``)
364
- 4. Lazy version is created (``LazyWellFilterConfig``)
363
+ 3. Field name is snake_case of class name (``PartitionFilterConfig`` → ``partition_filter_config``)
364
+ 4. Lazy version is created (``LazyPartitionFilterConfig``)
365
365
  5. Both the field and lazy class are added to the global config
366
366
 
367
367
  **Benefits:**
@@ -384,9 +384,9 @@ Sets all inherited fields from parent classes to ``None`` by default:
384
384
  .. code-block:: python
385
385
 
386
386
  @dataclass
387
- class StepWellFilterConfig:
387
+ class StepPartitionFilterConfig:
388
388
  persistent: bool = True
389
- well_filter: str = None
389
+ partition_filter: str = None
390
390
 
391
391
  @dataclass
392
392
  class StreamingDefaults:
@@ -395,10 +395,10 @@ Sets all inherited fields from parent classes to ``None`` by default:
395
395
 
396
396
  @global_pipeline_config(inherit_as_none=True) # Default
397
397
  @dataclass
398
- class StreamingConfig(StepWellFilterConfig, StreamingDefaults):
398
+ class StreamingConfig(StepPartitionFilterConfig, StreamingDefaults):
399
399
  """Uses multiple inheritance.
400
400
 
401
- All inherited fields (persistent, well_filter, host, port)
401
+ All inherited fields (persistent, partition_filter, host, port)
402
402
  are automatically set to None for proper lazy resolution.
403
403
  """
404
404
  stream_type: str = "napari"
@@ -197,9 +197,9 @@ The ``inherit_as_none`` parameter enables proper dual-axis inheritance when usin
197
197
 
198
198
  # Base class with some fields
199
199
  @dataclass
200
- class StepWellFilterConfig:
200
+ class StepPartitionFilterConfig:
201
201
  persistent: bool = True
202
- well_filter: str = None
202
+ partition_filter: str = None
203
203
  timeout: int = 30
204
204
 
205
205
  # Another base class with different fields
@@ -212,11 +212,11 @@ The ``inherit_as_none`` parameter enables proper dual-axis inheritance when usin
212
212
  # Use inherit_as_none for multiple inheritance
213
213
  @global_pipeline_config(inherit_as_none=True) # Default behavior
214
214
  @dataclass
215
- class StreamingConfig(StepWellFilterConfig, StreamingDefaults):
215
+ class StreamingConfig(StepPartitionFilterConfig, StreamingDefaults):
216
216
  """Config using multiple inheritance.
217
217
 
218
218
  With inherit_as_none=True, all inherited fields
219
- (persistent, well_filter, timeout, host, port, transport_mode)
219
+ (persistent, partition_filter, timeout, host, port, transport_mode)
220
220
  are automatically set to None for proper lazy resolution.
221
221
  """
222
222
  stream_type: str = "napari"
@@ -227,7 +227,7 @@ The ``inherit_as_none`` parameter enables proper dual-axis inheritance when usin
227
227
  print(f"stream_type: {config.stream_type}") # "napari" (explicit)
228
228
  print(f"buffer_size: {config.buffer_size}") # 1024 (explicit)
229
229
  print(f"persistent: {config.persistent}") # None (inherited, set to None)
230
- print(f"well_filter: {config.well_filter}") # None (was already None)
230
+ print(f"partition_filter: {config.partition_filter}") # None (was already None)
231
231
  print(f"timeout: {config.timeout}") # None (inherited, set to None)
232
232
  print(f"host: {config.host}") # None (inherited, set to None)
233
233
  print(f"port: {config.port}") # None (inherited, set to None)
@@ -242,9 +242,9 @@ This enables polymorphic access to inherited fields without type-specific attrib
242
242
  from objectstate import config_context
243
243
 
244
244
  # Create a base config with values
245
- base_config = StepWellFilterConfig(
245
+ base_config = StepPartitionFilterConfig(
246
246
  persistent=False,
247
- well_filter="A01",
247
+ partition_filter="A01",
248
248
  timeout=60
249
249
  )
250
250
 
@@ -261,7 +261,7 @@ This enables polymorphic access to inherited fields without type-specific attrib
261
261
  # - stream_type from streaming_config ("custom")
262
262
  # - buffer_size from streaming_config (2048)
263
263
  # - persistent from base_config (False)
264
- # - well_filter from base_config ("A01")
264
+ # - partition_filter from base_config ("A01")
265
265
  # - timeout from base_config (60)
266
266
  # - host, port, transport_mode from StreamingDefaults
267
267
  pass
@@ -275,7 +275,7 @@ You can disable this behavior if you want inherited fields to keep their default
275
275
 
276
276
  @global_pipeline_config(inherit_as_none=False)
277
277
  @dataclass
278
- class ConcreteStreamingConfig(StepWellFilterConfig, StreamingDefaults):
278
+ class ConcreteStreamingConfig(StepPartitionFilterConfig, StreamingDefaults):
279
279
  """Config with concrete inherited defaults.
280
280
 
281
281
  Inherited fields keep their default values from parent classes.
@@ -283,8 +283,8 @@ You can disable this behavior if you want inherited fields to keep their default
283
283
  stream_type: str = "napari"
284
284
 
285
285
  config = ConcreteStreamingConfig()
286
- print(f"persistent: {config.persistent}") # True (from StepWellFilterConfig)
287
- print(f"timeout: {config.timeout}") # 30 (from StepWellFilterConfig)
286
+ print(f"persistent: {config.persistent}") # True (from StepPartitionFilterConfig)
287
+ print(f"timeout: {config.timeout}") # 30 (from StepPartitionFilterConfig)
288
288
  print(f"host: {config.host}") # "localhost" (from StreamingDefaults)
289
289
  print(f"port: {config.port}") # 5000 (from StreamingDefaults)
290
290
 
@@ -69,13 +69,13 @@ Get the source scope_id and type for an inherited field value:
69
69
  state = ObjectStateRegistry.get_by_scope("plate_123::step_0")
70
70
 
71
71
  # Get provenance for a field
72
- provenance = state.get_provenance("well_filter")
72
+ provenance = state.get_provenance("partition_filter")
73
73
  if provenance:
74
74
  source_scope, source_type = provenance
75
- print(f"well_filter comes from scope: {source_scope}")
76
- print(f"well_filter comes from type: {source_type.__name__}")
75
+ print(f"partition_filter comes from scope: {source_scope}")
76
+ print(f"partition_filter comes from type: {source_type.__name__}")
77
77
  else:
78
- print("well_filter is explicitly set on this object")
78
+ print("partition_filter is explicitly set on this object")
79
79
 
80
80
  **Returns:**
81
81
  * ``Tuple[str, type]`` if the value is inherited
@@ -95,7 +95,7 @@ Resolve a field value AND find its provenance source in ONE walk:
95
95
  # Resolve value and get provenance simultaneously
96
96
  value, source_scope, source_type = resolve_with_provenance(
97
97
  container_type=PathPlanningConfig,
98
- field_name="well_filter"
98
+ field_name="partition_filter"
99
99
  )
100
100
 
101
101
  print(f"Value: {value}")
@@ -105,7 +105,7 @@ Resolve a field value AND find its provenance source in ONE walk:
105
105
  **Parameters:**
106
106
 
107
107
  * ``container_type``: The type containing the field (e.g., ``LazyPathPlanningConfig``)
108
- * ``field_name``: Name of the field to find provenance for (e.g., ``"well_filter"``)
108
+ * ``field_name``: Name of the field to find provenance for (e.g., ``"partition_filter"``)
109
109
 
110
110
  **Returns:**
111
111
 
@@ -126,7 +126,7 @@ Convenience wrapper that returns only the scope and type (not the value):
126
126
 
127
127
  source_scope, source_type = get_field_provenance(
128
128
  container_type=PathPlanningConfig,
129
- field_name="well_filter"
129
+ field_name="partition_filter"
130
130
  )
131
131
 
132
132
  print(f"Field comes from: {source_scope} ({source_type.__name__})")
@@ -165,7 +165,7 @@ Use provenance to understand where a value comes from:
165
165
  state = ObjectStateRegistry.get_by_scope("plate_123::step_0")
166
166
 
167
167
  # Check where various fields come from
168
- for field_name in ["well_filter", "output_dir", "num_workers"]:
168
+ for field_name in ["partition_filter", "output_dir", "num_workers"]:
169
169
  provenance = state.get_provenance(field_name)
170
170
  if provenance:
171
171
  source_scope, source_type = provenance
@@ -177,7 +177,7 @@ Example output:
177
177
 
178
178
  .. code-block:: text
179
179
 
180
- well_filter: inherited from (GlobalPipelineConfig)
180
+ partition_filter: inherited from (GlobalPipelineConfig)
181
181
  output_dir: inherited from plate_123 (PlateConfig)
182
182
  num_workers: explicitly set on this object
183
183
 
@@ -209,7 +209,7 @@ Trace the full inheritance chain for a field:
209
209
  print(" → Value is a class default (no override)")
210
210
 
211
211
  # Use it
212
- trace_field_origin(PathPlanningConfig, "well_filter")
212
+ trace_field_origin(PathPlanningConfig, "partition_filter")
213
213
  trace_field_origin(PathPlanningConfig, "output_dir_suffix")
214
214
 
215
215
  UI Integration
@@ -242,8 +242,8 @@ Provenance information can be displayed in UI elements to help users understand
242
242
  }
243
243
 
244
244
  # Display in UI
245
- info = get_field_display_info("well_filter")
246
- print(f"well_filter: {info['value']} [{info['origin']}]")
245
+ info = get_field_display_info("partition_filter")
246
+ print(f"partition_filter: {info['value']} [{info['origin']}]")
247
247
 
248
248
  Provenance in Snapshots
249
249
  -------------------------
@@ -25,6 +25,7 @@ keywords = ["configuration", "dataclass", "lazy", "inheritance", "contextvars",
25
25
 
26
26
  dependencies = [
27
27
  "dill>=0.4.0",
28
+ "pycodify>=0.2.0,<0.3",
28
29
  "python-introspect>=0.2.0,<0.3",
29
30
  ]
30
31
 
@@ -211,13 +211,8 @@ def check_github_workflow():
211
211
  print(" ❌ .github/workflows/publish.yml not found")
212
212
  return False
213
213
 
214
- content = workflow_file.read_text()
215
- if 'PYPI_API_TOKEN' not in content:
216
- print(" ❌ PYPI_API_TOKEN not referenced in workflow")
217
- return False
218
-
219
- print(" ✅ GitHub Actions workflow configured")
220
- print(" Remember to set PYPI_API_TOKEN secret in GitHub!")
214
+ print(" ✅ Publication workflow present")
215
+ print(" Authentication is checked by the workflow's publisher, not artifact readiness.")
221
216
  return True
222
217
 
223
218
 
@@ -261,7 +256,7 @@ def main():
261
256
  if passed == total:
262
257
  print("\n🎉 All checks passed! Ready for release!")
263
258
  print("\nNext steps:")
264
- print(" 1. Set PYPI_API_TOKEN in GitHub secrets")
259
+ print(" 1. Verify the publication workflow's PyPI authorization")
265
260
  print(" 2. Run: python scripts/update_and_release.py")
266
261
  return 0
267
262
  else:
@@ -73,6 +73,7 @@ from objectstate.lazy_factory import (
73
73
  is_global_config_instance,
74
74
  # Raw-value-preserving utilities
75
75
  replace_raw,
76
+ retain_only_authored_paths,
76
77
  )
77
78
 
78
79
  # Resolver
@@ -100,9 +101,6 @@ from objectstate.context_manager import (
100
101
  register_hierarchy_relationship,
101
102
  unregister_hierarchy_relationship,
102
103
  get_ancestors_from_hierarchy,
103
- # Scope key utilities
104
- get_root_from_scope_key,
105
- is_scope_affected,
106
104
  # Context stack building (framework-agnostic)
107
105
  build_context_stack,
108
106
  # Threading utility for context propagation
@@ -181,8 +179,6 @@ __all__ = [
181
179
  'extract_all_configs',
182
180
  'get_base_global_config',
183
181
  'get_context_type_stack',
184
- 'get_root_from_scope_key',
185
- 'is_scope_affected',
186
182
  'build_context_stack',
187
183
  'spawn_thread_with_context',
188
184
  # Placeholder
@@ -216,13 +212,14 @@ __all__ = [
216
212
  'ObjectStateEditSession',
217
213
  ]
218
214
 
219
- __version__ = "1.3.1"
215
+ __version__ = "2.1.0"
220
216
  __author__ = 'OpenHCS Team'
221
217
  __description__ = 'Generic configuration framework for lazy dataclass resolution'
222
218
 
223
219
 
224
220
  # Object state management
225
221
  from objectstate.object_state_registry import ObjectStateRegistry
222
+ from objectstate.scope_key import ScopeKey
226
223
  from objectstate.object_state import ObjectState
227
224
  from objectstate.object_state_metadata import (
228
225
  ObjectStateMetadataContract,
@@ -259,6 +256,8 @@ __all__ += [
259
256
  # Object state
260
257
  'ObjectState',
261
258
  'ObjectStateRegistry',
259
+ 'ScopeKey',
260
+ 'retain_only_authored_paths',
262
261
  'ObjectStateMetadataContract',
263
262
  'ObjectStateMetadataContractRegistry',
264
263
  'ObjectStateMetadataStore',
@@ -0,0 +1,137 @@
1
+ """Python source for lazily resolved dataclasses, through pycodify.
2
+
3
+ Importing this module registers two formatters. Both write raw field values,
4
+ never resolved ones: a ``None`` field inherits from context and stays ``None``
5
+ in the source.
6
+
7
+ - :class:`LazyResolutionFormatter` (any :class:`LazyResolutionDataclass`):
8
+ clean mode writes the fields whose raw value differs from the field default.
9
+ - :class:`LazyDataclassFormatter` (factory-made :class:`LazyDataclass`): clean
10
+ mode writes a field when it was passed to the constructor or holds a concrete
11
+ value; an inherited ``None`` is omitted, and so is a nested lazy dataclass
12
+ that carries nothing.
13
+
14
+ Full mode writes every field.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import MISSING, Field, fields, is_dataclass
20
+
21
+ from pycodify import (
22
+ FormatContext,
23
+ SourceFormatter,
24
+ SourceFragment,
25
+ imported_name,
26
+ to_source,
27
+ )
28
+
29
+ from objectstate.field_access import DataclassFieldAccess
30
+ from objectstate.lazy_factory import LazyDataclass, LazyResolutionDataclass
31
+
32
+
33
+ def _set_field_names(value: LazyDataclass) -> frozenset[str]:
34
+ return frozenset(object.__getattribute__(value, "_explicitly_set_fields"))
35
+
36
+
37
+ def lazy_dataclass_carries_values(value: LazyDataclass) -> bool:
38
+ """Whether a lazy dataclass has a constructor-set field or a concrete value."""
39
+
40
+ return bool(_set_field_names(value)) or any(
41
+ DataclassFieldAccess.raw_value(value, field.name) is not None
42
+ for field in fields(value)
43
+ )
44
+
45
+
46
+ def _is_instance_of_dataclass(value: object) -> bool:
47
+ return is_dataclass(value) and not isinstance(value, type)
48
+
49
+
50
+ def raw_values_equal(left: object, right: object) -> bool:
51
+ """Whether two values are equal, comparing dataclasses by raw field values."""
52
+
53
+ if _is_instance_of_dataclass(left) and _is_instance_of_dataclass(right):
54
+ if type(left) is not type(right):
55
+ return False
56
+ return all(
57
+ raw_values_equal(
58
+ DataclassFieldAccess.raw_value(left, field.name),
59
+ DataclassFieldAccess.raw_value(right, field.name),
60
+ )
61
+ for field in fields(left)
62
+ if field.init
63
+ )
64
+ return left == right
65
+
66
+
67
+ def field_needs_source(current: object, default: object, context: FormatContext) -> bool:
68
+ """Whether an owner's field holding ``current`` must be written.
69
+
70
+ For owners (such as a step) that format their own constructor fields and
71
+ hold lazy dataclasses among them. Full mode writes every field; clean mode
72
+ writes a lazy dataclass that carries values different from the default,
73
+ and any other value that differs from the default.
74
+ """
75
+
76
+ if not context.clean_mode:
77
+ return True
78
+ if isinstance(current, LazyDataclass):
79
+ if not lazy_dataclass_carries_values(current):
80
+ return False
81
+ return not raw_values_equal(current, default)
82
+ return current != default
83
+
84
+
85
+ def _field_default(field: Field) -> object:
86
+ if field.default is not MISSING:
87
+ return field.default
88
+ if field.default_factory is not MISSING:
89
+ return field.default_factory()
90
+ return MISSING
91
+
92
+
93
+ class LazyResolutionFormatter(SourceFormatter):
94
+ priority = 36
95
+ formats: type = LazyResolutionDataclass
96
+
97
+ def can_format(self, value: object) -> bool:
98
+ return isinstance(value, self.formats)
99
+
100
+ def clean_field_needs_source(self, value: object, field: Field, current: object) -> bool:
101
+ return not raw_values_equal(current, _field_default(field))
102
+
103
+ def format(self, value: object, context: FormatContext) -> SourceFragment:
104
+ lazy_type = type(value)
105
+ class_ref = imported_name(lazy_type.__module__, lazy_type.__qualname__, context)
106
+ imports = set(class_ref.imports)
107
+ field_ctx = context.indented()
108
+ lines = []
109
+ for field in fields(value):
110
+ if not field.init:
111
+ continue
112
+ current = DataclassFieldAccess.raw_value(value, field.name)
113
+ if context.clean_mode and not self.clean_field_needs_source(value, field, current):
114
+ continue
115
+ fragment = to_source(current, field_ctx)
116
+ imports |= fragment.imports
117
+ lines.append(f"{field.name}={fragment.code}")
118
+
119
+ if not lines:
120
+ return SourceFragment(f"{class_ref.code}()", frozenset(imports))
121
+ inner = f",\n{field_ctx.indent_str}".join(lines)
122
+ code = f"{class_ref.code}(\n{field_ctx.indent_str}{inner}\n{context.indent_str})"
123
+ return SourceFragment(code, frozenset(imports))
124
+
125
+
126
+ class LazyDataclassFormatter(LazyResolutionFormatter):
127
+ priority = 37
128
+ formats = LazyDataclass
129
+
130
+ def clean_field_needs_source(self, value: object, field: Field, current: object) -> bool:
131
+ if current is None:
132
+ return False
133
+ if field.name in _set_field_names(value):
134
+ return True
135
+ if isinstance(current, LazyDataclass):
136
+ return lazy_dataclass_carries_values(current)
137
+ return True