tnfr 3.0.3__py3-none-any.whl → 8.5.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 +375 -56
- 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 +723 -0
- tnfr/alias.pyi +108 -0
- tnfr/backends/__init__.py +354 -0
- tnfr/backends/jax_backend.py +173 -0
- tnfr/backends/numpy_backend.py +238 -0
- tnfr/backends/optimized_numpy.py +420 -0
- tnfr/backends/torch_backend.py +408 -0
- tnfr/cache.py +171 -0
- tnfr/cache.pyi +13 -0
- tnfr/cli/__init__.py +110 -0
- tnfr/cli/__init__.pyi +26 -0
- tnfr/cli/arguments.py +489 -0
- tnfr/cli/arguments.pyi +29 -0
- tnfr/cli/execution.py +914 -0
- tnfr/cli/execution.pyi +70 -0
- tnfr/cli/interactive_validator.py +614 -0
- tnfr/cli/utils.py +51 -0
- tnfr/cli/utils.pyi +7 -0
- tnfr/cli/validate.py +236 -0
- tnfr/compat/__init__.py +85 -0
- tnfr/compat/dataclass.py +136 -0
- tnfr/compat/jsonschema_stub.py +61 -0
- tnfr/compat/matplotlib_stub.py +73 -0
- tnfr/compat/numpy_stub.py +155 -0
- tnfr/config/__init__.py +224 -0
- tnfr/config/__init__.pyi +10 -0
- tnfr/config/constants.py +104 -0
- tnfr/config/constants.pyi +12 -0
- tnfr/config/defaults.py +54 -0
- tnfr/config/defaults_core.py +212 -0
- tnfr/config/defaults_init.py +33 -0
- tnfr/config/defaults_metric.py +104 -0
- tnfr/config/feature_flags.py +81 -0
- tnfr/config/feature_flags.pyi +16 -0
- tnfr/config/glyph_constants.py +31 -0
- tnfr/config/init.py +77 -0
- tnfr/config/init.pyi +8 -0
- tnfr/config/operator_names.py +254 -0
- tnfr/config/operator_names.pyi +36 -0
- tnfr/config/physics_derivation.py +354 -0
- tnfr/config/presets.py +83 -0
- tnfr/config/presets.pyi +7 -0
- tnfr/config/security.py +927 -0
- tnfr/config/thresholds.py +114 -0
- tnfr/config/tnfr_config.py +498 -0
- tnfr/constants/__init__.py +92 -0
- tnfr/constants/__init__.pyi +92 -0
- tnfr/constants/aliases.py +33 -0
- tnfr/constants/aliases.pyi +27 -0
- tnfr/constants/init.py +33 -0
- tnfr/constants/init.pyi +12 -0
- tnfr/constants/metric.py +104 -0
- tnfr/constants/metric.pyi +19 -0
- tnfr/core/__init__.py +33 -0
- tnfr/core/container.py +226 -0
- tnfr/core/default_implementations.py +329 -0
- tnfr/core/interfaces.py +279 -0
- tnfr/dynamics/__init__.py +238 -0
- tnfr/dynamics/__init__.pyi +83 -0
- tnfr/dynamics/adaptation.py +267 -0
- tnfr/dynamics/adaptation.pyi +7 -0
- tnfr/dynamics/adaptive_sequences.py +189 -0
- tnfr/dynamics/adaptive_sequences.pyi +14 -0
- tnfr/dynamics/aliases.py +23 -0
- tnfr/dynamics/aliases.pyi +19 -0
- tnfr/dynamics/bifurcation.py +232 -0
- tnfr/dynamics/canonical.py +229 -0
- tnfr/dynamics/canonical.pyi +48 -0
- tnfr/dynamics/coordination.py +385 -0
- tnfr/dynamics/coordination.pyi +25 -0
- tnfr/dynamics/dnfr.py +3034 -0
- tnfr/dynamics/dnfr.pyi +26 -0
- tnfr/dynamics/dynamic_limits.py +225 -0
- tnfr/dynamics/feedback.py +252 -0
- tnfr/dynamics/feedback.pyi +24 -0
- tnfr/dynamics/fused_dnfr.py +454 -0
- tnfr/dynamics/homeostasis.py +157 -0
- tnfr/dynamics/homeostasis.pyi +14 -0
- tnfr/dynamics/integrators.py +661 -0
- tnfr/dynamics/integrators.pyi +36 -0
- tnfr/dynamics/learning.py +310 -0
- tnfr/dynamics/learning.pyi +33 -0
- tnfr/dynamics/metabolism.py +254 -0
- tnfr/dynamics/nbody.py +796 -0
- tnfr/dynamics/nbody_tnfr.py +783 -0
- tnfr/dynamics/propagation.py +326 -0
- tnfr/dynamics/runtime.py +908 -0
- tnfr/dynamics/runtime.pyi +77 -0
- tnfr/dynamics/sampling.py +36 -0
- tnfr/dynamics/sampling.pyi +7 -0
- tnfr/dynamics/selectors.py +711 -0
- tnfr/dynamics/selectors.pyi +85 -0
- tnfr/dynamics/structural_clip.py +207 -0
- tnfr/errors/__init__.py +37 -0
- tnfr/errors/contextual.py +492 -0
- tnfr/execution.py +223 -0
- tnfr/execution.pyi +45 -0
- tnfr/extensions/__init__.py +205 -0
- tnfr/extensions/__init__.pyi +18 -0
- tnfr/extensions/base.py +173 -0
- tnfr/extensions/base.pyi +35 -0
- tnfr/extensions/business/__init__.py +71 -0
- tnfr/extensions/business/__init__.pyi +11 -0
- tnfr/extensions/business/cookbook.py +88 -0
- tnfr/extensions/business/cookbook.pyi +8 -0
- tnfr/extensions/business/health_analyzers.py +202 -0
- tnfr/extensions/business/health_analyzers.pyi +9 -0
- tnfr/extensions/business/patterns.py +183 -0
- tnfr/extensions/business/patterns.pyi +8 -0
- tnfr/extensions/medical/__init__.py +73 -0
- tnfr/extensions/medical/__init__.pyi +11 -0
- tnfr/extensions/medical/cookbook.py +88 -0
- tnfr/extensions/medical/cookbook.pyi +8 -0
- tnfr/extensions/medical/health_analyzers.py +181 -0
- tnfr/extensions/medical/health_analyzers.pyi +9 -0
- tnfr/extensions/medical/patterns.py +163 -0
- tnfr/extensions/medical/patterns.pyi +8 -0
- tnfr/flatten.py +262 -0
- tnfr/flatten.pyi +21 -0
- tnfr/gamma.py +354 -0
- tnfr/gamma.pyi +36 -0
- tnfr/glyph_history.py +377 -0
- tnfr/glyph_history.pyi +35 -0
- tnfr/glyph_runtime.py +19 -0
- tnfr/glyph_runtime.pyi +8 -0
- tnfr/immutable.py +218 -0
- tnfr/immutable.pyi +36 -0
- tnfr/initialization.py +203 -0
- tnfr/initialization.pyi +65 -0
- tnfr/io.py +10 -0
- tnfr/io.pyi +13 -0
- tnfr/locking.py +37 -0
- tnfr/locking.pyi +7 -0
- tnfr/mathematics/__init__.py +79 -0
- tnfr/mathematics/backend.py +453 -0
- tnfr/mathematics/backend.pyi +99 -0
- tnfr/mathematics/dynamics.py +408 -0
- tnfr/mathematics/dynamics.pyi +90 -0
- tnfr/mathematics/epi.py +391 -0
- tnfr/mathematics/epi.pyi +65 -0
- tnfr/mathematics/generators.py +242 -0
- tnfr/mathematics/generators.pyi +29 -0
- tnfr/mathematics/metrics.py +119 -0
- tnfr/mathematics/metrics.pyi +16 -0
- tnfr/mathematics/operators.py +239 -0
- tnfr/mathematics/operators.pyi +59 -0
- tnfr/mathematics/operators_factory.py +124 -0
- tnfr/mathematics/operators_factory.pyi +11 -0
- tnfr/mathematics/projection.py +87 -0
- tnfr/mathematics/projection.pyi +33 -0
- tnfr/mathematics/runtime.py +182 -0
- tnfr/mathematics/runtime.pyi +64 -0
- tnfr/mathematics/spaces.py +256 -0
- tnfr/mathematics/spaces.pyi +83 -0
- tnfr/mathematics/transforms.py +305 -0
- tnfr/mathematics/transforms.pyi +62 -0
- tnfr/metrics/__init__.py +79 -0
- tnfr/metrics/__init__.pyi +20 -0
- tnfr/metrics/buffer_cache.py +163 -0
- tnfr/metrics/buffer_cache.pyi +24 -0
- tnfr/metrics/cache_utils.py +214 -0
- tnfr/metrics/coherence.py +2009 -0
- tnfr/metrics/coherence.pyi +129 -0
- tnfr/metrics/common.py +158 -0
- tnfr/metrics/common.pyi +35 -0
- tnfr/metrics/core.py +316 -0
- tnfr/metrics/core.pyi +13 -0
- tnfr/metrics/diagnosis.py +833 -0
- tnfr/metrics/diagnosis.pyi +86 -0
- tnfr/metrics/emergence.py +245 -0
- tnfr/metrics/export.py +179 -0
- tnfr/metrics/export.pyi +7 -0
- tnfr/metrics/glyph_timing.py +379 -0
- tnfr/metrics/glyph_timing.pyi +81 -0
- tnfr/metrics/learning_metrics.py +280 -0
- tnfr/metrics/learning_metrics.pyi +21 -0
- tnfr/metrics/phase_coherence.py +351 -0
- tnfr/metrics/phase_compatibility.py +349 -0
- tnfr/metrics/reporting.py +183 -0
- tnfr/metrics/reporting.pyi +25 -0
- tnfr/metrics/sense_index.py +1203 -0
- tnfr/metrics/sense_index.pyi +9 -0
- tnfr/metrics/trig.py +373 -0
- tnfr/metrics/trig.pyi +13 -0
- tnfr/metrics/trig_cache.py +233 -0
- tnfr/metrics/trig_cache.pyi +10 -0
- tnfr/multiscale/__init__.py +32 -0
- tnfr/multiscale/hierarchical.py +517 -0
- tnfr/node.py +763 -0
- tnfr/node.pyi +139 -0
- tnfr/observers.py +255 -130
- tnfr/observers.pyi +31 -0
- tnfr/ontosim.py +144 -137
- tnfr/ontosim.pyi +28 -0
- tnfr/operators/__init__.py +1672 -0
- tnfr/operators/__init__.pyi +31 -0
- tnfr/operators/algebra.py +277 -0
- tnfr/operators/canonical_patterns.py +420 -0
- tnfr/operators/cascade.py +267 -0
- tnfr/operators/cycle_detection.py +358 -0
- tnfr/operators/definitions.py +4108 -0
- tnfr/operators/definitions.pyi +78 -0
- tnfr/operators/grammar.py +1164 -0
- tnfr/operators/grammar.pyi +140 -0
- tnfr/operators/hamiltonian.py +710 -0
- tnfr/operators/health_analyzer.py +809 -0
- tnfr/operators/jitter.py +272 -0
- tnfr/operators/jitter.pyi +11 -0
- tnfr/operators/lifecycle.py +314 -0
- tnfr/operators/metabolism.py +618 -0
- tnfr/operators/metrics.py +2138 -0
- tnfr/operators/network_analysis/__init__.py +27 -0
- tnfr/operators/network_analysis/source_detection.py +186 -0
- tnfr/operators/nodal_equation.py +395 -0
- tnfr/operators/pattern_detection.py +660 -0
- tnfr/operators/patterns.py +669 -0
- tnfr/operators/postconditions/__init__.py +38 -0
- tnfr/operators/postconditions/mutation.py +236 -0
- tnfr/operators/preconditions/__init__.py +1226 -0
- tnfr/operators/preconditions/coherence.py +305 -0
- tnfr/operators/preconditions/dissonance.py +236 -0
- tnfr/operators/preconditions/emission.py +128 -0
- tnfr/operators/preconditions/mutation.py +580 -0
- tnfr/operators/preconditions/reception.py +125 -0
- tnfr/operators/preconditions/resonance.py +364 -0
- tnfr/operators/registry.py +74 -0
- tnfr/operators/registry.pyi +9 -0
- tnfr/operators/remesh.py +1809 -0
- tnfr/operators/remesh.pyi +26 -0
- tnfr/operators/structural_units.py +268 -0
- tnfr/operators/unified_grammar.py +105 -0
- tnfr/parallel/__init__.py +54 -0
- tnfr/parallel/auto_scaler.py +234 -0
- tnfr/parallel/distributed.py +384 -0
- tnfr/parallel/engine.py +238 -0
- tnfr/parallel/gpu_engine.py +420 -0
- tnfr/parallel/monitoring.py +248 -0
- tnfr/parallel/partitioner.py +459 -0
- tnfr/py.typed +0 -0
- tnfr/recipes/__init__.py +22 -0
- tnfr/recipes/cookbook.py +743 -0
- tnfr/rng.py +178 -0
- tnfr/rng.pyi +26 -0
- tnfr/schemas/__init__.py +8 -0
- tnfr/schemas/grammar.json +94 -0
- tnfr/sdk/__init__.py +107 -0
- tnfr/sdk/__init__.pyi +19 -0
- tnfr/sdk/adaptive_system.py +173 -0
- tnfr/sdk/adaptive_system.pyi +21 -0
- tnfr/sdk/builders.py +370 -0
- tnfr/sdk/builders.pyi +51 -0
- tnfr/sdk/fluent.py +1121 -0
- tnfr/sdk/fluent.pyi +74 -0
- tnfr/sdk/templates.py +342 -0
- tnfr/sdk/templates.pyi +41 -0
- tnfr/sdk/utils.py +341 -0
- tnfr/secure_config.py +46 -0
- tnfr/security/__init__.py +70 -0
- tnfr/security/database.py +514 -0
- tnfr/security/subprocess.py +503 -0
- tnfr/security/validation.py +290 -0
- tnfr/selector.py +247 -0
- tnfr/selector.pyi +19 -0
- tnfr/sense.py +378 -0
- tnfr/sense.pyi +23 -0
- tnfr/services/__init__.py +17 -0
- tnfr/services/orchestrator.py +325 -0
- tnfr/sparse/__init__.py +39 -0
- tnfr/sparse/representations.py +492 -0
- tnfr/structural.py +705 -0
- tnfr/structural.pyi +83 -0
- tnfr/telemetry/__init__.py +35 -0
- tnfr/telemetry/cache_metrics.py +226 -0
- tnfr/telemetry/cache_metrics.pyi +64 -0
- tnfr/telemetry/nu_f.py +422 -0
- tnfr/telemetry/nu_f.pyi +108 -0
- tnfr/telemetry/verbosity.py +36 -0
- tnfr/telemetry/verbosity.pyi +15 -0
- tnfr/tokens.py +58 -0
- tnfr/tokens.pyi +36 -0
- tnfr/tools/__init__.py +20 -0
- tnfr/tools/domain_templates.py +478 -0
- tnfr/tools/sequence_generator.py +846 -0
- tnfr/topology/__init__.py +13 -0
- tnfr/topology/asymmetry.py +151 -0
- tnfr/trace.py +543 -0
- tnfr/trace.pyi +42 -0
- tnfr/tutorials/__init__.py +38 -0
- tnfr/tutorials/autonomous_evolution.py +285 -0
- tnfr/tutorials/interactive.py +1576 -0
- tnfr/tutorials/structural_metabolism.py +238 -0
- tnfr/types.py +775 -0
- tnfr/types.pyi +357 -0
- tnfr/units.py +68 -0
- tnfr/units.pyi +13 -0
- tnfr/utils/__init__.py +282 -0
- tnfr/utils/__init__.pyi +215 -0
- tnfr/utils/cache.py +4223 -0
- tnfr/utils/cache.pyi +470 -0
- tnfr/utils/callbacks.py +375 -0
- tnfr/utils/callbacks.pyi +49 -0
- tnfr/utils/chunks.py +108 -0
- tnfr/utils/chunks.pyi +22 -0
- tnfr/utils/data.py +428 -0
- tnfr/utils/data.pyi +74 -0
- tnfr/utils/graph.py +85 -0
- tnfr/utils/graph.pyi +10 -0
- tnfr/utils/init.py +821 -0
- tnfr/utils/init.pyi +80 -0
- tnfr/utils/io.py +559 -0
- tnfr/utils/io.pyi +66 -0
- tnfr/utils/numeric.py +114 -0
- tnfr/utils/numeric.pyi +21 -0
- tnfr/validation/__init__.py +257 -0
- tnfr/validation/__init__.pyi +85 -0
- tnfr/validation/compatibility.py +460 -0
- tnfr/validation/compatibility.pyi +6 -0
- tnfr/validation/config.py +73 -0
- tnfr/validation/graph.py +139 -0
- tnfr/validation/graph.pyi +18 -0
- tnfr/validation/input_validation.py +755 -0
- tnfr/validation/invariants.py +712 -0
- tnfr/validation/rules.py +253 -0
- tnfr/validation/rules.pyi +44 -0
- tnfr/validation/runtime.py +279 -0
- tnfr/validation/runtime.pyi +28 -0
- tnfr/validation/sequence_validator.py +162 -0
- tnfr/validation/soft_filters.py +170 -0
- tnfr/validation/soft_filters.pyi +32 -0
- tnfr/validation/spectral.py +164 -0
- tnfr/validation/spectral.pyi +42 -0
- tnfr/validation/validator.py +1266 -0
- tnfr/validation/window.py +39 -0
- tnfr/validation/window.pyi +1 -0
- tnfr/visualization/__init__.py +98 -0
- tnfr/visualization/cascade_viz.py +256 -0
- tnfr/visualization/hierarchy.py +284 -0
- tnfr/visualization/sequence_plotter.py +784 -0
- tnfr/viz/__init__.py +60 -0
- tnfr/viz/matplotlib.py +278 -0
- tnfr/viz/matplotlib.pyi +35 -0
- tnfr-8.5.0.dist-info/METADATA +573 -0
- tnfr-8.5.0.dist-info/RECORD +353 -0
- tnfr-8.5.0.dist-info/entry_points.txt +3 -0
- tnfr-3.0.3.dist-info/licenses/LICENSE.txt → tnfr-8.5.0.dist-info/licenses/LICENSE.md +1 -1
- tnfr/constants.py +0 -183
- tnfr/dynamics.py +0 -543
- tnfr/helpers.py +0 -198
- tnfr/main.py +0 -37
- tnfr/operators.py +0 -296
- tnfr-3.0.3.dist-info/METADATA +0 -35
- tnfr-3.0.3.dist-info/RECORD +0 -13
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/WHEEL +0 -0
- {tnfr-3.0.3.dist-info → tnfr-8.5.0.dist-info}/top_level.txt +0 -0
tnfr/glyph_history.py
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""Utilities for tracking structural operator emission history and related metrics.
|
|
2
|
+
|
|
3
|
+
This module tracks the history of glyphs (structural symbols like AL, EN, IL, etc.)
|
|
4
|
+
that are emitted when structural operators (Emission, Reception, Coherence, etc.)
|
|
5
|
+
are applied to nodes in the TNFR network.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import Counter, deque
|
|
11
|
+
from collections.abc import Iterable, Mapping, MutableMapping
|
|
12
|
+
from itertools import islice
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
from .constants import get_param, normalise_state_token
|
|
16
|
+
from .glyph_runtime import last_glyph
|
|
17
|
+
from .types import TNFRGraph
|
|
18
|
+
from .utils import ensure_collection, get_logger
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
__all__ = (
|
|
23
|
+
"HistoryDict",
|
|
24
|
+
"push_glyph",
|
|
25
|
+
"recent_glyph",
|
|
26
|
+
"ensure_history",
|
|
27
|
+
"current_step_idx",
|
|
28
|
+
"append_metric",
|
|
29
|
+
"count_glyphs",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_NU_F_HISTORY_KEYS = (
|
|
33
|
+
"nu_f_rate_hz_str",
|
|
34
|
+
"nu_f_rate_hz",
|
|
35
|
+
"nu_f_ci_lower_hz_str",
|
|
36
|
+
"nu_f_ci_upper_hz_str",
|
|
37
|
+
"nu_f_ci_lower_hz",
|
|
38
|
+
"nu_f_ci_upper_hz",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ensure_history(
|
|
43
|
+
nd: MutableMapping[str, Any], window: int, *, create_zero: bool = False
|
|
44
|
+
) -> tuple[int, deque[str] | None]:
|
|
45
|
+
"""Validate ``window`` and ensure ``nd['glyph_history']`` deque."""
|
|
46
|
+
|
|
47
|
+
from tnfr.validation.window import validate_window
|
|
48
|
+
|
|
49
|
+
v_window = validate_window(window)
|
|
50
|
+
if v_window == 0 and not create_zero:
|
|
51
|
+
return v_window, None
|
|
52
|
+
hist = nd.setdefault("glyph_history", deque(maxlen=v_window))
|
|
53
|
+
if not isinstance(hist, deque) or hist.maxlen != v_window:
|
|
54
|
+
# Rebuild deque from any iterable, ignoring raw strings/bytes and scalars
|
|
55
|
+
if isinstance(hist, (str, bytes, bytearray)):
|
|
56
|
+
items: Iterable[Any] = ()
|
|
57
|
+
else:
|
|
58
|
+
try:
|
|
59
|
+
items = ensure_collection(hist, max_materialize=None)
|
|
60
|
+
except TypeError:
|
|
61
|
+
logger.debug("Discarding non-iterable glyph history value %r", hist)
|
|
62
|
+
items = ()
|
|
63
|
+
hist = deque((str(item) for item in items), maxlen=v_window)
|
|
64
|
+
nd["glyph_history"] = hist
|
|
65
|
+
return v_window, hist
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None:
|
|
69
|
+
"""Add ``glyph`` to node history with maximum size ``window``.
|
|
70
|
+
|
|
71
|
+
``window`` validation and deque creation are handled by
|
|
72
|
+
:func:`_ensure_history`.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
_, hist = _ensure_history(nd, window, create_zero=True)
|
|
76
|
+
hist.append(str(glyph))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool:
|
|
80
|
+
"""Return ``True`` if ``glyph`` appeared in last ``window`` emissions.
|
|
81
|
+
|
|
82
|
+
This is a **read-only** operation that checks the existing history without
|
|
83
|
+
modifying it. If ``window`` is zero, returns ``False``. Negative values
|
|
84
|
+
raise :class:`ValueError`.
|
|
85
|
+
|
|
86
|
+
Notes
|
|
87
|
+
-----
|
|
88
|
+
This function intentionally does NOT call ``_ensure_history`` to avoid
|
|
89
|
+
accidentally truncating the glyph_history deque when checking with a
|
|
90
|
+
smaller window than the deque's maxlen. This preserves the canonical
|
|
91
|
+
principle that reading history should not modify it.
|
|
92
|
+
|
|
93
|
+
Reuses ``validate_window`` and ``ensure_collection`` utilities.
|
|
94
|
+
"""
|
|
95
|
+
from tnfr.validation.window import validate_window
|
|
96
|
+
|
|
97
|
+
v_window = validate_window(window)
|
|
98
|
+
if v_window == 0:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
# Read existing history without modifying it
|
|
102
|
+
hist = nd.get("glyph_history")
|
|
103
|
+
if hist is None:
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
gl = str(glyph)
|
|
107
|
+
|
|
108
|
+
# Use canonical ensure_collection to materialize history
|
|
109
|
+
try:
|
|
110
|
+
items = list(ensure_collection(hist, max_materialize=None))
|
|
111
|
+
except (TypeError, ValueError):
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
# Check only the last v_window items
|
|
115
|
+
recent_items = items[-v_window:] if len(items) > v_window else items
|
|
116
|
+
return gl in recent_items
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class HistoryDict(dict[str, Any]):
|
|
120
|
+
"""Dict specialized for bounded history series and usage counts.
|
|
121
|
+
|
|
122
|
+
Usage counts are tracked explicitly via :meth:`get_increment`. Accessing
|
|
123
|
+
keys through ``__getitem__`` or :meth:`get` does not affect the internal
|
|
124
|
+
counters, avoiding surprising evictions on mere reads. Counting is now
|
|
125
|
+
handled with :class:`collections.Counter` alone, relying on
|
|
126
|
+
:meth:`Counter.most_common` to locate least-used entries when required.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
data:
|
|
131
|
+
Initial mapping to populate the dictionary.
|
|
132
|
+
maxlen:
|
|
133
|
+
Maximum length for history lists stored as values.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
data: Mapping[str, Any] | None = None,
|
|
139
|
+
*,
|
|
140
|
+
maxlen: int = 0,
|
|
141
|
+
) -> None:
|
|
142
|
+
super().__init__(data or {})
|
|
143
|
+
self._maxlen = maxlen
|
|
144
|
+
self._counts: Counter[str] = Counter()
|
|
145
|
+
if self._maxlen > 0:
|
|
146
|
+
for k, v in list(self.items()):
|
|
147
|
+
if isinstance(v, list):
|
|
148
|
+
super().__setitem__(k, deque(v, maxlen=self._maxlen))
|
|
149
|
+
self._counts[k] = 0
|
|
150
|
+
else:
|
|
151
|
+
for k in self:
|
|
152
|
+
self._counts[k] = 0
|
|
153
|
+
# ``_heap`` is no longer required with ``Counter.most_common``.
|
|
154
|
+
|
|
155
|
+
def _increment(self, key: str) -> None:
|
|
156
|
+
"""Increase usage count for ``key``."""
|
|
157
|
+
self._counts[key] += 1
|
|
158
|
+
|
|
159
|
+
def _to_deque(self, val: Any) -> deque[Any]:
|
|
160
|
+
"""Coerce ``val`` to a deque respecting ``self._maxlen``.
|
|
161
|
+
|
|
162
|
+
``Iterable`` inputs (excluding ``str`` and ``bytes``) are expanded into
|
|
163
|
+
the deque, while single values are wrapped. Existing deques are
|
|
164
|
+
returned unchanged.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
if isinstance(val, deque):
|
|
168
|
+
return val
|
|
169
|
+
if isinstance(val, Iterable) and not isinstance(val, (str, bytes)):
|
|
170
|
+
return deque(val, maxlen=self._maxlen)
|
|
171
|
+
return deque([val], maxlen=self._maxlen)
|
|
172
|
+
|
|
173
|
+
def _resolve_value(self, key: str, default: Any, *, insert: bool) -> Any:
|
|
174
|
+
if insert:
|
|
175
|
+
val = super().setdefault(key, default)
|
|
176
|
+
else:
|
|
177
|
+
val = super().__getitem__(key)
|
|
178
|
+
if self._maxlen > 0:
|
|
179
|
+
if not isinstance(val, Mapping):
|
|
180
|
+
val = self._to_deque(val)
|
|
181
|
+
super().__setitem__(key, val)
|
|
182
|
+
return val
|
|
183
|
+
|
|
184
|
+
def get_increment(self, key: str, default: Any = None) -> Any:
|
|
185
|
+
"""Return value for ``key`` and increment its usage counter."""
|
|
186
|
+
|
|
187
|
+
insert = key not in self
|
|
188
|
+
val = self._resolve_value(key, default, insert=insert)
|
|
189
|
+
self._increment(key)
|
|
190
|
+
return val
|
|
191
|
+
|
|
192
|
+
def __getitem__(self, key: str) -> Any: # type: ignore[override]
|
|
193
|
+
"""Return the tracked value for ``key`` ensuring deque normalisation."""
|
|
194
|
+
|
|
195
|
+
return self._resolve_value(key, None, insert=False)
|
|
196
|
+
|
|
197
|
+
def get(self, key: str, default: Any | None = None) -> Any: # type: ignore[override]
|
|
198
|
+
"""Return ``key`` when present; otherwise fall back to ``default``."""
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
return self._resolve_value(key, None, insert=False)
|
|
202
|
+
except KeyError:
|
|
203
|
+
return default
|
|
204
|
+
|
|
205
|
+
def __setitem__(self, key: str, value: Any) -> None: # type: ignore[override]
|
|
206
|
+
"""Store ``value`` for ``key`` while initialising usage tracking."""
|
|
207
|
+
|
|
208
|
+
super().__setitem__(key, value)
|
|
209
|
+
if key not in self._counts:
|
|
210
|
+
self._counts[key] = 0
|
|
211
|
+
|
|
212
|
+
def setdefault(self, key: str, default: Any | None = None) -> Any: # type: ignore[override]
|
|
213
|
+
"""Return existing value for ``key`` or insert ``default`` when absent."""
|
|
214
|
+
|
|
215
|
+
insert = key not in self
|
|
216
|
+
val = self._resolve_value(key, default, insert=insert)
|
|
217
|
+
if insert:
|
|
218
|
+
self._counts[key] = 0
|
|
219
|
+
return val
|
|
220
|
+
|
|
221
|
+
def pop_least_used(self) -> Any:
|
|
222
|
+
"""Remove and return the value with the smallest usage count."""
|
|
223
|
+
while self._counts:
|
|
224
|
+
key = min(self._counts, key=self._counts.get)
|
|
225
|
+
self._counts.pop(key, None)
|
|
226
|
+
if key in self:
|
|
227
|
+
return super().pop(key)
|
|
228
|
+
raise KeyError("HistoryDict is empty; cannot pop least used")
|
|
229
|
+
|
|
230
|
+
def pop_least_used_batch(self, k: int) -> None:
|
|
231
|
+
"""Remove up to ``k`` least-used entries from the history."""
|
|
232
|
+
|
|
233
|
+
for _ in range(max(0, int(k))):
|
|
234
|
+
try:
|
|
235
|
+
self.pop_least_used()
|
|
236
|
+
except KeyError:
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]:
|
|
241
|
+
"""Ensure ``G.graph['history']`` exists and return it.
|
|
242
|
+
|
|
243
|
+
``HISTORY_MAXLEN`` must be non-negative; otherwise a
|
|
244
|
+
:class:`ValueError` is raised. When ``HISTORY_MAXLEN`` is zero, a regular
|
|
245
|
+
``dict`` is used.
|
|
246
|
+
"""
|
|
247
|
+
maxlen, _ = _ensure_history({}, int(get_param(G, "HISTORY_MAXLEN")))
|
|
248
|
+
hist = G.graph.get("history")
|
|
249
|
+
sentinel_key = "_metrics_history_id"
|
|
250
|
+
replaced = False
|
|
251
|
+
if maxlen == 0:
|
|
252
|
+
if isinstance(hist, HistoryDict):
|
|
253
|
+
hist = dict(hist)
|
|
254
|
+
G.graph["history"] = hist
|
|
255
|
+
replaced = True
|
|
256
|
+
elif hist is None:
|
|
257
|
+
hist = {}
|
|
258
|
+
G.graph["history"] = hist
|
|
259
|
+
replaced = True
|
|
260
|
+
if replaced:
|
|
261
|
+
G.graph.pop(sentinel_key, None)
|
|
262
|
+
if isinstance(hist, MutableMapping):
|
|
263
|
+
_normalise_state_streams(hist)
|
|
264
|
+
return hist
|
|
265
|
+
if not isinstance(hist, HistoryDict) or hist._maxlen != maxlen:
|
|
266
|
+
hist = HistoryDict(hist, maxlen=maxlen)
|
|
267
|
+
G.graph["history"] = hist
|
|
268
|
+
replaced = True
|
|
269
|
+
excess = len(hist) - maxlen
|
|
270
|
+
if excess > 0:
|
|
271
|
+
hist.pop_least_used_batch(excess)
|
|
272
|
+
if replaced:
|
|
273
|
+
G.graph.pop(sentinel_key, None)
|
|
274
|
+
_normalise_state_streams(cast(MutableMapping[str, Any], hist))
|
|
275
|
+
return hist
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int:
|
|
279
|
+
"""Return the current step index from ``G`` history."""
|
|
280
|
+
|
|
281
|
+
graph = getattr(G, "graph", G)
|
|
282
|
+
return len(graph.get("history", {}).get("C_steps", []))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def append_metric(hist: MutableMapping[str, list[Any]], key: str, value: Any) -> None:
|
|
286
|
+
"""Append ``value`` to ``hist[key]`` list, creating it if missing."""
|
|
287
|
+
if key == "phase_state" and isinstance(value, str):
|
|
288
|
+
value = normalise_state_token(value)
|
|
289
|
+
elif key == "nodal_diag" and isinstance(value, Mapping):
|
|
290
|
+
snapshot: dict[Any, Any] = {}
|
|
291
|
+
for node, payload in value.items():
|
|
292
|
+
if isinstance(payload, Mapping):
|
|
293
|
+
state_value = payload.get("state")
|
|
294
|
+
if isinstance(payload, MutableMapping):
|
|
295
|
+
updated = payload
|
|
296
|
+
else:
|
|
297
|
+
updated = dict(payload)
|
|
298
|
+
if isinstance(state_value, str):
|
|
299
|
+
updated["state"] = normalise_state_token(state_value)
|
|
300
|
+
snapshot[node] = updated
|
|
301
|
+
else:
|
|
302
|
+
snapshot[node] = payload
|
|
303
|
+
hist.setdefault(key, []).append(snapshot)
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
hist.setdefault(key, []).append(value)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def count_glyphs(
|
|
310
|
+
G: TNFRGraph, window: int | None = None, *, last_only: bool = False
|
|
311
|
+
) -> Counter[str]:
|
|
312
|
+
"""Count recent glyphs in the network.
|
|
313
|
+
|
|
314
|
+
If ``window`` is ``None``, the full history for each node is used. A
|
|
315
|
+
``window`` of zero yields an empty :class:`Counter`. Negative values raise
|
|
316
|
+
:class:`ValueError`.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
if window is not None:
|
|
320
|
+
from tnfr.validation.window import validate_window
|
|
321
|
+
|
|
322
|
+
window = validate_window(window)
|
|
323
|
+
if window == 0:
|
|
324
|
+
return Counter()
|
|
325
|
+
|
|
326
|
+
counts: Counter[str] = Counter()
|
|
327
|
+
for _, nd in G.nodes(data=True):
|
|
328
|
+
if last_only:
|
|
329
|
+
g = last_glyph(nd)
|
|
330
|
+
if g:
|
|
331
|
+
counts[g] += 1
|
|
332
|
+
continue
|
|
333
|
+
hist = nd.get("glyph_history")
|
|
334
|
+
if not hist:
|
|
335
|
+
continue
|
|
336
|
+
if window is None:
|
|
337
|
+
seq = hist
|
|
338
|
+
else:
|
|
339
|
+
start = max(len(hist) - window, 0)
|
|
340
|
+
seq = islice(hist, start, None)
|
|
341
|
+
counts.update(seq)
|
|
342
|
+
|
|
343
|
+
return counts
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _normalise_state_streams(hist: MutableMapping[str, Any]) -> None:
|
|
347
|
+
"""Normalise legacy state tokens stored in telemetry history."""
|
|
348
|
+
|
|
349
|
+
phase_state = hist.get("phase_state")
|
|
350
|
+
if isinstance(phase_state, deque):
|
|
351
|
+
canonical = [normalise_state_token(str(item)) for item in phase_state]
|
|
352
|
+
if canonical != list(phase_state):
|
|
353
|
+
phase_state.clear()
|
|
354
|
+
phase_state.extend(canonical)
|
|
355
|
+
elif isinstance(phase_state, list):
|
|
356
|
+
canonical = [normalise_state_token(str(item)) for item in phase_state]
|
|
357
|
+
if canonical != phase_state:
|
|
358
|
+
hist["phase_state"] = canonical
|
|
359
|
+
|
|
360
|
+
diag_history = hist.get("nodal_diag")
|
|
361
|
+
if isinstance(diag_history, list):
|
|
362
|
+
for snapshot in diag_history:
|
|
363
|
+
if not isinstance(snapshot, Mapping):
|
|
364
|
+
continue
|
|
365
|
+
for node, payload in snapshot.items():
|
|
366
|
+
if not isinstance(payload, Mapping):
|
|
367
|
+
continue
|
|
368
|
+
state_value = payload.get("state")
|
|
369
|
+
if not isinstance(state_value, str):
|
|
370
|
+
continue
|
|
371
|
+
canonical = normalise_state_token(state_value)
|
|
372
|
+
if canonical == state_value:
|
|
373
|
+
continue
|
|
374
|
+
if isinstance(payload, MutableMapping):
|
|
375
|
+
payload["state"] = canonical
|
|
376
|
+
else:
|
|
377
|
+
snapshot[node] = {**payload, "state": canonical}
|
tnfr/glyph_history.pyi
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from collections.abc import Mapping, MutableMapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .types import TNFRGraph
|
|
8
|
+
|
|
9
|
+
__all__: tuple[str, ...]
|
|
10
|
+
|
|
11
|
+
class HistoryDict(dict[str, Any]):
|
|
12
|
+
_maxlen: int
|
|
13
|
+
_counts: Counter[str]
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, data: Mapping[str, Any] | None = ..., *, maxlen: int = ...
|
|
17
|
+
) -> None: ...
|
|
18
|
+
def get_increment(self, key: str, default: Any = ...) -> Any: ...
|
|
19
|
+
def __getitem__(self, key: str) -> Any: ...
|
|
20
|
+
def get(self, key: str, default: Any | None = ...) -> Any: ...
|
|
21
|
+
def __setitem__(self, key: str, value: Any) -> None: ...
|
|
22
|
+
def setdefault(self, key: str, default: Any | None = ...) -> Any: ...
|
|
23
|
+
def pop_least_used(self) -> Any: ...
|
|
24
|
+
def pop_least_used_batch(self, k: int) -> None: ...
|
|
25
|
+
|
|
26
|
+
def push_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> None: ...
|
|
27
|
+
def recent_glyph(nd: MutableMapping[str, Any], glyph: str, window: int) -> bool: ...
|
|
28
|
+
def ensure_history(G: TNFRGraph) -> HistoryDict | dict[str, Any]: ...
|
|
29
|
+
def current_step_idx(G: TNFRGraph | Mapping[str, Any]) -> int: ...
|
|
30
|
+
def append_metric(
|
|
31
|
+
hist: MutableMapping[str, list[Any]], key: str, value: Any
|
|
32
|
+
) -> None: ...
|
|
33
|
+
def count_glyphs(
|
|
34
|
+
G: TNFRGraph, window: int | None = ..., *, last_only: bool = ...
|
|
35
|
+
) -> Counter[str]: ...
|
tnfr/glyph_runtime.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Runtime helpers for structural operator glyphs decoupled from validation internals.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for working with glyphs (structural symbols like
|
|
4
|
+
AL, EN, IL, etc.) that represent the application of structural operators to nodes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
__all__ = ("last_glyph",)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def last_glyph(nd: Mapping[str, Any]) -> str | None:
|
|
16
|
+
"""Return the most recent glyph for node or ``None``."""
|
|
17
|
+
|
|
18
|
+
hist = nd.get("glyph_history")
|
|
19
|
+
return hist[-1] if hist else None
|
tnfr/glyph_runtime.pyi
ADDED
tnfr/immutable.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
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
|
+
import threading
|
|
11
|
+
import weakref
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from dataclasses import asdict, is_dataclass
|
|
15
|
+
from functools import lru_cache, partial, singledispatch, wraps
|
|
16
|
+
from types import MappingProxyType
|
|
17
|
+
from typing import Any, Callable, Iterable, Iterator, cast
|
|
18
|
+
|
|
19
|
+
from ._compat import TypeAlias
|
|
20
|
+
|
|
21
|
+
# Types considered immutable without further inspection
|
|
22
|
+
IMMUTABLE_SIMPLE = frozenset({int, float, complex, str, bool, bytes, type(None)})
|
|
23
|
+
|
|
24
|
+
FrozenPrimitive: TypeAlias = int | float | complex | str | bool | bytes | None
|
|
25
|
+
"""Primitive immutable values handled directly by :func:`_freeze`."""
|
|
26
|
+
|
|
27
|
+
FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
|
|
28
|
+
"""Frozen representation for generic iterables."""
|
|
29
|
+
|
|
30
|
+
FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
|
|
31
|
+
"""Frozen representation for mapping ``items()`` snapshots."""
|
|
32
|
+
|
|
33
|
+
FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
|
|
34
|
+
"""Tagged iterable snapshot identifying the original container type."""
|
|
35
|
+
|
|
36
|
+
FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
|
|
37
|
+
"""Tagged mapping snapshot identifying the original mapping flavour."""
|
|
38
|
+
|
|
39
|
+
FrozenSnapshot: TypeAlias = (
|
|
40
|
+
FrozenPrimitive
|
|
41
|
+
| FrozenCollectionItems
|
|
42
|
+
| FrozenTaggedCollection
|
|
43
|
+
| 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
|
+
"""Apply :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(
|
|
99
|
+
value: tuple[Any, ...], seen: set[int] | None = None
|
|
100
|
+
) -> FrozenCollectionItems: # noqa: F401
|
|
101
|
+
assert seen is not None
|
|
102
|
+
return tuple(_freeze(v, seen) for v in value)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _freeze_iterable(
|
|
106
|
+
container: Iterable[Any], tag: str, seen: set[int]
|
|
107
|
+
) -> FrozenTaggedCollection:
|
|
108
|
+
return (tag, tuple(_freeze(v, seen) for v in container))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _freeze_iterable_with_tag(
|
|
112
|
+
value: Iterable[Any], seen: set[int] | None = None, *, tag: str
|
|
113
|
+
) -> FrozenTaggedCollection:
|
|
114
|
+
assert seen is not None
|
|
115
|
+
return _freeze_iterable(value, tag, seen)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _register_iterable(cls: type, tag: str) -> None:
|
|
119
|
+
handler = _check_cycle(partial(_freeze_iterable_with_tag, tag=tag))
|
|
120
|
+
_freeze.register(cls)(
|
|
121
|
+
cast(Callable[[Any, set[int] | None], FrozenSnapshot], handler)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
for _cls, _tag in (
|
|
126
|
+
(list, "list"),
|
|
127
|
+
(set, "set"),
|
|
128
|
+
(frozenset, "frozenset"),
|
|
129
|
+
(bytearray, "bytearray"),
|
|
130
|
+
):
|
|
131
|
+
_register_iterable(_cls, _tag)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@_freeze.register(Mapping)
|
|
135
|
+
@_check_cycle
|
|
136
|
+
def _freeze_mapping(
|
|
137
|
+
value: Mapping[Any, Any], seen: set[int] | None = None
|
|
138
|
+
) -> FrozenTaggedMapping: # noqa: F401
|
|
139
|
+
assert seen is not None
|
|
140
|
+
tag = "dict" if hasattr(value, "__setitem__") else "mapping"
|
|
141
|
+
return (tag, tuple((k, _freeze(v, seen)) for k, v in value.items()))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _all_immutable(iterable: Iterable[Any]) -> bool:
|
|
145
|
+
return all(_is_immutable_inner(v) for v in iterable)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Dispatch table kept immutable to avoid accidental mutation.
|
|
149
|
+
ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
|
|
150
|
+
|
|
151
|
+
_IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler] = MappingProxyType(
|
|
152
|
+
{
|
|
153
|
+
"mapping": lambda v: _all_immutable(v[1]),
|
|
154
|
+
"frozenset": lambda v: _all_immutable(v[1]),
|
|
155
|
+
"list": lambda v: False,
|
|
156
|
+
"set": lambda v: False,
|
|
157
|
+
"bytearray": lambda v: False,
|
|
158
|
+
"dict": lambda v: False,
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@lru_cache(maxsize=1024)
|
|
164
|
+
@singledispatch
|
|
165
|
+
def _is_immutable_inner(value: Any) -> bool:
|
|
166
|
+
"""Return ``True`` when ``value`` belongs to the canonical immutable set."""
|
|
167
|
+
|
|
168
|
+
return type(value) in IMMUTABLE_SIMPLE
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@_is_immutable_inner.register(tuple)
|
|
172
|
+
def _is_immutable_inner_tuple(value: tuple[Any, ...]) -> bool: # noqa: F401
|
|
173
|
+
if value and isinstance(value[0], str):
|
|
174
|
+
handler = _IMMUTABLE_TAG_DISPATCH.get(value[0])
|
|
175
|
+
if handler is not None:
|
|
176
|
+
return handler(value)
|
|
177
|
+
return _all_immutable(value)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@_is_immutable_inner.register(frozenset)
|
|
181
|
+
def _is_immutable_inner_frozenset(value: frozenset[Any]) -> bool: # noqa: F401
|
|
182
|
+
return _all_immutable(value)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
_IMMUTABLE_CACHE: weakref.WeakKeyDictionary[Any, bool] = weakref.WeakKeyDictionary()
|
|
186
|
+
_IMMUTABLE_CACHE_LOCK = threading.Lock()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _is_immutable(value: Any) -> bool:
|
|
190
|
+
"""Check recursively if ``value`` is immutable with caching."""
|
|
191
|
+
with _IMMUTABLE_CACHE_LOCK:
|
|
192
|
+
try:
|
|
193
|
+
return _IMMUTABLE_CACHE[value]
|
|
194
|
+
except (KeyError, TypeError):
|
|
195
|
+
pass # Not in cache or value is unhashable
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
frozen = _freeze(value)
|
|
199
|
+
except (TypeError, ValueError):
|
|
200
|
+
result = False
|
|
201
|
+
else:
|
|
202
|
+
result = _is_immutable_inner(frozen)
|
|
203
|
+
|
|
204
|
+
with _IMMUTABLE_CACHE_LOCK:
|
|
205
|
+
try:
|
|
206
|
+
_IMMUTABLE_CACHE[value] = result
|
|
207
|
+
except TypeError:
|
|
208
|
+
pass # Value is unhashable, cannot cache
|
|
209
|
+
|
|
210
|
+
return result
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
__all__ = (
|
|
214
|
+
"_freeze",
|
|
215
|
+
"_is_immutable",
|
|
216
|
+
"_is_immutable_inner",
|
|
217
|
+
"_IMMUTABLE_CACHE",
|
|
218
|
+
)
|
tnfr/immutable.pyi
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Iterator, Mapping, Union
|
|
4
|
+
|
|
5
|
+
from ._compat import TypeAlias
|
|
6
|
+
|
|
7
|
+
FrozenPrimitive: TypeAlias = Union[int, float, complex, str, bool, bytes, None]
|
|
8
|
+
FrozenCollectionItems: TypeAlias = tuple["FrozenSnapshot", ...]
|
|
9
|
+
FrozenMappingItems: TypeAlias = tuple[tuple[Any, "FrozenSnapshot"], ...]
|
|
10
|
+
FrozenTaggedCollection: TypeAlias = tuple[str, FrozenCollectionItems]
|
|
11
|
+
FrozenTaggedMapping: TypeAlias = tuple[str, FrozenMappingItems]
|
|
12
|
+
FrozenSnapshot: TypeAlias = Union[
|
|
13
|
+
FrozenPrimitive,
|
|
14
|
+
FrozenCollectionItems,
|
|
15
|
+
FrozenTaggedCollection,
|
|
16
|
+
FrozenTaggedMapping,
|
|
17
|
+
]
|
|
18
|
+
ImmutableTagHandler: TypeAlias = Callable[[tuple[Any, ...]], bool]
|
|
19
|
+
|
|
20
|
+
__all__: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
def __getattr__(name: str) -> Any: ...
|
|
23
|
+
def _cycle_guard(value: Any, seen: set[int] | None = ...) -> Iterator[set[int]]: ...
|
|
24
|
+
def _check_cycle(
|
|
25
|
+
func: Callable[[Any, set[int] | None], FrozenSnapshot],
|
|
26
|
+
) -> Callable[[Any, set[int] | None], FrozenSnapshot]: ...
|
|
27
|
+
def _freeze(value: Any, seen: set[int] | None = ...) -> FrozenSnapshot: ...
|
|
28
|
+
def _freeze_mapping(
|
|
29
|
+
value: Mapping[Any, Any],
|
|
30
|
+
seen: set[int] | None = ...,
|
|
31
|
+
) -> FrozenTaggedMapping: ...
|
|
32
|
+
def _is_immutable(value: Any) -> bool: ...
|
|
33
|
+
def _is_immutable_inner(value: Any) -> bool: ...
|
|
34
|
+
|
|
35
|
+
_IMMUTABLE_CACHE: Any
|
|
36
|
+
_IMMUTABLE_TAG_DISPATCH: Mapping[str, ImmutableTagHandler]
|