glitchlings 0.4.4__cp313-cp313-manylinux_2_28_x86_64.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.
- glitchlings/__init__.py +67 -0
- glitchlings/__main__.py +8 -0
- glitchlings/_zoo_rust.cpython-313-x86_64-linux-gnu.so +0 -0
- glitchlings/compat.py +284 -0
- glitchlings/config.py +388 -0
- glitchlings/config.toml +3 -0
- glitchlings/dlc/__init__.py +7 -0
- glitchlings/dlc/_shared.py +153 -0
- glitchlings/dlc/huggingface.py +81 -0
- glitchlings/dlc/prime.py +254 -0
- glitchlings/dlc/pytorch.py +166 -0
- glitchlings/dlc/pytorch_lightning.py +215 -0
- glitchlings/lexicon/__init__.py +192 -0
- glitchlings/lexicon/_cache.py +110 -0
- glitchlings/lexicon/data/default_vector_cache.json +82 -0
- glitchlings/lexicon/metrics.py +162 -0
- glitchlings/lexicon/vector.py +651 -0
- glitchlings/lexicon/wordnet.py +232 -0
- glitchlings/main.py +364 -0
- glitchlings/util/__init__.py +195 -0
- glitchlings/util/adapters.py +27 -0
- glitchlings/zoo/__init__.py +168 -0
- glitchlings/zoo/_ocr_confusions.py +32 -0
- glitchlings/zoo/_rate.py +131 -0
- glitchlings/zoo/_rust_extensions.py +143 -0
- glitchlings/zoo/_sampling.py +54 -0
- glitchlings/zoo/_text_utils.py +100 -0
- glitchlings/zoo/adjax.py +128 -0
- glitchlings/zoo/apostrofae.py +127 -0
- glitchlings/zoo/assets/__init__.py +0 -0
- glitchlings/zoo/assets/apostrofae_pairs.json +32 -0
- glitchlings/zoo/core.py +582 -0
- glitchlings/zoo/jargoyle.py +335 -0
- glitchlings/zoo/mim1c.py +109 -0
- glitchlings/zoo/ocr_confusions.tsv +30 -0
- glitchlings/zoo/redactyl.py +193 -0
- glitchlings/zoo/reduple.py +148 -0
- glitchlings/zoo/rushmore.py +153 -0
- glitchlings/zoo/scannequin.py +171 -0
- glitchlings/zoo/typogre.py +231 -0
- glitchlings/zoo/zeedub.py +185 -0
- glitchlings-0.4.4.dist-info/METADATA +627 -0
- glitchlings-0.4.4.dist-info/RECORD +47 -0
- glitchlings-0.4.4.dist-info/WHEEL +5 -0
- glitchlings-0.4.4.dist-info/entry_points.txt +2 -0
- glitchlings-0.4.4.dist-info/licenses/LICENSE +201 -0
- 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
|
glitchlings/zoo/_rate.py
ADDED
|
@@ -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
|
+
]
|