glitchlings 0.4.4__cp310-cp310-win_amd64.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 glitchlings might be problematic. Click here for more details.

Files changed (47) hide show
  1. glitchlings/__init__.py +67 -0
  2. glitchlings/__main__.py +8 -0
  3. glitchlings/_zoo_rust.cp310-win_amd64.pyd +0 -0
  4. glitchlings/compat.py +284 -0
  5. glitchlings/config.py +388 -0
  6. glitchlings/config.toml +3 -0
  7. glitchlings/dlc/__init__.py +7 -0
  8. glitchlings/dlc/_shared.py +153 -0
  9. glitchlings/dlc/huggingface.py +81 -0
  10. glitchlings/dlc/prime.py +254 -0
  11. glitchlings/dlc/pytorch.py +166 -0
  12. glitchlings/dlc/pytorch_lightning.py +215 -0
  13. glitchlings/lexicon/__init__.py +192 -0
  14. glitchlings/lexicon/_cache.py +110 -0
  15. glitchlings/lexicon/data/default_vector_cache.json +82 -0
  16. glitchlings/lexicon/metrics.py +162 -0
  17. glitchlings/lexicon/vector.py +651 -0
  18. glitchlings/lexicon/wordnet.py +232 -0
  19. glitchlings/main.py +364 -0
  20. glitchlings/util/__init__.py +195 -0
  21. glitchlings/util/adapters.py +27 -0
  22. glitchlings/zoo/__init__.py +168 -0
  23. glitchlings/zoo/_ocr_confusions.py +32 -0
  24. glitchlings/zoo/_rate.py +131 -0
  25. glitchlings/zoo/_rust_extensions.py +143 -0
  26. glitchlings/zoo/_sampling.py +54 -0
  27. glitchlings/zoo/_text_utils.py +100 -0
  28. glitchlings/zoo/adjax.py +128 -0
  29. glitchlings/zoo/apostrofae.py +127 -0
  30. glitchlings/zoo/assets/__init__.py +0 -0
  31. glitchlings/zoo/assets/apostrofae_pairs.json +32 -0
  32. glitchlings/zoo/core.py +582 -0
  33. glitchlings/zoo/jargoyle.py +335 -0
  34. glitchlings/zoo/mim1c.py +109 -0
  35. glitchlings/zoo/ocr_confusions.tsv +30 -0
  36. glitchlings/zoo/redactyl.py +193 -0
  37. glitchlings/zoo/reduple.py +148 -0
  38. glitchlings/zoo/rushmore.py +153 -0
  39. glitchlings/zoo/scannequin.py +171 -0
  40. glitchlings/zoo/typogre.py +231 -0
  41. glitchlings/zoo/zeedub.py +185 -0
  42. glitchlings-0.4.4.dist-info/METADATA +627 -0
  43. glitchlings-0.4.4.dist-info/RECORD +47 -0
  44. glitchlings-0.4.4.dist-info/WHEEL +5 -0
  45. glitchlings-0.4.4.dist-info/entry_points.txt +2 -0
  46. glitchlings-0.4.4.dist-info/licenses/LICENSE +201 -0
  47. glitchlings-0.4.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,195 @@
1
+ import difflib
2
+ from collections.abc import Iterable
3
+
4
+ __all__ = [
5
+ "SAMPLE_TEXT",
6
+ "string_diffs",
7
+ "KeyNeighborMap",
8
+ "KeyboardLayouts",
9
+ "KeyNeighbors",
10
+ "KEYNEIGHBORS",
11
+ ]
12
+
13
+ SAMPLE_TEXT = (
14
+ "One morning, when Gregor Samsa woke from troubled dreams, he found himself "
15
+ "transformed in his bed into a horrible vermin. He lay on his armour-like back, and "
16
+ "if he lifted his head a little he could see his brown belly, slightly domed and "
17
+ "divided by arches into stiff sections. The bedding was hardly able to cover it and "
18
+ "seemed ready to slide off any moment. His many legs, pitifully thin compared with "
19
+ "the size of the rest of him, waved about helplessly as he looked."
20
+ )
21
+
22
+
23
+ def string_diffs(a: str, b: str) -> list[list[tuple[str, str, str]]]:
24
+ """Compare two strings using SequenceMatcher and return
25
+ grouped adjacent opcodes (excluding 'equal' tags).
26
+
27
+ Each element is a tuple: (tag, a_text, b_text).
28
+ """
29
+ sm = difflib.SequenceMatcher(None, a, b)
30
+ ops: list[list[tuple[str, str, str]]] = []
31
+ buffer: list[tuple[str, str, str]] = []
32
+
33
+ for tag, i1, i2, j1, j2 in sm.get_opcodes():
34
+ if tag == "equal":
35
+ # flush any buffered operations before skipping
36
+ if buffer:
37
+ ops.append(buffer)
38
+ buffer = []
39
+ continue
40
+
41
+ # append operation to buffer
42
+ buffer.append((tag, a[i1:i2], b[j1:j2]))
43
+
44
+ # flush trailing buffer
45
+ if buffer:
46
+ ops.append(buffer)
47
+
48
+ return ops
49
+
50
+
51
+ KeyNeighborMap = dict[str, list[str]]
52
+ KeyboardLayouts = dict[str, KeyNeighborMap]
53
+
54
+
55
+ def _build_neighbor_map(rows: Iterable[str]) -> KeyNeighborMap:
56
+ """Derive 8-neighbour adjacency lists from keyboard layout rows."""
57
+ grid: dict[tuple[int, int], str] = {}
58
+ for y, row in enumerate(rows):
59
+ for x, char in enumerate(row):
60
+ if char == " ":
61
+ continue
62
+ grid[(x, y)] = char.lower()
63
+
64
+ neighbors: KeyNeighborMap = {}
65
+ for (x, y), char in grid.items():
66
+ seen: list[str] = []
67
+ for dy in (-1, 0, 1):
68
+ for dx in (-1, 0, 1):
69
+ if dx == 0 and dy == 0:
70
+ continue
71
+ candidate = grid.get((x + dx, y + dy))
72
+ if candidate is None:
73
+ continue
74
+ seen.append(candidate)
75
+ # Preserve encounter order but drop duplicates for determinism
76
+ deduped = list(dict.fromkeys(seen))
77
+ neighbors[char] = deduped
78
+
79
+ return neighbors
80
+
81
+
82
+ _KEYNEIGHBORS: KeyboardLayouts = {
83
+ "CURATOR_QWERTY": {
84
+ "a": [*"qwsz"],
85
+ "b": [*"vghn "],
86
+ "c": [*"xdfv "],
87
+ "d": [*"serfcx"],
88
+ "e": [*"wsdrf34"],
89
+ "f": [*"drtgvc"],
90
+ "g": [*"ftyhbv"],
91
+ "h": [*"gyujnb"],
92
+ "i": [*"ujko89"],
93
+ "j": [*"huikmn"],
94
+ "k": [*"jilom,"],
95
+ "l": [*"kop;.,"],
96
+ "m": [*"njk, "],
97
+ "n": [*"bhjm "],
98
+ "o": [*"iklp90"],
99
+ "p": [*"o0-[;l"],
100
+ "q": [*"was 12"],
101
+ "r": [*"edft45"],
102
+ "s": [*"awedxz"],
103
+ "t": [*"r56ygf"],
104
+ "u": [*"y78ijh"],
105
+ "v": [*"cfgb "],
106
+ "w": [*"q23esa"],
107
+ "x": [*"zsdc "],
108
+ "y": [*"t67uhg"],
109
+ "z": [*"asx"],
110
+ }
111
+ }
112
+
113
+
114
+ def _register_layout(name: str, rows: Iterable[str]) -> None:
115
+ _KEYNEIGHBORS[name] = _build_neighbor_map(rows)
116
+
117
+
118
+ _register_layout(
119
+ "DVORAK",
120
+ (
121
+ "`1234567890[]\\",
122
+ " ',.pyfgcrl/=\\",
123
+ " aoeuidhtns-",
124
+ " ;qjkxbmwvz",
125
+ ),
126
+ )
127
+
128
+ _register_layout(
129
+ "COLEMAK",
130
+ (
131
+ "`1234567890-=",
132
+ " qwfpgjluy;[]\\",
133
+ " arstdhneio'",
134
+ " zxcvbkm,./",
135
+ ),
136
+ )
137
+
138
+ _register_layout(
139
+ "QWERTY",
140
+ (
141
+ "`1234567890-=",
142
+ " qwertyuiop[]\\",
143
+ " asdfghjkl;'",
144
+ " zxcvbnm,./",
145
+ ),
146
+ )
147
+
148
+ _register_layout(
149
+ "AZERTY",
150
+ (
151
+ "²&é\"'(-è_çà)=",
152
+ " azertyuiop^$",
153
+ " qsdfghjklmù*",
154
+ " <wxcvbn,;:!",
155
+ ),
156
+ )
157
+
158
+ _register_layout(
159
+ "QWERTZ",
160
+ (
161
+ "^1234567890ß´",
162
+ " qwertzuiopü+",
163
+ " asdfghjklöä#",
164
+ " yxcvbnm,.-",
165
+ ),
166
+ )
167
+
168
+ _register_layout(
169
+ "SPANISH_QWERTY",
170
+ (
171
+ "º1234567890'¡",
172
+ " qwertyuiop´+",
173
+ " asdfghjklñ´",
174
+ " <zxcvbnm,.-",
175
+ ),
176
+ )
177
+
178
+ _register_layout(
179
+ "SWEDISH_QWERTY",
180
+ (
181
+ "§1234567890+´",
182
+ " qwertyuiopå¨",
183
+ " asdfghjklöä'",
184
+ " <zxcvbnm,.-",
185
+ ),
186
+ )
187
+
188
+
189
+ class KeyNeighbors:
190
+ def __init__(self) -> None:
191
+ for layout_name, layout in _KEYNEIGHBORS.items():
192
+ setattr(self, layout_name, layout)
193
+
194
+
195
+ KEYNEIGHBORS: KeyNeighbors = KeyNeighbors()
@@ -0,0 +1,27 @@
1
+ """Adapter helpers shared across Python and DLC integrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+
7
+ from ..zoo import Gaggle, Glitchling, summon
8
+
9
+
10
+ def coerce_gaggle(
11
+ glitchlings: Glitchling | Gaggle | str | Iterable[str | Glitchling],
12
+ *,
13
+ seed: int,
14
+ ) -> Gaggle:
15
+ """Return a :class:`Gaggle` built from any supported glitchling specifier."""
16
+ if isinstance(glitchlings, Gaggle):
17
+ return glitchlings
18
+
19
+ if isinstance(glitchlings, (Glitchling, str)):
20
+ resolved: Iterable[str | Glitchling] = [glitchlings]
21
+ else:
22
+ resolved = glitchlings
23
+
24
+ return summon(list(resolved), seed=seed)
25
+
26
+
27
+ __all__ = ["coerce_gaggle"]
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import Any
5
+
6
+ from .adjax import Adjax, adjax
7
+ from .apostrofae import Apostrofae, apostrofae
8
+ from .core import (
9
+ Gaggle,
10
+ Glitchling,
11
+ is_rust_pipeline_enabled,
12
+ is_rust_pipeline_supported,
13
+ pipeline_feature_flag_enabled,
14
+ plan_glitchling_specs,
15
+ plan_glitchlings,
16
+ )
17
+ from .jargoyle import Jargoyle, jargoyle
18
+ from .jargoyle import dependencies_available as _jargoyle_available
19
+ from .mim1c import Mim1c, mim1c
20
+ from .redactyl import Redactyl, redactyl
21
+ from .reduple import Reduple, reduple
22
+ from .rushmore import Rushmore, rushmore
23
+ from .scannequin import Scannequin, scannequin
24
+ from .typogre import Typogre, typogre
25
+ from .zeedub import Zeedub, zeedub
26
+
27
+ __all__ = [
28
+ "Typogre",
29
+ "typogre",
30
+ "Mim1c",
31
+ "mim1c",
32
+ "Jargoyle",
33
+ "jargoyle",
34
+ "Apostrofae",
35
+ "apostrofae",
36
+ "Adjax",
37
+ "adjax",
38
+ "Reduple",
39
+ "reduple",
40
+ "Rushmore",
41
+ "rushmore",
42
+ "Redactyl",
43
+ "redactyl",
44
+ "Scannequin",
45
+ "scannequin",
46
+ "Zeedub",
47
+ "zeedub",
48
+ "Glitchling",
49
+ "Gaggle",
50
+ "plan_glitchlings",
51
+ "plan_glitchling_specs",
52
+ "is_rust_pipeline_enabled",
53
+ "is_rust_pipeline_supported",
54
+ "pipeline_feature_flag_enabled",
55
+ "summon",
56
+ "BUILTIN_GLITCHLINGS",
57
+ "DEFAULT_GLITCHLING_NAMES",
58
+ "parse_glitchling_spec",
59
+ "get_glitchling_class",
60
+ ]
61
+
62
+ _HAS_JARGOYLE = _jargoyle_available()
63
+
64
+ _BUILTIN_GLITCHLING_LIST: list[Glitchling] = [typogre, apostrofae, mim1c]
65
+ if _HAS_JARGOYLE:
66
+ _BUILTIN_GLITCHLING_LIST.append(jargoyle)
67
+ _BUILTIN_GLITCHLING_LIST.extend([adjax, reduple, rushmore, redactyl, scannequin, zeedub])
68
+
69
+ BUILTIN_GLITCHLINGS: dict[str, Glitchling] = {
70
+ glitchling.name.lower(): glitchling for glitchling in _BUILTIN_GLITCHLING_LIST
71
+ }
72
+
73
+ _BUILTIN_GLITCHLING_TYPES: dict[str, type[Glitchling]] = {
74
+ typogre.name.lower(): Typogre,
75
+ apostrofae.name.lower(): Apostrofae,
76
+ mim1c.name.lower(): Mim1c,
77
+ adjax.name.lower(): Adjax,
78
+ reduple.name.lower(): Reduple,
79
+ rushmore.name.lower(): Rushmore,
80
+ redactyl.name.lower(): Redactyl,
81
+ scannequin.name.lower(): Scannequin,
82
+ zeedub.name.lower(): Zeedub,
83
+ }
84
+ if _HAS_JARGOYLE:
85
+ _BUILTIN_GLITCHLING_TYPES[jargoyle.name.lower()] = Jargoyle
86
+
87
+ DEFAULT_GLITCHLING_NAMES: list[str] = list(BUILTIN_GLITCHLINGS.keys())
88
+
89
+
90
+ def parse_glitchling_spec(specification: str) -> Glitchling:
91
+ """Return a glitchling instance configured according to ``specification``."""
92
+ text = specification.strip()
93
+ if not text:
94
+ raise ValueError("Glitchling specification cannot be empty.")
95
+
96
+ if "(" not in text:
97
+ glitchling = BUILTIN_GLITCHLINGS.get(text.lower())
98
+ if glitchling is None:
99
+ raise ValueError(f"Glitchling '{text}' not found.")
100
+ return glitchling
101
+
102
+ if not text.endswith(")"):
103
+ raise ValueError(f"Invalid parameter syntax for glitchling '{text}'.")
104
+
105
+ name_part, arg_source = text[:-1].split("(", 1)
106
+ name = name_part.strip()
107
+ if not name:
108
+ raise ValueError(f"Invalid glitchling specification '{text}'.")
109
+
110
+ lower_name = name.lower()
111
+ glitchling_type = _BUILTIN_GLITCHLING_TYPES.get(lower_name)
112
+ if glitchling_type is None:
113
+ raise ValueError(f"Glitchling '{name}' not found.")
114
+
115
+ try:
116
+ call_expr = ast.parse(f"_({arg_source})", mode="eval").body
117
+ except SyntaxError as exc:
118
+ raise ValueError(f"Invalid parameter syntax for glitchling '{name}': {exc.msg}") from exc
119
+
120
+ if not isinstance(call_expr, ast.Call) or call_expr.args:
121
+ raise ValueError(f"Glitchling '{name}' parameters must be provided as keyword arguments.")
122
+
123
+ kwargs: dict[str, Any] = {}
124
+ for keyword in call_expr.keywords:
125
+ if keyword.arg is None:
126
+ raise ValueError(
127
+ f"Glitchling '{name}' does not support unpacking arbitrary keyword arguments."
128
+ )
129
+ try:
130
+ kwargs[keyword.arg] = ast.literal_eval(keyword.value)
131
+ except (ValueError, SyntaxError) as exc:
132
+ raise ValueError(
133
+ f"Failed to parse value for parameter '{keyword.arg}' on glitchling '{name}': {exc}"
134
+ ) from exc
135
+
136
+ try:
137
+ return glitchling_type(**kwargs)
138
+ except TypeError as exc:
139
+ raise ValueError(f"Failed to instantiate glitchling '{name}': {exc}") from exc
140
+
141
+
142
+ def get_glitchling_class(name: str) -> type[Glitchling]:
143
+ """Look up the glitchling class registered under ``name``."""
144
+ key = name.strip().lower()
145
+ if not key:
146
+ raise ValueError("Glitchling name cannot be empty.")
147
+
148
+ glitchling_type = _BUILTIN_GLITCHLING_TYPES.get(key)
149
+ if glitchling_type is None:
150
+ raise ValueError(f"Glitchling '{name}' not found.")
151
+
152
+ return glitchling_type
153
+
154
+
155
+ def summon(glitchlings: list[str | Glitchling], seed: int = 151) -> Gaggle:
156
+ """Summon glitchlings by name (using defaults) or instance (to change parameters)."""
157
+ summoned: list[Glitchling] = []
158
+ for entry in glitchlings:
159
+ if isinstance(entry, Glitchling):
160
+ summoned.append(entry)
161
+ continue
162
+
163
+ try:
164
+ summoned.append(parse_glitchling_spec(entry))
165
+ except ValueError as exc:
166
+ raise ValueError(str(exc)) from exc
167
+
168
+ return Gaggle(summoned, seed=seed)
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import resources
4
+
5
+ _CONFUSION_TABLE: list[tuple[str, list[str]]] | None = None
6
+
7
+
8
+ def load_confusion_table() -> list[tuple[str, list[str]]]:
9
+ """Load the OCR confusion table shared by Python and Rust implementations."""
10
+ global _CONFUSION_TABLE
11
+ if _CONFUSION_TABLE is not None:
12
+ return _CONFUSION_TABLE
13
+
14
+ data = resources.files(__package__) / "ocr_confusions.tsv"
15
+ text = data.read_text(encoding="utf-8")
16
+ indexed_entries: list[tuple[int, tuple[str, list[str]]]] = []
17
+ for line_number, line in enumerate(text.splitlines()):
18
+ stripped = line.strip()
19
+ if not stripped or stripped.startswith("#"):
20
+ continue
21
+ parts = stripped.split()
22
+ if len(parts) < 2:
23
+ continue
24
+ source, *replacements = parts
25
+ indexed_entries.append((line_number, (source, replacements)))
26
+
27
+ # Sort longer patterns first to avoid overlapping matches, mirroring the
28
+ # behaviour of the Rust `confusion_table` helper.
29
+ indexed_entries.sort(key=lambda item: (-len(item[1][0]), item[0]))
30
+ entries = [entry for _, entry in indexed_entries]
31
+ _CONFUSION_TABLE = entries
32
+ return entries
@@ -0,0 +1,131 @@
1
+ """Utilities for handling legacy parameter names across glitchling classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+
7
+
8
+ def resolve_rate(
9
+ *,
10
+ rate: float | None,
11
+ legacy_value: float | None,
12
+ default: float,
13
+ legacy_name: str,
14
+ ) -> float:
15
+ """Return the effective rate while enforcing mutual exclusivity.
16
+
17
+ This function centralizes the handling of legacy parameter names, allowing
18
+ glitchlings to maintain backwards compatibility while encouraging migration
19
+ to the standardized 'rate' parameter.
20
+
21
+ Parameters
22
+ ----------
23
+ rate : float | None
24
+ The preferred parameter value.
25
+ legacy_value : float | None
26
+ The deprecated legacy parameter value.
27
+ default : float
28
+ Default value if neither parameter is specified.
29
+ legacy_name : str
30
+ Name of the legacy parameter for error/warning messages.
31
+
32
+ Returns
33
+ -------
34
+ float
35
+ The resolved rate value.
36
+
37
+ Raises
38
+ ------
39
+ ValueError
40
+ If both rate and legacy_value are specified simultaneously.
41
+
42
+ Warnings
43
+ --------
44
+ DeprecationWarning
45
+ If the legacy parameter is used, a deprecation warning is issued.
46
+
47
+ Examples
48
+ --------
49
+ >>> resolve_rate(rate=0.5, legacy_value=None, default=0.1, legacy_name="old_rate")
50
+ 0.5
51
+ >>> resolve_rate(rate=None, legacy_value=0.3, default=0.1, legacy_name="old_rate")
52
+ 0.3 # Issues deprecation warning
53
+ >>> resolve_rate(rate=None, legacy_value=None, default=0.1, legacy_name="old_rate")
54
+ 0.1
55
+
56
+ """
57
+ if rate is not None and legacy_value is not None:
58
+ raise ValueError(f"Specify either 'rate' or '{legacy_name}', not both.")
59
+
60
+ if rate is not None:
61
+ return rate
62
+
63
+ if legacy_value is not None:
64
+ warnings.warn(
65
+ f"The '{legacy_name}' parameter is deprecated and will be removed in version 0.6.0. "
66
+ f"Use 'rate' instead.",
67
+ DeprecationWarning,
68
+ stacklevel=3,
69
+ )
70
+ return legacy_value
71
+
72
+ return default
73
+
74
+
75
+ def resolve_legacy_param(
76
+ *,
77
+ preferred_value: object,
78
+ legacy_value: object,
79
+ default: object,
80
+ preferred_name: str,
81
+ legacy_name: str,
82
+ ) -> object:
83
+ """Resolve a parameter that has both preferred and legacy names.
84
+
85
+ This is a generalized version of resolve_rate() that works with any type.
86
+
87
+ Parameters
88
+ ----------
89
+ preferred_value : object
90
+ The value from the preferred parameter name.
91
+ legacy_value : object
92
+ The value from the legacy parameter name.
93
+ default : object
94
+ Default value if neither parameter is specified.
95
+ preferred_name : str
96
+ Name of the preferred parameter.
97
+ legacy_name : str
98
+ Name of the legacy parameter for warning messages.
99
+
100
+ Returns
101
+ -------
102
+ object
103
+ The resolved parameter value.
104
+
105
+ Raises
106
+ ------
107
+ ValueError
108
+ If both preferred and legacy values are specified simultaneously.
109
+
110
+ Warnings
111
+ --------
112
+ DeprecationWarning
113
+ If the legacy parameter is used.
114
+
115
+ """
116
+ if preferred_value is not None and legacy_value is not None:
117
+ raise ValueError(f"Specify either '{preferred_name}' or '{legacy_name}', not both.")
118
+
119
+ if preferred_value is not None:
120
+ return preferred_value
121
+
122
+ if legacy_value is not None:
123
+ warnings.warn(
124
+ f"The '{legacy_name}' parameter is deprecated and will be removed in version 0.6.0. "
125
+ f"Use '{preferred_name}' instead.",
126
+ DeprecationWarning,
127
+ stacklevel=3,
128
+ )
129
+ return legacy_value
130
+
131
+ return default
@@ -0,0 +1,143 @@
1
+ """Centralized loading and fallback management for optional Rust extensions.
2
+
3
+ This module provides a single source of truth for importing Rust-accelerated
4
+ operations, eliminating duplicated try/except blocks across the codebase.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import Any, Callable
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ # Cache of loaded Rust operations to avoid repeated import attempts
16
+ _rust_operation_cache: dict[str, Callable[..., Any] | None] = {}
17
+ _rust_module_available: bool | None = None
18
+
19
+
20
+ def is_rust_module_available() -> bool:
21
+ """Check if the Rust extension module can be imported.
22
+
23
+ Returns
24
+ -------
25
+ bool
26
+ True if glitchlings._zoo_rust can be imported successfully.
27
+
28
+ Notes
29
+ -----
30
+ The result is cached after the first check to avoid repeated import attempts.
31
+ """
32
+ global _rust_module_available
33
+
34
+ if _rust_module_available is not None:
35
+ return _rust_module_available
36
+
37
+ try:
38
+ import glitchlings._zoo_rust # noqa: F401
39
+
40
+ _rust_module_available = True
41
+ log.debug("Rust extension module successfully loaded")
42
+ except (ImportError, ModuleNotFoundError):
43
+ _rust_module_available = False
44
+ log.debug("Rust extension module not available; using Python fallbacks")
45
+
46
+ return _rust_module_available
47
+
48
+
49
+ def get_rust_operation(operation_name: str) -> Callable[..., Any] | None:
50
+ """Load a specific Rust operation by name with caching.
51
+
52
+ Parameters
53
+ ----------
54
+ operation_name : str
55
+ The name of the operation to import from glitchlings._zoo_rust.
56
+
57
+ Returns
58
+ -------
59
+ Callable | None
60
+ The Rust operation callable if available, None otherwise.
61
+
62
+ Examples
63
+ --------
64
+ >>> fatfinger = get_rust_operation("fatfinger")
65
+ >>> if fatfinger is not None:
66
+ ... result = fatfinger(text, ...)
67
+ ... else:
68
+ ... result = python_fallback(text, ...)
69
+
70
+ Notes
71
+ -----
72
+ - Results are cached to avoid repeated imports
73
+ - Returns None if the Rust module is unavailable or the operation doesn't exist
74
+ - All import errors are logged at debug level
75
+ """
76
+ # Check cache first
77
+ if operation_name in _rust_operation_cache:
78
+ return _rust_operation_cache[operation_name]
79
+
80
+ # If the module isn't available, don't try to import individual operations
81
+ if not is_rust_module_available():
82
+ _rust_operation_cache[operation_name] = None
83
+ return None
84
+
85
+ try:
86
+ from glitchlings import _zoo_rust
87
+
88
+ operation = getattr(_zoo_rust, operation_name, None)
89
+ _rust_operation_cache[operation_name] = operation
90
+
91
+ if operation is None:
92
+ log.debug(f"Rust operation '{operation_name}' not found in extension module")
93
+ else:
94
+ log.debug(f"Rust operation '{operation_name}' loaded successfully")
95
+
96
+ return operation
97
+
98
+ except (ImportError, ModuleNotFoundError, AttributeError) as exc:
99
+ log.debug(f"Failed to load Rust operation '{operation_name}': {exc}")
100
+ _rust_operation_cache[operation_name] = None
101
+ return None
102
+
103
+
104
+ def clear_cache() -> None:
105
+ """Clear the operation cache, forcing re-import on next access.
106
+
107
+ This is primarily useful for testing scenarios where the Rust module
108
+ availability might change during runtime.
109
+ """
110
+ global _rust_module_available, _rust_operation_cache
111
+
112
+ _rust_module_available = None
113
+ _rust_operation_cache.clear()
114
+ log.debug("Rust extension cache cleared")
115
+
116
+
117
+ def preload_operations(*operation_names: str) -> dict[str, Callable[..., Any] | None]:
118
+ """Eagerly load multiple Rust operations at once.
119
+
120
+ Parameters
121
+ ----------
122
+ *operation_names : str
123
+ Names of operations to preload.
124
+
125
+ Returns
126
+ -------
127
+ dict[str, Callable | None]
128
+ Mapping of operation names to their callables (or None if unavailable).
129
+
130
+ Examples
131
+ --------
132
+ >>> ops = preload_operations("fatfinger", "reduplicate_words", "delete_random_words")
133
+ >>> fatfinger = ops["fatfinger"]
134
+ """
135
+ return {name: get_rust_operation(name) for name in operation_names}
136
+
137
+
138
+ __all__ = [
139
+ "is_rust_module_available",
140
+ "get_rust_operation",
141
+ "clear_cache",
142
+ "preload_operations",
143
+ ]