tnfr 4.5.1__py3-none-any.whl → 6.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.
- tnfr/__init__.py +270 -90
- tnfr/__init__.pyi +40 -0
- tnfr/_compat.py +11 -0
- tnfr/_version.py +7 -0
- tnfr/_version.pyi +7 -0
- tnfr/alias.py +631 -0
- tnfr/alias.pyi +140 -0
- tnfr/cache.py +732 -0
- tnfr/cache.pyi +232 -0
- tnfr/callback_utils.py +381 -0
- tnfr/callback_utils.pyi +105 -0
- tnfr/cli/__init__.py +89 -0
- tnfr/cli/__init__.pyi +47 -0
- tnfr/cli/arguments.py +199 -0
- tnfr/cli/arguments.pyi +33 -0
- tnfr/cli/execution.py +322 -0
- tnfr/cli/execution.pyi +80 -0
- tnfr/cli/utils.py +34 -0
- tnfr/cli/utils.pyi +8 -0
- tnfr/config/__init__.py +12 -0
- tnfr/config/__init__.pyi +8 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/init.py +36 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +106 -0
- tnfr/config/operator_names.pyi +28 -0
- tnfr/config/presets.py +104 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/constants/__init__.py +228 -0
- tnfr/constants/__init__.pyi +104 -0
- tnfr/constants/core.py +158 -0
- tnfr/constants/core.pyi +17 -0
- tnfr/constants/init.py +31 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +102 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/constants_glyphs.py +16 -0
- tnfr/constants_glyphs.pyi +12 -0
- tnfr/dynamics/__init__.py +136 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +201 -0
- tnfr/dynamics/aliases.py +22 -0
- tnfr/dynamics/coordination.py +343 -0
- tnfr/dynamics/dnfr.py +2315 -0
- tnfr/dynamics/dnfr.pyi +33 -0
- tnfr/dynamics/integrators.py +561 -0
- tnfr/dynamics/integrators.pyi +35 -0
- tnfr/dynamics/runtime.py +521 -0
- tnfr/dynamics/sampling.py +34 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +680 -0
- tnfr/execution.py +216 -0
- tnfr/execution.pyi +65 -0
- tnfr/flatten.py +283 -0
- tnfr/flatten.pyi +28 -0
- tnfr/gamma.py +320 -89
- tnfr/gamma.pyi +40 -0
- tnfr/glyph_history.py +337 -0
- tnfr/glyph_history.pyi +53 -0
- tnfr/grammar.py +23 -153
- tnfr/grammar.pyi +13 -0
- tnfr/helpers/__init__.py +151 -0
- tnfr/helpers/__init__.pyi +66 -0
- tnfr/helpers/numeric.py +88 -0
- tnfr/helpers/numeric.pyi +12 -0
- tnfr/immutable.py +214 -0
- tnfr/immutable.pyi +37 -0
- tnfr/initialization.py +199 -0
- tnfr/initialization.pyi +73 -0
- tnfr/io.py +311 -0
- tnfr/io.pyi +11 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/metrics/__init__.py +41 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/coherence.py +1469 -0
- tnfr/metrics/common.py +149 -0
- tnfr/metrics/common.pyi +15 -0
- tnfr/metrics/core.py +259 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +840 -0
- tnfr/metrics/diagnosis.pyi +89 -0
- tnfr/metrics/export.py +151 -0
- tnfr/metrics/glyph_timing.py +369 -0
- tnfr/metrics/reporting.py +152 -0
- tnfr/metrics/reporting.pyi +12 -0
- tnfr/metrics/sense_index.py +294 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +216 -0
- tnfr/metrics/trig.pyi +12 -0
- tnfr/metrics/trig_cache.py +105 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/node.py +255 -177
- tnfr/node.pyi +161 -0
- tnfr/observers.py +154 -150
- tnfr/observers.pyi +46 -0
- tnfr/ontosim.py +135 -134
- tnfr/ontosim.pyi +33 -0
- tnfr/operators/__init__.py +452 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/definitions.py +181 -0
- tnfr/operators/definitions.pyi +92 -0
- tnfr/operators/jitter.py +266 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/registry.py +80 -0
- tnfr/operators/registry.pyi +15 -0
- tnfr/operators/remesh.py +569 -0
- tnfr/presets.py +10 -23
- tnfr/presets.pyi +7 -0
- tnfr/py.typed +0 -0
- tnfr/rng.py +440 -0
- tnfr/rng.pyi +14 -0
- tnfr/selector.py +217 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +307 -142
- tnfr/sense.pyi +30 -0
- tnfr/structural.py +69 -164
- tnfr/structural.pyi +46 -0
- tnfr/telemetry/__init__.py +13 -0
- tnfr/telemetry/verbosity.py +37 -0
- tnfr/tokens.py +61 -0
- tnfr/tokens.pyi +41 -0
- tnfr/trace.py +520 -95
- tnfr/trace.pyi +68 -0
- tnfr/types.py +382 -17
- tnfr/types.pyi +145 -0
- tnfr/utils/__init__.py +158 -0
- tnfr/utils/__init__.pyi +133 -0
- tnfr/utils/cache.py +755 -0
- tnfr/utils/cache.pyi +156 -0
- tnfr/utils/data.py +267 -0
- tnfr/utils/data.pyi +73 -0
- tnfr/utils/graph.py +87 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +746 -0
- tnfr/utils/init.pyi +85 -0
- tnfr/utils/io.py +157 -0
- tnfr/utils/io.pyi +10 -0
- tnfr/utils/validators.py +130 -0
- tnfr/utils/validators.pyi +19 -0
- tnfr/validation/__init__.py +25 -0
- tnfr/validation/__init__.pyi +17 -0
- tnfr/validation/compatibility.py +59 -0
- tnfr/validation/compatibility.pyi +8 -0
- tnfr/validation/grammar.py +149 -0
- tnfr/validation/grammar.pyi +11 -0
- tnfr/validation/rules.py +194 -0
- tnfr/validation/rules.pyi +18 -0
- tnfr/validation/syntax.py +151 -0
- tnfr/validation/syntax.pyi +7 -0
- tnfr-6.0.0.dist-info/METADATA +135 -0
- tnfr-6.0.0.dist-info/RECORD +157 -0
- tnfr/cli.py +0 -322
- tnfr/config.py +0 -41
- tnfr/constants.py +0 -277
- tnfr/dynamics.py +0 -814
- tnfr/helpers.py +0 -264
- tnfr/main.py +0 -47
- tnfr/metrics.py +0 -597
- tnfr/operators.py +0 -525
- tnfr/program.py +0 -176
- tnfr/scenarios.py +0 -34
- tnfr/validators.py +0 -38
- tnfr-4.5.1.dist-info/METADATA +0 -221
- tnfr-4.5.1.dist-info/RECORD +0 -28
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/WHEEL +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/entry_points.txt +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {tnfr-4.5.1.dist-info → tnfr-6.0.0.dist-info}/top_level.txt +0 -0
tnfr/cache.pyi
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping, MutableMapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, ClassVar, Generic, Hashable, TypeVar
|
|
6
|
+
|
|
7
|
+
from cachetools import LRUCache
|
|
8
|
+
|
|
9
|
+
from .types import TimingContext
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CacheManager",
|
|
13
|
+
"CacheCapacityConfig",
|
|
14
|
+
"CacheStatistics",
|
|
15
|
+
"InstrumentedLRUCache",
|
|
16
|
+
"ManagedLRUCache",
|
|
17
|
+
"prune_lock_mapping",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
K = TypeVar("K", bound=Hashable)
|
|
21
|
+
V = TypeVar("V")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CacheCapacityConfig:
|
|
26
|
+
default_capacity: int | None
|
|
27
|
+
overrides: dict[str, int | None]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class CacheStatistics:
|
|
32
|
+
hits: int = ...
|
|
33
|
+
misses: int = ...
|
|
34
|
+
evictions: int = ...
|
|
35
|
+
total_time: float = ...
|
|
36
|
+
timings: int = ...
|
|
37
|
+
|
|
38
|
+
def merge(self, other: CacheStatistics) -> CacheStatistics: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CacheManager:
|
|
42
|
+
_MISSING: ClassVar[object]
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
storage: MutableMapping[str, Any] | None = ...,
|
|
47
|
+
*,
|
|
48
|
+
default_capacity: int | None = ...,
|
|
49
|
+
overrides: Mapping[str, int | None] | None = ...,
|
|
50
|
+
) -> None: ...
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _normalise_capacity(value: int | None) -> int | None: ...
|
|
54
|
+
|
|
55
|
+
def register(
|
|
56
|
+
self,
|
|
57
|
+
name: str,
|
|
58
|
+
factory: Callable[[], Any],
|
|
59
|
+
*,
|
|
60
|
+
lock_factory: Callable[[], threading.Lock | threading.RLock] | None = ...,
|
|
61
|
+
reset: Callable[[Any], Any] | None = ...,
|
|
62
|
+
create: bool = ...,
|
|
63
|
+
) -> None: ...
|
|
64
|
+
|
|
65
|
+
def configure(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
default_capacity: int | None | object = ...,
|
|
69
|
+
overrides: Mapping[str, int | None] | None = ...,
|
|
70
|
+
replace_overrides: bool = ...,
|
|
71
|
+
) -> None: ...
|
|
72
|
+
|
|
73
|
+
def configure_from_mapping(self, config: Mapping[str, Any]) -> None: ...
|
|
74
|
+
|
|
75
|
+
def export_config(self) -> CacheCapacityConfig: ...
|
|
76
|
+
|
|
77
|
+
def get_capacity(
|
|
78
|
+
self,
|
|
79
|
+
name: str,
|
|
80
|
+
*,
|
|
81
|
+
requested: int | None = ...,
|
|
82
|
+
fallback: int | None = ...,
|
|
83
|
+
use_default: bool = ...,
|
|
84
|
+
) -> int | None: ...
|
|
85
|
+
|
|
86
|
+
def has_override(self, name: str) -> bool: ...
|
|
87
|
+
|
|
88
|
+
def get_lock(self, name: str) -> threading.Lock | threading.RLock: ...
|
|
89
|
+
|
|
90
|
+
def names(self) -> Iterator[str]: ...
|
|
91
|
+
|
|
92
|
+
def get(self, name: str, *, create: bool = ...) -> Any: ...
|
|
93
|
+
|
|
94
|
+
def peek(self, name: str) -> Any: ...
|
|
95
|
+
|
|
96
|
+
def store(self, name: str, value: Any) -> None: ...
|
|
97
|
+
|
|
98
|
+
def update(
|
|
99
|
+
self,
|
|
100
|
+
name: str,
|
|
101
|
+
updater: Callable[[Any], Any],
|
|
102
|
+
*,
|
|
103
|
+
create: bool = ...,
|
|
104
|
+
) -> Any: ...
|
|
105
|
+
|
|
106
|
+
def clear(self, name: str | None = ...) -> None: ...
|
|
107
|
+
|
|
108
|
+
def increment_hit(
|
|
109
|
+
self,
|
|
110
|
+
name: str,
|
|
111
|
+
*,
|
|
112
|
+
amount: int = ...,
|
|
113
|
+
duration: float | None = ...,
|
|
114
|
+
) -> None: ...
|
|
115
|
+
|
|
116
|
+
def increment_miss(
|
|
117
|
+
self,
|
|
118
|
+
name: str,
|
|
119
|
+
*,
|
|
120
|
+
amount: int = ...,
|
|
121
|
+
duration: float | None = ...,
|
|
122
|
+
) -> None: ...
|
|
123
|
+
|
|
124
|
+
def increment_eviction(self, name: str, *, amount: int = ...) -> None: ...
|
|
125
|
+
|
|
126
|
+
def record_timing(self, name: str, duration: float) -> None: ...
|
|
127
|
+
|
|
128
|
+
def timer(self, name: str) -> TimingContext: ...
|
|
129
|
+
|
|
130
|
+
def get_metrics(self, name: str) -> CacheStatistics: ...
|
|
131
|
+
|
|
132
|
+
def iter_metrics(self) -> Iterator[tuple[str, CacheStatistics]]: ...
|
|
133
|
+
|
|
134
|
+
def aggregate_metrics(self) -> CacheStatistics: ...
|
|
135
|
+
|
|
136
|
+
def register_metrics_publisher(
|
|
137
|
+
self, publisher: Callable[[str, CacheStatistics], None]
|
|
138
|
+
) -> None: ...
|
|
139
|
+
|
|
140
|
+
def publish_metrics(
|
|
141
|
+
self,
|
|
142
|
+
*,
|
|
143
|
+
publisher: Callable[[str, CacheStatistics], None] | None = ...,
|
|
144
|
+
) -> None: ...
|
|
145
|
+
|
|
146
|
+
def log_metrics(
|
|
147
|
+
self, logger: logging.Logger, *, level: int = ...
|
|
148
|
+
) -> None: ...
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class InstrumentedLRUCache(MutableMapping[K, V], Generic[K, V]):
|
|
152
|
+
_MISSING: ClassVar[object]
|
|
153
|
+
|
|
154
|
+
def __init__(
|
|
155
|
+
self,
|
|
156
|
+
maxsize: int,
|
|
157
|
+
*,
|
|
158
|
+
manager: CacheManager | None = ...,
|
|
159
|
+
metrics_key: str | None = ...,
|
|
160
|
+
telemetry_callbacks: Iterable[Callable[[K, V], None]]
|
|
161
|
+
| Callable[[K, V], None]
|
|
162
|
+
| None = ...,
|
|
163
|
+
eviction_callbacks: Iterable[Callable[[K, V], None]]
|
|
164
|
+
| Callable[[K, V], None]
|
|
165
|
+
| None = ...,
|
|
166
|
+
locks: MutableMapping[K, Any] | None = ...,
|
|
167
|
+
getsizeof: Callable[[V], int] | None = ...,
|
|
168
|
+
count_overwrite_hit: bool = ...,
|
|
169
|
+
) -> None: ...
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def telemetry_callbacks(self) -> tuple[Callable[[K, V], None], ...]: ...
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def eviction_callbacks(self) -> tuple[Callable[[K, V], None], ...]: ...
|
|
176
|
+
|
|
177
|
+
def set_telemetry_callbacks(
|
|
178
|
+
self,
|
|
179
|
+
callbacks: Iterable[Callable[[K, V], None]]
|
|
180
|
+
| Callable[[K, V], None]
|
|
181
|
+
| None,
|
|
182
|
+
*,
|
|
183
|
+
append: bool = ...,
|
|
184
|
+
) -> None: ...
|
|
185
|
+
|
|
186
|
+
def set_eviction_callbacks(
|
|
187
|
+
self,
|
|
188
|
+
callbacks: Iterable[Callable[[K, V], None]]
|
|
189
|
+
| Callable[[K, V], None]
|
|
190
|
+
| None,
|
|
191
|
+
*,
|
|
192
|
+
append: bool = ...,
|
|
193
|
+
) -> None: ...
|
|
194
|
+
|
|
195
|
+
def pop(self, key: K, default: Any = ...) -> V: ...
|
|
196
|
+
|
|
197
|
+
def popitem(self) -> tuple[K, V]: ...
|
|
198
|
+
|
|
199
|
+
def clear(self) -> None: ...
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def maxsize(self) -> int: ...
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def currsize(self) -> int: ...
|
|
206
|
+
|
|
207
|
+
def get(self, key: K, default: V | None = ...) -> V | None: ...
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ManagedLRUCache(LRUCache[K, V], Generic[K, V]):
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
maxsize: int,
|
|
214
|
+
*,
|
|
215
|
+
manager: CacheManager | None = ...,
|
|
216
|
+
metrics_key: str | None = ...,
|
|
217
|
+
eviction_callbacks: Iterable[Callable[[K, V], None]]
|
|
218
|
+
| Callable[[K, V], None]
|
|
219
|
+
| None = ...,
|
|
220
|
+
telemetry_callbacks: Iterable[Callable[[K, V], None]]
|
|
221
|
+
| Callable[[K, V], None]
|
|
222
|
+
| None = ...,
|
|
223
|
+
locks: MutableMapping[K, Any] | None = ...,
|
|
224
|
+
) -> None: ...
|
|
225
|
+
|
|
226
|
+
def popitem(self) -> tuple[K, V]: ...
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def prune_lock_mapping(
|
|
230
|
+
cache: Mapping[K, Any] | MutableMapping[K, Any] | None,
|
|
231
|
+
locks: MutableMapping[K, Any] | None,
|
|
232
|
+
) -> None: ...
|
tnfr/callback_utils.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Callback registration and invocation helpers.
|
|
2
|
+
|
|
3
|
+
This module is thread-safe: all mutations of the callback registry stored in a
|
|
4
|
+
graph's ``G.graph`` are serialised using a process-wide lock obtained via
|
|
5
|
+
``locking.get_lock("callbacks")``. Callback functions themselves execute
|
|
6
|
+
outside of the lock and must therefore be independently thread-safe if they
|
|
7
|
+
modify shared state.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
from typing import Any, TypedDict
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from collections import defaultdict, deque
|
|
16
|
+
from collections.abc import Callable, Mapping, Iterable
|
|
17
|
+
|
|
18
|
+
import traceback
|
|
19
|
+
import threading
|
|
20
|
+
from .utils import get_logger, is_non_string_sequence
|
|
21
|
+
from .constants import DEFAULTS
|
|
22
|
+
from .locking import get_lock
|
|
23
|
+
|
|
24
|
+
from .trace import CallbackSpec
|
|
25
|
+
|
|
26
|
+
import networkx as nx
|
|
27
|
+
|
|
28
|
+
__all__ = (
|
|
29
|
+
"CallbackEvent",
|
|
30
|
+
"CallbackManager",
|
|
31
|
+
"callback_manager",
|
|
32
|
+
"CallbackError",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CallbackEvent(str, Enum):
|
|
39
|
+
"""Supported callback events."""
|
|
40
|
+
|
|
41
|
+
BEFORE_STEP = "before_step"
|
|
42
|
+
AFTER_STEP = "after_step"
|
|
43
|
+
ON_REMESH = "on_remesh"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CallbackManager:
|
|
47
|
+
"""Centralised registry and error tracking for callbacks."""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._lock = get_lock("callbacks")
|
|
51
|
+
self._error_limit_lock = threading.Lock()
|
|
52
|
+
self._error_limit = 100
|
|
53
|
+
self._error_limit_cache = self._error_limit
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# Error limit management
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
def get_callback_error_limit(self) -> int:
|
|
59
|
+
"""Return the current callback error retention limit."""
|
|
60
|
+
with self._error_limit_lock:
|
|
61
|
+
return self._error_limit
|
|
62
|
+
|
|
63
|
+
def set_callback_error_limit(self, limit: int) -> int:
|
|
64
|
+
"""Set the maximum number of callback errors retained."""
|
|
65
|
+
if limit < 1:
|
|
66
|
+
raise ValueError("limit must be positive")
|
|
67
|
+
with self._error_limit_lock:
|
|
68
|
+
previous = self._error_limit
|
|
69
|
+
self._error_limit = int(limit)
|
|
70
|
+
self._error_limit_cache = self._error_limit
|
|
71
|
+
return previous
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# Registry helpers
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
def _record_callback_error(
|
|
77
|
+
self,
|
|
78
|
+
G: "nx.Graph",
|
|
79
|
+
event: str,
|
|
80
|
+
ctx: dict[str, Any],
|
|
81
|
+
spec: CallbackSpec,
|
|
82
|
+
err: Exception,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Log and store a callback error for later inspection."""
|
|
85
|
+
|
|
86
|
+
logger.exception("callback %r failed for %s: %s", spec.name, event, err)
|
|
87
|
+
limit = self._error_limit_cache
|
|
88
|
+
err_list = G.graph.setdefault(
|
|
89
|
+
"_callback_errors", deque[CallbackError](maxlen=limit)
|
|
90
|
+
)
|
|
91
|
+
if err_list.maxlen != limit:
|
|
92
|
+
err_list = deque[CallbackError](err_list, maxlen=limit)
|
|
93
|
+
G.graph["_callback_errors"] = err_list
|
|
94
|
+
error: CallbackError = {
|
|
95
|
+
"event": event,
|
|
96
|
+
"step": ctx.get("step"),
|
|
97
|
+
"error": repr(err),
|
|
98
|
+
"traceback": traceback.format_exc(),
|
|
99
|
+
"fn": _func_id(spec.func),
|
|
100
|
+
"name": spec.name,
|
|
101
|
+
}
|
|
102
|
+
err_list.append(error)
|
|
103
|
+
|
|
104
|
+
def _ensure_callbacks_nolock(self, G: "nx.Graph") -> CallbackRegistry:
|
|
105
|
+
cbs = G.graph.setdefault("callbacks", defaultdict(dict))
|
|
106
|
+
dirty: set[str] = set(G.graph.pop("_callbacks_dirty", ()))
|
|
107
|
+
return _validate_registry(G, cbs, dirty)
|
|
108
|
+
|
|
109
|
+
def _ensure_callbacks(self, G: "nx.Graph") -> CallbackRegistry:
|
|
110
|
+
with self._lock:
|
|
111
|
+
return self._ensure_callbacks_nolock(G)
|
|
112
|
+
|
|
113
|
+
def register_callback(
|
|
114
|
+
self,
|
|
115
|
+
G: "nx.Graph",
|
|
116
|
+
event: CallbackEvent | str,
|
|
117
|
+
func: Callback,
|
|
118
|
+
*,
|
|
119
|
+
name: str | None = None,
|
|
120
|
+
) -> Callback:
|
|
121
|
+
"""Register ``func`` as callback for ``event``."""
|
|
122
|
+
|
|
123
|
+
event = _normalize_event(event)
|
|
124
|
+
_ensure_known_event(event)
|
|
125
|
+
if not callable(func):
|
|
126
|
+
raise TypeError("func must be callable")
|
|
127
|
+
with self._lock:
|
|
128
|
+
cbs = self._ensure_callbacks_nolock(G)
|
|
129
|
+
|
|
130
|
+
cb_name = name or getattr(func, "__name__", None)
|
|
131
|
+
spec = CallbackSpec(cb_name, func)
|
|
132
|
+
existing_map = cbs[event]
|
|
133
|
+
strict = bool(
|
|
134
|
+
G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"])
|
|
135
|
+
)
|
|
136
|
+
key = _reconcile_callback(event, existing_map, spec, strict)
|
|
137
|
+
|
|
138
|
+
existing_map[key] = spec
|
|
139
|
+
dirty = G.graph.setdefault("_callbacks_dirty", set())
|
|
140
|
+
dirty.add(event)
|
|
141
|
+
return func
|
|
142
|
+
|
|
143
|
+
def invoke_callbacks(
|
|
144
|
+
self,
|
|
145
|
+
G: "nx.Graph",
|
|
146
|
+
event: CallbackEvent | str,
|
|
147
|
+
ctx: dict[str, Any] | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Invoke all callbacks registered for ``event`` with context ``ctx``."""
|
|
150
|
+
|
|
151
|
+
event = _normalize_event(event)
|
|
152
|
+
with self._lock:
|
|
153
|
+
cbs = dict(self._ensure_callbacks_nolock(G).get(event, {}))
|
|
154
|
+
strict = bool(
|
|
155
|
+
G.graph.get("CALLBACKS_STRICT", DEFAULTS["CALLBACKS_STRICT"])
|
|
156
|
+
)
|
|
157
|
+
if ctx is None:
|
|
158
|
+
ctx = {}
|
|
159
|
+
for spec in cbs.values():
|
|
160
|
+
try:
|
|
161
|
+
spec.func(G, ctx)
|
|
162
|
+
except (
|
|
163
|
+
RuntimeError,
|
|
164
|
+
ValueError,
|
|
165
|
+
TypeError,
|
|
166
|
+
) as e:
|
|
167
|
+
with self._lock:
|
|
168
|
+
self._record_callback_error(G, event, ctx, spec, e)
|
|
169
|
+
if strict:
|
|
170
|
+
raise
|
|
171
|
+
except nx.NetworkXError as err:
|
|
172
|
+
with self._lock:
|
|
173
|
+
self._record_callback_error(G, event, ctx, spec, err)
|
|
174
|
+
logger.exception(
|
|
175
|
+
"callback %r raised NetworkXError for %s with ctx=%r",
|
|
176
|
+
spec.name,
|
|
177
|
+
event,
|
|
178
|
+
ctx,
|
|
179
|
+
)
|
|
180
|
+
raise
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
Callback = Callable[["nx.Graph", dict[str, Any]], None]
|
|
184
|
+
CallbackRegistry = dict[str, dict[str, "CallbackSpec"]]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class CallbackError(TypedDict):
|
|
188
|
+
"""Metadata for a failed callback invocation."""
|
|
189
|
+
|
|
190
|
+
event: str
|
|
191
|
+
step: int | None
|
|
192
|
+
error: str
|
|
193
|
+
traceback: str
|
|
194
|
+
fn: str
|
|
195
|
+
name: str | None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _func_id(fn: Callable[..., Any]) -> str:
|
|
199
|
+
"""Return a deterministic identifier for ``fn``.
|
|
200
|
+
|
|
201
|
+
Combines the function's module and qualified name to avoid the
|
|
202
|
+
nondeterminism of ``repr(fn)`` which includes the memory address.
|
|
203
|
+
"""
|
|
204
|
+
module = getattr(fn, "__module__", fn.__class__.__module__)
|
|
205
|
+
qualname = getattr(
|
|
206
|
+
fn,
|
|
207
|
+
"__qualname__",
|
|
208
|
+
getattr(fn, "__name__", fn.__class__.__qualname__),
|
|
209
|
+
)
|
|
210
|
+
return f"{module}.{qualname}"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _validate_registry(
|
|
214
|
+
G: "nx.Graph", cbs: Any, dirty: set[str]
|
|
215
|
+
) -> CallbackRegistry:
|
|
216
|
+
"""Validate and normalise the callback registry.
|
|
217
|
+
|
|
218
|
+
``cbs`` is coerced to a ``defaultdict(dict)`` and any events listed in
|
|
219
|
+
``dirty`` are rebuilt using :func:`_normalize_callbacks`. Unknown events are
|
|
220
|
+
removed. The cleaned registry is stored back on the graph and returned.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
if not isinstance(cbs, Mapping):
|
|
224
|
+
logger.warning(
|
|
225
|
+
"Invalid callbacks registry on graph; resetting to empty",
|
|
226
|
+
)
|
|
227
|
+
cbs = defaultdict(dict)
|
|
228
|
+
elif not isinstance(cbs, defaultdict) or cbs.default_factory is not dict:
|
|
229
|
+
cbs = defaultdict(
|
|
230
|
+
dict,
|
|
231
|
+
{
|
|
232
|
+
event: _normalize_callbacks(entries)
|
|
233
|
+
for event, entries in dict(cbs).items()
|
|
234
|
+
if _is_known_event(event)
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
for event in dirty:
|
|
239
|
+
if _is_known_event(event):
|
|
240
|
+
cbs[event] = _normalize_callbacks(cbs.get(event))
|
|
241
|
+
else:
|
|
242
|
+
cbs.pop(event, None)
|
|
243
|
+
|
|
244
|
+
G.graph["callbacks"] = cbs
|
|
245
|
+
return cbs
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _normalize_callbacks(entries: Any) -> dict[str, CallbackSpec]:
|
|
249
|
+
"""Return ``entries`` normalised into a callback mapping."""
|
|
250
|
+
if isinstance(entries, Mapping):
|
|
251
|
+
entries_iter = entries.values()
|
|
252
|
+
elif isinstance(entries, Iterable) and not isinstance(entries, (str, bytes, bytearray)):
|
|
253
|
+
entries_iter = entries
|
|
254
|
+
else:
|
|
255
|
+
return {}
|
|
256
|
+
|
|
257
|
+
new_map: dict[str, CallbackSpec] = {}
|
|
258
|
+
for entry in entries_iter:
|
|
259
|
+
spec = _normalize_callback_entry(entry)
|
|
260
|
+
if spec is None:
|
|
261
|
+
continue
|
|
262
|
+
key = spec.name or _func_id(spec.func)
|
|
263
|
+
new_map[key] = spec
|
|
264
|
+
return new_map
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _normalize_event(event: CallbackEvent | str) -> str:
|
|
268
|
+
"""Return ``event`` as a string."""
|
|
269
|
+
return event.value if isinstance(event, CallbackEvent) else str(event)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _is_known_event(event: str) -> bool:
|
|
273
|
+
"""Return ``True`` when ``event`` matches a declared :class:`CallbackEvent`."""
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
CallbackEvent(event)
|
|
277
|
+
except ValueError:
|
|
278
|
+
return False
|
|
279
|
+
else:
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _ensure_known_event(event: str) -> None:
|
|
284
|
+
"""Raise :class:`ValueError` when ``event`` is not a known callback."""
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
CallbackEvent(event)
|
|
288
|
+
except ValueError as exc: # pragma: no cover - defensive branch
|
|
289
|
+
raise ValueError(f"Unknown event: {event}") from exc
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _normalize_callback_entry(entry: Any) -> "CallbackSpec | None":
|
|
293
|
+
"""Normalize a callback specification.
|
|
294
|
+
|
|
295
|
+
Supported formats
|
|
296
|
+
-----------------
|
|
297
|
+
* :class:`CallbackSpec` instances (returned unchanged).
|
|
298
|
+
* Sequences ``(name: str, func: Callable)`` such as lists, tuples or other
|
|
299
|
+
iterables.
|
|
300
|
+
* Bare callables ``func`` whose name is taken from ``func.__name__``.
|
|
301
|
+
|
|
302
|
+
``None`` is returned when ``entry`` does not match any of the accepted
|
|
303
|
+
formats. The original ``entry`` is never mutated. Sequence inputs are
|
|
304
|
+
converted to ``tuple`` before validation to support generators; the
|
|
305
|
+
materialization consumes the iterable and failure results in ``None``.
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
if isinstance(entry, CallbackSpec):
|
|
309
|
+
return entry
|
|
310
|
+
elif is_non_string_sequence(entry):
|
|
311
|
+
try:
|
|
312
|
+
entry = tuple(entry)
|
|
313
|
+
except TypeError:
|
|
314
|
+
return None
|
|
315
|
+
if len(entry) != 2:
|
|
316
|
+
return None
|
|
317
|
+
name, fn = entry
|
|
318
|
+
if not isinstance(name, str) or not callable(fn):
|
|
319
|
+
return None
|
|
320
|
+
return CallbackSpec(name, fn)
|
|
321
|
+
elif callable(entry):
|
|
322
|
+
name = getattr(entry, "__name__", None)
|
|
323
|
+
return CallbackSpec(name, entry)
|
|
324
|
+
else:
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _reconcile_callback(
|
|
329
|
+
event: str,
|
|
330
|
+
existing_map: dict[str, CallbackSpec],
|
|
331
|
+
spec: CallbackSpec,
|
|
332
|
+
strict: bool,
|
|
333
|
+
) -> str:
|
|
334
|
+
"""Reconcile ``spec`` with ``existing_map``.
|
|
335
|
+
|
|
336
|
+
Ensures that callbacks remain unique by explicit name or function identity.
|
|
337
|
+
When a name collision occurs with a different function, ``strict`` controls
|
|
338
|
+
whether a :class:`ValueError` is raised or a warning is logged.
|
|
339
|
+
|
|
340
|
+
Parameters
|
|
341
|
+
----------
|
|
342
|
+
event:
|
|
343
|
+
Event under which ``spec`` will be registered. Only used for messages.
|
|
344
|
+
existing_map:
|
|
345
|
+
Current mapping of callbacks for ``event``.
|
|
346
|
+
spec:
|
|
347
|
+
Callback specification being registered.
|
|
348
|
+
strict:
|
|
349
|
+
Whether to raise on name collisions instead of logging a warning.
|
|
350
|
+
|
|
351
|
+
Returns
|
|
352
|
+
-------
|
|
353
|
+
str
|
|
354
|
+
Key under which ``spec`` should be stored in ``existing_map``.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
key = spec.name or _func_id(spec.func)
|
|
358
|
+
|
|
359
|
+
if spec.name is not None:
|
|
360
|
+
existing_spec = existing_map.get(key)
|
|
361
|
+
if existing_spec is not None and existing_spec.func is not spec.func:
|
|
362
|
+
msg = f"Callback {spec.name!r} already registered for {event}"
|
|
363
|
+
if strict:
|
|
364
|
+
raise ValueError(msg)
|
|
365
|
+
logger.warning(msg)
|
|
366
|
+
|
|
367
|
+
# Remove existing entries under the same key and any other using the same
|
|
368
|
+
# function identity to avoid duplicates.
|
|
369
|
+
existing_map.pop(key, None)
|
|
370
|
+
fn_key = next((k for k, s in existing_map.items() if s.func is spec.func), None)
|
|
371
|
+
if fn_key is not None:
|
|
372
|
+
existing_map.pop(fn_key, None)
|
|
373
|
+
|
|
374
|
+
return key
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ---------------------------------------------------------------------------
|
|
378
|
+
# Default manager instance and convenience wrappers
|
|
379
|
+
# ---------------------------------------------------------------------------
|
|
380
|
+
|
|
381
|
+
callback_manager = CallbackManager()
|
tnfr/callback_utils.pyi
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import traceback
|
|
5
|
+
from collections import defaultdict, deque
|
|
6
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, TypedDict
|
|
9
|
+
|
|
10
|
+
import networkx as nx
|
|
11
|
+
|
|
12
|
+
from .constants import DEFAULTS
|
|
13
|
+
from .locking import get_lock
|
|
14
|
+
from .trace import CallbackSpec
|
|
15
|
+
from .utils import get_logger, is_non_string_sequence
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
"CallbackEvent",
|
|
19
|
+
"CallbackManager",
|
|
20
|
+
"callback_manager",
|
|
21
|
+
"CallbackError",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger: Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CallbackEvent(str, Enum):
|
|
28
|
+
BEFORE_STEP = "before_step"
|
|
29
|
+
AFTER_STEP = "after_step"
|
|
30
|
+
ON_REMESH = "on_remesh"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Callback = Callable[[nx.Graph, dict[str, Any]], None]
|
|
34
|
+
CallbackRegistry = dict[str, dict[str, CallbackSpec]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CallbackError(TypedDict):
|
|
38
|
+
event: str
|
|
39
|
+
step: int | None
|
|
40
|
+
error: str
|
|
41
|
+
traceback: str
|
|
42
|
+
fn: str
|
|
43
|
+
name: str | None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CallbackManager:
|
|
47
|
+
def __init__(self) -> None: ...
|
|
48
|
+
|
|
49
|
+
def get_callback_error_limit(self) -> int: ...
|
|
50
|
+
|
|
51
|
+
def set_callback_error_limit(self, limit: int) -> int: ...
|
|
52
|
+
|
|
53
|
+
def register_callback(
|
|
54
|
+
self,
|
|
55
|
+
G: nx.Graph,
|
|
56
|
+
event: CallbackEvent | str,
|
|
57
|
+
func: Callback,
|
|
58
|
+
*,
|
|
59
|
+
name: str | None = ...,
|
|
60
|
+
) -> Callback: ...
|
|
61
|
+
|
|
62
|
+
def invoke_callbacks(
|
|
63
|
+
self,
|
|
64
|
+
G: nx.Graph,
|
|
65
|
+
event: CallbackEvent | str,
|
|
66
|
+
ctx: dict[str, Any] | None = ...,
|
|
67
|
+
) -> None: ...
|
|
68
|
+
|
|
69
|
+
def _record_callback_error(
|
|
70
|
+
self,
|
|
71
|
+
G: nx.Graph,
|
|
72
|
+
event: str,
|
|
73
|
+
ctx: dict[str, Any],
|
|
74
|
+
spec: CallbackSpec,
|
|
75
|
+
err: Exception,
|
|
76
|
+
) -> None: ...
|
|
77
|
+
|
|
78
|
+
def _ensure_callbacks_nolock(self, G: nx.Graph) -> CallbackRegistry: ...
|
|
79
|
+
|
|
80
|
+
def _ensure_callbacks(self, G: nx.Graph) -> CallbackRegistry: ...
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
callback_manager: CallbackManager
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _func_id(fn: Callable[..., Any]) -> str: ...
|
|
87
|
+
|
|
88
|
+
def _validate_registry(G: nx.Graph, cbs: Any, dirty: set[str]) -> CallbackRegistry: ...
|
|
89
|
+
|
|
90
|
+
def _normalize_callbacks(entries: Any) -> dict[str, CallbackSpec]: ...
|
|
91
|
+
|
|
92
|
+
def _normalize_event(event: CallbackEvent | str) -> str: ...
|
|
93
|
+
|
|
94
|
+
def _is_known_event(event: str) -> bool: ...
|
|
95
|
+
|
|
96
|
+
def _ensure_known_event(event: str) -> None: ...
|
|
97
|
+
|
|
98
|
+
def _normalize_callback_entry(entry: Any) -> CallbackSpec | None: ...
|
|
99
|
+
|
|
100
|
+
def _reconcile_callback(
|
|
101
|
+
event: str,
|
|
102
|
+
existing_map: dict[str, CallbackSpec],
|
|
103
|
+
spec: CallbackSpec,
|
|
104
|
+
strict: bool,
|
|
105
|
+
) -> str: ...
|