objectstate 3.0.3__tar.gz → 4.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.
Files changed (117) hide show
  1. {objectstate-3.0.3 → objectstate-4.0.0}/.github/badges/coverage.svg +1 -1
  2. objectstate-4.0.0/CHANGELOG.md +68 -0
  3. {objectstate-3.0.3 → objectstate-4.0.0}/PKG-INFO +6 -6
  4. {objectstate-3.0.3 → objectstate-4.0.0}/README.md +5 -5
  5. {objectstate-3.0.3 → objectstate-4.0.0}/docs/api/modules.rst +1 -1
  6. {objectstate-3.0.3 → objectstate-4.0.0}/docs/architecture.rst +19 -17
  7. {objectstate-3.0.3 → objectstate-4.0.0}/docs/examples/basic.rst +2 -2
  8. {objectstate-3.0.3 → objectstate-4.0.0}/docs/index.rst +1 -1
  9. {objectstate-3.0.3 → objectstate-4.0.0}/docs/provenance.rst +3 -1
  10. {objectstate-3.0.3 → objectstate-4.0.0}/docs/quickstart.rst +4 -4
  11. {objectstate-3.0.3 → objectstate-4.0.0}/docs/state_propagation.rst +66 -11
  12. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/__init__.py +9 -13
  13. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/context_manager.py +31 -28
  14. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/docs/configuration_framework.rst +9 -19
  15. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/docs/context_system.rst +22 -20
  16. objectstate-4.0.0/src/objectstate/global_config.py +96 -0
  17. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/lazy_factory.py +0 -59
  18. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/object_state.py +14 -93
  19. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/object_state_registry.py +21 -26
  20. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/resolution_sources.py +9 -27
  21. {objectstate-3.0.3 → objectstate-4.0.0}/tests/conftest.py +3 -6
  22. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_callable_baseline.py +2 -2
  23. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_construction_history.py +3 -3
  24. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_context_failures_surface.py +3 -3
  25. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_context_manager.py +0 -10
  26. objectstate-4.0.0/tests/test_global_config.py +37 -0
  27. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_object_state_restore.py +6 -6
  28. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_object_state_update_instance.py +2 -2
  29. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_one_field_resolution.py +19 -15
  30. objectstate-4.0.0/tests/test_one_holder_of_the_global_config.py +389 -0
  31. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_resolution_precedence_order.py +3 -3
  32. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_resolved_values_follow_registered_ancestors.py +356 -68
  33. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_scopeless_state_resolution.py +2 -2
  34. objectstate-3.0.3/src/objectstate/global_config.py +0 -174
  35. objectstate-3.0.3/tests/test_global_config.py +0 -68
  36. {objectstate-3.0.3 → objectstate-4.0.0}/.github/workflows/ci.yml +0 -0
  37. {objectstate-3.0.3 → objectstate-4.0.0}/.github/workflows/coverage-pages.yml +0 -0
  38. {objectstate-3.0.3 → objectstate-4.0.0}/.github/workflows/publish.yml +0 -0
  39. {objectstate-3.0.3 → objectstate-4.0.0}/.gitignore +0 -0
  40. {objectstate-3.0.3 → objectstate-4.0.0}/.readthedocs.yaml +0 -0
  41. {objectstate-3.0.3 → objectstate-4.0.0}/BADGE_SETUP.md +0 -0
  42. {objectstate-3.0.3 → objectstate-4.0.0}/LICENSE +0 -0
  43. {objectstate-3.0.3 → objectstate-4.0.0}/docs/.gitignore +0 -0
  44. {objectstate-3.0.3 → objectstate-4.0.0}/docs/Makefile +0 -0
  45. {objectstate-3.0.3 → objectstate-4.0.0}/docs/architectural_fixes.rst +0 -0
  46. {objectstate-3.0.3 → objectstate-4.0.0}/docs/archive/DOCUMENTATION_AUDIT.md +0 -0
  47. {objectstate-3.0.3 → objectstate-4.0.0}/docs/archive/DOCUMENTATION_FIXES.md +0 -0
  48. {objectstate-3.0.3 → objectstate-4.0.0}/docs/archive/README.md +0 -0
  49. {objectstate-3.0.3 → objectstate-4.0.0}/docs/conf.py +0 -0
  50. {objectstate-3.0.3 → objectstate-4.0.0}/docs/examples/atomic_operations.rst +0 -0
  51. {objectstate-3.0.3 → objectstate-4.0.0}/docs/examples/dual_axis.rst +0 -0
  52. {objectstate-3.0.3 → objectstate-4.0.0}/docs/examples/index.rst +0 -0
  53. {objectstate-3.0.3 → objectstate-4.0.0}/docs/examples/ui.rst +0 -0
  54. {objectstate-3.0.3 → objectstate-4.0.0}/docs/make.bat +0 -0
  55. {objectstate-3.0.3 → objectstate-4.0.0}/docs/performance/canonical-context-type-resolution-20261005.json +0 -0
  56. {objectstate-3.0.3 → objectstate-4.0.0}/docs/state_management.rst +0 -0
  57. {objectstate-3.0.3 → objectstate-4.0.0}/docs/undo_redo.rst +0 -0
  58. {objectstate-3.0.3 → objectstate-4.0.0}/paper.bib +0 -0
  59. {objectstate-3.0.3 → objectstate-4.0.0}/paper.md +0 -0
  60. {objectstate-3.0.3 → objectstate-4.0.0}/plans/n_dimensional_inheritance.md +0 -0
  61. {objectstate-3.0.3 → objectstate-4.0.0}/pyproject.toml +0 -0
  62. {objectstate-3.0.3 → objectstate-4.0.0}/scripts/check_ruff_added_lines.py +0 -0
  63. {objectstate-3.0.3 → objectstate-4.0.0}/scripts/release.py +0 -0
  64. {objectstate-3.0.3 → objectstate-4.0.0}/scripts/update_and_release.py +0 -0
  65. {objectstate-3.0.3 → objectstate-4.0.0}/scripts/update_version.py +0 -0
  66. {objectstate-3.0.3 → objectstate-4.0.0}/scripts/verify_release_ready.py +0 -0
  67. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/codegen.py +0 -0
  68. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/collection_containers.py +0 -0
  69. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/config.py +0 -0
  70. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/construction_binding.py +0 -0
  71. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/docs/object_state_lifecycle.rst +0 -0
  72. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/dual_axis_resolver.py +0 -0
  73. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/edit_session.py +0 -0
  74. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/field_access.py +0 -0
  75. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/inspection.py +0 -0
  76. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/live_context_resolver.py +0 -0
  77. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/object_state_metadata.py +0 -0
  78. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/parameter_owner.py +0 -0
  79. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/parameter_structure.py +0 -0
  80. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/parametric_axes.py +0 -0
  81. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/placeholder.py +0 -0
  82. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/reified_generics.py +0 -0
  83. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/scope_key.py +0 -0
  84. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/snapshot_model.py +0 -0
  85. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/subfield_semantics.py +0 -0
  86. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/time_travel_profile.py +0 -0
  87. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/token_cache.py +0 -0
  88. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/transaction_checkpoint.py +0 -0
  89. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/ui_visibility.py +0 -0
  90. {objectstate-3.0.3 → objectstate-4.0.0}/src/objectstate/value_semantics.py +0 -0
  91. {objectstate-3.0.3 → objectstate-4.0.0}/tests/__init__.py +0 -0
  92. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_atomic_block_context.py +0 -0
  93. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_authored_paths.py +0 -0
  94. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_codegen.py +0 -0
  95. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_config.py +0 -0
  96. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_field_access.py +0 -0
  97. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_history_persistence.py +0 -0
  98. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_inspection.py +0 -0
  99. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_integration.py +0 -0
  100. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_lazy_counterpart_registry.py +0 -0
  101. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_lazy_factory.py +0 -0
  102. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_live_context_resolver_answers.py +0 -0
  103. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_object_state_metadata_contracts.py +0 -0
  104. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_parameter_owner.py +0 -0
  105. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_parametric_axes.py +0 -0
  106. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_placeholder.py +0 -0
  107. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_registry_subscription.py +0 -0
  108. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_reified_generics.py +0 -0
  109. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_release_readiness.py +0 -0
  110. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_resolved_change_notifications.py +0 -0
  111. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_scope_key.py +0 -0
  112. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_subfield_semantics.py +0 -0
  113. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_timeline_projection.py +0 -0
  114. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_transaction_checkpoint.py +0 -0
  115. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_ui_visible_field_projection.py +0 -0
  116. {objectstate-3.0.3 → objectstate-4.0.0}/tests/test_undo_redo_cascade.py +0 -0
  117. {objectstate-3.0.3 → objectstate-4.0.0}/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: 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
+ <svg xmlns="http://www.w3.org/2000/svg" width="114" height="20" role="img" aria-label="coverage: 82.78%"><title>coverage: 82.78%</title><filter id="blur"><feGaussianBlur stdDeviation="16"/></filter><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="114" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="61" height="20" fill="#555"/><rect x="61" width="53" height="20" fill="#67ac09"/><rect width="114" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="315" y="150" fill-opacity=".8" filter="url(#blur)" textLength="510">coverage</text><text x="315" y="150" fill-opacity=".3" textLength="510">coverage</text></g><text x="315" y="140" textLength="510">coverage</text></g><g transform="scale(.1)"><g aria-hidden="true" fill="#010101"><text x="865" y="150" fill-opacity=".8" filter="url(#blur)" textLength="430">82.78%</text><text x="865" y="150" fill-opacity=".3" textLength="430">82.78%</text></g><text x="865" y="140" textLength="430">82.78%</text></g></g></svg>
@@ -0,0 +1,68 @@
1
+ # Changelog
2
+
3
+ ## 4.0.0
4
+
5
+ The global config has one holder (#56). While a state whose object is of the
6
+ global config type is registered at the global scope (`scope_id=""`), that state
7
+ holds the global config: the live global config is the object its parameters
8
+ describe and the saved one is the object it last saved. While no such state is
9
+ registered, the object given to `set_global_config` is both. The global config is
10
+ one value per process; threads no longer each have their own.
11
+
12
+ Removed, and what to call instead:
13
+
14
+ - `ensure_global_config_context(config_type, config)` (also
15
+ `objectstate.lazy_factory.ensure_global_config_context`): call
16
+ `set_global_config(config_type, config)`. Not needed at all beside a registered
17
+ global state: registering the state makes its object the global config.
18
+ - `set_global_config_for_editing(config_type, config)`: call
19
+ `set_global_config(config_type, config)`.
20
+ - `set_live_global_config(config_type, config)`: with a registered global state,
21
+ edit the state (`state.update_parameter(path, value)`); the live global config
22
+ follows. Without one there is no separate live value; call `set_global_config`.
23
+ - `set_saved_global_config(config_type, config)`: with a registered global state,
24
+ save it (`state.mark_saved()`); to replace the object, live and saved, call
25
+ `state.update_object_instance(config)` or `set_global_config`. Without one, call
26
+ `set_global_config`.
27
+ - `get_current_global_config(config_type, use_live=...)`: call
28
+ `get_live_global_config(config_type)` or `get_saved_global_config(config_type)`.
29
+ - `clear_current_temp_global()`: nothing; no library code leaves the caller's
30
+ entered context any more. Leave a `config_context` block to leave its context.
31
+ - `objectstate.global_config.GlobalContextValues`: with a registered global state,
32
+ checkpoint the state (`ObjectStateTransactionCheckpoint.capture(state)` /
33
+ `.restore(state)`). Without one, keep `get_live_global_config(config_type)` and
34
+ put it back with `set_global_config`, or `forget_stored_global_config` when it
35
+ was `None`.
36
+ - `ObjectState.prepare_history_global_context()`: nothing; read
37
+ `get_live_global_config` / `get_saved_global_config`.
38
+ - `objectstate.lazy_factory.rebuild_lazy_config_with_new_global_reference(...)`:
39
+ nothing (it had no caller; it copied a lazy config and stored a global config as
40
+ a side effect).
41
+ - `ObjectStateRegistry._resolve_again_below` is now `resolve_again_below`.
42
+
43
+ Added: `set_global_config`, `forget_stored_global_config(config_type)` (a session
44
+ reset or a test that must leave nothing stored), `ObjectStateRegistry.global_config_state(config_type)`;
45
+ `get_live_global_config` and `get_saved_global_config` are exported from
46
+ `objectstate`.
47
+
48
+ Behaviour changes:
49
+
50
+ - `set_global_config` while a global state is registered replaces that state's
51
+ object (`update_object_instance`): unsaved edits of the global state are dropped
52
+ and its descendants are told at once. A call that only repeated what the state
53
+ already held must be deleted, not kept.
54
+ - `set_global_config` without a global state tells registered states at once (the
55
+ setters used to tell nobody until the next read).
56
+ - Registering a global state makes its object the global config at once, also when
57
+ nothing was stored before. Unregistering it makes the global config what
58
+ `set_global_config` last stored while no state held it (or the declared defaults).
59
+ - Every thread reads the same global config. A thread that used to see the declared
60
+ defaults because nothing was stored in it now sees the process's global config.
61
+ - Editing, replacing, restoring or time-travelling the global state no longer
62
+ leaves the caller's entered `config_context`: a block entered before the change
63
+ keeps the layers it was entered with until it is left.
64
+ - A global state that is not registered no longer changes the global config when it
65
+ is edited, saved, restored or given another object.
66
+ - `get_live_global_config` returns the object the registered global state builds
67
+ from its parameters (one object until the next change), not the instance the
68
+ application first passed in. Compare values, not identity.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: objectstate
3
- Version: 3.0.3
3
+ Version: 4.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
@@ -53,7 +53,7 @@ Description-Content-Type: text/markdown
53
53
  - Y-Axis: Sibling inheritance within same context
54
54
  - **Contextvars-Based**: Uses Python's `contextvars` for clean context management
55
55
  - **UI Integration**: Placeholder text generation for configuration forms
56
- - **Thread-Safe**: Thread-local global configuration storage
56
+ - **One global config**: held by the registered global state, or stored where no state exists
57
57
  - **100% Generic**: No application-specific dependencies
58
58
  - **Small dependency surface**: Uses ``python-introspect`` for callable and
59
59
  dataclass analysis
@@ -112,7 +112,7 @@ resolution state.
112
112
  When using the decorator pattern with `auto_create_decorator`, you need to establish the global configuration context for lazy resolution:
113
113
 
114
114
  ```python
115
- from objectstate import ensure_global_config_context
115
+ from objectstate import set_global_config
116
116
 
117
117
  # After creating your global config instance
118
118
  global_config = GlobalPipelineConfig(
@@ -121,15 +121,15 @@ global_config = GlobalPipelineConfig(
121
121
  )
122
122
 
123
123
  # REQUIRED: Establish global config context for lazy resolution
124
- ensure_global_config_context(GlobalPipelineConfig, global_config)
124
+ set_global_config(GlobalPipelineConfig, global_config)
125
125
 
126
126
  # Now lazy configs can resolve from the global context
127
127
  ```
128
128
 
129
129
  **Key differences:**
130
130
  - `set_base_config_type(MyConfig)`: Sets the **type** (class) for the framework
131
- - `ensure_global_config_context(GlobalConfig, instance)`: Sets the **instance** (concrete values) for resolution
132
- - Call `ensure_global_config_context()` at application startup (GUI) or before pipeline execution
131
+ - `set_global_config(GlobalConfig, instance)`: Sets the **instance** (concrete values) for resolution
132
+ - Call `set_global_config()` at application startup (GUI) or before pipeline execution
133
133
 
134
134
  ## Installation
135
135
 
@@ -16,7 +16,7 @@
16
16
  - Y-Axis: Sibling inheritance within same context
17
17
  - **Contextvars-Based**: Uses Python's `contextvars` for clean context management
18
18
  - **UI Integration**: Placeholder text generation for configuration forms
19
- - **Thread-Safe**: Thread-local global configuration storage
19
+ - **One global config**: held by the registered global state, or stored where no state exists
20
20
  - **100% Generic**: No application-specific dependencies
21
21
  - **Small dependency surface**: Uses ``python-introspect`` for callable and
22
22
  dataclass analysis
@@ -75,7 +75,7 @@ resolution state.
75
75
  When using the decorator pattern with `auto_create_decorator`, you need to establish the global configuration context for lazy resolution:
76
76
 
77
77
  ```python
78
- from objectstate import ensure_global_config_context
78
+ from objectstate import set_global_config
79
79
 
80
80
  # After creating your global config instance
81
81
  global_config = GlobalPipelineConfig(
@@ -84,15 +84,15 @@ global_config = GlobalPipelineConfig(
84
84
  )
85
85
 
86
86
  # REQUIRED: Establish global config context for lazy resolution
87
- ensure_global_config_context(GlobalPipelineConfig, global_config)
87
+ set_global_config(GlobalPipelineConfig, global_config)
88
88
 
89
89
  # Now lazy configs can resolve from the global context
90
90
  ```
91
91
 
92
92
  **Key differences:**
93
93
  - `set_base_config_type(MyConfig)`: Sets the **type** (class) for the framework
94
- - `ensure_global_config_context(GlobalConfig, instance)`: Sets the **instance** (concrete values) for resolution
95
- - Call `ensure_global_config_context()` at application startup (GUI) or before pipeline execution
94
+ - `set_global_config(GlobalConfig, instance)`: Sets the **instance** (concrete values) for resolution
95
+ - Call `set_global_config()` at application startup (GUI) or before pipeline execution
96
96
 
97
97
  ## Installation
98
98
 
@@ -12,7 +12,7 @@ Configuration and lazy types
12
12
  Builds an application-specific configuration decorator and related lazy
13
13
  types.
14
14
 
15
- ``config_context`` and ``ensure_global_config_context``
15
+ ``config_context`` and ``set_global_config``
16
16
  Install concrete configuration used during resolution.
17
17
 
18
18
  ``set_base_config_type``
@@ -487,8 +487,8 @@ How It Works
487
487
  Global Configuration Context
488
488
  -----------------------------
489
489
 
490
- Understanding set_base_config_type vs ensure_global_config_context
491
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
490
+ Understanding set_base_config_type vs set_global_config
491
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
492
492
 
493
493
  The framework provides two related but distinct functions:
494
494
 
@@ -507,26 +507,27 @@ Sets the **type** (class) for the framework:
507
507
  * Required for type validation and registry
508
508
  * Does not set any concrete values
509
509
 
510
- **ensure_global_config_context(config_type: Type, instance: Any)**
510
+ **set_global_config(config_type: Type, instance: Any)**
511
511
 
512
512
  Sets the **instance** (concrete values) for resolution:
513
513
 
514
514
  .. code-block:: python
515
515
 
516
- from objectstate import ensure_global_config_context
516
+ from objectstate import set_global_config
517
517
 
518
518
  global_config = GlobalPipelineConfig(
519
519
  num_workers=8,
520
520
  output_dir="/data"
521
521
  )
522
522
 
523
- ensure_global_config_context(GlobalPipelineConfig, global_config)
523
+ set_global_config(GlobalPipelineConfig, global_config)
524
524
 
525
- * Call after creating global config instance
526
- * Uses thread-local storage for thread safety
527
- * Required for lazy resolution to work
528
- * Internally calls ``set_global_config_for_editing()``
529
- * Should be called at application startup (GUI) or before pipeline execution
525
+ * Call after creating global config instance, in a process that registers no
526
+ ``ObjectState`` for it (a script, a compile worker, an execution server)
527
+ * One value per process: every thread reads it
528
+ * Not needed when an ``ObjectState`` of the global config is registered at scope
529
+ ``""``: that state's object is the global config, live and saved
530
+ (see :doc:`state_propagation`). Calling it then replaces that state's object.
530
531
 
531
532
  **Key Differences:**
532
533
 
@@ -536,7 +537,7 @@ Sets the **instance** (concrete values) for resolution:
536
537
 
537
538
  * - Aspect
538
539
  - set_base_config_type
539
- - ensure_global_config_context
540
+ - set_global_config
540
541
  * - What it sets
541
542
  - Type (class)
542
543
  - Instance (concrete values)
@@ -546,17 +547,18 @@ Sets the **instance** (concrete values) for resolution:
546
547
  * - Purpose
547
548
  - Type registration
548
549
  - Enable lazy resolution
549
- * - Thread safety
550
- - Global registry
551
- - Thread-local storage
550
+ * - Scope
551
+ - One per process
552
+ - One per process
552
553
 
553
554
  Thread Safety
554
555
  -------------
555
556
 
556
- The framework is fully thread-safe through:
557
+ Threads see:
557
558
 
558
- 1. **Thread-local storage** for global configs
559
+ 1. **One global config per process**, read by every thread
559
560
  2. **ContextVars** for temporary contexts
560
561
  3. **Immutable frozen dataclasses** (when using ``frozen=True``)
561
562
 
562
- This makes it safe to use in multi-threaded applications, including web servers and async applications.
563
+ Entered contexts are per thread or task. The global config and the registry are shared
564
+ and not synchronised: keep edits in one thread.
@@ -174,7 +174,7 @@ Using the decorator pattern for automatic field injection and lazy class generat
174
174
  .. code-block:: python
175
175
 
176
176
  from dataclasses import dataclass
177
- from objectstate import auto_create_decorator, ensure_global_config_context
177
+ from objectstate import auto_create_decorator, set_global_config
178
178
 
179
179
  # Create global config with auto_create_decorator
180
180
  @auto_create_decorator
@@ -213,7 +213,7 @@ Using the decorator pattern for automatic field injection and lazy class generat
213
213
  )
214
214
 
215
215
  # REQUIRED: Establish global context for lazy resolution
216
- ensure_global_config_context(GlobalPipelineConfig, global_config)
216
+ set_global_config(GlobalPipelineConfig, global_config)
217
217
 
218
218
  # Now you can access injected configs
219
219
  print(f"Database: {global_config.database_config.host}:{global_config.database_config.port}")
@@ -19,7 +19,7 @@ Key Features
19
19
 
20
20
  * **Contextvars-Based**: Uses Python's ``contextvars`` for clean context management
21
21
  * **UI Integration**: Placeholder text generation for configuration forms
22
- * **Thread-Safe**: Thread-local global configuration storage
22
+ * **One global config**: held by the registered global state, or stored where no state exists
23
23
  * **100% Generic**: No application-specific dependencies
24
24
  * **Small dependency surface**: Uses ``python-introspect`` for signature and
25
25
  dataclass analysis
@@ -119,7 +119,9 @@ A ``ResolvedField`` named tuple:
119
119
 
120
120
  * ``value``: The resolved field value (may be ``None``)
121
121
  * ``source_scope``: The scope id of the layer that supplied the value. ``None`` for a layer
122
- entered with ``config_context(obj)`` without a ``scope_id``.
122
+ entered with ``config_context(obj)`` without a ``scope_id``. ``ObjectState.get_provenance``
123
+ never reports such a layer: a state resolves against the global config and its registered
124
+ ancestors only (:ref:`state-resolution-inputs`).
123
125
  * ``source_type``: The type the value was read from: the container type or one of its bases.
124
126
 
125
127
  When no layer holds a value, ``value`` is the declared default and the source names the
@@ -174,7 +174,7 @@ When using the decorator pattern with ``auto_create_decorator``, you need to est
174
174
 
175
175
  from objectstate import (
176
176
  auto_create_decorator,
177
- ensure_global_config_context,
177
+ set_global_config,
178
178
  )
179
179
  from dataclasses import dataclass
180
180
 
@@ -192,7 +192,7 @@ When using the decorator pattern with ``auto_create_decorator``, you need to est
192
192
  )
193
193
 
194
194
  # REQUIRED: Establish global config context
195
- ensure_global_config_context(GlobalPipelineConfig, global_config)
195
+ set_global_config(GlobalPipelineConfig, global_config)
196
196
 
197
197
  # Now lazy configs can resolve from the global context
198
198
 
@@ -200,8 +200,8 @@ Understanding the Difference
200
200
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
201
201
 
202
202
  * ``set_base_config_type(MyConfig)``: Sets the **type** (class) for the framework
203
- * ``ensure_global_config_context(GlobalConfig, instance)``: Sets the **instance** (concrete values) for resolution
204
- * Call ``ensure_global_config_context()`` at application startup (GUI) or before pipeline execution
203
+ * ``set_global_config(GlobalConfig, instance)``: Sets the **instance** (concrete values) for resolution
204
+ * Call ``set_global_config()`` at application startup (GUI) or before pipeline execution
205
205
 
206
206
  Complete Example
207
207
  ----------------
@@ -95,17 +95,35 @@ Use parent forwarding when all of the following are true:
95
95
  For a nested dataclass field in the same state, call ``update_parameter()`` on
96
96
  the dotted path instead.
97
97
 
98
+ .. _state-resolution-inputs:
99
+
98
100
  What a resolved value is computed from
99
101
  --------------------------------------
100
102
 
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
103
+ A resolved value of a state depends only on the state's place in the registered
104
+ hierarchy. It is computed from three things: which states are registered at the
105
+ scopes above it, what each of them stores for the field's config type and field name,
106
+ and what the global config stores. Nothing else is an input. In particular a
107
+ ``config_context`` that the caller has entered is not: a state built, read or resolved
108
+ again inside such a block has the values, saved values, reported sources, dirty
109
+ fields and notifications it has outside it, during the block and after it. The state
110
+ resolves against a layer stack of its own (``build_context_stack``) that starts from
111
+ the global config and holds the registered ancestors and the state's object; the
112
+ caller's context is set aside while it does and is active again afterwards. So the
113
+ reported source of a resolved value is always a scope where a state is registered, or
114
+ ``""`` for the global config.
115
+
116
+ This is the only statement of the rule. A lazy config object that no state owns is a
117
+ different route: plain attribute access on it resolves against whatever
118
+ ``config_context`` is entered (see ``context_system.rst`` in the package docs), which
119
+ is how a compiler resolves a pipeline's objects.
120
+
121
+ Each resolved snapshot (live
104
122
  and saved) keeps the objects it was computed from (``ResolutionSources``): the global
105
123
  config instance of the thread that resolved it and the object of every state
106
124
  registered above. Every resolved read (``get_resolved_value``,
107
125
  ``get_saved_resolved_value``, ``get_provenance``, ``to_resolved_object``,
108
- ``state.fields``) first compares those objects with the ones the state would be given
126
+ ``state.fields``, ``dirty_fields``) first compares those objects with the ones the state would be given
109
127
  now. When they are the same objects the read answers from the snapshot. When one is
110
128
  another object, the two are compared field by field, the way field resolution reads a
111
129
  context layer, and only the fields of the state that are unset and can inherit a
@@ -114,7 +132,7 @@ registered state than before, every field is.
114
132
 
115
133
  So no caller announces a change. Registering, unregistering or replacing an ancestor,
116
134
  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
135
+ global config with ``set_global_config`` all reach
118
136
  a state on its next read, whether the state is registered or not. A registered state
119
137
  is also told at once, so its subscribers hear of the change without a read; a state
120
138
  that is not registered is referenced by nothing and tells its subscribers on its next
@@ -126,10 +144,47 @@ Limits:
126
144
  ``ObjectState.to_object`` returns the same function object for a state that wraps a
127
145
  plain function, so states registered below a function-backed state follow its edits
128
146
  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>`_).
147
+ - Nothing is synchronised. Every thread reads the same registry and the same global
148
+ config, so a thread that edits while another reads must be kept apart by the
149
+ application (in a GUI: edit in the GUI thread).
150
+
151
+ The state of the global config
152
+ ------------------------------
153
+
154
+ The global config is the value every resolution starts from. There is one per config
155
+ type per process, read as edited (live) and as saved, and one thing holds it
156
+ (``objectstate.global_config``):
157
+
158
+ - The state registered at the global scope (``scope_id=""``) whose object is of that
159
+ type. The live global config is the object its parameters describe
160
+ (``object_for_resolution()``) and the saved global config is the object it last
161
+ saved. Nothing copies them anywhere: ``get_live_global_config``,
162
+ ``get_saved_global_config``, ``config_context`` and every state's resolution ask the
163
+ registered state. So an edit, a save, a restore, a replaced object, an undo or a
164
+ redo of that state is that change of the global config, and its registered
165
+ descendants are told at once.
166
+ - While no such state is registered: the object given to ``set_global_config``. Nothing
167
+ edits it, so the live and the saved read are the same object. A process without
168
+ states (a compile worker, an execution server, a script) uses only this.
169
+
170
+ Registering the global state is all it takes to make its object the global config; no
171
+ ``set_global_config`` call is needed beside it. Unregistering it (or travelling to a
172
+ point in history where it was not registered) ends that at once: the global config is
173
+ then again what ``set_global_config`` last stored while no state held it, or the
174
+ declared defaults.
175
+
176
+ ``set_global_config(config_type, config)`` is the one way to replace the global config
177
+ from outside. While a state holds the global config the call is
178
+ ``state.update_object_instance(config)``: the state takes the new object as its live
179
+ and saved value (unsaved edits are dropped) and its descendants and their subscribers
180
+ follow. A stored object that disagrees with a registered global state cannot be made.
181
+
182
+ Threads play no part. The registry is one per process and so is the stored object, so
183
+ a worker thread reads the global config the GUI thread edits, with or without
184
+ ``spawn_thread_with_context`` (which copies the entered ``config_context`` layers, not
185
+ the global config, into the new thread).
186
+
187
+ A ``config_context`` block entered before the global config changes keeps the layers
188
+ it was entered with until it is left.
189
+
135
190
 
@@ -9,7 +9,7 @@ Key Features:
9
9
  - Dual-axis inheritance (context hierarchy + sibling inheritance)
10
10
  - Contextvars-based context management
11
11
  - Placeholder text generation for UI
12
- - Thread-local global configuration storage
12
+ - One global config per process, held by the registered global state
13
13
 
14
14
  Quick Start:
15
15
  >>> from objectstate import (
@@ -47,7 +47,7 @@ Modules:
47
47
  - dual_axis_resolver: Dual-axis inheritance resolver
48
48
  - context_manager: Contextvars-based context management
49
49
  - placeholder: Placeholder text generation for UI
50
- - global_config: Thread-local global configuration storage
50
+ - global_config: The global config and the one thing that holds it
51
51
  - config: Framework configuration (pluggable types and behaviors)
52
52
  """
53
53
 
@@ -58,7 +58,6 @@ from objectstate.lazy_factory import (
58
58
  register_lazy_type_mapping,
59
59
  get_base_type_for_lazy,
60
60
  get_lazy_type_for_base,
61
- ensure_global_config_context,
62
61
  # Constructor patching for code execution
63
62
  patch_lazy_constructors,
64
63
  # Global config type checking
@@ -86,7 +85,6 @@ from objectstate.dual_axis_resolver import (
86
85
  from objectstate.context_manager import (
87
86
  config_context,
88
87
  get_current_temp_global,
89
- clear_current_temp_global,
90
88
  merge_configs,
91
89
  extract_all_configs,
92
90
  get_base_global_config,
@@ -111,10 +109,8 @@ from objectstate.placeholder import LazyDefaultPlaceholderService
111
109
 
112
110
  # Global config
113
111
  from objectstate.global_config import (
114
- get_current_global_config,
115
- set_global_config_for_editing,
116
- set_saved_global_config,
117
- set_live_global_config,
112
+ set_global_config,
113
+ forget_stored_global_config,
118
114
  get_saved_global_config,
119
115
  get_live_global_config,
120
116
  )
@@ -155,7 +151,6 @@ __all__ = [
155
151
  'register_lazy_type_mapping',
156
152
  'get_base_type_for_lazy',
157
153
  'get_lazy_type_for_base',
158
- 'ensure_global_config_context',
159
154
  # Constructor patching for code execution
160
155
  'patch_lazy_constructors',
161
156
  # Global config type checking
@@ -172,7 +167,6 @@ __all__ = [
172
167
  # Context
173
168
  'config_context',
174
169
  'get_current_temp_global',
175
- 'clear_current_temp_global',
176
170
  'merge_configs',
177
171
  'extract_all_configs',
178
172
  'get_base_global_config',
@@ -182,8 +176,10 @@ __all__ = [
182
176
  # Placeholder
183
177
  'LazyDefaultPlaceholderService',
184
178
  # Global config
185
- 'get_current_global_config',
186
- 'set_global_config_for_editing',
179
+ 'set_global_config',
180
+ 'forget_stored_global_config',
181
+ 'get_live_global_config',
182
+ 'get_saved_global_config',
187
183
  # Configuration
188
184
  'set_base_config_type',
189
185
  'get_base_config_type',
@@ -210,7 +206,7 @@ __all__ = [
210
206
  'ObjectStateEditSession',
211
207
  ]
212
208
 
213
- __version__ = "3.0.3"
209
+ __version__ = "4.0.0"
214
210
  __author__ = 'OpenHCS Team'
215
211
  __description__ = 'Generic configuration framework for lazy dataclass resolution'
216
212
 
@@ -182,7 +182,7 @@ def config_context(
182
182
  mask_with_none: If True, None values override/mask base config values, and
183
183
  field resolution does not look at any layer outside this one.
184
184
  If False (default), None values are ignored (normal inheritance).
185
- Use True when editing GlobalPipelineConfig to mask thread-local
185
+ Use True when editing GlobalPipelineConfig to mask the
186
186
  loaded instance with static class defaults.
187
187
  use_live_global: If True (default), use LIVE global config (UI sees unsaved edits).
188
188
  If False, use SAVED global config (compiler sees saved values).
@@ -585,13 +585,20 @@ def build_context_stack(
585
585
  use_live: bool = True,
586
586
  ):
587
587
  """
588
- Enter every context layer needed to resolve ``object_instance``; leave them on exit.
588
+ Enter the complete layer stack that resolves ``object_instance``; leave it on exit.
589
+
590
+ The stack is built from the arguments and the global config alone. A context the
591
+ caller has entered is set aside for the duration of the block and is active again
592
+ after it: none of its layers is below, between or above the layers entered here.
593
+ This is how an ``ObjectState`` resolves, so a state's resolved values depend on
594
+ its object, the states registered above it and the global config, and never on
595
+ where the read was made. A lazy object that no state owns is resolved by plain
596
+ attribute access against whatever ``config_context`` is entered.
589
597
 
590
598
  Layers, outermost first:
591
- 1. The global config, under scope ``""``. ``config_context`` puts it below the
592
- first layer entered while no context is active. When ``object_instance`` is
593
- itself a global config, a fresh instance of its type holding the declared
594
- defaults is entered above it, also under scope ``""``, and masks it.
599
+ 1. The global config, under scope ``""``. When ``object_instance`` is itself a
600
+ global config, a fresh instance of its type holding the declared defaults is
601
+ entered above it, also under scope ``""``, and masks it.
595
602
  2. Each ancestor, under its own scope id. Global-config ancestors are left out
596
603
  because layer 1 already stands for them.
597
604
  3. ``object_instance``, under ``current_scope_id``.
@@ -613,6 +620,14 @@ def build_context_stack(
613
620
  )
614
621
 
615
622
  with ExitStack() as stack:
623
+ # With no context active, the first layer entered starts from the global config.
624
+ for variable, no_context in (
625
+ (current_temp_global, None),
626
+ (context_type_stack, ()),
627
+ (context_layer_stack, NO_LAYERS),
628
+ ):
629
+ stack.callback(variable.reset, variable.set(no_context))
630
+
616
631
  if isinstance(object_instance, GlobalConfigBase):
617
632
  enter(stack, type(object_instance)(), "", mask=True)
618
633
 
@@ -650,20 +665,21 @@ def get_base_global_config(use_live: bool = True):
650
665
  """
651
666
  Get the global config a context starts from when none is active.
652
667
 
653
- This is the config set with ensure_global_config_context(), or an instance of the
654
- base config type holding its declared defaults when none was set. Raises when no
655
- base config type is set or its defaults cannot be constructed.
668
+ This is the global config of the base config type (see ``objectstate.global_config``),
669
+ or an instance of that type holding its declared defaults when there is none.
670
+ Raises when no base config type is set or its defaults cannot be constructed.
656
671
 
657
672
  Args:
658
673
  use_live: If True (default), return LIVE config (UI sees unsaved edits).
659
674
  If False, return SAVED config (compiler sees saved values).
660
675
  """
661
676
  from objectstate.config import get_base_config_type
662
- from objectstate.global_config import get_current_global_config
677
+ from objectstate.global_config import get_live_global_config, get_saved_global_config
663
678
 
664
679
  base_config_type = get_base_config_type()
665
680
 
666
- current_global = get_current_global_config(base_config_type, use_live=use_live)
681
+ read = get_live_global_config if use_live else get_saved_global_config
682
+ current_global = read(base_config_type)
667
683
  if current_global is not None:
668
684
  return current_global
669
685
 
@@ -683,17 +699,6 @@ def get_current_temp_global():
683
699
  return current_temp_global.get(None)
684
700
 
685
701
 
686
- def clear_current_temp_global():
687
- """
688
- Leave the active context without restoring an outer one.
689
-
690
- After this no context is active: lazy fields fall back to their declared
691
- defaults until the next ``config_context`` is entered.
692
- """
693
- current_temp_global.set(None)
694
- context_layer_stack.set(NO_LAYERS)
695
-
696
-
697
702
  # Utility functions for debugging and introspection
698
703
 
699
704
  def get_context_info() -> Dict[str, Any]:
@@ -884,12 +889,10 @@ def spawn_thread_with_context(
884
889
  ):
885
890
  """Spawn a thread with current context propagated.
886
891
 
887
- This ensures thread-local context (like GlobalPipelineConfig from
888
- config framework) is accessible in spawned thread.
889
-
890
- Python's threading module creates isolated thread-local storage, so contextvars
891
- don't automatically propagate to spawned threads. This utility wraps
892
- threading.Thread to propagate the current context using contextvars.copy_context().
892
+ A new thread starts with no ``config_context`` entered. This starts one with the
893
+ layers the caller has entered (``contextvars.copy_context()``), so plain lazy
894
+ objects read in it what they read in the caller. The global config and the
895
+ registry are one per process and need no copying.
893
896
 
894
897
  Args:
895
898
  target: The function to run in thread