tnfr 4.5.2__py3-none-any.whl → 7.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of tnfr might be problematic. Click here for more details.
- tnfr/__init__.py +275 -51
- tnfr/__init__.pyi +33 -0
- tnfr/_compat.py +10 -0
- tnfr/_generated_version.py +34 -0
- tnfr/_version.py +49 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +117 -31
- tnfr/alias.pyi +108 -0
- tnfr/cache.py +6 -572
- tnfr/cache.pyi +16 -0
- tnfr/callback_utils.py +16 -38
- tnfr/callback_utils.pyi +79 -0
- tnfr/cli/__init__.py +34 -14
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +211 -28
- tnfr/cli/arguments.pyi +27 -0
- tnfr/cli/execution.py +470 -50
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/utils.py +18 -3
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +13 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/{constants_glyphs.py → config/constants.py} +26 -20
- tnfr/config/constants.pyi +12 -0
- tnfr/config/feature_flags.py +83 -0
- tnfr/{config.py → config/init.py} +11 -7
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +93 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +84 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +80 -29
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +31 -0
- tnfr/constants/core.py +4 -4
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +1 -1
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +7 -15
- tnfr/constants/metric.pyi +19 -0
- tnfr/dynamics/__init__.py +165 -633
- tnfr/dynamics/__init__.pyi +82 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/dnfr.py +2283 -400
- tnfr/dynamics/dnfr.pyi +24 -0
- tnfr/dynamics/integrators.py +406 -98
- tnfr/dynamics/integrators.pyi +34 -0
- tnfr/dynamics/runtime.py +881 -0
- tnfr/dynamics/sampling.py +10 -5
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +719 -0
- tnfr/execution.py +70 -48
- tnfr/execution.pyi +45 -0
- tnfr/flatten.py +13 -9
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +66 -53
- tnfr/gamma.pyi +34 -0
- tnfr/glyph_history.py +110 -52
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +16 -0
- tnfr/glyph_runtime.pyi +9 -0
- tnfr/immutable.py +69 -28
- tnfr/immutable.pyi +34 -0
- tnfr/initialization.py +16 -16
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +6 -240
- tnfr/io.pyi +16 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +81 -0
- tnfr/mathematics/backend.py +426 -0
- tnfr/mathematics/dynamics.py +398 -0
- tnfr/mathematics/epi.py +254 -0
- tnfr/mathematics/generators.py +222 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/operators.py +233 -0
- tnfr/mathematics/operators_factory.py +71 -0
- tnfr/mathematics/projection.py +78 -0
- tnfr/mathematics/runtime.py +173 -0
- tnfr/mathematics/spaces.py +247 -0
- tnfr/mathematics/transforms.py +292 -0
- tnfr/metrics/__init__.py +10 -10
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +993 -324
- tnfr/metrics/common.py +23 -16
- tnfr/metrics/common.pyi +46 -0
- tnfr/metrics/core.py +251 -35
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +708 -111
- tnfr/metrics/diagnosis.pyi +85 -0
- tnfr/metrics/export.py +27 -15
- tnfr/metrics/glyph_timing.py +232 -42
- tnfr/metrics/reporting.py +33 -22
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +987 -43
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +214 -23
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +115 -22
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +542 -136
- tnfr/node.pyi +178 -0
- tnfr/observers.py +152 -35
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +23 -19
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +601 -82
- tnfr/operators/__init__.pyi +45 -0
- tnfr/operators/definitions.py +513 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +760 -0
- tnfr/operators/jitter.py +107 -38
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +75 -0
- tnfr/operators/registry.pyi +13 -0
- tnfr/operators/remesh.py +149 -88
- tnfr/py.typed +0 -0
- tnfr/rng.py +46 -143
- tnfr/rng.pyi +14 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/selector.py +25 -19
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +72 -62
- tnfr/sense.pyi +23 -0
- tnfr/structural.py +522 -262
- tnfr/structural.pyi +69 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/nu_f.py +423 -0
- tnfr/telemetry/nu_f.pyi +123 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +1 -3
- tnfr/tokens.pyi +36 -0
- tnfr/trace.py +270 -113
- tnfr/trace.pyi +40 -0
- tnfr/types.py +574 -6
- tnfr/types.pyi +331 -0
- tnfr/units.py +69 -0
- tnfr/units.pyi +16 -0
- tnfr/utils/__init__.py +217 -0
- tnfr/utils/__init__.pyi +202 -0
- tnfr/utils/cache.py +2395 -0
- tnfr/utils/cache.pyi +468 -0
- tnfr/utils/chunks.py +104 -0
- tnfr/utils/chunks.pyi +21 -0
- tnfr/{collections_utils.py → utils/data.py} +147 -90
- tnfr/utils/data.pyi +64 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +770 -0
- tnfr/utils/init.pyi +78 -0
- tnfr/utils/io.py +456 -0
- tnfr/{helpers → utils}/numeric.py +51 -24
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +113 -0
- tnfr/validation/__init__.pyi +77 -0
- tnfr/validation/compatibility.py +95 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/grammar.py +71 -0
- tnfr/validation/grammar.pyi +40 -0
- tnfr/validation/graph.py +138 -0
- tnfr/validation/graph.pyi +17 -0
- tnfr/validation/rules.py +281 -0
- tnfr/validation/rules.pyi +55 -0
- tnfr/validation/runtime.py +263 -0
- tnfr/validation/runtime.pyi +31 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +37 -0
- tnfr/validation/spectral.py +159 -0
- tnfr/validation/spectral.pyi +46 -0
- tnfr/validation/syntax.py +40 -0
- tnfr/validation/syntax.pyi +10 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/viz/__init__.py +9 -0
- tnfr/viz/matplotlib.py +246 -0
- tnfr-7.0.0.dist-info/METADATA +179 -0
- tnfr-7.0.0.dist-info/RECORD +185 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/licenses/LICENSE.md +1 -1
- tnfr/grammar.py +0 -344
- tnfr/graph_utils.py +0 -84
- tnfr/helpers/__init__.py +0 -71
- tnfr/import_utils.py +0 -228
- tnfr/json_utils.py +0 -162
- tnfr/logging_utils.py +0 -116
- tnfr/presets.py +0 -60
- tnfr/validators.py +0 -84
- tnfr/value_utils.py +0 -59
- tnfr-4.5.2.dist-info/METADATA +0 -379
- tnfr-4.5.2.dist-info/RECORD +0 -67
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.2.dist-info → tnfr-7.0.0.dist-info}/top_level.txt +0 -0
tnfr/operators/jitter.py
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
+
"""Jitter operators for reproducible phase perturbations."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
|
-
from typing import Any, TYPE_CHECKING
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
import threading
|
|
6
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
7
|
|
|
6
|
-
from ..
|
|
8
|
+
from ..rng import base_seed, cache_enabled
|
|
9
|
+
from ..rng import clear_rng_cache as _clear_rng_cache
|
|
7
10
|
from ..rng import (
|
|
8
|
-
ScopedCounterCache,
|
|
9
11
|
make_rng,
|
|
10
|
-
base_seed,
|
|
11
|
-
cache_enabled,
|
|
12
|
-
clear_rng_cache as _clear_rng_cache,
|
|
13
12
|
seed_hash,
|
|
14
13
|
)
|
|
15
|
-
from ..
|
|
14
|
+
from ..types import NodeId, TNFRGraph
|
|
15
|
+
from ..utils import (
|
|
16
|
+
CacheManager,
|
|
17
|
+
InstrumentedLRUCache,
|
|
18
|
+
ScopedCounterCache,
|
|
19
|
+
build_cache_manager,
|
|
20
|
+
ensure_node_offset_map,
|
|
21
|
+
get_nodenx,
|
|
22
|
+
)
|
|
16
23
|
|
|
17
24
|
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
18
|
-
from ..node import
|
|
25
|
+
from ..node import NodeProtocol
|
|
19
26
|
|
|
20
27
|
# Guarded by the cache lock to ensure thread-safe access. ``seq`` stores
|
|
21
|
-
# per-scope jitter sequence counters in an LRU cache bounded to avoid
|
|
28
|
+
# per-scope jitter sequence counters in an instrumented LRU cache bounded to avoid
|
|
22
29
|
# unbounded memory usage.
|
|
23
30
|
_JITTER_MAX_ENTRIES = 1024
|
|
24
31
|
|
|
@@ -26,18 +33,53 @@ _JITTER_MAX_ENTRIES = 1024
|
|
|
26
33
|
class JitterCache:
|
|
27
34
|
"""Container for jitter-related caches."""
|
|
28
35
|
|
|
29
|
-
def __init__(
|
|
30
|
-
self
|
|
31
|
-
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
max_entries: int = _JITTER_MAX_ENTRIES,
|
|
39
|
+
*,
|
|
40
|
+
manager: CacheManager | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
self._manager = manager or build_cache_manager()
|
|
43
|
+
if not self._manager.has_override("scoped_counter:jitter"):
|
|
44
|
+
self._manager.configure(
|
|
45
|
+
overrides={"scoped_counter:jitter": int(max_entries)}
|
|
46
|
+
)
|
|
47
|
+
self._sequence = ScopedCounterCache(
|
|
48
|
+
"jitter",
|
|
49
|
+
max_entries=None,
|
|
50
|
+
manager=self._manager,
|
|
51
|
+
default_max_entries=int(max_entries),
|
|
52
|
+
)
|
|
53
|
+
self._settings_key = "jitter_settings"
|
|
54
|
+
self._manager.register(
|
|
55
|
+
self._settings_key,
|
|
56
|
+
lambda: {"max_entries": self._sequence.max_entries},
|
|
57
|
+
reset=self._reset_settings,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _reset_settings(self, settings: dict[str, Any] | None) -> dict[str, Any]:
|
|
61
|
+
return {"max_entries": self._sequence.max_entries}
|
|
62
|
+
|
|
63
|
+
def _refresh_settings(self) -> None:
|
|
64
|
+
self._manager.update(
|
|
65
|
+
self._settings_key,
|
|
66
|
+
lambda _: {"max_entries": self._sequence.max_entries},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def manager(self) -> CacheManager:
|
|
71
|
+
"""Expose the cache manager backing this cache."""
|
|
72
|
+
|
|
73
|
+
return self._manager
|
|
32
74
|
|
|
33
75
|
@property
|
|
34
|
-
def seq(self) ->
|
|
35
|
-
"""Expose the sequence cache for tests and diagnostics."""
|
|
76
|
+
def seq(self) -> InstrumentedLRUCache[tuple[int, int], int]:
|
|
77
|
+
"""Expose the instrumented sequence cache for tests and diagnostics."""
|
|
36
78
|
|
|
37
79
|
return self._sequence.cache
|
|
38
80
|
|
|
39
81
|
@property
|
|
40
|
-
def lock(self):
|
|
82
|
+
def lock(self) -> threading.Lock | threading.RLock:
|
|
41
83
|
"""Return the lock protecting the sequence cache."""
|
|
42
84
|
|
|
43
85
|
return self._sequence.lock
|
|
@@ -53,21 +95,26 @@ class JitterCache:
|
|
|
53
95
|
"""Set the maximum number of cached jitter sequences."""
|
|
54
96
|
|
|
55
97
|
self._sequence.configure(max_entries=int(value))
|
|
56
|
-
self.
|
|
98
|
+
self._refresh_settings()
|
|
57
99
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
@property
|
|
101
|
+
def settings(self) -> dict[str, Any]:
|
|
102
|
+
"""Return jitter cache settings stored on the manager."""
|
|
103
|
+
|
|
104
|
+
return cast(dict[str, Any], self._manager.get(self._settings_key))
|
|
105
|
+
|
|
106
|
+
def setup(self, force: bool = False, max_entries: int | None = None) -> None:
|
|
61
107
|
"""Ensure jitter cache matches the configured size."""
|
|
62
108
|
|
|
63
109
|
self._sequence.configure(force=force, max_entries=max_entries)
|
|
64
|
-
self.
|
|
110
|
+
self._refresh_settings()
|
|
65
111
|
|
|
66
112
|
def clear(self) -> None:
|
|
67
113
|
"""Clear cached RNGs and jitter state."""
|
|
68
114
|
|
|
69
115
|
_clear_rng_cache()
|
|
70
116
|
self._sequence.clear()
|
|
117
|
+
self._manager.clear(self._settings_key)
|
|
71
118
|
|
|
72
119
|
def bump(self, key: tuple[int, int]) -> int:
|
|
73
120
|
"""Return current jitter sequence counter for ``key`` and increment it."""
|
|
@@ -78,40 +125,57 @@ class JitterCache:
|
|
|
78
125
|
class JitterCacheManager:
|
|
79
126
|
"""Manager exposing the jitter cache without global reassignment."""
|
|
80
127
|
|
|
81
|
-
def __init__(
|
|
82
|
-
self
|
|
128
|
+
def __init__(
|
|
129
|
+
self,
|
|
130
|
+
cache: JitterCache | None = None,
|
|
131
|
+
*,
|
|
132
|
+
manager: CacheManager | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
if cache is not None:
|
|
135
|
+
self.cache = cache
|
|
136
|
+
self._manager = cache.manager
|
|
137
|
+
else:
|
|
138
|
+
self._manager = manager or build_cache_manager()
|
|
139
|
+
self.cache = JitterCache(manager=self._manager)
|
|
83
140
|
|
|
84
141
|
# Convenience passthrough properties
|
|
85
142
|
@property
|
|
86
|
-
def seq(self) ->
|
|
143
|
+
def seq(self) -> InstrumentedLRUCache[tuple[int, int], int]:
|
|
144
|
+
"""Expose the underlying instrumented jitter sequence cache."""
|
|
145
|
+
|
|
87
146
|
return self.cache.seq
|
|
88
147
|
|
|
89
148
|
@property
|
|
90
149
|
def settings(self) -> dict[str, Any]:
|
|
150
|
+
"""Return persisted jitter cache configuration."""
|
|
151
|
+
|
|
91
152
|
return self.cache.settings
|
|
92
153
|
|
|
93
154
|
@property
|
|
94
|
-
def lock(self):
|
|
155
|
+
def lock(self) -> threading.Lock | threading.RLock:
|
|
156
|
+
"""Return the lock associated with the jitter cache."""
|
|
157
|
+
|
|
95
158
|
return self.cache.lock
|
|
96
159
|
|
|
97
160
|
@property
|
|
98
161
|
def max_entries(self) -> int:
|
|
99
162
|
"""Return the maximum number of cached jitter entries."""
|
|
163
|
+
|
|
100
164
|
return self.cache.max_entries
|
|
101
165
|
|
|
102
166
|
@max_entries.setter
|
|
103
167
|
def max_entries(self, value: int) -> None:
|
|
104
168
|
"""Set the maximum number of cached jitter entries."""
|
|
169
|
+
|
|
105
170
|
self.cache.max_entries = value
|
|
106
171
|
|
|
107
|
-
def setup(
|
|
108
|
-
self, force: bool = False, max_entries: int | None = None
|
|
109
|
-
) -> None:
|
|
172
|
+
def setup(self, force: bool = False, max_entries: int | None = None) -> None:
|
|
110
173
|
"""Ensure jitter cache matches the configured size.
|
|
111
174
|
|
|
112
175
|
``max_entries`` may be provided to explicitly resize the cache.
|
|
113
176
|
When omitted the existing ``cache.max_entries`` is preserved.
|
|
114
177
|
"""
|
|
178
|
+
|
|
115
179
|
if max_entries is not None:
|
|
116
180
|
self.cache.setup(force=True, max_entries=max_entries)
|
|
117
181
|
else:
|
|
@@ -119,6 +183,7 @@ class JitterCacheManager:
|
|
|
119
183
|
|
|
120
184
|
def clear(self) -> None:
|
|
121
185
|
"""Clear cached RNGs and jitter state."""
|
|
186
|
+
|
|
122
187
|
self.cache.clear()
|
|
123
188
|
|
|
124
189
|
def bump(self, key: tuple[int, int]) -> int:
|
|
@@ -148,27 +213,31 @@ def reset_jitter_manager() -> None:
|
|
|
148
213
|
_JITTER_MANAGER = None
|
|
149
214
|
|
|
150
215
|
|
|
151
|
-
def _node_offset(G, n) -> int:
|
|
216
|
+
def _node_offset(G: TNFRGraph, n: NodeId) -> int:
|
|
152
217
|
"""Deterministic node index used for jitter seeds."""
|
|
153
218
|
mapping = ensure_node_offset_map(G)
|
|
154
219
|
return int(mapping.get(n, 0))
|
|
155
220
|
|
|
156
221
|
|
|
157
|
-
def _resolve_jitter_seed(node:
|
|
158
|
-
|
|
159
|
-
if
|
|
160
|
-
raise ImportError("
|
|
161
|
-
if isinstance(node,
|
|
162
|
-
|
|
222
|
+
def _resolve_jitter_seed(node: NodeProtocol) -> tuple[int, int]:
|
|
223
|
+
node_nx_type = get_nodenx()
|
|
224
|
+
if node_nx_type is None:
|
|
225
|
+
raise ImportError("NodeNX is unavailable")
|
|
226
|
+
if isinstance(node, node_nx_type):
|
|
227
|
+
graph = cast(TNFRGraph, getattr(node, "G"))
|
|
228
|
+
node_id = cast(NodeId, getattr(node, "n"))
|
|
229
|
+
return _node_offset(graph, node_id), id(graph)
|
|
163
230
|
uid = getattr(node, "_noise_uid", None)
|
|
164
231
|
if uid is None:
|
|
165
232
|
uid = id(node)
|
|
166
233
|
setattr(node, "_noise_uid", uid)
|
|
167
|
-
|
|
234
|
+
graph = cast(TNFRGraph | None, getattr(node, "G", None))
|
|
235
|
+
scope = graph if graph is not None else node
|
|
236
|
+
return int(uid), id(scope)
|
|
168
237
|
|
|
169
238
|
|
|
170
239
|
def random_jitter(
|
|
171
|
-
node:
|
|
240
|
+
node: NodeProtocol,
|
|
172
241
|
amplitude: float,
|
|
173
242
|
) -> float:
|
|
174
243
|
"""Return deterministic noise in ``[-amplitude, amplitude]`` for ``node``.
|
|
@@ -184,7 +253,7 @@ def random_jitter(
|
|
|
184
253
|
seed_root = base_seed(node.G)
|
|
185
254
|
seed_key, scope_id = _resolve_jitter_seed(node)
|
|
186
255
|
|
|
187
|
-
cache_key = (seed_root, scope_id)
|
|
256
|
+
cache_key = (seed_root, scope_id, seed_key)
|
|
188
257
|
seq = 0
|
|
189
258
|
if cache_enabled(node.G):
|
|
190
259
|
manager = get_jitter_manager()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Registry mapping operator names to their classes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import pkgutil
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from ..config.operator_names import canonical_operator_name
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover - type checking only
|
|
12
|
+
from .definitions import Operator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
OPERATORS: dict[str, type["Operator"]] = {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def register_operator(cls: type["Operator"]) -> type["Operator"]:
|
|
19
|
+
"""Register ``cls`` under its declared ``name`` in :data:`OPERATORS`."""
|
|
20
|
+
|
|
21
|
+
name = getattr(cls, "name", None)
|
|
22
|
+
if not isinstance(name, str) or not name:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"Operator {cls.__name__} must declare a non-empty 'name' attribute"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
existing = OPERATORS.get(name)
|
|
28
|
+
if existing is not None and existing is not cls:
|
|
29
|
+
raise ValueError(f"Operator '{name}' is already registered")
|
|
30
|
+
|
|
31
|
+
OPERATORS[name] = cls
|
|
32
|
+
return cls
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_operator_class(name: str) -> type["Operator"]:
|
|
36
|
+
"""Return the operator class registered for ``name`` or its canonical alias."""
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
return OPERATORS[name]
|
|
40
|
+
except KeyError:
|
|
41
|
+
canonical = canonical_operator_name(name)
|
|
42
|
+
if canonical == name:
|
|
43
|
+
raise
|
|
44
|
+
try:
|
|
45
|
+
return OPERATORS[canonical]
|
|
46
|
+
except KeyError as exc: # pragma: no cover - defensive branch
|
|
47
|
+
raise KeyError(name) from exc
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def discover_operators() -> None:
|
|
51
|
+
"""Import all operator submodules so their decorators run."""
|
|
52
|
+
|
|
53
|
+
package = importlib.import_module("tnfr.operators")
|
|
54
|
+
package_path = getattr(package, "__path__", None)
|
|
55
|
+
if not package_path:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if getattr(package, "_operators_discovered", False): # pragma: no cover - cache
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
prefix = f"{package.__name__}."
|
|
62
|
+
for module_info in pkgutil.walk_packages(package_path, prefix):
|
|
63
|
+
if module_info.name == f"{prefix}registry":
|
|
64
|
+
continue
|
|
65
|
+
importlib.import_module(module_info.name)
|
|
66
|
+
|
|
67
|
+
setattr(package, "_operators_discovered", True)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = (
|
|
71
|
+
"OPERATORS",
|
|
72
|
+
"register_operator",
|
|
73
|
+
"discover_operators",
|
|
74
|
+
"get_operator_class",
|
|
75
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .definitions import Operator
|
|
4
|
+
|
|
5
|
+
__all__: Any
|
|
6
|
+
|
|
7
|
+
def __getattr__(name: str) -> Any: ...
|
|
8
|
+
|
|
9
|
+
OPERATORS: dict[str, type[Operator]]
|
|
10
|
+
|
|
11
|
+
def discover_operators() -> None: ...
|
|
12
|
+
def register_operator(cls: type[Operator]) -> type[Operator]: ...
|
|
13
|
+
def get_operator_class(name: str) -> type[Operator]: ...
|