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/helpers/__init__.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Curated high-level helpers exposed by :mod:`tnfr.helpers`.
|
|
2
|
+
|
|
3
|
+
The module is intentionally small and surfaces utilities that are stable for
|
|
4
|
+
external use, covering data preparation, glyph history management, and graph
|
|
5
|
+
cache invalidation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import Counter
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Callable, Mapping, MutableMapping, Protocol, cast
|
|
12
|
+
|
|
13
|
+
from ..types import TNFRGraph
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING: # pragma: no cover - import-time only for typing
|
|
16
|
+
from ..utils import (
|
|
17
|
+
CacheManager,
|
|
18
|
+
EdgeCacheManager,
|
|
19
|
+
cached_node_list,
|
|
20
|
+
cached_nodes_and_A,
|
|
21
|
+
edge_version_cache,
|
|
22
|
+
edge_version_update,
|
|
23
|
+
ensure_node_index_map,
|
|
24
|
+
ensure_node_offset_map,
|
|
25
|
+
get_graph,
|
|
26
|
+
get_graph_mapping,
|
|
27
|
+
increment_edge_version,
|
|
28
|
+
mark_dnfr_prep_dirty,
|
|
29
|
+
node_set_checksum,
|
|
30
|
+
stable_json,
|
|
31
|
+
)
|
|
32
|
+
from ..glyph_history import HistoryDict
|
|
33
|
+
from .numeric import (
|
|
34
|
+
angle_diff,
|
|
35
|
+
clamp,
|
|
36
|
+
clamp01,
|
|
37
|
+
kahan_sum_nd,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = (
|
|
41
|
+
"CacheManager",
|
|
42
|
+
"EdgeCacheManager",
|
|
43
|
+
"angle_diff",
|
|
44
|
+
"cached_node_list",
|
|
45
|
+
"cached_nodes_and_A",
|
|
46
|
+
"clamp",
|
|
47
|
+
"clamp01",
|
|
48
|
+
"edge_version_cache",
|
|
49
|
+
"edge_version_update",
|
|
50
|
+
"ensure_node_index_map",
|
|
51
|
+
"ensure_node_offset_map",
|
|
52
|
+
"get_graph",
|
|
53
|
+
"get_graph_mapping",
|
|
54
|
+
"increment_edge_version",
|
|
55
|
+
"kahan_sum_nd",
|
|
56
|
+
"mark_dnfr_prep_dirty",
|
|
57
|
+
"node_set_checksum",
|
|
58
|
+
"stable_json",
|
|
59
|
+
"count_glyphs",
|
|
60
|
+
"ensure_history",
|
|
61
|
+
"last_glyph",
|
|
62
|
+
"push_glyph",
|
|
63
|
+
"recent_glyph",
|
|
64
|
+
"__getattr__",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_UTIL_EXPORTS = {
|
|
69
|
+
"CacheManager",
|
|
70
|
+
"EdgeCacheManager",
|
|
71
|
+
"cached_node_list",
|
|
72
|
+
"cached_nodes_and_A",
|
|
73
|
+
"edge_version_cache",
|
|
74
|
+
"edge_version_update",
|
|
75
|
+
"ensure_node_index_map",
|
|
76
|
+
"ensure_node_offset_map",
|
|
77
|
+
"get_graph",
|
|
78
|
+
"get_graph_mapping",
|
|
79
|
+
"increment_edge_version",
|
|
80
|
+
"mark_dnfr_prep_dirty",
|
|
81
|
+
"node_set_checksum",
|
|
82
|
+
"stable_json",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def __getattr__(name: str) -> Any: # pragma: no cover - simple delegation
|
|
87
|
+
if name in _UTIL_EXPORTS:
|
|
88
|
+
from .. import utils as _utils
|
|
89
|
+
|
|
90
|
+
value = getattr(_utils, name)
|
|
91
|
+
globals()[name] = value
|
|
92
|
+
return value
|
|
93
|
+
raise AttributeError(name)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def __dir__() -> list[str]: # pragma: no cover - simple reflection
|
|
97
|
+
return sorted(set(__all__))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _PushGlyphCallable(Protocol):
|
|
101
|
+
def __call__(self, nd: MutableMapping[str, Any], glyph: str, window: int) -> None:
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _RecentGlyphCallable(Protocol):
|
|
106
|
+
def __call__(self, nd: MutableMapping[str, Any], glyph: str, window: int) -> bool:
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class _EnsureHistoryCallable(Protocol):
|
|
111
|
+
def __call__(self, G: TNFRGraph) -> "HistoryDict | dict[str, Any]":
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class _LastGlyphCallable(Protocol):
|
|
116
|
+
def __call__(self, nd: Mapping[str, Any]) -> str | None:
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class _CountGlyphsCallable(Protocol):
|
|
121
|
+
def __call__(
|
|
122
|
+
self, G: TNFRGraph, window: int | None = ..., *, last_only: bool = ...
|
|
123
|
+
) -> Counter[str]:
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _glyph_history_proxy(name: str) -> Callable[..., Any]:
|
|
128
|
+
"""Return a wrapper that delegates to :mod:`tnfr.glyph_history` lazily."""
|
|
129
|
+
|
|
130
|
+
target: dict[str, Callable[..., Any] | None] = {"func": None}
|
|
131
|
+
|
|
132
|
+
def _call(*args: Any, **kwargs: Any) -> Any:
|
|
133
|
+
func = target["func"]
|
|
134
|
+
if func is None:
|
|
135
|
+
from .. import glyph_history as _glyph_history
|
|
136
|
+
|
|
137
|
+
func = getattr(_glyph_history, name)
|
|
138
|
+
target["func"] = func
|
|
139
|
+
return func(*args, **kwargs)
|
|
140
|
+
|
|
141
|
+
_call.__name__ = name
|
|
142
|
+
_call.__qualname__ = name
|
|
143
|
+
_call.__doc__ = f"Proxy for :func:`tnfr.glyph_history.{name}`."
|
|
144
|
+
return _call
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
count_glyphs = cast(_CountGlyphsCallable, _glyph_history_proxy("count_glyphs"))
|
|
148
|
+
ensure_history = cast(_EnsureHistoryCallable, _glyph_history_proxy("ensure_history"))
|
|
149
|
+
last_glyph = cast(_LastGlyphCallable, _glyph_history_proxy("last_glyph"))
|
|
150
|
+
push_glyph = cast(_PushGlyphCallable, _glyph_history_proxy("push_glyph"))
|
|
151
|
+
recent_glyph = cast(_RecentGlyphCallable, _glyph_history_proxy("recent_glyph"))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..cache import CacheManager as CacheManager
|
|
6
|
+
from ..glyph_history import (
|
|
7
|
+
HistoryDict,
|
|
8
|
+
count_glyphs as count_glyphs,
|
|
9
|
+
ensure_history as ensure_history,
|
|
10
|
+
last_glyph as last_glyph,
|
|
11
|
+
push_glyph as push_glyph,
|
|
12
|
+
recent_glyph as recent_glyph,
|
|
13
|
+
)
|
|
14
|
+
from ..utils.cache import (
|
|
15
|
+
EdgeCacheManager as EdgeCacheManager,
|
|
16
|
+
cached_node_list as cached_node_list,
|
|
17
|
+
cached_nodes_and_A as cached_nodes_and_A,
|
|
18
|
+
edge_version_cache as edge_version_cache,
|
|
19
|
+
edge_version_update as edge_version_update,
|
|
20
|
+
ensure_node_index_map as ensure_node_index_map,
|
|
21
|
+
ensure_node_offset_map as ensure_node_offset_map,
|
|
22
|
+
node_set_checksum as node_set_checksum,
|
|
23
|
+
stable_json as stable_json,
|
|
24
|
+
)
|
|
25
|
+
from ..utils.graph import (
|
|
26
|
+
get_graph as get_graph,
|
|
27
|
+
get_graph_mapping as get_graph_mapping,
|
|
28
|
+
increment_edge_version as increment_edge_version,
|
|
29
|
+
mark_dnfr_prep_dirty as mark_dnfr_prep_dirty,
|
|
30
|
+
)
|
|
31
|
+
from .numeric import (
|
|
32
|
+
angle_diff as angle_diff,
|
|
33
|
+
clamp as clamp,
|
|
34
|
+
clamp01 as clamp01,
|
|
35
|
+
kahan_sum_nd as kahan_sum_nd,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = (
|
|
39
|
+
"CacheManager",
|
|
40
|
+
"EdgeCacheManager",
|
|
41
|
+
"angle_diff",
|
|
42
|
+
"cached_node_list",
|
|
43
|
+
"cached_nodes_and_A",
|
|
44
|
+
"clamp",
|
|
45
|
+
"clamp01",
|
|
46
|
+
"edge_version_cache",
|
|
47
|
+
"edge_version_update",
|
|
48
|
+
"ensure_node_index_map",
|
|
49
|
+
"ensure_node_offset_map",
|
|
50
|
+
"get_graph",
|
|
51
|
+
"get_graph_mapping",
|
|
52
|
+
"increment_edge_version",
|
|
53
|
+
"kahan_sum_nd",
|
|
54
|
+
"mark_dnfr_prep_dirty",
|
|
55
|
+
"node_set_checksum",
|
|
56
|
+
"stable_json",
|
|
57
|
+
"count_glyphs",
|
|
58
|
+
"ensure_history",
|
|
59
|
+
"last_glyph",
|
|
60
|
+
"push_glyph",
|
|
61
|
+
"recent_glyph",
|
|
62
|
+
"__getattr__",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def __getattr__(name: str) -> Any: ...
|
tnfr/helpers/numeric.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Numeric helper functions and compensated summation utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Sequence
|
|
6
|
+
import math
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
"clamp",
|
|
10
|
+
"clamp01",
|
|
11
|
+
"within_range",
|
|
12
|
+
"similarity_abs",
|
|
13
|
+
"kahan_sum_nd",
|
|
14
|
+
"angle_diff",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def clamp(x: float, a: float, b: float) -> float:
|
|
19
|
+
"""Return ``x`` clamped to the ``[a, b]`` interval."""
|
|
20
|
+
return max(a, min(b, x))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def clamp01(x: float) -> float:
|
|
24
|
+
"""Clamp ``x`` to the ``[0,1]`` interval."""
|
|
25
|
+
return clamp(float(x), 0.0, 1.0)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def within_range(val: float, lower: float, upper: float, tol: float = 1e-9) -> bool:
|
|
29
|
+
"""Return ``True`` if ``val`` lies in ``[lower, upper]`` within ``tol``.
|
|
30
|
+
|
|
31
|
+
The comparison uses absolute differences instead of :func:`math.isclose`.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
v = float(val)
|
|
35
|
+
return lower <= v <= upper or abs(v - lower) <= tol or abs(v - upper) <= tol
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _norm01(x: float, lo: float, hi: float) -> float:
|
|
39
|
+
"""Normalize ``x`` to the unit interval given bounds.
|
|
40
|
+
|
|
41
|
+
``lo`` and ``hi`` delimit the original value range. When ``hi`` is not
|
|
42
|
+
greater than ``lo`` the function returns ``0.0`` to avoid division by
|
|
43
|
+
zero. The result is clamped to ``[0,1]``.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if hi <= lo:
|
|
47
|
+
return 0.0
|
|
48
|
+
return clamp01((float(x) - float(lo)) / (float(hi) - float(lo)))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def similarity_abs(a: float, b: float, lo: float, hi: float) -> float:
|
|
52
|
+
"""Return absolute similarity of ``a`` and ``b`` over ``[lo, hi]``.
|
|
53
|
+
|
|
54
|
+
It computes ``1`` minus the normalized absolute difference between
|
|
55
|
+
``a`` and ``b``. Values are scaled using :func:`_norm01` so the result
|
|
56
|
+
falls within ``[0,1]``.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
return 1.0 - _norm01(abs(float(a) - float(b)), 0.0, hi - lo)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def kahan_sum_nd(
|
|
63
|
+
values: Iterable[Sequence[float]], dims: int
|
|
64
|
+
) -> tuple[float, ...]:
|
|
65
|
+
"""Return compensated sums of ``values`` with ``dims`` components.
|
|
66
|
+
|
|
67
|
+
Each component of the tuples in ``values`` is summed independently using the
|
|
68
|
+
Kahan–Babuška (Neumaier) algorithm to reduce floating point error.
|
|
69
|
+
"""
|
|
70
|
+
if dims < 1:
|
|
71
|
+
raise ValueError("dims must be >= 1")
|
|
72
|
+
totals = [0.0] * dims
|
|
73
|
+
comps = [0.0] * dims
|
|
74
|
+
for vs in values:
|
|
75
|
+
for i in range(dims):
|
|
76
|
+
v = vs[i]
|
|
77
|
+
t = totals[i] + v
|
|
78
|
+
if abs(totals[i]) >= abs(v):
|
|
79
|
+
comps[i] += (totals[i] - t) + v
|
|
80
|
+
else:
|
|
81
|
+
comps[i] += (v - t) + totals[i]
|
|
82
|
+
totals[i] = t
|
|
83
|
+
return tuple(float(totals[i] + comps[i]) for i in range(dims))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def angle_diff(a: float, b: float) -> float:
|
|
87
|
+
"""Return the minimal difference between two angles in radians."""
|
|
88
|
+
return (float(a) - float(b) + math.pi) % math.tau - math.pi
|
tnfr/helpers/numeric.pyi
ADDED
tnfr/immutable.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Utilities for freezing objects and checking immutability.
|
|
2
|
+
|
|
3
|
+
Handlers registered via :func:`functools.singledispatch` live in this module
|
|
4
|
+
and are triggered indirectly by the dispatcher when matching types are
|
|
5
|
+
encountered.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from contextlib import contextmanager
|
|
11
|
+
from dataclasses import asdict, is_dataclass
|
|
12
|
+
from functools import lru_cache, partial, singledispatch, wraps
|
|
13
|
+
from typing import Any, Callable, Iterable, Iterator, cast
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from types import MappingProxyType
|
|
16
|
+
import threading
|
|
17
|
+
import weakref
|
|
18
|
+
|
|
19
|
+
from ._compat import TypeAlias
|
|
20
|
+
|
|
21
|
+
# Types considered immutable without further inspection
|
|
22
|
+
IMMUTABLE_SIMPLE = frozenset(
|
|
23
|
+
{int, float, complex, str, bool, bytes, type(None)}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
|
|
28
|
+
"""Primitive immutable values handled directly by :func:`_freeze`."""
|
|
29
|
+
|
|
30
|
+
FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
|
|
31
|
+
"""Frozen representation for generic iterables."""
|
|
32
|
+
|
|
33
|
+
FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
|
|
34
|
+
"""Frozen representation for mapping ``items()`` snapshots."""
|
|
35
|
+
|
|
36
|
+
FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
|
|
37
|
+
"""Tagged iterable snapshot identifying the original container type."""
|
|
38
|
+
|
|
39
|
+
FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
|
|
40
|
+
"""Tagged mapping snapshot identifying the original mapping flavour."""
|
|
41
|
+
|
|
42
|
+
FrozenSnapshot: TypeAlias = (
|
|
43
|
+
FrozenPrimitive | FrozenCollectionItems | FrozenTaggedCollection | FrozenTaggedMapping
|
|
44
|
+
)
|
|
45
|
+
"""Union describing the immutable snapshot returned by :func:`_freeze`."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@contextmanager
|
|
49
|
+
def _cycle_guard(value: Any, seen: set[int] | None = None) -> Iterator[set[int]]:
|
|
50
|
+
"""Context manager that detects reference cycles during freezing."""
|
|
51
|
+
if seen is None:
|
|
52
|
+
seen = set()
|
|
53
|
+
obj_id = id(value)
|
|
54
|
+
if obj_id in seen:
|
|
55
|
+
raise ValueError("cycle detected")
|
|
56
|
+
seen.add(obj_id)
|
|
57
|
+
try:
|
|
58
|
+
yield seen
|
|
59
|
+
finally:
|
|
60
|
+
seen.remove(obj_id)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check_cycle(
|
|
64
|
+
func: Callable[[Any, set[int] | None], FrozenSnapshot]
|
|
65
|
+
) -> Callable[[Any, set[int] | None], FrozenSnapshot]:
|
|
66
|
+
"""Decorator applying :func:`_cycle_guard` to ``func``."""
|
|
67
|
+
|
|
68
|
+
@wraps(func)
|
|
69
|
+
def wrapper(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
|
|
70
|
+
with _cycle_guard(value, seen) as guard_seen:
|
|
71
|
+
return func(value, guard_seen)
|
|
72
|
+
|
|
73
|
+
return wrapper
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _freeze_dataclass(value: Any, seen: set[int]) -> FrozenTaggedMapping:
|
|
77
|
+
params = getattr(type(value), "__dataclass_params__", None)
|
|
78
|
+
frozen = bool(params and params.frozen)
|
|
79
|
+
data = asdict(value)
|
|
80
|
+
tag = "mapping" if frozen else "dict"
|
|
81
|
+
return (tag, tuple((k, _freeze(v, seen)) for k, v in data.items()))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@singledispatch
|
|
85
|
+
@_check_cycle
|
|
86
|
+
def _freeze(value: Any, seen: set[int] | None = None) -> FrozenSnapshot:
|
|
87
|
+
"""Recursively convert ``value`` into an immutable representation."""
|
|
88
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
89
|
+
assert seen is not None
|
|
90
|
+
return _freeze_dataclass(value, seen)
|
|
91
|
+
if type(value) in IMMUTABLE_SIMPLE:
|
|
92
|
+
return value
|
|
93
|
+
raise TypeError
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@_freeze.register(tuple)
|
|
97
|
+
@_check_cycle
|
|
98
|
+
def _freeze_tuple(value: tuple[Any, ...], seen: set[int] | None = None) -> FrozenCollectionItems: # noqa: F401
|
|
99
|
+
assert seen is not None
|
|
100
|
+
return tuple(_freeze(v, seen) for v in value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _freeze_iterable(
|
|
104
|
+
container: Iterable[Any], tag: str, seen: set[int]
|
|
105
|
+
) -> FrozenTaggedCollection:
|
|
106
|
+
return (tag, tuple(_freeze(v, seen) for v in container))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _freeze_iterable_with_tag(
|
|
110
|
+
value: Iterable[Any], seen: set[int] | None = None, *, tag: str
|
|
111
|
+
) -> FrozenTaggedCollection:
|
|
112
|
+
assert seen is not None
|
|
113
|
+
return _freeze_iterable(value, tag, seen)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _register_iterable(cls: type, tag: str) -> None:
|
|
117
|
+
handler = _check_cycle(partial(_freeze_iterable_with_tag, tag=tag))
|
|
118
|
+
_freeze.register(cls)(cast(Callable[[Any, set[int] | None], FrozenSnapshot], handler))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
for _cls, _tag in (
|
|
122
|
+
(list, "list"),
|
|
123
|
+
(set, "set"),
|
|
124
|
+
(frozenset, "frozenset"),
|
|
125
|
+
(bytearray, "bytearray"),
|
|
126
|
+
):
|
|
127
|
+
_register_iterable(_cls, _tag)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@_freeze.register(Mapping)
|
|
131
|
+
@_check_cycle
|
|
132
|
+
def _freeze_mapping(
|
|
133
|
+
value: Mapping[Any, Any], seen: set[int] | None = None
|
|
134
|
+
) -> FrozenTaggedMapping: # noqa: F401
|
|
135
|
+
assert seen is not None
|
|
136
|
+
tag = "dict" if hasattr(value, "__setitem__") else "mapping"
|
|
137
|
+
return (tag, tuple((k, _freeze(v, seen)) for k, v in value.items()))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _all_immutable(iterable: Iterable[Any]) -> bool:
|
|
141
|
+
return all(_is_immutable_inner(v) for v in iterable)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Dispatch table kept immutable to avoid accidental mutation.
|
|
145
|
+
ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
|
|
146
|
+
|
|
147
|
+
_IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler] = MappingProxyType(
|
|
148
|
+
{
|
|
149
|
+
"mapping": lambda v: _all_immutable(v[1]),
|
|
150
|
+
"frozenset": lambda v: _all_immutable(v[1]),
|
|
151
|
+
"list": lambda v: False,
|
|
152
|
+
"set": lambda v: False,
|
|
153
|
+
"bytearray": lambda v: False,
|
|
154
|
+
"dict": lambda v: False,
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@lru_cache(maxsize=1024)
|
|
160
|
+
@singledispatch
|
|
161
|
+
def _is_immutable_inner(value: Any) -> bool:
|
|
162
|
+
return type(value) in IMMUTABLE_SIMPLE
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@_is_immutable_inner.register(tuple)
|
|
166
|
+
def _is_immutable_inner_tuple(value: tuple[Any, ...]) -> bool: # noqa: F401
|
|
167
|
+
if value and isinstance(value[0], str):
|
|
168
|
+
handler = _IMMUTABLE_TAG_DISPATCH.get(value[0])
|
|
169
|
+
if handler is not None:
|
|
170
|
+
return handler(value)
|
|
171
|
+
return _all_immutable(value)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@_is_immutable_inner.register(frozenset)
|
|
175
|
+
def _is_immutable_inner_frozenset(value: frozenset[Any]) -> bool: # noqa: F401
|
|
176
|
+
return _all_immutable(value)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
_IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = (
|
|
180
|
+
weakref.WeakKeyDictionary()
|
|
181
|
+
)
|
|
182
|
+
_IMMUTABLE_CACHE_LOCK = threading.Lock()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _is_immutable(value: Any) -> bool:
|
|
186
|
+
"""Check recursively if ``value`` is immutable with caching."""
|
|
187
|
+
with _IMMUTABLE_CACHE_LOCK:
|
|
188
|
+
try:
|
|
189
|
+
return _IMMUTABLE_CACHE[value]
|
|
190
|
+
except (KeyError, TypeError):
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
frozen = _freeze(value)
|
|
195
|
+
except (TypeError, ValueError):
|
|
196
|
+
result = False
|
|
197
|
+
else:
|
|
198
|
+
result = _is_immutable_inner(frozen)
|
|
199
|
+
|
|
200
|
+
with _IMMUTABLE_CACHE_LOCK:
|
|
201
|
+
try:
|
|
202
|
+
_IMMUTABLE_CACHE[value] = result
|
|
203
|
+
except TypeError:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
__all__ = (
|
|
210
|
+
"_freeze",
|
|
211
|
+
"_is_immutable",
|
|
212
|
+
"_is_immutable_inner",
|
|
213
|
+
"_IMMUTABLE_CACHE",
|
|
214
|
+
)
|
tnfr/immutable.pyi
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Any, Callable, Iterator, Mapping
|
|
2
|
+
|
|
3
|
+
from ._compat import TypeAlias
|
|
4
|
+
|
|
5
|
+
FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
|
|
6
|
+
FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
|
|
7
|
+
FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
|
|
8
|
+
FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
|
|
9
|
+
FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
|
|
10
|
+
FrozenSnapshot: TypeAlias = (
|
|
11
|
+
FrozenPrimitive | FrozenCollectionItems | FrozenTaggedCollection | FrozenTaggedMapping
|
|
12
|
+
)
|
|
13
|
+
ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
|
|
14
|
+
|
|
15
|
+
__all__: tuple[str, ...]
|
|
16
|
+
|
|
17
|
+
def __getattr__(name: str) -> Any: ...
|
|
18
|
+
|
|
19
|
+
def _cycle_guard(value: Any, seen: set[int] | None = ...) -> Iterator[set[int]]: ...
|
|
20
|
+
|
|
21
|
+
def _check_cycle(
|
|
22
|
+
func: Callable[[Any, set[int] | None], FrozenSnapshot],
|
|
23
|
+
) -> Callable[[Any, set[int] | None], FrozenSnapshot]: ...
|
|
24
|
+
|
|
25
|
+
def _freeze(value: Any, seen: set[int] | None = ...) -> FrozenSnapshot: ...
|
|
26
|
+
|
|
27
|
+
def _freeze_mapping(
|
|
28
|
+
value: Mapping[Any, Any],
|
|
29
|
+
seen: set[int] | None = ...,
|
|
30
|
+
) -> FrozenTaggedMapping: ...
|
|
31
|
+
|
|
32
|
+
def _is_immutable(value: Any) -> bool: ...
|
|
33
|
+
|
|
34
|
+
def _is_immutable_inner(value: Any) -> bool: ...
|
|
35
|
+
|
|
36
|
+
_IMMUTABLE_CACHE: Any
|
|
37
|
+
_IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler]
|