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.
- {objectstate-2.0.0 → objectstate-2.1.1}/.github/badges/coverage.svg +1 -1
- {objectstate-2.0.0 → objectstate-2.1.1}/PKG-INFO +24 -7
- {objectstate-2.0.0 → objectstate-2.1.1}/README.md +22 -6
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/architectural_fixes.rst +3 -3
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/architecture.rst +10 -10
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/dual_axis.rst +11 -11
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/provenance.rst +12 -12
- {objectstate-2.0.0 → objectstate-2.1.1}/pyproject.toml +1 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/__init__.py +1 -1
- objectstate-2.1.1/src/objectstate/codegen.py +137 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/context_manager.py +8 -13
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/docs/configuration_framework.rst +1 -1
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/dual_axis_resolver.py +10 -47
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/lazy_factory.py +15 -12
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/live_context_resolver.py +3 -2
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/object_state.py +25 -43
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/object_state_registry.py +4 -6
- objectstate-2.1.1/tests/test_codegen.py +113 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/.github/workflows/ci.yml +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/.github/workflows/coverage-pages.yml +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/.github/workflows/publish.yml +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/.gitignore +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/.readthedocs.yaml +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/BADGE_SETUP.md +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/LICENSE +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/.gitignore +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/Makefile +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/api/modules.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/archive/DOCUMENTATION_AUDIT.md +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/archive/DOCUMENTATION_FIXES.md +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/archive/README.md +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/conf.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/atomic_operations.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/basic.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/index.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/examples/ui.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/index.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/make.bat +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/performance/canonical-context-type-resolution-20261005.json +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/quickstart.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/state_management.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/state_propagation.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/docs/undo_redo.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/paper.bib +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/paper.md +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/plans/n_dimensional_inheritance.md +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/scripts/check_ruff_added_lines.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/scripts/release.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/scripts/update_and_release.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/scripts/update_version.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/scripts/verify_release_ready.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/collection_containers.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/config.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/construction_binding.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/docs/context_system.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/docs/object_state_lifecycle.rst +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/edit_session.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/field_access.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/global_config.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/inspection.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/object_state_metadata.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/parameter_owner.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/parameter_structure.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/parametric_axes.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/placeholder.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/reified_generics.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/scope_key.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/snapshot_model.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/subfield_semantics.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/time_travel_profile.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/token_cache.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/transaction_checkpoint.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/ui_visibility.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/src/objectstate/value_semantics.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/__init__.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/conftest.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_authored_paths.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_callable_baseline.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_config.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_construction_history.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_context_manager.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_field_access.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_global_config.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_history_persistence.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_inspection.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_integration.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_lazy_factory.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_object_state_metadata_contracts.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_object_state_restore.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_object_state_update_instance.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_parameter_owner.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_parametric_axes.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_placeholder.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_registry_subscription.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_reified_generics.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_release_readiness.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_resolved_change_notifications.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_scope_key.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_subfield_semantics.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_timeline_projection.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_transaction_checkpoint.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_ui_visible_field_projection.py +0 -0
- {objectstate-2.0.0 → objectstate-2.1.1}/tests/test_undo_redo_cascade.py +0 -0
- {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:
|
|
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.
|
|
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
|
|
294
|
-
|
|
310
|
+
class PartitionFilterConfig:
|
|
311
|
+
partition_filter: str = None
|
|
295
312
|
mode: str = "include"
|
|
296
313
|
|
|
297
314
|
# After module loading, GlobalPipelineConfig automatically has:
|
|
298
|
-
# -
|
|
299
|
-
# And
|
|
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., `
|
|
305
|
-
- Lazy version is automatically created (e.g., `
|
|
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
|
|
258
|
-
|
|
273
|
+
class PartitionFilterConfig:
|
|
274
|
+
partition_filter: str = None
|
|
259
275
|
mode: str = "include"
|
|
260
276
|
|
|
261
277
|
# After module loading, GlobalPipelineConfig automatically has:
|
|
262
|
-
# -
|
|
263
|
-
# And
|
|
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., `
|
|
269
|
-
- Lazy version is automatically created (e.g., `
|
|
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 ``
|
|
69
|
-
wouldn't update placeholders for inheriting fields like ``PathPlanningConfig.
|
|
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
|
|
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
|
|
352
|
-
|
|
351
|
+
class PartitionFilterConfig:
|
|
352
|
+
partition_filter: str = None
|
|
353
353
|
mode: str = "include"
|
|
354
354
|
|
|
355
355
|
# After module loading, GlobalPipelineConfig has:
|
|
356
|
-
# -
|
|
357
|
-
# And
|
|
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 (``
|
|
364
|
-
4. Lazy version is created (``
|
|
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
|
|
387
|
+
class StepPartitionFilterConfig:
|
|
388
388
|
persistent: bool = True
|
|
389
|
-
|
|
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(
|
|
398
|
+
class StreamingConfig(StepPartitionFilterConfig, StreamingDefaults):
|
|
399
399
|
"""Uses multiple inheritance.
|
|
400
400
|
|
|
401
|
-
All inherited fields (persistent,
|
|
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
|
|
200
|
+
class StepPartitionFilterConfig:
|
|
201
201
|
persistent: bool = True
|
|
202
|
-
|
|
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(
|
|
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,
|
|
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"
|
|
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 =
|
|
245
|
+
base_config = StepPartitionFilterConfig(
|
|
246
246
|
persistent=False,
|
|
247
|
-
|
|
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
|
-
# -
|
|
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(
|
|
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
|
|
287
|
-
print(f"timeout: {config.timeout}") # 30 (from
|
|
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("
|
|
72
|
+
provenance = state.get_provenance("partition_filter")
|
|
73
73
|
if provenance:
|
|
74
74
|
source_scope, source_type = provenance
|
|
75
|
-
print(f"
|
|
76
|
-
print(f"
|
|
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("
|
|
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="
|
|
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., ``"
|
|
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="
|
|
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 ["
|
|
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
|
-
|
|
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, "
|
|
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("
|
|
246
|
-
print(f"
|
|
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
|
-------------------------
|
|
@@ -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:
|
|
191
|
-
#
|
|
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
|
|
734
|
-
usable when resolving
|
|
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
|
|
738
|
-
for
|
|
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.,
|
|
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(
|
|
66
|
+
class PathPlanningConfig(PartitionFilterConfig):
|
|
67
67
|
output_dir_suffix: str = ""
|
|
68
68
|
sub_dir: str = ""
|
|
69
69
|
|