glitchlings 0.2.3__cp310-cp310-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.
@@ -0,0 +1,181 @@
1
+ import difflib
2
+ from collections.abc import Iterable
3
+
4
+ SAMPLE_TEXT = "One morning, when Gregor Samsa woke from troubled dreams, he found himself transformed in his bed into a horrible vermin. He lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightly domed and divided by arches into stiff sections. The bedding was hardly able to cover it and seemed ready to slide off any moment. His many legs, pitifully thin compared with the size of the rest of him, waved about helplessly as he looked."
5
+
6
+
7
+ def string_diffs(a: str, b: str) -> list[list[tuple[str, str, str]]]:
8
+ """
9
+ Compare two strings using SequenceMatcher and return
10
+ grouped adjacent opcodes (excluding 'equal' tags).
11
+
12
+ Each element is a tuple: (tag, a_text, b_text).
13
+ """
14
+ sm = difflib.SequenceMatcher(None, a, b)
15
+ ops: list[list[tuple[str, str, str]]] = []
16
+ buffer: list[tuple[str, str, str]] = []
17
+
18
+ for tag, i1, i2, j1, j2 in sm.get_opcodes():
19
+ if tag == "equal":
20
+ # flush any buffered operations before skipping
21
+ if buffer:
22
+ ops.append(buffer)
23
+ buffer = []
24
+ continue
25
+
26
+ # append operation to buffer
27
+ buffer.append((tag, a[i1:i2], b[j1:j2]))
28
+
29
+ # flush trailing buffer
30
+ if buffer:
31
+ ops.append(buffer)
32
+
33
+ return ops
34
+
35
+
36
+ KeyNeighborMap = dict[str, list[str]]
37
+ KeyboardLayouts = dict[str, KeyNeighborMap]
38
+
39
+
40
+ def _build_neighbor_map(rows: Iterable[str]) -> KeyNeighborMap:
41
+ """Derive 8-neighbour adjacency lists from keyboard layout rows."""
42
+
43
+ grid: dict[tuple[int, int], str] = {}
44
+ for y, row in enumerate(rows):
45
+ for x, char in enumerate(row):
46
+ if char == " ":
47
+ continue
48
+ grid[(x, y)] = char.lower()
49
+
50
+ neighbors: KeyNeighborMap = {}
51
+ for (x, y), char in grid.items():
52
+ seen: list[str] = []
53
+ for dy in (-1, 0, 1):
54
+ for dx in (-1, 0, 1):
55
+ if dx == 0 and dy == 0:
56
+ continue
57
+ candidate = grid.get((x + dx, y + dy))
58
+ if candidate is None:
59
+ continue
60
+ seen.append(candidate)
61
+ # Preserve encounter order but drop duplicates for determinism
62
+ deduped = list(dict.fromkeys(seen))
63
+ neighbors[char] = deduped
64
+
65
+ return neighbors
66
+
67
+
68
+ _KEYNEIGHBORS: KeyboardLayouts = {
69
+ "CURATOR_QWERTY": {
70
+ "a": [*"qwsz"],
71
+ "b": [*"vghn "],
72
+ "c": [*"xdfv "],
73
+ "d": [*"serfcx"],
74
+ "e": [*"wsdrf34"],
75
+ "f": [*"drtgvc"],
76
+ "g": [*"ftyhbv"],
77
+ "h": [*"gyujnb"],
78
+ "i": [*"ujko89"],
79
+ "j": [*"huikmn"],
80
+ "k": [*"jilom,"],
81
+ "l": [*"kop;.,"],
82
+ "m": [*"njk, "],
83
+ "n": [*"bhjm "],
84
+ "o": [*"iklp90"],
85
+ "p": [*"o0-[;l"],
86
+ "q": [*"was 12"],
87
+ "r": [*"edft45"],
88
+ "s": [*"awedxz"],
89
+ "t": [*"r56ygf"],
90
+ "u": [*"y78ijh"],
91
+ "v": [*"cfgb "],
92
+ "w": [*"q23esa"],
93
+ "x": [*"zsdc "],
94
+ "y": [*"t67uhg"],
95
+ "z": [*"asx"],
96
+ }
97
+ }
98
+
99
+
100
+ def _register_layout(name: str, rows: Iterable[str]) -> None:
101
+ _KEYNEIGHBORS[name] = _build_neighbor_map(rows)
102
+
103
+
104
+ _register_layout(
105
+ "DVORAK",
106
+ (
107
+ "`1234567890[]\\",
108
+ " ',.pyfgcrl/=\\",
109
+ " aoeuidhtns-",
110
+ " ;qjkxbmwvz",
111
+ ),
112
+ )
113
+
114
+ _register_layout(
115
+ "COLEMAK",
116
+ (
117
+ "`1234567890-=",
118
+ " qwfpgjluy;[]\\",
119
+ " arstdhneio'",
120
+ " zxcvbkm,./",
121
+ ),
122
+ )
123
+
124
+ _register_layout(
125
+ "QWERTY",
126
+ (
127
+ "`1234567890-=",
128
+ " qwertyuiop[]\\",
129
+ " asdfghjkl;'",
130
+ " zxcvbnm,./",
131
+ ),
132
+ )
133
+
134
+ _register_layout(
135
+ "AZERTY",
136
+ (
137
+ "²&é\"'(-è_çà)=",
138
+ " azertyuiop^$",
139
+ " qsdfghjklmù*",
140
+ " <wxcvbn,;:!",
141
+ ),
142
+ )
143
+
144
+ _register_layout(
145
+ "QWERTZ",
146
+ (
147
+ "^1234567890ß´",
148
+ " qwertzuiopü+",
149
+ " asdfghjklöä#",
150
+ " yxcvbnm,.-",
151
+ ),
152
+ )
153
+
154
+ _register_layout(
155
+ "SPANISH_QWERTY",
156
+ (
157
+ "º1234567890'¡",
158
+ " qwertyuiop´+",
159
+ " asdfghjklñ´",
160
+ " <zxcvbnm,.-",
161
+ ),
162
+ )
163
+
164
+ _register_layout(
165
+ "SWEDISH_QWERTY",
166
+ (
167
+ "§1234567890+´",
168
+ " qwertyuiopå¨",
169
+ " asdfghjklöä'",
170
+ " <zxcvbnm,.-",
171
+ ),
172
+ )
173
+
174
+
175
+ class KeyNeighbors:
176
+ def __init__(self) -> None:
177
+ for layout_name, layout in _KEYNEIGHBORS.items():
178
+ setattr(self, layout_name, layout)
179
+
180
+
181
+ KEYNEIGHBORS: KeyNeighbors = KeyNeighbors()
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from typing import Any
5
+
6
+ from .typogre import Typogre, typogre
7
+ from .mim1c import Mim1c, mim1c
8
+ from .jargoyle import Jargoyle, jargoyle, dependencies_available as _jargoyle_available
9
+ from .reduple import Reduple, reduple
10
+ from .rushmore import Rushmore, rushmore
11
+ from .redactyl import Redactyl, redactyl
12
+ from .scannequin import Scannequin, scannequin
13
+ from .core import Glitchling, Gaggle
14
+
15
+ __all__ = [
16
+ "Typogre",
17
+ "typogre",
18
+ "Mim1c",
19
+ "mim1c",
20
+ "Jargoyle",
21
+ "jargoyle",
22
+ "Reduple",
23
+ "reduple",
24
+ "Rushmore",
25
+ "rushmore",
26
+ "Redactyl",
27
+ "redactyl",
28
+ "Scannequin",
29
+ "scannequin",
30
+ "Glitchling",
31
+ "Gaggle",
32
+ "summon",
33
+ "BUILTIN_GLITCHLINGS",
34
+ "DEFAULT_GLITCHLING_NAMES",
35
+ "parse_glitchling_spec",
36
+ ]
37
+
38
+ _HAS_JARGOYLE = _jargoyle_available()
39
+
40
+ _BUILTIN_GLITCHLING_LIST: list[Glitchling] = [typogre, mim1c]
41
+ if _HAS_JARGOYLE:
42
+ _BUILTIN_GLITCHLING_LIST.append(jargoyle)
43
+ _BUILTIN_GLITCHLING_LIST.extend([reduple, rushmore, redactyl, scannequin])
44
+
45
+ BUILTIN_GLITCHLINGS: dict[str, Glitchling] = {
46
+ glitchling.name.lower(): glitchling for glitchling in _BUILTIN_GLITCHLING_LIST
47
+ }
48
+
49
+ _BUILTIN_GLITCHLING_TYPES: dict[str, type[Glitchling]] = {
50
+ typogre.name.lower(): Typogre,
51
+ mim1c.name.lower(): Mim1c,
52
+ reduple.name.lower(): Reduple,
53
+ rushmore.name.lower(): Rushmore,
54
+ redactyl.name.lower(): Redactyl,
55
+ scannequin.name.lower(): Scannequin,
56
+ }
57
+ if _HAS_JARGOYLE:
58
+ _BUILTIN_GLITCHLING_TYPES[jargoyle.name.lower()] = Jargoyle
59
+
60
+ DEFAULT_GLITCHLING_NAMES: list[str] = list(BUILTIN_GLITCHLINGS.keys())
61
+
62
+
63
+ def parse_glitchling_spec(specification: str) -> Glitchling:
64
+ """Return a glitchling instance configured according to ``specification``."""
65
+
66
+ text = specification.strip()
67
+ if not text:
68
+ raise ValueError("Glitchling specification cannot be empty.")
69
+
70
+ if "(" not in text:
71
+ glitchling = BUILTIN_GLITCHLINGS.get(text.lower())
72
+ if glitchling is None:
73
+ raise ValueError(f"Glitchling '{text}' not found.")
74
+ return glitchling
75
+
76
+ if not text.endswith(")"):
77
+ raise ValueError(f"Invalid parameter syntax for glitchling '{text}'.")
78
+
79
+ name_part, arg_source = text[:-1].split("(", 1)
80
+ name = name_part.strip()
81
+ if not name:
82
+ raise ValueError(f"Invalid glitchling specification '{text}'.")
83
+
84
+ lower_name = name.lower()
85
+ glitchling_type = _BUILTIN_GLITCHLING_TYPES.get(lower_name)
86
+ if glitchling_type is None:
87
+ raise ValueError(f"Glitchling '{name}' not found.")
88
+
89
+ try:
90
+ call_expr = ast.parse(f"_({arg_source})", mode="eval").body
91
+ except SyntaxError as exc:
92
+ raise ValueError(
93
+ f"Invalid parameter syntax for glitchling '{name}': {exc.msg}"
94
+ ) from exc
95
+
96
+ if not isinstance(call_expr, ast.Call) or call_expr.args:
97
+ raise ValueError(
98
+ f"Glitchling '{name}' parameters must be provided as keyword arguments."
99
+ )
100
+
101
+ kwargs: dict[str, Any] = {}
102
+ for keyword in call_expr.keywords:
103
+ if keyword.arg is None:
104
+ raise ValueError(
105
+ f"Glitchling '{name}' does not support unpacking arbitrary keyword arguments."
106
+ )
107
+ try:
108
+ kwargs[keyword.arg] = ast.literal_eval(keyword.value)
109
+ except (ValueError, SyntaxError) as exc:
110
+ raise ValueError(
111
+ f"Failed to parse value for parameter '{keyword.arg}' on glitchling '{name}': {exc}"
112
+ ) from exc
113
+
114
+ try:
115
+ return glitchling_type(**kwargs)
116
+ except TypeError as exc:
117
+ raise ValueError(f"Failed to instantiate glitchling '{name}': {exc}") from exc
118
+
119
+
120
+ def summon(glitchlings: list[str | Glitchling], seed: int = 151) -> Gaggle:
121
+ """Summon glitchlings by name (using defaults) or instance (to change parameters)."""
122
+
123
+ summoned: list[Glitchling] = []
124
+ for entry in glitchlings:
125
+ if isinstance(entry, Glitchling):
126
+ summoned.append(entry)
127
+ continue
128
+
129
+ try:
130
+ summoned.append(parse_glitchling_spec(entry))
131
+ except ValueError as exc:
132
+ raise ValueError(str(exc)) from exc
133
+
134
+ return Gaggle(summoned, seed=seed)
@@ -0,0 +1,34 @@
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(
30
+ key=lambda item: (-len(item[1][0]), item[0])
31
+ )
32
+ entries = [entry for _, entry in indexed_entries]
33
+ _CONFUSION_TABLE = entries
34
+ return entries
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def resolve_rate(
5
+ *,
6
+ rate: float | None,
7
+ legacy_value: float | None,
8
+ default: float,
9
+ legacy_name: str,
10
+ ) -> float:
11
+ """Return the effective rate while enforcing mutual exclusivity."""
12
+
13
+ if rate is not None and legacy_value is not None:
14
+ raise ValueError(
15
+ f"Specify either 'rate' or '{legacy_name}', not both."
16
+ )
17
+ if rate is not None:
18
+ return rate
19
+ if legacy_value is not None:
20
+ return legacy_value
21
+ return default