glitchlings 0.1.1__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.
@@ -1,224 +1,231 @@
1
- from .core import Glitchling, AttackWave, AttackOrder
2
- from ..util import KEYNEIGHBORS
3
- import random
4
- import re
5
- from typing import Literal, Optional
6
-
7
- # Removed dependency on external 'typo' library for deterministic control.
8
-
9
-
10
- def unichar(text: str, rng: random.Random) -> str:
11
- """Collapse one random doubled letter (like 'ee' in 'seed') to a single occurrence."""
12
- # capture doubled letter followed by trailing word chars so we don't match punctuation
13
- matches = list(re.finditer(r"((.)\2)(?=\w)", text))
14
- if not matches:
15
- return text
16
- m = rng.choice(matches)
17
- start, end = m.span(1)
18
- # Replace the doubled pair with a single char
19
- return text[:start] + text[start] + text[end:]
20
-
21
-
22
- def subs(text, index, rng: random.Random, key_neighbors=None):
23
- if key_neighbors is None:
24
- key_neighbors = getattr(KEYNEIGHBORS, "CURATOR_QWERTY")
25
- char = text[index]
26
- neighbors = key_neighbors.get(char, [])
27
- if not neighbors:
28
- return text
29
- new_char = rng.choice(neighbors)
30
- return text[:index] + new_char + text[index + 1 :]
31
-
32
-
33
- def indel(
34
- text: str,
35
- index: int,
36
- op: Literal["delete", "insert", "swap"],
37
- rng: random.Random,
38
- key_neighbors=None,
39
- ):
40
- if key_neighbors is None:
41
- key_neighbors = getattr(KEYNEIGHBORS, "CURATOR_QWERTY")
42
- if index < 0 or index >= len(text):
43
- return text
44
- if op == "delete":
45
- return text[:index] + text[index + 1 :]
46
- if op == "swap":
47
- if index >= len(text) - 1:
48
- return text
49
- return text[:index] + text[index + 1] + text[index] + text[index + 2 :]
50
- # insert (choose neighbor of this char) – if none, just duplicate char
51
- char = text[index]
52
- candidates = key_neighbors.get(char, []) or [char]
53
- new_char = rng.choice(candidates)
54
- return text[:index] + new_char + text[index:]
55
-
56
-
57
- def repeated_char(text: str, rng: random.Random) -> str:
58
- """Repeat a random non-space character once (e.g., 'cat' -> 'caat')."""
59
- positions = [i for i, c in enumerate(text) if not c.isspace()]
60
- if not positions:
61
- return text
62
- i = rng.choice(positions)
63
- return text[:i] + text[i] + text[i:]
64
-
65
-
66
- def random_space(text: str, rng: random.Random) -> str:
67
- """Insert a space at a random boundary between characters (excluding ends)."""
68
- if len(text) < 2:
69
- return text
70
- idx = rng.randrange(1, len(text))
71
- return text[:idx] + " " + text[idx:]
72
-
73
-
74
- def skipped_space(text: str, rng: random.Random) -> str:
75
- """Remove a random existing single space (simulate missed space press)."""
76
- space_positions = [m.start() for m in re.finditer(r" ", text)]
77
- if not space_positions:
78
- return text
79
- idx = rng.choice(space_positions)
80
- # collapse this one space: remove it
81
- return text[:idx] + text[idx + 1 :]
82
-
83
-
84
- def _is_word_char(c: str) -> bool:
85
- return c.isalnum() or c == "_"
86
-
87
-
88
- def _eligible_idx(s: str, i: int, preserve_first_last: bool) -> bool:
89
- """O(1) check whether index i is eligible under preserve_first_last."""
90
- if i < 0 or i >= len(s):
91
- return False
92
- if not _is_word_char(s[i]):
93
- return False
94
- if not preserve_first_last:
95
- return True
96
- # interior-of-word only
97
- left_ok = i > 0 and _is_word_char(s[i - 1])
98
- right_ok = i + 1 < len(s) and _is_word_char(s[i + 1])
99
- return left_ok and right_ok
100
-
101
-
102
- def _draw_eligible_index(
103
- rng: random.Random, s: str, preserve_first_last: bool, max_tries: int = 16
104
- ) -> Optional[int]:
105
- """Try a few uniform draws; if none hit, do a single wraparound scan."""
106
- n = len(s)
107
- if n == 0:
108
- return None
109
- for _ in range(max_tries):
110
- i = rng.randrange(n)
111
- if _eligible_idx(s, i, preserve_first_last):
112
- return i
113
- # Fallback: linear scan starting from a random point (rare path)
114
- start = rng.randrange(n)
115
- i = start
116
- while True:
117
- if _eligible_idx(s, i, preserve_first_last):
118
- return i
119
- i += 1
120
- if i == n:
121
- i = 0
122
- if i == start:
123
- return None
124
-
125
-
126
- def fatfinger(
127
- text: str,
128
- max_change_rate: float = 0.02,
129
- preserve_first_last: bool = False,
130
- keyboard: str = "CURATOR_QWERTY",
131
- seed: int | None = None,
132
- rng: random.Random | None = None,
133
- ) -> str:
134
- """Introduce character-level "fat finger" edits.
135
-
136
- Parameters
137
- - text: Input string to corrupt.
138
- - max_change_rate: Max proportion of characters to edit (default 0.02).
139
- - preserve_first_last: If True, avoid modifying first/last character of words (default False).
140
- - keyboard: Name of keyboard neighbor map from util.KEYNEIGHBORS to use (default "CURATOR_QWERTY").
141
- - seed: Optional seed used if `rng` is not provided; creates a dedicated Random.
142
- - rng: Optional random.Random to use; if provided, overrides `seed`.
143
-
144
- Notes
145
- - Chooses indices lazily from the current text after each edit to keep offsets valid.
146
- - Uses the glitchling's own RNG for determinism when run via Gaggle/summon.
147
- """
148
- if rng is None:
149
- rng = random.Random(seed)
150
- if not text:
151
- return ""
152
-
153
- s = text
154
- max_changes = max(1, int(len(s) * max_change_rate))
155
-
156
- # Prebind for speed
157
- layout = getattr(KEYNEIGHBORS, keyboard)
158
- choose = rng.choice
159
-
160
- # Actions that require a specific index vs. "global" actions
161
- positional_actions = ("char_swap", "missing_char", "extra_char", "nearby_char")
162
- global_actions = ("skipped_space", "random_space", "unichar", "repeated_char")
163
- all_actions = positional_actions + global_actions
164
-
165
- # Pre-draw action types (cheap); pick indices lazily on each step
166
- actions_drawn = [choose(all_actions) for _ in range(max_changes)]
167
-
168
- for action in actions_drawn:
169
- if action in positional_actions:
170
- idx = _draw_eligible_index(rng, s, preserve_first_last)
171
- if idx is None:
172
- continue # nothing eligible; skip
173
-
174
- if action == "char_swap":
175
- # Try swapping to the right; if not possible, optionally try left
176
- j = idx + 1
177
- if j < len(s) and (
178
- not preserve_first_last or _eligible_idx(s, j, True)
179
- ):
180
- s = s[:idx] + s[j] + s[idx] + s[j + 1 :]
181
- else:
182
- j = idx - 1
183
- if j >= 0 and (
184
- not preserve_first_last or _eligible_idx(s, j, True)
185
- ):
186
- s = s[:j] + s[idx] + s[j] + s[idx + 1 :]
187
- # else: give up this action
188
-
189
- elif action == "missing_char":
190
- s = s[:idx] + s[idx + 1 :]
191
-
192
- elif action == "extra_char":
193
- ch = s[idx]
194
- neighbors = layout.get(ch.lower(), []) or [ch]
195
- ins = choose(neighbors) or ch
196
- s = s[:idx] + ins + s[idx:]
197
-
198
- elif action == "nearby_char":
199
- ch = s[idx]
200
- neighbors = layout.get(ch.lower(), [])
201
- if neighbors:
202
- rep = choose(neighbors)
203
- s = s[:idx] + rep + s[idx + 1 :]
204
-
205
- else:
206
- # "Global" actions that internally pick their own positions
207
- if action == "skipped_space":
208
- s = skipped_space(s, rng)
209
- elif action == "random_space":
210
- s = random_space(s, rng)
211
- elif action == "unichar":
212
- s = unichar(s, rng)
213
- elif action == "repeated_char":
214
- s = repeated_char(s, rng)
215
-
216
- return s
217
-
218
-
219
- typogre = Glitchling(
220
- name="Typogre",
221
- corruption_function=fatfinger,
222
- scope=AttackWave.CHARACTER,
223
- order=AttackOrder.EARLY,
224
- )
1
+ from .core import Glitchling, AttackWave, AttackOrder
2
+ from ..util import KEYNEIGHBORS
3
+ import random
4
+ import re
5
+ from typing import Literal, Optional
6
+
7
+ # Removed dependency on external 'typo' library for deterministic control.
8
+
9
+
10
+ def unichar(text: str, rng: random.Random) -> str:
11
+ """Collapse one random doubled letter (like 'ee' in 'seed') to a single occurrence."""
12
+ # capture doubled letter followed by trailing word chars so we don't match punctuation
13
+ matches = list(re.finditer(r"((.)\2)(?=\w)", text))
14
+ if not matches:
15
+ return text
16
+ m = rng.choice(matches)
17
+ start, end = m.span(1)
18
+ # Replace the doubled pair with a single char
19
+ return text[:start] + text[start] + text[end:]
20
+
21
+
22
+ def subs(text, index, rng: random.Random, key_neighbors=None):
23
+ if key_neighbors is None:
24
+ key_neighbors = getattr(KEYNEIGHBORS, "CURATOR_QWERTY")
25
+ char = text[index]
26
+ neighbors = key_neighbors.get(char, [])
27
+ if not neighbors:
28
+ return text
29
+ new_char = rng.choice(neighbors)
30
+ return text[:index] + new_char + text[index + 1 :]
31
+
32
+
33
+ def indel(
34
+ text: str,
35
+ index: int,
36
+ op: Literal["delete", "insert", "swap"],
37
+ rng: random.Random,
38
+ key_neighbors=None,
39
+ ):
40
+ if key_neighbors is None:
41
+ key_neighbors = getattr(KEYNEIGHBORS, "CURATOR_QWERTY")
42
+ if index < 0 or index >= len(text):
43
+ return text
44
+ if op == "delete":
45
+ return text[:index] + text[index + 1 :]
46
+ if op == "swap":
47
+ if index >= len(text) - 1:
48
+ return text
49
+ return text[:index] + text[index + 1] + text[index] + text[index + 2 :]
50
+ # insert (choose neighbor of this char) – if none, just duplicate char
51
+ char = text[index]
52
+ candidates = key_neighbors.get(char, []) or [char]
53
+ new_char = rng.choice(candidates)
54
+ return text[:index] + new_char + text[index:]
55
+
56
+
57
+ def repeated_char(text: str, rng: random.Random) -> str:
58
+ """Repeat a random non-space character once (e.g., 'cat' -> 'caat')."""
59
+ positions = [i for i, c in enumerate(text) if not c.isspace()]
60
+ if not positions:
61
+ return text
62
+ i = rng.choice(positions)
63
+ return text[:i] + text[i] + text[i:]
64
+
65
+
66
+ def random_space(text: str, rng: random.Random) -> str:
67
+ """Insert a space at a random boundary between characters (excluding ends)."""
68
+ if len(text) < 2:
69
+ return text
70
+ idx = rng.randrange(1, len(text))
71
+ return text[:idx] + " " + text[idx:]
72
+
73
+
74
+ def skipped_space(text: str, rng: random.Random) -> str:
75
+ """Remove a random existing single space (simulate missed space press)."""
76
+ space_positions = [m.start() for m in re.finditer(r" ", text)]
77
+ if not space_positions:
78
+ return text
79
+ idx = rng.choice(space_positions)
80
+ # collapse this one space: remove it
81
+ return text[:idx] + text[idx + 1 :]
82
+
83
+
84
+ def _is_word_char(c: str) -> bool:
85
+ return c.isalnum() or c == "_"
86
+
87
+
88
+ def _eligible_idx(s: str, i: int) -> bool:
89
+ """O(1) check whether index i is eligible under preserve_first_last."""
90
+ if i < 0 or i >= len(s):
91
+ return False
92
+ if not _is_word_char(s[i]):
93
+ return False
94
+ # interior-of-word only
95
+ left_ok = i > 0 and _is_word_char(s[i - 1])
96
+ right_ok = i + 1 < len(s) and _is_word_char(s[i + 1])
97
+ return left_ok and right_ok
98
+
99
+
100
+ def _draw_eligible_index(
101
+ rng: random.Random, s: str, max_tries: int = 16
102
+ ) -> Optional[int]:
103
+ """Try a few uniform draws; if none hit, do a single wraparound scan."""
104
+ n = len(s)
105
+ if n == 0:
106
+ return None
107
+ for _ in range(max_tries):
108
+ i = rng.randrange(n)
109
+ if _eligible_idx(s, i):
110
+ return i
111
+ # Fallback: linear scan starting from a random point (rare path)
112
+ start = rng.randrange(n)
113
+ i = start
114
+ while True:
115
+ if _eligible_idx(s, i):
116
+ return i
117
+ i += 1
118
+ if i == n:
119
+ i = 0
120
+ if i == start:
121
+ return None
122
+
123
+
124
+ def fatfinger(
125
+ text: str,
126
+ max_change_rate: float = 0.02,
127
+ keyboard: str = "CURATOR_QWERTY",
128
+ seed: int | None = None,
129
+ rng: random.Random | None = None,
130
+ ) -> str:
131
+ """Introduce character-level "fat finger" edits.
132
+
133
+ Parameters
134
+ - text: Input string to corrupt.
135
+ - max_change_rate: Max proportion of characters to edit (default 0.02).
136
+ - keyboard: Name of keyboard neighbor map from util.KEYNEIGHBORS to use (default "CURATOR_QWERTY").
137
+ - seed: Optional seed used if `rng` is not provided; creates a dedicated Random.
138
+ - rng: Optional random.Random to use; if provided, overrides `seed`.
139
+
140
+ Notes
141
+ - Chooses indices lazily from the current text after each edit to keep offsets valid.
142
+ - Uses the glitchling's own RNG for determinism when run via Gaggle/summon.
143
+ """
144
+ if rng is None:
145
+ rng = random.Random(seed)
146
+ if not text:
147
+ return ""
148
+
149
+ s = text
150
+ max_changes = max(1, int(len(s) * max_change_rate))
151
+
152
+ # Prebind for speed
153
+ layout = getattr(KEYNEIGHBORS, keyboard)
154
+ choose = rng.choice
155
+
156
+ # Actions that require a specific index vs. "global" actions
157
+ positional_actions = ("char_swap", "missing_char", "extra_char", "nearby_char")
158
+ global_actions = ("skipped_space", "random_space", "unichar", "repeated_char")
159
+ all_actions = positional_actions + global_actions
160
+
161
+ # Pre-draw action types (cheap); pick indices lazily on each step
162
+ actions_drawn = [choose(all_actions) for _ in range(max_changes)]
163
+
164
+ for action in actions_drawn:
165
+ if action in positional_actions:
166
+ idx = _draw_eligible_index(rng, s)
167
+ if idx is None:
168
+ continue # nothing eligible; skip
169
+
170
+ if action == "char_swap":
171
+ # Try swapping with neighbor while respecting word boundaries
172
+
173
+ j = idx + 1
174
+ s = s[:idx] + s[j] + s[idx] + s[j + 1 :]
175
+
176
+ elif action == "missing_char":
177
+ if _eligible_idx(s, idx):
178
+ s = s[:idx] + s[idx + 1 :]
179
+
180
+ elif action == "extra_char":
181
+ ch = s[idx]
182
+ neighbors = layout.get(ch.lower(), []) or [ch]
183
+ ins = choose(neighbors) or ch
184
+ s = s[:idx] + ins + s[idx:]
185
+
186
+ elif action == "nearby_char":
187
+ ch = s[idx]
188
+ neighbors = layout.get(ch.lower(), [])
189
+ if neighbors:
190
+ rep = choose(neighbors)
191
+ s = s[:idx] + rep + s[idx + 1 :]
192
+
193
+ else:
194
+ # "Global" actions that internally pick their own positions
195
+ if action == "skipped_space":
196
+ s = skipped_space(s, rng)
197
+ elif action == "random_space":
198
+ s = random_space(s, rng)
199
+ elif action == "unichar":
200
+ s = unichar(s, rng)
201
+ elif action == "repeated_char":
202
+ s = repeated_char(s, rng)
203
+
204
+ return s
205
+
206
+
207
+ class Typogre(Glitchling):
208
+ """Glitchling that introduces deterministic keyboard-typing errors."""
209
+
210
+ def __init__(
211
+ self,
212
+ *,
213
+ max_change_rate: float = 0.02,
214
+ keyboard: str = "CURATOR_QWERTY",
215
+ seed: int | None = None,
216
+ ) -> None:
217
+ super().__init__(
218
+ name="Typogre",
219
+ corruption_function=fatfinger,
220
+ scope=AttackWave.CHARACTER,
221
+ order=AttackOrder.EARLY,
222
+ seed=seed,
223
+ max_change_rate=max_change_rate,
224
+ keyboard=keyboard,
225
+ )
226
+
227
+
228
+ typogre = Typogre()
229
+
230
+
231
+ __all__ = ["Typogre", "typogre"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glitchlings
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Monsters for your language games.
5
5
  Project-URL: Homepage, https://github.com/osoleve/glitchlings
6
6
  Project-URL: Repository, https://github.com/osoleve/glitchlings.git
@@ -209,7 +209,7 @@ License: Apache License
209
209
  See the License for the specific language governing permissions and
210
210
  limitations under the License.
211
211
  License-File: LICENSE
212
- Keywords: adversarial,data,evaluation,glitch,nlp,text
212
+ Keywords: adversarial augmentation,nlp,text,text augmentation
213
213
  Classifier: Development Status :: 3 - Alpha
214
214
  Classifier: Intended Audience :: Developers
215
215
  Classifier: License :: OSI Approved :: Apache Software License
@@ -223,10 +223,9 @@ Requires-Python: >=3.12
223
223
  Requires-Dist: confusable-homoglyphs>=3.3.1
224
224
  Requires-Dist: datasets>=4.0.0
225
225
  Requires-Dist: jellyfish>=1.2.0
226
+ Requires-Dist: nltk>=3.9.1
226
227
  Provides-Extra: dev
227
228
  Requires-Dist: pytest>=8.0.0; extra == 'dev'
228
- Provides-Extra: jargoyle
229
- Requires-Dist: nltk>=3.9.1; extra == 'jargoyle'
230
229
  Provides-Extra: prime
231
230
  Requires-Dist: verifiers>=0.1.3.post0; extra == 'prime'
232
231
  Description-Content-Type: text/markdown
@@ -270,10 +269,16 @@ pip install -U glitchlings
270
269
  ```
271
270
 
272
271
  ```python
273
- from glitchlings import summon, SAMPLE_TEXT
272
+ from glitchlings import Gaggle, SAMPLE_TEXT, Typogre, Mim1c, Reduple, Rushmore
274
273
 
275
- gaggle = summon(["reduple", "mim1c", "typogre", "rushmore"])
276
- gaggle(SAMPLE_TEXT)
274
+ gaggle = Gaggle([
275
+ Typogre(max_change_rate=0.03),
276
+ Mim1c(replacement_rate=0.02),
277
+ Reduple(seed=404),
278
+ Rushmore(max_deletion_rate=0.02),
279
+ ])
280
+
281
+ print(gaggle(SAMPLE_TEXT))
277
282
  ```
278
283
 
279
284
  > Onҽ m‎ھ‎rning, wһen Gregor Samƽa woke from trouble𝐝 𝑑reams, he found himself transformed in his bed into a horrible vermin‎٠‎ He l lay on his armour-like back, and if he lifted his head a little he could see his brown belly, slightlh domed and divided by arches ino stiff sections. The bedding was adly able to cover it and and seemed ready to slide off any moment. His many legxs, pitifully thin compared with the size of the the rest of him, waved about helplessly ashe looked looked.
@@ -286,14 +291,23 @@ Conversely, training a model to perform well in the presence of the types of per
286
291
 
287
292
  ## Your First Battle
288
293
 
289
- Summon your chosen `Glitchling` (_or a few, if ya nasty_) and call it on your text or slot it into `Dataset.map(...)`, supplying a seed if desired.
290
- Some `Glitchling`s may have additional keyword arguments but they will always be optional with what I decide are "reasonable defaults".
291
- Seed defaults to 151, obviously.
294
+ Summon your chosen `Glitchling` (_or a few, if ya nasty_) and call it on your text or slot it into `Dataset.map(...)`, supplying a seed if desired.
295
+ Glitchlings are standard Python classes, so you can instantiate them with whatever parameters fit your scenario:
296
+
297
+ ```python
298
+ from glitchlings import Gaggle, Typogre, Mim1c
299
+
300
+ custom_typogre = Typogre(max_change_rate=0.1)
301
+ selective_mimic = Mim1c(replacement_rate=0.05, classes=["LATIN", "GREEK"])
292
302
 
293
- Calling a `Glitchling` on a `str` transparently calls `.corrupt(str, ...) -> str`.
303
+ gaggle = Gaggle([custom_typogre, selective_mimic], seed=99)
304
+ print(gaggle("Summoned heroes do not fear the glitch."))
305
+ ```
306
+
307
+ Calling a `Glitchling` on a `str` transparently calls `.corrupt(str, ...) -> str`.
294
308
  This means that as long as your glitchlings get along logically, they play nicely with one another.
295
309
 
296
- When summoned as a `Gaggle`, the `Glitchling`s will automatically order themselves into attack waves, based on the scope of the change they make:
310
+ When summoned as or gathered into a `Gaggle`, the `Glitchling`s will automatically order themselves into attack waves, based on the scope of the change they make:
297
311
 
298
312
  1. Document
299
313
  2. Paragraph
@@ -303,6 +317,23 @@ When summoned as a `Gaggle`, the `Glitchling`s will automatically order themselv
303
317
 
304
318
  They're horrible little gremlins, but they're not _unreasonable_.
305
319
 
320
+ ## Command-Line Interface (CLI)
321
+
322
+ Keyboard warriors can challenge them directly via the `glitchlings` command:
323
+
324
+ ```bash
325
+ # Discover which glitchlings are currently on the loose.
326
+ glitchlings --list
327
+
328
+ # Run Typogre against the contents of a file and inspect the diff.
329
+ glitchlings -g typogre --file documents/report.txt --diff
330
+
331
+ # Pipe text straight into the CLI for an on-the-fly corruption.
332
+ echo "Beware LLM-written flavor-text" | glitchlings -g mim1c
333
+ ```
334
+
335
+ Use `--help` for a complete breakdown of available options.
336
+
306
337
  ## Starter 'lings
307
338
 
308
339
  For maintainability reasons, all `Glitchling` have consented to be given nicknames once they're in your care. See the [Monster Manual](MONSTER_MANUAL.md) for a complete bestiary.
@@ -311,13 +342,12 @@ For maintainability reasons, all `Glitchling` have consented to be given nicknam
311
342
 
312
343
  _What a nice word, would be a shame if something happened to it._
313
344
 
314
- > _**Fatfinger.**_ Typogre introduces character-level errors (duplicating, dropping, adding, or swapping) based on the layout of a (currently QWERTY) keyboard.
345
+ > _**Fatfinger.**_ Typogre introduces character-level errors (duplicating, dropping, adding, or swapping) based on the layout of a keyboard (QWERTY by default, with Dvorak and Colemak variants built-in).
315
346
  >
316
347
  > Args
317
348
  >
318
349
  > - `max_change_rate (float)`: The maximum number of edits to make as a percentage of the length (default: 0.02, 2%).
319
- > - `preserve_first_last (bool)`: Avoid editing the first and last character of a word (default: False).
320
- > - `keyboard (str)`: Keyboard layout key-neighbor map to use (default: "CURATOR_QWERTY").
350
+ > - `keyboard (str)`: Keyboard layout key-neighbor map to use (default: "CURATOR_QWERTY"; also accepts "QWERTY", "DVORAK", "COLEMAK", and "AZERTY").
321
351
  > - `seed (int)`: The random seed for reproducibility (default: 151).
322
352
 
323
353
  ### Mim1c
@@ -347,12 +377,12 @@ _How can a computer need reading glasses?_
347
377
 
348
378
  _Uh oh. The worst person you know just bought a thesaurus._
349
379
 
350
- > _**Sesquipedalianism.**_ Jargoyle, the insufferable `Glitchling`, replaces nouns with synonyms at random, without regard for connotational or denotational differences.
380
+ > _**Sesquipedalianism.**_ Jargoyle, the insufferable `Glitchling`, replaces words from selected parts of speech with synonyms at random, without regard for connotational or denotational differences.
351
381
  >
352
382
  > Args
353
383
  >
354
384
  > - `replacement_rate (float)`: The maximum proportion of words to replace (default: 0.1, 10%).
355
- > - `part_of_speech`: The WordNet part of speech to target (default: nouns). Accepts `wn.NOUN`, `wn.VERB`, `wn.ADJ`, or `wn.ADV`.
385
+ > - `part_of_speech`: The WordNet part(s) of speech to target (default: nouns). Accepts `wn.NOUN`, `wn.VERB`, `wn.ADJ`, `wn.ADV`, any iterable of those tags, or the string `"any"` to include them all.
356
386
  > - `seed (int)`: The random seed for reproducibility (default: 151).
357
387
 
358
388
  ### Reduple
@@ -406,19 +436,15 @@ Cave paintings and oral tradition contain many depictions of strange, otherworld
406
436
  These _Apocryphal `Glitchling`_ are said to possess unique abilities or behaviors.
407
437
  If you encounter one of these elusive beings, please document your findings and share them with _The Curator_.
408
438
 
409
- ### Reproducible Corruption
439
+ ### Ensuring Reproducible Corruption
410
440
 
411
- Every `Glitchling` owns its own independent `random.Random` instance. That means:
441
+ Every `Glitchling` should own its own independent `random.Random` instance. That means:
412
442
 
413
443
  - No `random.seed(...)` calls touch Python's global RNG.
414
444
  - Supplying a `seed` when you construct a `Glitchling` (or when you `summon(...)`) makes its behavior reproducible.
415
445
  - Re-running a `Gaggle` with the same master seed and the same input text (_and same external data!_) yields identical corruption output.
416
446
  - Corruption functions are written to accept an `rng` parameter internally so that all randomness is centralized and testable.
417
447
 
418
- #### Caveats
419
-
420
- - If you mutate a glitchling's parameters after you've used it (e.g. `typogre.set_param(...)`) the outputs may not be the same as before the change. So don't do that.
421
-
422
448
  #### At Wits' End?
423
449
 
424
450
  If you're trying to add a new glitchling and can't seem to make it deterministic, here are some places to look for determinism-breaking code:
@@ -0,0 +1,20 @@
1
+ glitchlings/__init__.py,sha256=yD0BaldUpcc_QlHVca1z1iwpOp8ne1H9YVQHc85d1So,580
2
+ glitchlings/__main__.py,sha256=EOiBgay0x6B9VlSDzSQvMuoq6bHJdSvFSgcAVGGKkd4,121
3
+ glitchlings/main.py,sha256=1pdVqytcrkh_GxOb0UPnZ0NzYKMoUnXmAWQB4cY5SEg,6199
4
+ glitchlings/dlc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ glitchlings/dlc/prime.py,sha256=WnLIon2WbdPGx_PK4vF6nOwJICXudZ6zKGR1hVES4Oc,1452
6
+ glitchlings/util/__init__.py,sha256=OCpWFtloU-sATBv2XpBGlkR7UFR6RemUtuCheuRA4yw,4018
7
+ glitchlings/zoo/__init__.py,sha256=hXQci2tysMoRHXiR6NDkWtGkKgcO0xxsMB91eiM_Llc,1344
8
+ glitchlings/zoo/core.py,sha256=PHKkNxFpBmTEgX7pAEJDiDQOWmqkdPp4qPk_dhu56sM,6778
9
+ glitchlings/zoo/jargoyle.py,sha256=fvBP4ngqZ9BHLmpIjiLqGedriwAMuZc6ryqKT5GWfPw,6924
10
+ glitchlings/zoo/mim1c.py,sha256=X4jW4YrNqbyG0IEDx7wXUsPTwrUXGw2vXUO1kC2yY94,2471
11
+ glitchlings/zoo/redactyl.py,sha256=XAd57e8oM_s3cEKoEzS6lDgVsyrdN-6L1lA-4_HQGok,2613
12
+ glitchlings/zoo/reduple.py,sha256=yXGBAqZyeBSuDkLT8BXAXiEs14GUFKxSo3tdlm1ZChM,2065
13
+ glitchlings/zoo/rushmore.py,sha256=7p8MzNbln_Xs393F7cYNfji57Y-dsvSWBTE_IPDrO34,2001
14
+ glitchlings/zoo/scannequin.py,sha256=We4I_3RFckkiSC5ERZs112jD-TH-VxAojtNTLzBgmwY,4019
15
+ glitchlings/zoo/typogre.py,sha256=hA_UF3PL-hcEmosafB1IRUNk95ip2n-36bK26OU3jZc,7424
16
+ glitchlings-0.1.2.dist-info/METADATA,sha256=dz3pti6nqokKwMXfklVMB4IyK0QsbqKXiy7tKCyGa4E,24978
17
+ glitchlings-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ glitchlings-0.1.2.dist-info/entry_points.txt,sha256=kGOwuAsjFDLtztLisaXtOouq9wFVMOJg5FzaAkg-Hto,54
19
+ glitchlings-0.1.2.dist-info/licenses/LICENSE,sha256=YCvGip-LoaRyu6h0nPo71q6eHEkzUpsE11psDJOIRkw,11337
20
+ glitchlings-0.1.2.dist-info/RECORD,,