objectstate 1.2.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {objectstate-1.2.0 → objectstate-2.0.0}/.github/workflows/publish.yml +11 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/PKG-INFO +2 -2
- objectstate-2.0.0/docs/performance/canonical-context-type-resolution-20261005.json +45 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/pyproject.toml +1 -1
- {objectstate-1.2.0 → objectstate-2.0.0}/scripts/verify_release_ready.py +3 -8
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/__init__.py +5 -6
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/context_manager.py +0 -50
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/dual_axis_resolver.py +31 -33
- objectstate-2.0.0/src/objectstate/inspection.py +162 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/lazy_factory.py +43 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/object_state.py +28 -76
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/object_state_registry.py +31 -71
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/parameter_structure.py +15 -2
- objectstate-2.0.0/src/objectstate/scope_key.py +88 -0
- objectstate-2.0.0/tests/test_authored_paths.py +29 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_construction_history.py +2 -21
- objectstate-2.0.0/tests/test_inspection.py +51 -0
- objectstate-2.0.0/tests/test_release_readiness.py +17 -0
- objectstate-2.0.0/tests/test_scope_key.py +47 -0
- objectstate-1.2.0/src/objectstate/history_migration.py +0 -19
- {objectstate-1.2.0 → objectstate-2.0.0}/.github/badges/coverage.svg +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/.github/workflows/ci.yml +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/.github/workflows/coverage-pages.yml +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/.gitignore +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/.readthedocs.yaml +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/BADGE_SETUP.md +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/LICENSE +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/README.md +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/.gitignore +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/Makefile +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/api/modules.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/architectural_fixes.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/architecture.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/archive/DOCUMENTATION_AUDIT.md +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/archive/DOCUMENTATION_FIXES.md +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/archive/README.md +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/conf.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/examples/atomic_operations.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/examples/basic.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/examples/dual_axis.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/examples/index.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/examples/ui.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/index.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/make.bat +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/provenance.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/quickstart.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/state_management.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/state_propagation.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/docs/undo_redo.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/paper.bib +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/paper.md +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/plans/n_dimensional_inheritance.md +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/scripts/check_ruff_added_lines.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/scripts/release.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/scripts/update_and_release.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/scripts/update_version.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/collection_containers.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/config.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/construction_binding.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/docs/configuration_framework.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/docs/context_system.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/docs/object_state_lifecycle.rst +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/edit_session.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/field_access.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/global_config.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/live_context_resolver.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/object_state_metadata.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/parameter_owner.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/parametric_axes.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/placeholder.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/reified_generics.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/snapshot_model.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/subfield_semantics.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/time_travel_profile.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/token_cache.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/transaction_checkpoint.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/ui_visibility.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/src/objectstate/value_semantics.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/__init__.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/conftest.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_callable_baseline.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_config.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_context_manager.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_field_access.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_global_config.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_history_persistence.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_integration.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_lazy_factory.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_object_state_metadata_contracts.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_object_state_restore.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_object_state_update_instance.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_parameter_owner.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_parametric_axes.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_placeholder.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_registry_subscription.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_reified_generics.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_resolved_change_notifications.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_subfield_semantics.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_timeline_projection.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_transaction_checkpoint.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_ui_visible_field_projection.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_undo_redo_cascade.py +0 -0
- {objectstate-1.2.0 → objectstate-2.0.0}/tests/test_value_semantics.py +0 -0
|
@@ -4,6 +4,12 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
6
|
- 'v*'
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
inputs:
|
|
9
|
+
release_tag:
|
|
10
|
+
description: Existing release tag to publish
|
|
11
|
+
required: true
|
|
12
|
+
type: string
|
|
7
13
|
|
|
8
14
|
permissions:
|
|
9
15
|
contents: write
|
|
@@ -13,6 +19,8 @@ jobs:
|
|
|
13
19
|
build-and-publish:
|
|
14
20
|
name: Build and publish to PyPI
|
|
15
21
|
runs-on: ubuntu-latest
|
|
22
|
+
env:
|
|
23
|
+
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
|
|
16
24
|
environment:
|
|
17
25
|
name: pypi
|
|
18
26
|
url: https://pypi.org/p/objectstate
|
|
@@ -20,6 +28,8 @@ jobs:
|
|
|
20
28
|
steps:
|
|
21
29
|
- name: Checkout code
|
|
22
30
|
uses: actions/checkout@v4
|
|
31
|
+
with:
|
|
32
|
+
ref: refs/tags/${{ env.RELEASE_TAG }}
|
|
23
33
|
|
|
24
34
|
- name: Set up Python
|
|
25
35
|
uses: actions/setup-python@v5
|
|
@@ -42,6 +52,7 @@ jobs:
|
|
|
42
52
|
- name: Create GitHub Release
|
|
43
53
|
uses: softprops/action-gh-release@v2
|
|
44
54
|
with:
|
|
55
|
+
tag_name: ${{ env.RELEASE_TAG }}
|
|
45
56
|
files: dist/*
|
|
46
57
|
generate_release_notes: true
|
|
47
58
|
env:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: objectstate
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Generic lazy dataclass configuration framework with dual-axis inheritance and contextvars-based resolution
|
|
5
5
|
Project-URL: Homepage, https://github.com/OpenHCSDev/objectstate
|
|
6
6
|
Project-URL: Documentation, https://objectstate.readthedocs.io
|
|
@@ -21,7 +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: python-introspect
|
|
24
|
+
Requires-Dist: python-introspect<0.3,>=0.2.0
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: black==26.5.1; extra == 'dev'
|
|
27
27
|
Requires-Dist: mypy==2.3.0; extra == 'dev'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": "PASS_PRIVATE_SAVED_CONSUMER_ONLY",
|
|
3
|
+
"baseline": "Installed ObjectState 1.3.0 from merged 7db151907a52a95743fbeae5a64220b28bc56690, PythonIntrospect 0.2.0",
|
|
4
|
+
"change": "resolve_with_provenance queries the existing canonical type-keyed maps admitted by extract_all_configs instead of scanning every value and reconstructing each owner type.",
|
|
5
|
+
"controls": {
|
|
6
|
+
"passed": 27,
|
|
7
|
+
"command": "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python -m pytest -q -o addopts= -p no:cacheprovider tests/test_context_manager.py tests/test_object_state_restore.py tests/test_callable_baseline.py tests/test_placeholder.py",
|
|
8
|
+
"laws": "Canonical map admission, duplicate/invalid declarations, MRO/None/provenance, real constructors/default factories, callable identity, detached mutable alias graphs"
|
|
9
|
+
},
|
|
10
|
+
"one_pair": [
|
|
11
|
+
{
|
|
12
|
+
"case": "Illumination Example3",
|
|
13
|
+
"first_job": true,
|
|
14
|
+
"steps": 10,
|
|
15
|
+
"baseline_seconds": 0.08565246313810349,
|
|
16
|
+
"candidate_seconds": 0.0676199309527874,
|
|
17
|
+
"saved_seconds": 0.018032532185316086
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"case": "Original 3D monolayer",
|
|
21
|
+
"first_job": false,
|
|
22
|
+
"steps": 31,
|
|
23
|
+
"baseline_seconds": 0.2247260957956314,
|
|
24
|
+
"candidate_seconds": 0.19058690778911114,
|
|
25
|
+
"saved_seconds": 0.03413918800652027
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"parity": "Exact typed configuration/steps, provenance, canonical source and ancestor scopes PASS",
|
|
29
|
+
"guards": "Before/after source, actual eight installed dependency trees and wheel metadata, four native ABIs, authored input sources and helper hashes PASS; all owned processes terminal",
|
|
30
|
+
"preserved": "Context constructors/merges and extraction epochs, selected live field reads, same-type scope precedence, MRO class-first precedence, None inheritance, AttributeError behavior, fallback provenance and direct-owner default factories",
|
|
31
|
+
"limits": [
|
|
32
|
+
"ONE saved-owner pair, not a whole compiler, ordinary runtime or total speedup measurement.",
|
|
33
|
+
"Normal full Registry READY, document parsing, orchestrator initialization, graph/provider admission and execution were outside the clocks.",
|
|
34
|
+
"3D follows Illumination in each process; it is not an independent cold first-job observation.",
|
|
35
|
+
"This generalized cleanup is not the dominant performance route; the first-case 18 ms saving does not close the remaining total target gap.",
|
|
36
|
+
"Arbitrary corruption of the admitted map/type keys or in-operation instance __class__ rebinding was not established equivalent. Mutable field values remain live.",
|
|
37
|
+
"The public resolver accepting manually supplied maps is unchanged. No new cache, value snapshot or installed dependency mutation."
|
|
38
|
+
],
|
|
39
|
+
"custody": {
|
|
40
|
+
"private_packet": "20261005/canonical-context-type-resolution-v1",
|
|
41
|
+
"baseline_resolver_sha256": "17fcda62912050bdcdd018bee2fca5f81c2f4940c5f27f2af3dc19b7691e1fb0",
|
|
42
|
+
"candidate_resolver_sha256": "e8e939bfb97ad2d07cb0f4893e43229fc30926274e7bdc7a4491264feeaeae97",
|
|
43
|
+
"version_change": "1.3.1 is a separate patch release; immutable 1.3.0 tags and installed wheel are untouched."
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -211,13 +211,8 @@ def check_github_workflow():
|
|
|
211
211
|
print(" ❌ .github/workflows/publish.yml not found")
|
|
212
212
|
return False
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
print(" ❌ PYPI_API_TOKEN not referenced in workflow")
|
|
217
|
-
return False
|
|
218
|
-
|
|
219
|
-
print(" ✅ GitHub Actions workflow configured")
|
|
220
|
-
print(" Remember to set PYPI_API_TOKEN secret in GitHub!")
|
|
214
|
+
print(" ✅ Publication workflow present")
|
|
215
|
+
print(" Authentication is checked by the workflow's publisher, not artifact readiness.")
|
|
221
216
|
return True
|
|
222
217
|
|
|
223
218
|
|
|
@@ -261,7 +256,7 @@ def main():
|
|
|
261
256
|
if passed == total:
|
|
262
257
|
print("\n🎉 All checks passed! Ready for release!")
|
|
263
258
|
print("\nNext steps:")
|
|
264
|
-
print(" 1.
|
|
259
|
+
print(" 1. Verify the publication workflow's PyPI authorization")
|
|
265
260
|
print(" 2. Run: python scripts/update_and_release.py")
|
|
266
261
|
return 0
|
|
267
262
|
else:
|
|
@@ -73,6 +73,7 @@ from objectstate.lazy_factory import (
|
|
|
73
73
|
is_global_config_instance,
|
|
74
74
|
# Raw-value-preserving utilities
|
|
75
75
|
replace_raw,
|
|
76
|
+
retain_only_authored_paths,
|
|
76
77
|
)
|
|
77
78
|
|
|
78
79
|
# Resolver
|
|
@@ -100,9 +101,6 @@ from objectstate.context_manager import (
|
|
|
100
101
|
register_hierarchy_relationship,
|
|
101
102
|
unregister_hierarchy_relationship,
|
|
102
103
|
get_ancestors_from_hierarchy,
|
|
103
|
-
# Scope key utilities
|
|
104
|
-
get_root_from_scope_key,
|
|
105
|
-
is_scope_affected,
|
|
106
104
|
# Context stack building (framework-agnostic)
|
|
107
105
|
build_context_stack,
|
|
108
106
|
# Threading utility for context propagation
|
|
@@ -181,8 +179,6 @@ __all__ = [
|
|
|
181
179
|
'extract_all_configs',
|
|
182
180
|
'get_base_global_config',
|
|
183
181
|
'get_context_type_stack',
|
|
184
|
-
'get_root_from_scope_key',
|
|
185
|
-
'is_scope_affected',
|
|
186
182
|
'build_context_stack',
|
|
187
183
|
'spawn_thread_with_context',
|
|
188
184
|
# Placeholder
|
|
@@ -216,13 +212,14 @@ __all__ = [
|
|
|
216
212
|
'ObjectStateEditSession',
|
|
217
213
|
]
|
|
218
214
|
|
|
219
|
-
__version__ = "
|
|
215
|
+
__version__ = "2.0.0"
|
|
220
216
|
__author__ = 'OpenHCS Team'
|
|
221
217
|
__description__ = 'Generic configuration framework for lazy dataclass resolution'
|
|
222
218
|
|
|
223
219
|
|
|
224
220
|
# Object state management
|
|
225
221
|
from objectstate.object_state_registry import ObjectStateRegistry
|
|
222
|
+
from objectstate.scope_key import ScopeKey
|
|
226
223
|
from objectstate.object_state import ObjectState
|
|
227
224
|
from objectstate.object_state_metadata import (
|
|
228
225
|
ObjectStateMetadataContract,
|
|
@@ -259,6 +256,8 @@ __all__ += [
|
|
|
259
256
|
# Object state
|
|
260
257
|
'ObjectState',
|
|
261
258
|
'ObjectStateRegistry',
|
|
259
|
+
'ScopeKey',
|
|
260
|
+
'retain_only_authored_paths',
|
|
262
261
|
'ObjectStateMetadataContract',
|
|
263
262
|
'ObjectStateMetadataContractRegistry',
|
|
264
263
|
'ObjectStateMetadataStore',
|
|
@@ -329,56 +329,6 @@ def _is_global_type(t):
|
|
|
329
329
|
_known_hierarchy: dict = {}
|
|
330
330
|
|
|
331
331
|
|
|
332
|
-
def get_root_from_scope_key(scope_key: str) -> str:
|
|
333
|
-
"""Extract root (plate path) from scope_key for visibility checks.
|
|
334
|
-
|
|
335
|
-
scope_key format:
|
|
336
|
-
- Pipeline-level: just plate path (e.g., "/path/to/plate")
|
|
337
|
-
- Step-level: plate_path::step_token (e.g., "/path/to/plate::step_a")
|
|
338
|
-
- Global: empty string
|
|
339
|
-
|
|
340
|
-
Returns the portion before "::" (or the whole string if no "::" present).
|
|
341
|
-
"""
|
|
342
|
-
if not scope_key:
|
|
343
|
-
return ""
|
|
344
|
-
return scope_key.split("::")[0]
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def is_scope_affected(target_scope_id: str | None, editing_scope_id: str | None) -> bool:
|
|
348
|
-
"""Check if target scope is affected by edit at editing scope.
|
|
349
|
-
|
|
350
|
-
Uses scope_id hierarchy - no type introspection needed:
|
|
351
|
-
- None/"" (global) → affects all
|
|
352
|
-
- "plate_path" (pipeline) → affects same plate + all its steps
|
|
353
|
-
- "plate_path::token" (step) → affects only that step
|
|
354
|
-
|
|
355
|
-
Args:
|
|
356
|
-
target_scope_id: The scope being checked for affectedness (None = global)
|
|
357
|
-
editing_scope_id: The scope where the edit occurred (None = global)
|
|
358
|
-
|
|
359
|
-
Returns:
|
|
360
|
-
True if target should refresh when editing_scope changes
|
|
361
|
-
"""
|
|
362
|
-
# Normalize None to empty string
|
|
363
|
-
target = target_scope_id or ""
|
|
364
|
-
editing = editing_scope_id or ""
|
|
365
|
-
|
|
366
|
-
# Global edit affects all
|
|
367
|
-
if not editing:
|
|
368
|
-
return True
|
|
369
|
-
|
|
370
|
-
# Different plate roots = not affected
|
|
371
|
-
target_root = get_root_from_scope_key(target)
|
|
372
|
-
editing_root = get_root_from_scope_key(editing)
|
|
373
|
-
if target_root != editing_root:
|
|
374
|
-
return False
|
|
375
|
-
|
|
376
|
-
# Same root: parent affects children, not vice versa
|
|
377
|
-
# editing="plate" affects target="plate::step" ✓
|
|
378
|
-
# editing="plate::step" affects target="plate" ✗
|
|
379
|
-
return target == editing or target.startswith(editing + "::")
|
|
380
|
-
|
|
381
|
-
|
|
382
332
|
def _normalize_type_for_hierarchy(t):
|
|
383
333
|
"""Normalize a type for hierarchy registry, but preserve lazy-global distinction.
|
|
384
334
|
|
|
@@ -324,24 +324,23 @@ def resolve_with_provenance(container_type: type, field_name: str) -> Tuple[Any,
|
|
|
324
324
|
if _debug and field_name == 'well_filter':
|
|
325
325
|
logger.debug(f"🔍 Phase 1 - Layer scope={scope_id!r}, checking same-type only (inner to outer)")
|
|
326
326
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
continue
|
|
327
|
+
# extract_all_configs admits one instance per canonical owner type.
|
|
328
|
+
config_instance = layer_configs.get(container_base)
|
|
329
|
+
if config_instance is None:
|
|
330
|
+
continue
|
|
331
|
+
try:
|
|
332
|
+
value = object.__getattribute__(config_instance, field_name)
|
|
333
|
+
if _debug and field_name in ('well_filter', 'enabled'):
|
|
334
|
+
logger.debug(f"🔍 {container_base.__name__}.{field_name} @ scope={scope_id} = {value!r} (from object.__getattribute__)")
|
|
335
|
+
if value is not None:
|
|
336
|
+
# Found concrete value in hierarchy - return immediately
|
|
337
|
+
if _debug and field_name in ('well_filter', 'enabled'):
|
|
338
|
+
logger.debug(f"🔍 FOUND concrete value in hierarchy at scope={scope_id!r}, returning {value!r}")
|
|
339
|
+
return value, scope_id, container_base
|
|
340
|
+
# Don't set fallback here - let Phase 2 walk MRO to find
|
|
341
|
+
# the highest type that defines this field
|
|
342
|
+
except AttributeError:
|
|
343
|
+
continue
|
|
345
344
|
|
|
346
345
|
# PHASE 2: MRO fallback - no concrete value in hierarchy, try MRO inheritance
|
|
347
346
|
#
|
|
@@ -367,25 +366,24 @@ def resolve_with_provenance(container_type: type, field_name: str) -> Tuple[Any,
|
|
|
367
366
|
|
|
368
367
|
# Walk scopes from inner→outer for this MRO type
|
|
369
368
|
for scope_id, layer_configs in reversed(all_layer_configs):
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
continue
|
|
369
|
+
config_instance = layer_configs.get(mro_type)
|
|
370
|
+
if config_instance is None:
|
|
371
|
+
continue
|
|
374
372
|
|
|
375
|
-
|
|
376
|
-
|
|
373
|
+
saw_type_anywhere = True
|
|
374
|
+
last_scope_for_type = scope_id # update as we walk inner→outer (last = outermost)
|
|
377
375
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
376
|
+
try:
|
|
377
|
+
value = object.__getattribute__(config_instance, field_name)
|
|
378
|
+
except AttributeError:
|
|
379
|
+
continue
|
|
382
380
|
|
|
383
|
-
|
|
384
|
-
|
|
381
|
+
if _debug and field_name == 'well_filter':
|
|
382
|
+
logger.debug(f"🔍 MRO: {mro_type.__name__}.{field_name} @ {scope_id!r} = {value!r}")
|
|
385
383
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
384
|
+
if value is not None:
|
|
385
|
+
# Found MRO-inherited value
|
|
386
|
+
return value, scope_id, mro_type
|
|
389
387
|
|
|
390
388
|
# No non-None found for this MRO type across any scope.
|
|
391
389
|
# Record fallback for the FIRST MRO type that exists anywhere.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Read-only inspection of registered ObjectStates and their fields."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from objectstate.object_state import ObjectState
|
|
10
|
+
from objectstate.object_state_registry import ObjectStateRegistry
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
DIRTY_MARKER = "*"
|
|
15
|
+
"""Marks a field (or a group of fields) whose value differs from the saved value."""
|
|
16
|
+
|
|
17
|
+
SIGNATURE_DIFF_MARKER = "_"
|
|
18
|
+
"""Marks a field whose raw value differs from its declared signature default."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def field_markers(*, dirty: bool, signature_diff: bool) -> tuple[str, ...]:
|
|
22
|
+
"""The markers views prefix to a field, or to a group of fields, in this order."""
|
|
23
|
+
|
|
24
|
+
return tuple(
|
|
25
|
+
marker
|
|
26
|
+
for marker, present in ((DIRTY_MARKER, dirty), (SIGNATURE_DIFF_MARKER, signature_diff))
|
|
27
|
+
if present
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def state_markers(state: ObjectState) -> tuple[str, ...]:
|
|
32
|
+
"""The markers of a whole ObjectState: any dirty field, any signature difference."""
|
|
33
|
+
|
|
34
|
+
return field_markers(
|
|
35
|
+
dirty=bool(state.dirty_fields),
|
|
36
|
+
signature_diff=bool(state.signature_diff_fields),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def type_name(value: object) -> str:
|
|
41
|
+
return "None" if value is None else type(value).__name__
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def type_qualname(value_type: type | Callable[..., object]) -> str:
|
|
45
|
+
return f"{value_type.__module__}.{value_type.__qualname__}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class FieldProvenance:
|
|
50
|
+
"""The ancestor scope and declaring type a field's inherited value comes from."""
|
|
51
|
+
|
|
52
|
+
source_scope_id: str | None
|
|
53
|
+
source_type: type
|
|
54
|
+
field_name: str
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def source_field_path(self) -> str | None:
|
|
58
|
+
"""The field's path in the source scope's own state, if that scope is registered."""
|
|
59
|
+
|
|
60
|
+
source_state = ObjectStateRegistry.get_by_scope(self.source_scope_id)
|
|
61
|
+
if source_state is None:
|
|
62
|
+
return None
|
|
63
|
+
return source_state.project_ui_visible_field_path(self.source_type, self.field_name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class FieldInspection:
|
|
68
|
+
"""One field of one ObjectState: raw and resolved value, edit state and provenance."""
|
|
69
|
+
|
|
70
|
+
state: ObjectState
|
|
71
|
+
field_path: str
|
|
72
|
+
raw_value: object
|
|
73
|
+
resolved_value: object
|
|
74
|
+
resolved_value_available: bool
|
|
75
|
+
provenance: FieldProvenance | None
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def of(cls, state: ObjectState, field_path: str):
|
|
79
|
+
"""Inspect ``field_path``; a value that fails to resolve is reported unavailable."""
|
|
80
|
+
|
|
81
|
+
resolved_value = None
|
|
82
|
+
resolved_value_available = False
|
|
83
|
+
try:
|
|
84
|
+
resolved_value = state.get_resolved_value(field_path)
|
|
85
|
+
resolved_value_available = True
|
|
86
|
+
except Exception:
|
|
87
|
+
logger.debug(
|
|
88
|
+
"Failed to resolve ObjectState field %s on scope %r.",
|
|
89
|
+
field_path,
|
|
90
|
+
state.scope_id,
|
|
91
|
+
exc_info=True,
|
|
92
|
+
)
|
|
93
|
+
provenance = None
|
|
94
|
+
match state.get_provenance(field_path):
|
|
95
|
+
case (source_scope_id, source_type) if source_type is not None:
|
|
96
|
+
provenance = FieldProvenance(
|
|
97
|
+
source_scope_id=source_scope_id,
|
|
98
|
+
source_type=source_type,
|
|
99
|
+
field_name=field_path.rsplit(".", 1)[-1],
|
|
100
|
+
)
|
|
101
|
+
return cls(
|
|
102
|
+
state=state,
|
|
103
|
+
field_path=field_path,
|
|
104
|
+
raw_value=state.parameters[field_path],
|
|
105
|
+
resolved_value=resolved_value,
|
|
106
|
+
resolved_value_available=resolved_value_available,
|
|
107
|
+
provenance=provenance,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def dirty(self) -> bool:
|
|
112
|
+
return self.field_path in self.state.dirty_fields
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def signature_diff(self) -> bool:
|
|
116
|
+
return self.field_path in self.state.signature_diff_fields
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def last_changed(self) -> bool:
|
|
120
|
+
return self.field_path == self.state.last_changed_field
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def raw_value_is_none(self) -> bool:
|
|
124
|
+
return self.raw_value is None
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def resolved_value_is_none(self) -> bool:
|
|
128
|
+
return self.resolved_value_available and self.resolved_value is None
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def inherited_value(self) -> bool:
|
|
132
|
+
"""The field holds no value of its own and inherits a resolved one."""
|
|
133
|
+
|
|
134
|
+
return self.raw_value is None and self.resolved_value_available and (
|
|
135
|
+
self.resolved_value is not None
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def semantic_markers(self) -> tuple[str, ...]:
|
|
140
|
+
return field_markers(dirty=self.dirty, signature_diff=self.signature_diff)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def field_name(self) -> str:
|
|
144
|
+
return self.field_path.rsplit(".", 1)[-1]
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def container_path(self) -> str:
|
|
148
|
+
return self.field_path.rsplit(".", 1)[0] if "." in self.field_path else ""
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def raw_value_type(self) -> str:
|
|
152
|
+
return type_name(self.raw_value)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def resolved_value_type(self) -> str | None:
|
|
156
|
+
return type_name(self.resolved_value) if self.resolved_value_available else None
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def declaring_type(self) -> str:
|
|
160
|
+
"""Qualified name of the dataclass that declares this field."""
|
|
161
|
+
|
|
162
|
+
return type_qualname(self.state.type_for_path(self.field_path))
|
|
@@ -250,6 +250,49 @@ def replace_raw(instance, **changes):
|
|
|
250
250
|
return type(instance)(**field_values)
|
|
251
251
|
|
|
252
252
|
|
|
253
|
+
|
|
254
|
+
def retain_only_authored_paths(value, authored_field_paths) -> Any:
|
|
255
|
+
"""Copy a lazy dataclass, marking only ``authored_field_paths`` as explicitly set.
|
|
256
|
+
|
|
257
|
+
An ObjectState rebuild constructs every nested lazy dataclass from all its
|
|
258
|
+
flattened fields, so every constructor keyword reads as explicit. Clean
|
|
259
|
+
source generation needs the authored paths ObjectState already tracks
|
|
260
|
+
(its signature-diff fields); this applies them to the lazy metadata without
|
|
261
|
+
changing any field value.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
from objectstate.field_access import DottedFieldPath
|
|
265
|
+
|
|
266
|
+
paths = tuple(DottedFieldPath(path) for path in authored_field_paths)
|
|
267
|
+
return _copy_with_only_authored_paths(value, DottedFieldPath(""), paths)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _copy_with_only_authored_paths(value, prefix, authored_field_paths):
|
|
271
|
+
from objectstate.field_access import DataclassFieldAccess
|
|
272
|
+
|
|
273
|
+
field_values = {}
|
|
274
|
+
for field_definition in fields(value):
|
|
275
|
+
field_value = DataclassFieldAccess.raw_value(value, field_definition.name)
|
|
276
|
+
if isinstance(field_value, LazyDataclass):
|
|
277
|
+
field_value = _copy_with_only_authored_paths(
|
|
278
|
+
field_value,
|
|
279
|
+
prefix.child(field_definition.name),
|
|
280
|
+
authored_field_paths,
|
|
281
|
+
)
|
|
282
|
+
field_values[field_definition.name] = field_value
|
|
283
|
+
|
|
284
|
+
projected = type(value)(**field_values)
|
|
285
|
+
object.__setattr__(
|
|
286
|
+
projected,
|
|
287
|
+
"_explicitly_set_fields",
|
|
288
|
+
{
|
|
289
|
+
field_definition.name
|
|
290
|
+
for field_definition in fields(projected)
|
|
291
|
+
if prefix.child(field_definition.name).contains_any(authored_field_paths)
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
return projected
|
|
295
|
+
|
|
253
296
|
# ContextEventCoordinator removed - replaced with contextvars-based context system
|
|
254
297
|
|
|
255
298
|
|