objectstate 2.0.0__tar.gz → 2.1.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 (104) hide show
  1. {objectstate-2.0.0 → objectstate-2.1.1}/.github/badges/coverage.svg +1 -1
  2. {objectstate-2.0.0 → objectstate-2.1.1}/PKG-INFO +24 -7
  3. {objectstate-2.0.0 → objectstate-2.1.1}/README.md +22 -6
  4. {objectstate-2.0.0 → objectstate-2.1.1}/docs/architectural_fixes.rst +3 -3
  5. {objectstate-2.0.0 → objectstate-2.1.1}/docs/architecture.rst +10 -10
  6. {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/dual_axis.rst +11 -11
  7. {objectstate-2.0.0 → objectstate-2.1.1}/docs/provenance.rst +12 -12
  8. {objectstate-2.0.0 → objectstate-2.1.1}/pyproject.toml +1 -0
  9. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/__init__.py +1 -1
  10. objectstate-2.1.1/src/objectstate/codegen.py +137 -0
  11. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/context_manager.py +8 -13
  12. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/docs/configuration_framework.rst +1 -1
  13. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/dual_axis_resolver.py +10 -47
  14. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/lazy_factory.py +15 -12
  15. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/live_context_resolver.py +3 -2
  16. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/object_state.py +25 -43
  17. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/object_state_registry.py +4 -6
  18. objectstate-2.1.1/tests/test_codegen.py +113 -0
  19. {objectstate-2.0.0 → objectstate-2.1.1}/.github/workflows/ci.yml +0 -0
  20. {objectstate-2.0.0 → objectstate-2.1.1}/.github/workflows/coverage-pages.yml +0 -0
  21. {objectstate-2.0.0 → objectstate-2.1.1}/.github/workflows/publish.yml +0 -0
  22. {objectstate-2.0.0 → objectstate-2.1.1}/.gitignore +0 -0
  23. {objectstate-2.0.0 → objectstate-2.1.1}/.readthedocs.yaml +0 -0
  24. {objectstate-2.0.0 → objectstate-2.1.1}/BADGE_SETUP.md +0 -0
  25. {objectstate-2.0.0 → objectstate-2.1.1}/LICENSE +0 -0
  26. {objectstate-2.0.0 → objectstate-2.1.1}/docs/.gitignore +0 -0
  27. {objectstate-2.0.0 → objectstate-2.1.1}/docs/Makefile +0 -0
  28. {objectstate-2.0.0 → objectstate-2.1.1}/docs/api/modules.rst +0 -0
  29. {objectstate-2.0.0 → objectstate-2.1.1}/docs/archive/DOCUMENTATION_AUDIT.md +0 -0
  30. {objectstate-2.0.0 → objectstate-2.1.1}/docs/archive/DOCUMENTATION_FIXES.md +0 -0
  31. {objectstate-2.0.0 → objectstate-2.1.1}/docs/archive/README.md +0 -0
  32. {objectstate-2.0.0 → objectstate-2.1.1}/docs/conf.py +0 -0
  33. {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/atomic_operations.rst +0 -0
  34. {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/basic.rst +0 -0
  35. {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/index.rst +0 -0
  36. {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/ui.rst +0 -0
  37. {objectstate-2.0.0 → objectstate-2.1.1}/docs/index.rst +0 -0
  38. {objectstate-2.0.0 → objectstate-2.1.1}/docs/make.bat +0 -0
  39. {objectstate-2.0.0 → objectstate-2.1.1}/docs/performance/canonical-context-type-resolution-20261005.json +0 -0
  40. {objectstate-2.0.0 → objectstate-2.1.1}/docs/quickstart.rst +0 -0
  41. {objectstate-2.0.0 → objectstate-2.1.1}/docs/state_management.rst +0 -0
  42. {objectstate-2.0.0 → objectstate-2.1.1}/docs/state_propagation.rst +0 -0
  43. {objectstate-2.0.0 → objectstate-2.1.1}/docs/undo_redo.rst +0 -0
  44. {objectstate-2.0.0 → objectstate-2.1.1}/paper.bib +0 -0
  45. {objectstate-2.0.0 → objectstate-2.1.1}/paper.md +0 -0
  46. {objectstate-2.0.0 → objectstate-2.1.1}/plans/n_dimensional_inheritance.md +0 -0
  47. {objectstate-2.0.0 → objectstate-2.1.1}/scripts/check_ruff_added_lines.py +0 -0
  48. {objectstate-2.0.0 → objectstate-2.1.1}/scripts/release.py +0 -0
  49. {objectstate-2.0.0 → objectstate-2.1.1}/scripts/update_and_release.py +0 -0
  50. {objectstate-2.0.0 → objectstate-2.1.1}/scripts/update_version.py +0 -0
  51. {objectstate-2.0.0 → objectstate-2.1.1}/scripts/verify_release_ready.py +0 -0
  52. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/collection_containers.py +0 -0
  53. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/config.py +0 -0
  54. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/construction_binding.py +0 -0
  55. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/docs/context_system.rst +0 -0
  56. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/docs/object_state_lifecycle.rst +0 -0
  57. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/edit_session.py +0 -0
  58. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/field_access.py +0 -0
  59. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/global_config.py +0 -0
  60. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/inspection.py +0 -0
  61. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/object_state_metadata.py +0 -0
  62. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/parameter_owner.py +0 -0
  63. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/parameter_structure.py +0 -0
  64. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/parametric_axes.py +0 -0
  65. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/placeholder.py +0 -0
  66. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/reified_generics.py +0 -0
  67. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/scope_key.py +0 -0
  68. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/snapshot_model.py +0 -0
  69. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/subfield_semantics.py +0 -0
  70. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/time_travel_profile.py +0 -0
  71. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/token_cache.py +0 -0
  72. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/transaction_checkpoint.py +0 -0
  73. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/ui_visibility.py +0 -0
  74. {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/value_semantics.py +0 -0
  75. {objectstate-2.0.0 → objectstate-2.1.1}/tests/__init__.py +0 -0
  76. {objectstate-2.0.0 → objectstate-2.1.1}/tests/conftest.py +0 -0
  77. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_authored_paths.py +0 -0
  78. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_callable_baseline.py +0 -0
  79. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_config.py +0 -0
  80. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_construction_history.py +0 -0
  81. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_context_manager.py +0 -0
  82. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_field_access.py +0 -0
  83. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_global_config.py +0 -0
  84. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_history_persistence.py +0 -0
  85. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_inspection.py +0 -0
  86. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_integration.py +0 -0
  87. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_lazy_factory.py +0 -0
  88. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_object_state_metadata_contracts.py +0 -0
  89. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_object_state_restore.py +0 -0
  90. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_object_state_update_instance.py +0 -0
  91. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_parameter_owner.py +0 -0
  92. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_parametric_axes.py +0 -0
  93. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_placeholder.py +0 -0
  94. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_registry_subscription.py +0 -0
  95. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_reified_generics.py +0 -0
  96. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_release_readiness.py +0 -0
  97. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_resolved_change_notifications.py +0 -0
  98. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_scope_key.py +0 -0
  99. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_subfield_semantics.py +0 -0
  100. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_timeline_projection.py +0 -0
  101. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_transaction_checkpoint.py +0 -0
  102. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_ui_visible_field_projection.py +0 -0
  103. {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_undo_redo_cascade.py +0 -0
  104. {objectstate-2.0.0 → objectstate-2.1.1}/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: 75.78%"><title>coverage: 75.78%</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="#67ac09"/><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">75.78%</text><text x="865" y="150" fill-opacity=".3" textLength="430">75.78%</text></g><text x="865" y="140" textLength="430">75.78%</text></g></g></svg>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: objectstate
3
- Version: 2.0.0
3
+ Version: 2.1.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,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
 
@@ -212,7 +212,7 @@ __all__ = [
212
212
  'ObjectStateEditSession',
213
213
  ]
214
214
 
215
- __version__ = "2.0.0"
215
+ __version__ = "2.1.1"
216
216
  __author__ = 'OpenHCS Team'
217
217
  __description__ = 'Generic configuration framework for lazy dataclass resolution'
218
218
 
@@ -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
@@ -187,8 +187,9 @@ def config_context(obj, mask_with_none: bool = False, use_live_global: bool = Tr
187
187
  # to_base_config() passes None values to base class constructor,
188
188
  # but frozen dataclasses with non-Optional defaults substitute the
189
189
  # default value for None. This breaks cross-hierarchy inheritance.
190
- # Example: LazyWellFilterConfig(well_filter_mode=None) becomes
191
- # WellFilterConfig(well_filter_mode=INCLUDE) instead of keeping None.
190
+ # Example: LazyPartitionFilterConfig(partition_filter_mode=None)
191
+ # becomes PartitionFilterConfig(partition_filter_mode=INCLUDE)
192
+ # instead of keeping None.
192
193
  #
193
194
  # Instead, pass lazy dataclass directly to _merge_nested_dataclass,
194
195
  # which uses object.__getattribute__ to get raw values and correctly
@@ -730,12 +731,12 @@ def _find_live_values_for_type(target_type: type, live_context: dict) -> dict |
730
731
  Find live values for a target type in live_context.
731
732
 
732
733
  Handles type normalization (lazy vs base types) AND inheritance.
733
- For sibling inheritance, a StepWellFilterConfig's values should be
734
- usable when resolving WellFilterConfig's placeholders.
734
+ For sibling inheritance, a StepPartitionFilterConfig's values should be
735
+ usable when resolving PartitionFilterConfig's placeholders.
735
736
 
736
737
  IMPORTANT: Prefers subclass matches over exact matches.
737
- This ensures StepWellFilterConfig values (with concrete value) are used
738
- for WellFilterConfig resolution, not WellFilterConfig values (with None).
738
+ This ensures StepPartitionFilterConfig values (with concrete value) are used
739
+ for PartitionFilterConfig resolution, not PartitionFilterConfig values (with None).
739
740
 
740
741
  Args:
741
742
  target_type: The type to find values for
@@ -755,7 +756,7 @@ def _find_live_values_for_type(target_type: type, live_context: dict) -> dict |
755
756
  return config_values
756
757
 
757
758
  # First pass: look for subclass match (more specific wins) after normalization
758
- # e.g., StepWellFilterConfig values for WellFilterConfig resolution
759
+ # e.g., StepPartitionFilterConfig values for PartitionFilterConfig resolution
759
760
  for config_type, config_values in live_context.items():
760
761
  config_base = _normalize_type(config_type)
761
762
  try:
@@ -899,12 +900,6 @@ def get_base_global_config(use_live: bool = True):
899
900
  # Get the appropriate global config (live or saved)
900
901
  current_global = get_current_global_config(base_config_type, use_live=use_live)
901
902
  if current_global is not None:
902
- # DEBUG: Log well_filter value
903
- try:
904
- wf_value = object.__getattribute__(current_global.well_filter_config, 'well_filter')
905
- logger.debug(f"🔍 get_base_global_config: use_live={use_live}, well_filter={wf_value}")
906
- except:
907
- pass
908
903
  return current_global
909
904
 
910
905
  # Fallback to default if none was set
@@ -63,7 +63,7 @@ Nested configurations use the ``@global_config`` decorator to inject fields into
63
63
 
64
64
  @global_config
65
65
  @dataclass(frozen=True)
66
- class PathPlanningConfig(WellFilterConfig):
66
+ class PathPlanningConfig(PartitionFilterConfig):
67
67
  output_dir_suffix: str = ""
68
68
  sub_dir: str = ""
69
69