objectstate 3.0.1__tar.gz → 3.0.3__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-3.0.1 → objectstate-3.0.3}/.github/badges/coverage.svg +1 -1
- {objectstate-3.0.1 → objectstate-3.0.3}/PKG-INFO +1 -1
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/state_propagation.rst +39 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/__init__.py +1 -1
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/docs/configuration_framework.rst +1 -1
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/docs/context_system.rst +31 -4
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/dual_axis_resolver.py +9 -4
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/object_state.py +167 -53
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/object_state_registry.py +53 -60
- objectstate-3.0.3/src/objectstate/resolution_sources.py +176 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/scope_key.py +12 -4
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/transaction_checkpoint.py +3 -0
- objectstate-3.0.3/tests/test_resolution_precedence_order.py +315 -0
- objectstate-3.0.3/tests/test_resolved_values_follow_registered_ancestors.py +630 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/.github/workflows/ci.yml +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/.github/workflows/coverage-pages.yml +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/.github/workflows/publish.yml +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/.gitignore +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/.readthedocs.yaml +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/BADGE_SETUP.md +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/LICENSE +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/README.md +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/.gitignore +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/Makefile +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/api/modules.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/architectural_fixes.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/architecture.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/archive/DOCUMENTATION_AUDIT.md +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/archive/DOCUMENTATION_FIXES.md +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/archive/README.md +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/conf.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/examples/atomic_operations.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/examples/basic.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/examples/dual_axis.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/examples/index.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/examples/ui.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/index.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/make.bat +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/performance/canonical-context-type-resolution-20261005.json +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/provenance.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/quickstart.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/state_management.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/docs/undo_redo.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/paper.bib +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/paper.md +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/plans/n_dimensional_inheritance.md +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/pyproject.toml +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/scripts/check_ruff_added_lines.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/scripts/release.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/scripts/update_and_release.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/scripts/update_version.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/scripts/verify_release_ready.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/codegen.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/collection_containers.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/config.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/construction_binding.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/context_manager.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/docs/object_state_lifecycle.rst +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/edit_session.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/field_access.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/global_config.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/inspection.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/lazy_factory.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/live_context_resolver.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/object_state_metadata.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/parameter_owner.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/parameter_structure.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/parametric_axes.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/placeholder.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/reified_generics.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/snapshot_model.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/subfield_semantics.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/time_travel_profile.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/token_cache.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/ui_visibility.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/src/objectstate/value_semantics.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/__init__.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/conftest.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_atomic_block_context.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_authored_paths.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_callable_baseline.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_codegen.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_config.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_construction_history.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_context_failures_surface.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_context_manager.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_field_access.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_global_config.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_history_persistence.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_inspection.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_integration.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_lazy_counterpart_registry.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_lazy_factory.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_live_context_resolver_answers.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_object_state_metadata_contracts.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_object_state_restore.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_object_state_update_instance.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_one_field_resolution.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_parameter_owner.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_parametric_axes.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_placeholder.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_registry_subscription.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_reified_generics.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_release_readiness.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_resolved_change_notifications.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_scope_key.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_scopeless_state_resolution.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_subfield_semantics.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_timeline_projection.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_transaction_checkpoint.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_ui_visible_field_projection.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/tests/test_undo_redo_cascade.py +0 -0
- {objectstate-3.0.1 → objectstate-3.0.3}/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: 82.11%"><title>coverage: 82.11%</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">82.11%</text><text x="865" y="150" fill-opacity=".3" textLength="430">82.11%</text></g><text x="865" y="140" textLength="430">82.11%</text></g></g></svg>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: objectstate
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.3
|
|
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
|
|
@@ -94,3 +94,42 @@ Use parent forwarding when all of the following are true:
|
|
|
94
94
|
|
|
95
95
|
For a nested dataclass field in the same state, call ``update_parameter()`` on
|
|
96
96
|
the dotted path instead.
|
|
97
|
+
|
|
98
|
+
What a resolved value is computed from
|
|
99
|
+
--------------------------------------
|
|
100
|
+
|
|
101
|
+
A resolved value of a state is computed from three things: which states are
|
|
102
|
+
registered at the scopes above it, what each of them stores for the field's config
|
|
103
|
+
type and field name, and what the global config stores. Each resolved snapshot (live
|
|
104
|
+
and saved) keeps the objects it was computed from (``ResolutionSources``): the global
|
|
105
|
+
config instance of the thread that resolved it and the object of every state
|
|
106
|
+
registered above. Every resolved read (``get_resolved_value``,
|
|
107
|
+
``get_saved_resolved_value``, ``get_provenance``, ``to_resolved_object``,
|
|
108
|
+
``state.fields``) first compares those objects with the ones the state would be given
|
|
109
|
+
now. When they are the same objects the read answers from the snapshot. When one is
|
|
110
|
+
another object, the two are compared field by field, the way field resolution reads a
|
|
111
|
+
context layer, and only the fields of the state that are unset and can inherit a
|
|
112
|
+
differing ``(config type, field)`` are resolved again. When other scopes have a
|
|
113
|
+
registered state than before, every field is.
|
|
114
|
+
|
|
115
|
+
So no caller announces a change. Registering, unregistering or replacing an ancestor,
|
|
116
|
+
editing, saving, restoring, undoing or redoing an ancestor's value, and replacing the
|
|
117
|
+
global config with ``set_live_global_config`` / ``set_saved_global_config`` all reach
|
|
118
|
+
a state on its next read, whether the state is registered or not. A registered state
|
|
119
|
+
is also told at once, so its subscribers hear of the change without a read; a state
|
|
120
|
+
that is not registered is referenced by nothing and tells its subscribers on its next
|
|
121
|
+
read.
|
|
122
|
+
|
|
123
|
+
Limits:
|
|
124
|
+
|
|
125
|
+
- Objects are compared by identity. A source that is changed in place is not seen.
|
|
126
|
+
``ObjectState.to_object`` returns the same function object for a state that wraps a
|
|
127
|
+
plain function, so states registered below a function-backed state follow its edits
|
|
128
|
+
only through the registry (that is, when they are registered themselves).
|
|
129
|
+
- The global config is stored per thread. A snapshot follows the global config of the
|
|
130
|
+
thread that resolved it; a read from another thread answers from the snapshot and
|
|
131
|
+
does not resolve it again for that thread's global config.
|
|
132
|
+
- A state built or resolved inside a ``config_context`` the caller entered takes that
|
|
133
|
+
context's layers into its snapshot and keeps them after the block
|
|
134
|
+
(`issue #53 <https://github.com/OpenHCSDev/ObjectState/issues/53>`_).
|
|
135
|
+
|
|
@@ -149,7 +149,7 @@ Usage Example
|
|
|
149
149
|
)
|
|
150
150
|
)
|
|
151
151
|
|
|
152
|
-
The lazy configs resolve
|
|
152
|
+
The lazy configs resolve class first: ``DatabaseConfig`` is looked up in the ``AppConfig`` layer, then in the ``GlobalAppConfig`` layer, and only if neither holds a value is a base class of ``DatabaseConfig`` tried in those layers. A value on the more specific class in the global config therefore beats a value on a base class in the app config. :ref:`precedence-order` states the order with an example.
|
|
153
153
|
|
|
154
154
|
Framework Modules
|
|
155
155
|
----------------
|
|
@@ -195,10 +195,37 @@ The dual-axis resolver reads the layer stack of the active context:
|
|
|
195
195
|
|
|
196
196
|
value, source_scope, source_type = resolve_with_provenance(LazyDatabaseConfig, "port")
|
|
197
197
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
Attribute access on a lazy config returns the ``value`` of this same result.
|
|
199
|
+
|
|
200
|
+
.. _precedence-order:
|
|
201
|
+
|
|
202
|
+
Precedence Order
|
|
203
|
+
~~~~~~~~~~~~~~~~
|
|
204
|
+
|
|
205
|
+
Resolution is class first. For each class in the MRO of the type being read, most specific
|
|
206
|
+
first, the resolver asks every layer from the innermost scope to the outermost for a config
|
|
207
|
+
of that class and returns the first value that is not ``None``, together with the layer's
|
|
208
|
+
scope id and the class it was read from. Only when no layer holds a value for a class is
|
|
209
|
+
the next, less specific class tried.
|
|
210
|
+
|
|
211
|
+
So the class decides before the scope does. Take ``Leaf(Base)``, a field ``value`` read on
|
|
212
|
+
``Leaf`` in a step, and two layers that hold a value:
|
|
213
|
+
|
|
214
|
+
========================== ====================== =====================
|
|
215
|
+
Layer (scope) ``Base.value`` ``Leaf.value``
|
|
216
|
+
========================== ====================== =====================
|
|
217
|
+
plate (inner) ``"base at plate"`` ``None``
|
|
218
|
+
global (outer) ``None`` ``"leaf at global"``
|
|
219
|
+
========================== ====================== =====================
|
|
220
|
+
|
|
221
|
+
The read returns ``"leaf at global"`` with source ``("", Leaf)``: ``Leaf`` is tried in
|
|
222
|
+
every scope before ``Base`` is tried in any, although the plate is the nearer scope. A
|
|
223
|
+
value on the same class in a nearer scope does win: with ``Leaf.value`` also set on the
|
|
224
|
+
plate, the plate's value is returned.
|
|
225
|
+
|
|
226
|
+
This is the only statement of the order. ``objectstate.dual_axis_resolver`` implements it
|
|
227
|
+
in ``_resolve_in_layers``; ``tests/test_resolution_precedence_order.py`` checks it for
|
|
228
|
+
every set of (class, scope) sites.
|
|
202
229
|
|
|
203
230
|
Usage Pattern
|
|
204
231
|
------------
|
|
@@ -151,11 +151,16 @@ def resolve_in_stack(stack, container_type: type, field_name: str) -> ResolvedFi
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def _resolve_in_layers(layers: tuple, container_type: type, field_name: str) -> ResolvedField:
|
|
154
|
-
"""
|
|
154
|
+
"""Resolve class first: the MRO is the outer loop, the layers the inner one.
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
For each class in the container type's MRO, most specific first, every layer is
|
|
157
|
+
asked from the innermost scope to the outermost; the first value that is not
|
|
158
|
+
``None`` wins. ``None`` in an inner layer means "inherit", so a less specific
|
|
159
|
+
class is tried only after every layer was asked for the more specific one.
|
|
160
|
+
A layer that masks the layers outside it ends the walk outward.
|
|
161
|
+
|
|
162
|
+
The order is stated with an example in ``docs/context_system.rst`` under
|
|
163
|
+
"Precedence Order"; this function is its implementation.
|
|
159
164
|
"""
|
|
160
165
|
container_base = _normalize_to_base(container_type)
|
|
161
166
|
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import copy
|
|
14
14
|
import logging
|
|
15
|
+
import threading
|
|
15
16
|
from collections.abc import Callable, Sequence
|
|
16
17
|
from dataclasses import fields as dataclass_fields
|
|
17
18
|
from dataclasses import is_dataclass
|
|
@@ -33,6 +34,7 @@ from objectstate.object_state_registry import ObjectStateRegistry
|
|
|
33
34
|
from objectstate.scope_key import ScopeKey
|
|
34
35
|
from objectstate.parameter_owner import ParameterOwner
|
|
35
36
|
from objectstate.parameter_structure import ParameterStructure
|
|
37
|
+
from objectstate.resolution_sources import ResolutionSources
|
|
36
38
|
from objectstate.global_config import GlobalContextValues
|
|
37
39
|
from objectstate.lazy_factory import is_global_config_type
|
|
38
40
|
from objectstate.subfield_semantics import (
|
|
@@ -226,6 +228,10 @@ class ObjectState:
|
|
|
226
228
|
# Maps dotted_path → (source_scope_id, source_type) for inherited fields
|
|
227
229
|
# source_type may differ from local container_type due to MRO inheritance
|
|
228
230
|
self._live_provenance: dict[str, tuple[str | None, type | None]] = {}
|
|
231
|
+
# What each resolved snapshot was computed from, keyed by ``use_saved``. A
|
|
232
|
+
# snapshot is current only while ``_resolution_sources()`` still returns the
|
|
233
|
+
# same objects; a snapshot without an entry is resolved again in full.
|
|
234
|
+
self._resolved_from: dict[bool, ResolutionSources] = {}
|
|
229
235
|
|
|
230
236
|
# === Saved baseline (2 attributes) ===
|
|
231
237
|
self._saved_resolved: dict[str, Any] = {}
|
|
@@ -264,9 +270,8 @@ class ObjectState:
|
|
|
264
270
|
# === Time-Travel ===
|
|
265
271
|
# Instance-level time-travel removed - use ObjectStateRegistry class-level DAG instead
|
|
266
272
|
|
|
267
|
-
# Initialize baselines (
|
|
268
|
-
self.
|
|
269
|
-
assert self._live_resolved is not None # Guaranteed by _ensure_live_resolved
|
|
273
|
+
# Initialize baselines (nothing to tell anyone during init)
|
|
274
|
+
self._live_resolved = self._compute_resolved_snapshot()
|
|
270
275
|
|
|
271
276
|
# CRITICAL: Initialize _saved_parameters BEFORE _compute_resolved_snapshot(use_saved=True)
|
|
272
277
|
# because that method reads from _saved_parameters to get raw values.
|
|
@@ -476,8 +481,7 @@ class ObjectState:
|
|
|
476
481
|
def to_saved_resolved_object(self) -> Any:
|
|
477
482
|
"""Reconstruct this state as an object with saved resolved values."""
|
|
478
483
|
self._check_and_sync_delegate()
|
|
479
|
-
|
|
480
|
-
self._saved_resolved = self._compute_resolved_snapshot(use_saved=True)
|
|
484
|
+
self._ensure_saved_resolved()
|
|
481
485
|
return self._reconstruct_from_resolved(
|
|
482
486
|
"", self._saved_resolved, self._extraction_target, self._parameter_structure
|
|
483
487
|
)
|
|
@@ -720,45 +724,167 @@ class ObjectState:
|
|
|
720
724
|
self._forwarding_to_parent = False
|
|
721
725
|
|
|
722
726
|
def _ensure_live_resolved(self, notify_flash: bool = True) -> set[str]:
|
|
723
|
-
"""
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
727
|
+
"""Bring both resolved snapshots up to date; return the changed live paths.
|
|
728
|
+
|
|
729
|
+
Two things make a field out of date: it was invalidated on this state
|
|
730
|
+
(``_invalid_fields``), or the snapshot was computed from other sources than
|
|
731
|
+
the state has now (``_fields_with_other_sources``): an ancestor registered,
|
|
732
|
+
unregistered or replaced, an ancestor's value edited, saved or restored, the
|
|
733
|
+
global config replaced. Both are found here, on every resolved read, so no
|
|
734
|
+
caller has to announce a change and a state that is not registered follows
|
|
735
|
+
its ancestors like a registered one. Only the fields that can be affected are
|
|
736
|
+
resolved again; when nothing changed the cost is the comparison of the
|
|
737
|
+
sources.
|
|
729
738
|
|
|
730
739
|
Args:
|
|
731
|
-
notify_flash:
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
Set of paths that changed (for flash). Empty if no changes.
|
|
736
|
-
|
|
737
|
-
NOTE: This method handles CACHE + FLASH only. Caller handles _sync_materialized_state().
|
|
740
|
+
notify_flash: True (reads): when anything was resolved again, dirty and
|
|
741
|
+
signature fields are updated and subscribers are told here. False:
|
|
742
|
+
the caller does that with the returned paths
|
|
743
|
+
(``_sync_materialized_state``).
|
|
738
744
|
"""
|
|
739
|
-
# First access - populate cache
|
|
740
745
|
if self._live_resolved is None:
|
|
746
|
+
# First access - populate cache; there is no earlier value to compare with.
|
|
741
747
|
self._live_resolved = self._compute_resolved_snapshot()
|
|
742
748
|
self._invalid_fields.clear()
|
|
743
|
-
return set()
|
|
749
|
+
return set()
|
|
750
|
+
|
|
751
|
+
sources, saved_sources = self._resolution_sources()
|
|
752
|
+
saved_resolved_again = self._resolve_saved_again_if_sources_changed(saved_sources)
|
|
753
|
+
if saved_resolved_again:
|
|
754
|
+
sources, _ = self._resolution_sources()
|
|
744
755
|
|
|
745
|
-
|
|
746
|
-
if
|
|
747
|
-
|
|
748
|
-
|
|
756
|
+
stale = self._fields_with_other_sources(sources, use_saved=False)
|
|
757
|
+
if stale is None:
|
|
758
|
+
# Other states are registered above this one: every inherited field may
|
|
759
|
+
# have another source. Pending field invalidations are covered.
|
|
760
|
+
previous = self._live_resolved
|
|
761
|
+
self._live_resolved = self._compute_resolved_snapshot()
|
|
749
762
|
self._invalid_fields.clear()
|
|
763
|
+
changed_values = {
|
|
764
|
+
path: (previous.get(path), value)
|
|
765
|
+
for path, value in self._live_resolved.items()
|
|
766
|
+
if previous.get(path) != value
|
|
767
|
+
}
|
|
768
|
+
if changed_values:
|
|
769
|
+
self._set_last_changed_values(changed_values)
|
|
770
|
+
changed_paths = self.notification_fields_for_value_changes(changed_values)
|
|
771
|
+
live_resolved_again = True
|
|
750
772
|
else:
|
|
751
|
-
|
|
752
|
-
|
|
773
|
+
self._invalid_fields.update(stale)
|
|
774
|
+
live_resolved_again = bool(self._invalid_fields)
|
|
775
|
+
if live_resolved_again:
|
|
776
|
+
changed_paths = self._recompute_invalid_fields(sources)
|
|
777
|
+
self._invalid_fields.clear()
|
|
778
|
+
else:
|
|
779
|
+
changed_paths = set()
|
|
780
|
+
self._resolved_from[False] = sources.recorded_after(self._resolved_from.get(False))
|
|
753
781
|
|
|
754
|
-
if notify_flash and
|
|
755
|
-
self.
|
|
756
|
-
changed_paths,
|
|
757
|
-
context="_ensure_live_resolved",
|
|
758
|
-
)
|
|
782
|
+
if notify_flash and (live_resolved_again or saved_resolved_again):
|
|
783
|
+
self._sync_materialized_state(changed_value_fields=changed_paths)
|
|
759
784
|
|
|
760
785
|
return changed_paths
|
|
761
786
|
|
|
787
|
+
def _resolution_sources(
|
|
788
|
+
self, *, sync_delegates: bool = False
|
|
789
|
+
) -> tuple[ResolutionSources, ResolutionSources]:
|
|
790
|
+
"""What this state's fields are resolved from today, live then saved (so the
|
|
791
|
+
pair is indexed by ``use_saved``): the global config stored for the running
|
|
792
|
+
thread and the objects of the other states registered above.
|
|
793
|
+
|
|
794
|
+
This state's own object is left out (its own parameters are not an outside
|
|
795
|
+
source), and so are global config objects of ancestors, for which
|
|
796
|
+
``build_context_stack`` enters the global config itself.
|
|
797
|
+
"""
|
|
798
|
+
from objectstate.config import get_base_config_type
|
|
799
|
+
from objectstate.global_config import get_live_global_config, get_saved_global_config
|
|
800
|
+
|
|
801
|
+
live: list[tuple[str, Any]] = []
|
|
802
|
+
saved: list[tuple[str, Any]] = []
|
|
803
|
+
for scope, state in ObjectStateRegistry.registered_ancestors(self.scope_id):
|
|
804
|
+
if state is self:
|
|
805
|
+
continue
|
|
806
|
+
saved_object = state.object_for_resolution(use_saved=True)
|
|
807
|
+
if is_global_config_type(type(saved_object)):
|
|
808
|
+
continue
|
|
809
|
+
saved.append((scope, saved_object))
|
|
810
|
+
live.append(
|
|
811
|
+
(scope, state.object_for_resolution(sync_delegate=sync_delegates))
|
|
812
|
+
)
|
|
813
|
+
thread = threading.current_thread()
|
|
814
|
+
global_config_type = get_base_config_type()
|
|
815
|
+
return (
|
|
816
|
+
ResolutionSources(thread, get_live_global_config(global_config_type), tuple(live)),
|
|
817
|
+
ResolutionSources(thread, get_saved_global_config(global_config_type), tuple(saved)),
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
def object_for_resolution(self, use_saved: bool = False, sync_delegate: bool = False) -> Any:
|
|
821
|
+
"""The object a state below this one enters as a context layer.
|
|
822
|
+
|
|
823
|
+
Saved: the object as last saved, without delegate sync or reconstruction.
|
|
824
|
+
Live: rebuilt from the current parameters when they changed since the last
|
|
825
|
+
call; ``sync_delegate`` decides whether a replaced delegate is taken over
|
|
826
|
+
first, which must not happen while a snapshot is being recomputed.
|
|
827
|
+
"""
|
|
828
|
+
if use_saved:
|
|
829
|
+
return self._extraction_target
|
|
830
|
+
if self._cached_object is not None and not sync_delegate:
|
|
831
|
+
return self._cached_object
|
|
832
|
+
return self.to_object(update_delegate=False, sync_delegate=sync_delegate)
|
|
833
|
+
|
|
834
|
+
def _fields_with_other_sources(
|
|
835
|
+
self, sources: ResolutionSources, *, use_saved: bool
|
|
836
|
+
) -> set[str] | None:
|
|
837
|
+
"""Fields of one snapshot that ``sources`` may resolve differently.
|
|
838
|
+
|
|
839
|
+
These are the fields this state leaves unset whose config type inherits a
|
|
840
|
+
``(config type, field)`` that an ancestor or the global config stores
|
|
841
|
+
differently than when the snapshot was computed. A field this state sets
|
|
842
|
+
itself has no outside source. ``None`` means every field.
|
|
843
|
+
"""
|
|
844
|
+
resolved_from = self._resolved_from.get(use_saved)
|
|
845
|
+
if sources.same_objects_as(resolved_from):
|
|
846
|
+
return set()
|
|
847
|
+
differing = sources.fields_that_differ(resolved_from)
|
|
848
|
+
if differing is None:
|
|
849
|
+
return None
|
|
850
|
+
own_values = self._saved_parameters if use_saved else self.parameters
|
|
851
|
+
return {
|
|
852
|
+
path
|
|
853
|
+
for config_type, field_name in differing
|
|
854
|
+
for path in self.inheritance_field_paths(config_type, field_name)
|
|
855
|
+
if own_values.get(path) is None
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
def _resolve_saved_again_if_sources_changed(self, sources: ResolutionSources) -> bool:
|
|
859
|
+
"""Resolve the saved snapshot again when its sources changed; say whether it was.
|
|
860
|
+
|
|
861
|
+
The saved sources change when an ancestor is saved, restored or replaced and
|
|
862
|
+
when the saved global config is replaced, not while values are edited, so the
|
|
863
|
+
snapshot is resolved in full.
|
|
864
|
+
"""
|
|
865
|
+
stale = self._fields_with_other_sources(sources, use_saved=True)
|
|
866
|
+
if stale is not None and not stale:
|
|
867
|
+
self._resolved_from[True] = sources.recorded_after(self._resolved_from.get(True))
|
|
868
|
+
return False
|
|
869
|
+
self._saved_resolved = self._compute_resolved_snapshot(use_saved=True)
|
|
870
|
+
return True
|
|
871
|
+
|
|
872
|
+
def resolve_again_if_sources_changed(self) -> None:
|
|
873
|
+
"""Resolve again now, and tell subscribers, if this state's sources changed.
|
|
874
|
+
|
|
875
|
+
Every resolved read makes this check itself. ``ObjectStateRegistry`` calls it
|
|
876
|
+
when registration changes so subscribers are told at once.
|
|
877
|
+
"""
|
|
878
|
+
if self._live_resolved is not None:
|
|
879
|
+
self._ensure_live_resolved()
|
|
880
|
+
|
|
881
|
+
def _ensure_saved_resolved(self) -> None:
|
|
882
|
+
"""Make ``_saved_resolved`` hold every field, resolved from today's sources."""
|
|
883
|
+
if self._live_resolved is not None:
|
|
884
|
+
self._ensure_live_resolved()
|
|
885
|
+
elif not self._saved_resolved:
|
|
886
|
+
self._saved_resolved = self._compute_resolved_snapshot(use_saved=True)
|
|
887
|
+
|
|
762
888
|
# DELETED: _create_nested_states() - No longer needed with flat storage
|
|
763
889
|
# Nested ObjectStates are no longer created - flat storage handles all parameters
|
|
764
890
|
|
|
@@ -983,7 +1109,7 @@ class ObjectState:
|
|
|
983
1109
|
)
|
|
984
1110
|
# Establish the pre-mutation resolved authority even for a state that
|
|
985
1111
|
# has not yet been displayed. Raw edits can preserve the resolved value.
|
|
986
|
-
self._ensure_live_resolved(notify_flash=False)
|
|
1112
|
+
changed_before_the_edit = self._ensure_live_resolved(notify_flash=False)
|
|
987
1113
|
ObjectStateRegistry.ensure_baseline_snapshot()
|
|
988
1114
|
|
|
989
1115
|
# Update state directly (no type conversion - that's VIEW responsibility)
|
|
@@ -1051,7 +1177,7 @@ class ObjectState:
|
|
|
1051
1177
|
|
|
1052
1178
|
# Materialize the origin once after its live context is authoritative,
|
|
1053
1179
|
# then cascade only to descendants.
|
|
1054
|
-
changed_paths = self._ensure_live_resolved(notify_flash=False)
|
|
1180
|
+
changed_paths = self._ensure_live_resolved(notify_flash=False) | changed_before_the_edit
|
|
1055
1181
|
for changed_param in changed_semantic_params:
|
|
1056
1182
|
# SCOPE + TYPE + FIELD AWARE INVALIDATION:
|
|
1057
1183
|
# Get the CONTAINER type for this field (e.g., PartitionFilterConfig for
|
|
@@ -1157,9 +1283,7 @@ class ObjectState:
|
|
|
1157
1283
|
# Auto-detect delegate changes before resolving values
|
|
1158
1284
|
self._check_and_sync_delegate()
|
|
1159
1285
|
|
|
1160
|
-
|
|
1161
|
-
if not self._saved_resolved:
|
|
1162
|
-
self._saved_resolved = self._compute_resolved_snapshot(use_saved=True)
|
|
1286
|
+
self._ensure_saved_resolved()
|
|
1163
1287
|
|
|
1164
1288
|
# Check if this is a container/dataclass field (has subfields in _saved_resolved)
|
|
1165
1289
|
prefix = f"{param_name}."
|
|
@@ -1190,8 +1314,7 @@ class ObjectState:
|
|
|
1190
1314
|
self._check_and_sync_delegate()
|
|
1191
1315
|
self._ensure_live_resolved()
|
|
1192
1316
|
assert self._live_resolved is not None
|
|
1193
|
-
|
|
1194
|
-
self._saved_resolved = self._compute_resolved_snapshot(use_saved=True)
|
|
1317
|
+
self._ensure_saved_resolved()
|
|
1195
1318
|
|
|
1196
1319
|
path = owner_field_path
|
|
1197
1320
|
raw_value = self.parameters.get(path.value, MISSING)
|
|
@@ -1730,8 +1853,8 @@ class ObjectState:
|
|
|
1730
1853
|
f"Updated ObjectState(scope={self.scope_id!r}) to new instance of type {type(new_instance).__name__}"
|
|
1731
1854
|
)
|
|
1732
1855
|
|
|
1733
|
-
def _recompute_invalid_fields(self) -> set[str]:
|
|
1734
|
-
"""Recompute only the invalid fields
|
|
1856
|
+
def _recompute_invalid_fields(self, sources: ResolutionSources) -> set[str]:
|
|
1857
|
+
"""Recompute only the invalid fields of the live snapshot, from ``sources``.
|
|
1735
1858
|
|
|
1736
1859
|
PERFORMANCE: For explicitly set values, use parameters directly.
|
|
1737
1860
|
Only build context stack for inherited (None) values.
|
|
@@ -1785,14 +1908,6 @@ class ObjectState:
|
|
|
1785
1908
|
from objectstate.dual_axis_resolver import resolve_with_provenance
|
|
1786
1909
|
from objectstate.lazy_factory import has_lazy_resolution
|
|
1787
1910
|
|
|
1788
|
-
# Get ancestor objects for context stack building
|
|
1789
|
-
# CRITICAL: Skip delegate sync to avoid re-entrant invalidate_cache() calls
|
|
1790
|
-
# that would destroy _live_resolved while we're computing it.
|
|
1791
|
-
ancestor_objects_with_scopes = ObjectStateRegistry.get_ancestor_objects_with_scopes(
|
|
1792
|
-
self.scope_id,
|
|
1793
|
-
skip_delegate_sync=True
|
|
1794
|
-
)
|
|
1795
|
-
|
|
1796
1911
|
# ARCHITECTURAL FIX: Reconstruct current object from parameters for real-time MRO.
|
|
1797
1912
|
# Using object_instance (original saved state) gives stale values, breaking inheritance.
|
|
1798
1913
|
# Calling to_object() with sync_delegate=False reconstructs from CURRENT parameters
|
|
@@ -1801,7 +1916,7 @@ class ObjectState:
|
|
|
1801
1916
|
|
|
1802
1917
|
with build_context_stack(
|
|
1803
1918
|
object_instance=current_obj,
|
|
1804
|
-
ancestor_objects_with_scopes=
|
|
1919
|
+
ancestor_objects_with_scopes=sources.ancestors,
|
|
1805
1920
|
current_scope_id=self.scope_id,
|
|
1806
1921
|
):
|
|
1807
1922
|
# For each inherited field, resolve using dual-axis resolution with provenance
|
|
@@ -2092,9 +2207,8 @@ class ObjectState:
|
|
|
2092
2207
|
raise TypeError("ObjectState.parameters must remain a canonical flat dictionary.")
|
|
2093
2208
|
if not isinstance(self._saved_parameters, dict):
|
|
2094
2209
|
raise TypeError("ObjectState._saved_parameters must remain a canonical flat dictionary.")
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
)
|
|
2210
|
+
sources = self._resolution_sources(sync_delegates=True)[use_saved]
|
|
2211
|
+
self._resolved_from[use_saved] = sources
|
|
2098
2212
|
current_obj = (
|
|
2099
2213
|
self.saved_object if use_saved else self.to_object(update_delegate=False)
|
|
2100
2214
|
)
|
|
@@ -2105,7 +2219,7 @@ class ObjectState:
|
|
|
2105
2219
|
self._parameter_structure,
|
|
2106
2220
|
current_obj,
|
|
2107
2221
|
self.scope_id,
|
|
2108
|
-
ancestors,
|
|
2222
|
+
sources.ancestors,
|
|
2109
2223
|
use_saved=use_saved,
|
|
2110
2224
|
)
|
|
2111
2225
|
if not use_saved:
|