glitchlings 0.1.0__py3-none-any.whl → 0.1.2__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.
@@ -0,0 +1,42 @@
1
+ from .zoo import (
2
+ Typogre,
3
+ typogre,
4
+ Mim1c,
5
+ mim1c,
6
+ Jargoyle,
7
+ jargoyle,
8
+ Redactyl,
9
+ redactyl,
10
+ Reduple,
11
+ reduple,
12
+ Rushmore,
13
+ rushmore,
14
+ Scannequin,
15
+ scannequin,
16
+ Glitchling,
17
+ Gaggle,
18
+ summon,
19
+ )
20
+ from .util import SAMPLE_TEXT
21
+
22
+
23
+ __all__ = [
24
+ "Typogre",
25
+ "typogre",
26
+ "Mim1c",
27
+ "mim1c",
28
+ "Jargoyle",
29
+ "jargoyle",
30
+ "Redactyl",
31
+ "redactyl",
32
+ "Reduple",
33
+ "reduple",
34
+ "Rushmore",
35
+ "rushmore",
36
+ "Scannequin",
37
+ "scannequin",
38
+ "summon",
39
+ "Glitchling",
40
+ "Gaggle",
41
+ "SAMPLE_TEXT",
42
+ ]
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from .main import main
6
+
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
@@ -1,50 +1,52 @@
1
- from enum import Enum
2
- import functools as ft
3
-
4
- import verifiers as vf
5
- from datasets import Dataset
6
-
7
- from zoo import Glitchling, Gaggle, mim1c, typogre, summon
8
-
9
-
10
- class CR(Enum):
11
- """Challenge Rating levels for tutorial environments."""
12
-
13
- Zero = 0.1
14
- Half = 0.5
15
- One = 1
16
- Two = 1.5
17
- Three = 4
18
- Four = 9
19
-
20
-
21
- def tutorial_level(
22
- env: vf.Environment | str, seed=151, CR: CR = CR.One
23
- ) -> vf.Environment:
24
- """Create a low-corruption environment."""
25
-
26
- mim1c.set_param("replacement_rate", 0.01 * CR.value)
27
- typogre.set_param("max_change_rate", 0.025 * CR.value)
28
-
29
- glitchlings: Gaggle = summon([mim1c, typogre], seed=seed)
30
-
31
- if isinstance(env, str):
32
- env = vf.load_environment(env)
33
-
34
- assert isinstance(env, vf.Environment), "Invalid environment type"
35
-
36
- if "prompt" in env.dataset.column_names:
37
- env.dataset = glitchlings.corrupt_dataset(env.dataset, ["prompt"])
38
- elif "question" in env.dataset.column_names:
39
- env.dataset = glitchlings.corrupt_dataset(env.dataset, ["question"])
40
- else:
41
- raise ValueError("Can't find prompt or question column")
42
-
43
- return env
44
-
45
-
46
- def load_environment(
47
- env: str | vf.Environment, seed=151, CR: CR = CR.One, loader=tutorial_level
48
- ) -> vf.Environment:
49
- """Load an environment by name."""
50
- return loader(env, seed=seed, CR=CR)
1
+ from enum import Enum
2
+ import functools as ft
3
+
4
+ import verifiers as vf
5
+ from datasets import Dataset
6
+
7
+ from ..zoo import Glitchling, Gaggle, Mim1c, Typogre, summon
8
+
9
+
10
+ class Difficulty(Enum):
11
+ """Difficulty levels for tutorial environments."""
12
+
13
+ Easy = 0.25
14
+ Normal = 1.0
15
+ Hard = 1.75
16
+ Extreme = 3
17
+ Impossible = 9
18
+
19
+
20
+ def tutorial_level(
21
+ env: vf.Environment | str, seed=151, difficulty: Difficulty = Difficulty.Normal
22
+ ) -> vf.Environment:
23
+ """Create a low-corruption environment."""
24
+
25
+ tuned_mim1c = Mim1c(replacement_rate=0.01 * difficulty.value)
26
+ tuned_typogre = Typogre(max_change_rate=0.025 * difficulty.value)
27
+
28
+ glitchlings: Gaggle = summon([tuned_mim1c, tuned_typogre], seed=seed)
29
+
30
+ if isinstance(env, str):
31
+ env = vf.load_environment(env)
32
+
33
+ assert isinstance(env, vf.Environment), "Invalid environment type"
34
+
35
+ if "prompt" in env.dataset.column_names:
36
+ env.dataset = glitchlings.corrupt_dataset(env.dataset, ["prompt"])
37
+ elif "question" in env.dataset.column_names:
38
+ env.dataset = glitchlings.corrupt_dataset(env.dataset, ["question"])
39
+ else:
40
+ raise ValueError("Can't find prompt or question column")
41
+
42
+ return env
43
+
44
+
45
+ def load_environment(
46
+ env: str | vf.Environment,
47
+ seed=151,
48
+ difficulty: Difficulty = Difficulty.Normal,
49
+ loader=tutorial_level,
50
+ ) -> vf.Environment:
51
+ """Load an environment by name."""
52
+ return loader(env, seed=seed, difficulty=difficulty)
glitchlings/main.py ADDED
@@ -0,0 +1,238 @@
1
+ """Command line interface for summoning and running glitchlings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import difflib
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ from . import SAMPLE_TEXT
11
+ from .zoo import (
12
+ Glitchling,
13
+ Gaggle,
14
+ jargoyle,
15
+ mim1c,
16
+ typogre,
17
+ reduple,
18
+ rushmore,
19
+ redactyl,
20
+ scannequin,
21
+ summon,
22
+ )
23
+
24
+
25
+ BUILTIN_GLITCHLINGS: dict[str, Glitchling] = {
26
+ g.name.lower(): g
27
+ for g in [
28
+ typogre,
29
+ mim1c,
30
+ jargoyle,
31
+ reduple,
32
+ rushmore,
33
+ redactyl,
34
+ scannequin,
35
+ ]
36
+ }
37
+
38
+ DEFAULT_GLITCHLING_NAMES: list[str] = list(BUILTIN_GLITCHLINGS.keys())
39
+ MAX_NAME_WIDTH = max(len(glitchling.name) for glitchling in BUILTIN_GLITCHLINGS.values())
40
+
41
+
42
+ def build_parser() -> argparse.ArgumentParser:
43
+ """Create and configure the CLI argument parser.
44
+
45
+ Returns:
46
+ argparse.ArgumentParser: The configured argument parser instance.
47
+ """
48
+
49
+ parser = argparse.ArgumentParser(
50
+ description=(
51
+ "Summon glitchlings to corrupt text. Provide input text as an argument, "
52
+ "via --file, or pipe it on stdin."
53
+ )
54
+ )
55
+ parser.add_argument(
56
+ "text",
57
+ nargs="?",
58
+ help="Text to corrupt. If omitted, stdin is used or --sample provides fallback text.",
59
+ )
60
+ parser.add_argument(
61
+ "-g",
62
+ "--glitchling",
63
+ dest="glitchlings",
64
+ action="append",
65
+ metavar="NAME",
66
+ help="Glitchling to apply (repeat for multiples). Defaults to all built-ins.",
67
+ )
68
+ parser.add_argument(
69
+ "-s",
70
+ "--seed",
71
+ type=int,
72
+ default=151,
73
+ help="Seed controlling deterministic corruption order (default: 151).",
74
+ )
75
+ parser.add_argument(
76
+ "-f",
77
+ "--file",
78
+ type=Path,
79
+ help="Read input text from a file instead of the command line argument.",
80
+ )
81
+ parser.add_argument(
82
+ "--sample",
83
+ action="store_true",
84
+ help="Use the included SAMPLE_TEXT when no other input is provided.",
85
+ )
86
+ parser.add_argument(
87
+ "--diff",
88
+ action="store_true",
89
+ help="Show a unified diff between the original and corrupted text.",
90
+ )
91
+ parser.add_argument(
92
+ "--list",
93
+ action="store_true",
94
+ help="List available glitchlings and exit.",
95
+ )
96
+ return parser
97
+
98
+
99
+ def list_glitchlings() -> None:
100
+ """Print information about the available built-in glitchlings."""
101
+
102
+ for key in DEFAULT_GLITCHLING_NAMES:
103
+ glitchling = BUILTIN_GLITCHLINGS[key]
104
+ display_name = glitchling.name
105
+ scope = glitchling.level.name.title()
106
+ order = glitchling.order.name.lower()
107
+ print(f"{display_name:>{MAX_NAME_WIDTH}} — scope: {scope}, order: {order}")
108
+
109
+
110
+ def read_text(args: argparse.Namespace, parser: argparse.ArgumentParser) -> str:
111
+ """Resolve the input text based on CLI arguments.
112
+
113
+ Args:
114
+ args: Parsed arguments from the CLI.
115
+ parser: The argument parser used for emitting user-facing errors.
116
+
117
+ Returns:
118
+ str: The text to corrupt.
119
+
120
+ Raises:
121
+ SystemExit: Raised indirectly via ``parser.error`` on failure.
122
+ """
123
+
124
+ if args.file is not None:
125
+ try:
126
+ return args.file.read_text(encoding="utf-8")
127
+ except OSError as exc: # pragma: no cover - exercised via CLI
128
+ parser.error(str(exc))
129
+
130
+ if args.text:
131
+ return args.text
132
+
133
+ if not sys.stdin.isatty():
134
+ return sys.stdin.read()
135
+
136
+ if args.sample:
137
+ return SAMPLE_TEXT
138
+
139
+ parser.error(
140
+ "No input text provided. Supply text as an argument, use --file, pipe input, or pass --sample."
141
+ )
142
+ raise AssertionError("parser.error should exit")
143
+
144
+
145
+ def summon_glitchlings(
146
+ names: list[str] | None, parser: argparse.ArgumentParser, seed: int
147
+ ) -> Gaggle:
148
+ """Instantiate the requested glitchlings and bundle them in a ``Gaggle``.
149
+
150
+ Args:
151
+ names: Optional list of glitchling names provided by the user.
152
+ parser: The argument parser used for emitting user-facing errors.
153
+ seed: Master seed controlling deterministic corruption order.
154
+
155
+ Returns:
156
+ Gaggle: A ready-to-use collection of glitchlings.
157
+
158
+ Raises:
159
+ SystemExit: Raised indirectly via ``parser.error`` when a provided glitchling
160
+ name is invalid.
161
+ """
162
+
163
+ if names:
164
+ normalized = [name.lower() for name in names]
165
+ else:
166
+ normalized = DEFAULT_GLITCHLING_NAMES
167
+
168
+ try:
169
+ return summon(normalized, seed=seed)
170
+ except ValueError as exc:
171
+ parser.error(str(exc))
172
+ raise AssertionError("parser.error should exit")
173
+
174
+
175
+ def show_diff(original: str, corrupted: str) -> None:
176
+ """Display a unified diff between the original and corrupted text."""
177
+
178
+ diff_lines = list(
179
+ difflib.unified_diff(
180
+ original.splitlines(keepends=True),
181
+ corrupted.splitlines(keepends=True),
182
+ fromfile="original",
183
+ tofile="corrupted",
184
+ lineterm="",
185
+ )
186
+ )
187
+ if diff_lines:
188
+ for line in diff_lines:
189
+ print(line)
190
+ else:
191
+ print("No changes detected.")
192
+
193
+
194
+ def run_cli(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
195
+ """Execute the CLI workflow using the provided arguments.
196
+
197
+ Args:
198
+ args: Parsed CLI arguments.
199
+ parser: Argument parser used for error reporting.
200
+
201
+ Returns:
202
+ int: Exit code for the process (``0`` on success).
203
+ """
204
+
205
+ if args.list:
206
+ list_glitchlings()
207
+ return 0
208
+
209
+ text = read_text(args, parser)
210
+ gaggle = summon_glitchlings(args.glitchlings, parser, args.seed)
211
+
212
+ corrupted = gaggle(text)
213
+
214
+ if args.diff:
215
+ show_diff(text, corrupted)
216
+ else:
217
+ print(corrupted)
218
+
219
+ return 0
220
+
221
+
222
+ def main(argv: list[str] | None = None) -> int:
223
+ """Entry point for the ``glitchlings`` command line interface.
224
+
225
+ Args:
226
+ argv: Optional list of command line arguments. Defaults to ``sys.argv``.
227
+
228
+ Returns:
229
+ int: Exit code suitable for use with ``sys.exit``.
230
+ """
231
+
232
+ parser = build_parser()
233
+ args = parser.parse_args(argv)
234
+ return run_cli(args, parser)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ sys.exit(main())
@@ -0,0 +1,151 @@
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
+
145
+ class KeyNeighbors:
146
+ def __init__(self) -> None:
147
+ for layout_name, layout in _KEYNEIGHBORS.items():
148
+ setattr(self, layout_name, layout)
149
+
150
+
151
+ KEYNEIGHBORS: KeyNeighbors = KeyNeighbors()
@@ -1,50 +1,57 @@
1
- from .typogre import typogre
2
- from .mim1c import mim1c
3
- from .jargoyle import jargoyle
4
- from .reduple import reduple
5
- from .rushmore import rushmore
6
- from .redactyl import redactyl
7
- from .scannequin import scannequin
8
- from .core import Glitchling, Gaggle
9
-
10
- __all__ = [
11
- "typogre",
12
- "mim1c",
13
- "jargoyle",
14
- "reduple",
15
- "rushmore",
16
- "redactyl",
17
- "scannequin",
18
- "Glitchling",
19
- "Gaggle",
20
- "summon",
21
- ]
22
-
23
-
24
- def summon(glitchlings: list[str | Glitchling], seed: int = 151) -> Gaggle:
25
- """Summon glitchlings by name (using defaults) or instance (to change parameters)."""
26
- available = {
27
- g.name.lower(): g
28
- for g in [
29
- typogre,
30
- mim1c,
31
- jargoyle,
32
- reduple,
33
- rushmore,
34
- redactyl,
35
- scannequin,
36
- ]
37
- }
38
- summoned = []
39
- for entry in glitchlings:
40
- if isinstance(entry, Glitchling):
41
- summoned.append(entry)
42
- continue
43
-
44
- g = available.get(entry.lower())
45
- if g:
46
- summoned.append(g)
47
- else:
48
- raise ValueError(f"Glitchling '{entry}' not found.")
49
-
50
- return Gaggle(summoned, seed=seed)
1
+ from .typogre import Typogre, typogre
2
+ from .mim1c import Mim1c, mim1c
3
+ from .jargoyle import Jargoyle, jargoyle
4
+ from .reduple import Reduple, reduple
5
+ from .rushmore import Rushmore, rushmore
6
+ from .redactyl import Redactyl, redactyl
7
+ from .scannequin import Scannequin, scannequin
8
+ from .core import Glitchling, Gaggle
9
+
10
+ __all__ = [
11
+ "Typogre",
12
+ "typogre",
13
+ "Mim1c",
14
+ "mim1c",
15
+ "Jargoyle",
16
+ "jargoyle",
17
+ "Reduple",
18
+ "reduple",
19
+ "Rushmore",
20
+ "rushmore",
21
+ "Redactyl",
22
+ "redactyl",
23
+ "Scannequin",
24
+ "scannequin",
25
+ "Glitchling",
26
+ "Gaggle",
27
+ "summon",
28
+ ]
29
+
30
+
31
+ def summon(glitchlings: list[str | Glitchling], seed: int = 151) -> Gaggle:
32
+ """Summon glitchlings by name (using defaults) or instance (to change parameters)."""
33
+ available = {
34
+ g.name.lower(): g
35
+ for g in [
36
+ typogre,
37
+ mim1c,
38
+ jargoyle,
39
+ reduple,
40
+ rushmore,
41
+ redactyl,
42
+ scannequin,
43
+ ]
44
+ }
45
+ summoned = []
46
+ for entry in glitchlings:
47
+ if isinstance(entry, Glitchling):
48
+ summoned.append(entry)
49
+ continue
50
+
51
+ g = available.get(entry.lower())
52
+ if g:
53
+ summoned.append(g)
54
+ else:
55
+ raise ValueError(f"Glitchling '{entry}' not found.")
56
+
57
+ return Gaggle(summoned, seed=seed)