objectstate 3.0.2__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.2 → objectstate-3.0.3}/.github/badges/coverage.svg +1 -1
- {objectstate-3.0.2 → objectstate-3.0.3}/PKG-INFO +1 -1
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/state_propagation.rst +39 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/__init__.py +1 -1
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/object_state.py +147 -94
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/object_state_registry.py +13 -35
- objectstate-3.0.3/src/objectstate/resolution_sources.py +176 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/scope_key.py +12 -4
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/transaction_checkpoint.py +3 -0
- objectstate-3.0.3/tests/test_resolved_values_follow_registered_ancestors.py +630 -0
- objectstate-3.0.2/tests/test_resolved_values_follow_registered_ancestors.py +0 -341
- {objectstate-3.0.2 → objectstate-3.0.3}/.github/workflows/ci.yml +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/.github/workflows/coverage-pages.yml +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/.github/workflows/publish.yml +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/.gitignore +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/.readthedocs.yaml +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/BADGE_SETUP.md +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/LICENSE +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/README.md +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/.gitignore +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/Makefile +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/api/modules.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/architectural_fixes.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/architecture.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/archive/DOCUMENTATION_AUDIT.md +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/archive/DOCUMENTATION_FIXES.md +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/archive/README.md +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/conf.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/examples/atomic_operations.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/examples/basic.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/examples/dual_axis.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/examples/index.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/examples/ui.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/index.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/make.bat +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/performance/canonical-context-type-resolution-20261005.json +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/provenance.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/quickstart.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/state_management.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/docs/undo_redo.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/paper.bib +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/paper.md +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/plans/n_dimensional_inheritance.md +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/pyproject.toml +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/scripts/check_ruff_added_lines.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/scripts/release.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/scripts/update_and_release.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/scripts/update_version.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/scripts/verify_release_ready.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/codegen.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/collection_containers.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/config.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/construction_binding.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/context_manager.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/docs/configuration_framework.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/docs/context_system.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/docs/object_state_lifecycle.rst +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/dual_axis_resolver.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/edit_session.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/field_access.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/global_config.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/inspection.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/lazy_factory.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/live_context_resolver.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/object_state_metadata.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/parameter_owner.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/parameter_structure.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/parametric_axes.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/placeholder.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/reified_generics.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/snapshot_model.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/subfield_semantics.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/time_travel_profile.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/token_cache.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/ui_visibility.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/src/objectstate/value_semantics.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/__init__.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/conftest.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_atomic_block_context.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_authored_paths.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_callable_baseline.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_codegen.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_config.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_construction_history.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_context_failures_surface.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_context_manager.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_field_access.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_global_config.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_history_persistence.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_inspection.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_integration.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_lazy_counterpart_registry.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_lazy_factory.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_live_context_resolver_answers.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_object_state_metadata_contracts.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_object_state_restore.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_object_state_update_instance.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_one_field_resolution.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_parameter_owner.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_parametric_axes.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_placeholder.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_registry_subscription.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_reified_generics.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_release_readiness.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_resolution_precedence_order.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_resolved_change_notifications.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_scope_key.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_scopeless_state_resolution.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_subfield_semantics.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_timeline_projection.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_transaction_checkpoint.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_ui_visible_field_projection.py +0 -0
- {objectstate-3.0.2 → objectstate-3.0.3}/tests/test_undo_redo_cascade.py +0 -0
- {objectstate-3.0.2 → 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
|
+
|
|
@@ -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,10 +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]] = {}
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
self.
|
|
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] = {}
|
|
233
235
|
|
|
234
236
|
# === Saved baseline (2 attributes) ===
|
|
235
237
|
self._saved_resolved: dict[str, Any] = {}
|
|
@@ -268,9 +270,8 @@ class ObjectState:
|
|
|
268
270
|
# === Time-Travel ===
|
|
269
271
|
# Instance-level time-travel removed - use ObjectStateRegistry class-level DAG instead
|
|
270
272
|
|
|
271
|
-
# Initialize baselines (
|
|
272
|
-
self.
|
|
273
|
-
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()
|
|
274
275
|
|
|
275
276
|
# CRITICAL: Initialize _saved_parameters BEFORE _compute_resolved_snapshot(use_saved=True)
|
|
276
277
|
# because that method reads from _saved_parameters to get raw values.
|
|
@@ -723,91 +724,153 @@ class ObjectState:
|
|
|
723
724
|
self._forwarding_to_parent = False
|
|
724
725
|
|
|
725
726
|
def _ensure_live_resolved(self, notify_flash: bool = True) -> set[str]:
|
|
726
|
-
"""
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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.
|
|
732
738
|
|
|
733
739
|
Args:
|
|
734
|
-
notify_flash:
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
Set of paths that changed (for flash). Empty if no changes.
|
|
739
|
-
|
|
740
|
-
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``).
|
|
741
744
|
"""
|
|
742
|
-
# First access - populate cache
|
|
743
745
|
if self._live_resolved is None:
|
|
746
|
+
# First access - populate cache; there is no earlier value to compare with.
|
|
744
747
|
self._live_resolved = self._compute_resolved_snapshot()
|
|
745
748
|
self._invalid_fields.clear()
|
|
746
|
-
return set()
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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()
|
|
755
|
+
|
|
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()
|
|
758
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
|
|
759
772
|
else:
|
|
760
|
-
|
|
761
|
-
|
|
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))
|
|
762
781
|
|
|
763
|
-
if notify_flash and
|
|
764
|
-
self.
|
|
765
|
-
changed_paths,
|
|
766
|
-
context="_ensure_live_resolved",
|
|
767
|
-
)
|
|
782
|
+
if notify_flash and (live_resolved_again or saved_resolved_again):
|
|
783
|
+
self._sync_materialized_state(changed_value_fields=changed_paths)
|
|
768
784
|
|
|
769
785
|
return changed_paths
|
|
770
786
|
|
|
771
|
-
def
|
|
772
|
-
|
|
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.
|
|
773
793
|
|
|
774
|
-
This state is left out
|
|
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.
|
|
775
797
|
"""
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
for _, state in ObjectStateRegistry.registered_ancestors(self.scope_id)
|
|
779
|
-
if state is not self
|
|
780
|
-
)
|
|
798
|
+
from objectstate.config import get_base_config_type
|
|
799
|
+
from objectstate.global_config import get_live_global_config, get_saved_global_config
|
|
781
800
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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)),
|
|
788
818
|
)
|
|
789
819
|
|
|
790
|
-
def
|
|
791
|
-
"""
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
|
800
856
|
}
|
|
801
|
-
if changed_values:
|
|
802
|
-
self._set_last_changed_values(changed_values)
|
|
803
|
-
changed_paths = self.notification_fields_for_value_changes(changed_values)
|
|
804
|
-
self._sync_materialized_state(
|
|
805
|
-
changed_value_fields=changed_paths, emit_notifications=notify
|
|
806
|
-
)
|
|
807
|
-
return changed_paths
|
|
808
857
|
|
|
809
|
-
def
|
|
810
|
-
"""Resolve again when
|
|
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.
|
|
811
874
|
|
|
812
875
|
Every resolved read makes this check itself. ``ObjectStateRegistry`` calls it
|
|
813
876
|
when registration changes so subscribers are told at once.
|
|
@@ -816,9 +879,9 @@ class ObjectState:
|
|
|
816
879
|
self._ensure_live_resolved()
|
|
817
880
|
|
|
818
881
|
def _ensure_saved_resolved(self) -> None:
|
|
819
|
-
"""Make ``_saved_resolved`` hold every field, resolved from today's
|
|
820
|
-
if self._live_resolved is not None
|
|
821
|
-
self.
|
|
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()
|
|
822
885
|
elif not self._saved_resolved:
|
|
823
886
|
self._saved_resolved = self._compute_resolved_snapshot(use_saved=True)
|
|
824
887
|
|
|
@@ -1046,7 +1109,7 @@ class ObjectState:
|
|
|
1046
1109
|
)
|
|
1047
1110
|
# Establish the pre-mutation resolved authority even for a state that
|
|
1048
1111
|
# has not yet been displayed. Raw edits can preserve the resolved value.
|
|
1049
|
-
self._ensure_live_resolved(notify_flash=False)
|
|
1112
|
+
changed_before_the_edit = self._ensure_live_resolved(notify_flash=False)
|
|
1050
1113
|
ObjectStateRegistry.ensure_baseline_snapshot()
|
|
1051
1114
|
|
|
1052
1115
|
# Update state directly (no type conversion - that's VIEW responsibility)
|
|
@@ -1114,7 +1177,7 @@ class ObjectState:
|
|
|
1114
1177
|
|
|
1115
1178
|
# Materialize the origin once after its live context is authoritative,
|
|
1116
1179
|
# then cascade only to descendants.
|
|
1117
|
-
changed_paths = self._ensure_live_resolved(notify_flash=False)
|
|
1180
|
+
changed_paths = self._ensure_live_resolved(notify_flash=False) | changed_before_the_edit
|
|
1118
1181
|
for changed_param in changed_semantic_params:
|
|
1119
1182
|
# SCOPE + TYPE + FIELD AWARE INVALIDATION:
|
|
1120
1183
|
# Get the CONTAINER type for this field (e.g., PartitionFilterConfig for
|
|
@@ -1790,8 +1853,8 @@ class ObjectState:
|
|
|
1790
1853
|
f"Updated ObjectState(scope={self.scope_id!r}) to new instance of type {type(new_instance).__name__}"
|
|
1791
1854
|
)
|
|
1792
1855
|
|
|
1793
|
-
def _recompute_invalid_fields(self) -> set[str]:
|
|
1794
|
-
"""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``.
|
|
1795
1858
|
|
|
1796
1859
|
PERFORMANCE: For explicitly set values, use parameters directly.
|
|
1797
1860
|
Only build context stack for inherited (None) values.
|
|
@@ -1845,14 +1908,6 @@ class ObjectState:
|
|
|
1845
1908
|
from objectstate.dual_axis_resolver import resolve_with_provenance
|
|
1846
1909
|
from objectstate.lazy_factory import has_lazy_resolution
|
|
1847
1910
|
|
|
1848
|
-
# Get ancestor objects for context stack building
|
|
1849
|
-
# CRITICAL: Skip delegate sync to avoid re-entrant invalidate_cache() calls
|
|
1850
|
-
# that would destroy _live_resolved while we're computing it.
|
|
1851
|
-
ancestor_objects_with_scopes = ObjectStateRegistry.get_ancestor_objects_with_scopes(
|
|
1852
|
-
self.scope_id,
|
|
1853
|
-
skip_delegate_sync=True
|
|
1854
|
-
)
|
|
1855
|
-
|
|
1856
1911
|
# ARCHITECTURAL FIX: Reconstruct current object from parameters for real-time MRO.
|
|
1857
1912
|
# Using object_instance (original saved state) gives stale values, breaking inheritance.
|
|
1858
1913
|
# Calling to_object() with sync_delegate=False reconstructs from CURRENT parameters
|
|
@@ -1861,7 +1916,7 @@ class ObjectState:
|
|
|
1861
1916
|
|
|
1862
1917
|
with build_context_stack(
|
|
1863
1918
|
object_instance=current_obj,
|
|
1864
|
-
ancestor_objects_with_scopes=
|
|
1919
|
+
ancestor_objects_with_scopes=sources.ancestors,
|
|
1865
1920
|
current_scope_id=self.scope_id,
|
|
1866
1921
|
):
|
|
1867
1922
|
# For each inherited field, resolve using dual-axis resolution with provenance
|
|
@@ -2152,10 +2207,8 @@ class ObjectState:
|
|
|
2152
2207
|
raise TypeError("ObjectState.parameters must remain a canonical flat dictionary.")
|
|
2153
2208
|
if not isinstance(self._saved_parameters, dict):
|
|
2154
2209
|
raise TypeError("ObjectState._saved_parameters must remain a canonical flat dictionary.")
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
self.scope_id, use_saved=use_saved
|
|
2158
|
-
)
|
|
2210
|
+
sources = self._resolution_sources(sync_delegates=True)[use_saved]
|
|
2211
|
+
self._resolved_from[use_saved] = sources
|
|
2159
2212
|
current_obj = (
|
|
2160
2213
|
self.saved_object if use_saved else self.to_object(update_delegate=False)
|
|
2161
2214
|
)
|
|
@@ -2166,7 +2219,7 @@ class ObjectState:
|
|
|
2166
2219
|
self._parameter_structure,
|
|
2167
2220
|
current_obj,
|
|
2168
2221
|
self.scope_id,
|
|
2169
|
-
ancestors,
|
|
2222
|
+
sources.ancestors,
|
|
2170
2223
|
use_saved=use_saved,
|
|
2171
2224
|
)
|
|
2172
2225
|
if not use_saved:
|
|
@@ -447,16 +447,16 @@ class ObjectStateRegistry:
|
|
|
447
447
|
def _resolve_again_below(cls, changed_scopes: Iterable[ScopeKey]) -> None:
|
|
448
448
|
"""Tell registered states at or below ``changed_scopes`` that registration changed.
|
|
449
449
|
|
|
450
|
-
Each state compares
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
450
|
+
Each state compares what it is resolved from now with what its resolved values
|
|
451
|
+
were computed from (``ObjectState.resolve_again_if_sources_changed``), so a
|
|
452
|
+
state whose ancestors are the same does no work. This call only makes the
|
|
453
|
+
comparison happen now, so subscribers hear of changed values without waiting
|
|
454
|
+
for the next read.
|
|
455
455
|
"""
|
|
456
456
|
changed_scopes = tuple(changed_scopes)
|
|
457
457
|
for state in tuple(cls._states.values()):
|
|
458
458
|
if any(scope.contains(state.scope_id) for scope in changed_scopes):
|
|
459
|
-
state.
|
|
459
|
+
state.resolve_again_if_sources_changed()
|
|
460
460
|
|
|
461
461
|
@classmethod
|
|
462
462
|
def registered_ancestors(
|
|
@@ -737,17 +737,10 @@ class ObjectStateRegistry:
|
|
|
737
737
|
Returns:
|
|
738
738
|
List of (scope_id, object) tuples from ancestor scopes, ordered least→most specific.
|
|
739
739
|
"""
|
|
740
|
-
# Saved: the object as last saved, without delegate sync or reconstruction.
|
|
741
|
-
# Live: rebuilt from the current parameters; ``skip_delegate_sync`` decides
|
|
742
|
-
# whether the delegate is synchronised first.
|
|
743
740
|
return [
|
|
744
741
|
(
|
|
745
742
|
ancestor_key,
|
|
746
|
-
state.
|
|
747
|
-
if use_saved
|
|
748
|
-
else state.to_object(
|
|
749
|
-
update_delegate=False, sync_delegate=not skip_delegate_sync
|
|
750
|
-
),
|
|
743
|
+
state.object_for_resolution(use_saved, sync_delegate=not skip_delegate_sync),
|
|
751
744
|
)
|
|
752
745
|
for ancestor_key, state in cls.registered_ancestors(scope_id)
|
|
753
746
|
]
|
|
@@ -882,29 +875,14 @@ class ObjectStateRegistry:
|
|
|
882
875
|
state.invalidate_field(dotted_path)
|
|
883
876
|
invalidated_paths.add(dotted_path)
|
|
884
877
|
|
|
885
|
-
# If invalidating saved baseline, remove from saved_resolved so it recomputes
|
|
886
|
-
if invalidate_saved and dotted_path in state._saved_resolved:
|
|
887
|
-
del state._saved_resolved[dotted_path]
|
|
888
|
-
if _debug:
|
|
889
|
-
logger.debug(f"Invalidated saved_resolved cache for {dotted_path}")
|
|
890
|
-
|
|
891
|
-
# Trigger recompute immediately to detect if resolved values actually changed.
|
|
892
|
-
# This ensures callbacks fire only when values change, not just when fields are invalidated.
|
|
893
|
-
# Prevents false flashes when Reset is clicked on already-reset fields.
|
|
894
878
|
if invalidated_paths:
|
|
895
|
-
changed_paths = state._ensure_live_resolved(notify_flash=False)
|
|
896
|
-
|
|
897
|
-
# If we invalidated saved baseline, recompute it now with fresh ancestor values
|
|
898
879
|
if invalidate_saved:
|
|
899
|
-
#
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
state._sync_materialized_state(changed_value_fields=changed_paths)
|
|
906
|
-
else:
|
|
907
|
-
state._sync_materialized_state(changed_value_fields=changed_paths)
|
|
880
|
+
# Without recorded sources the saved snapshot is resolved again in full.
|
|
881
|
+
state._resolved_from.pop(True, None)
|
|
882
|
+
# Resolve now, so callbacks fire only for values that really changed (no
|
|
883
|
+
# flash when Reset is clicked on a field that is already reset).
|
|
884
|
+
changed_paths = state._ensure_live_resolved(notify_flash=False)
|
|
885
|
+
state._sync_materialized_state(changed_value_fields=changed_paths)
|
|
908
886
|
|
|
909
887
|
# ========== REGISTRY-LEVEL TIME-TRAVEL (DAG Model) ==========
|
|
910
888
|
|