glitchlings 0.4.2__cp312-cp312-macosx_11_0_universal2.whl → 0.4.4__cp312-cp312-macosx_11_0_universal2.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 +4 -0
- glitchlings/_zoo_rust.cpython-312-darwin.so +0 -0
- glitchlings/compat.py +80 -11
- glitchlings/config.py +32 -19
- glitchlings/config.toml +1 -1
- glitchlings/dlc/__init__.py +3 -1
- glitchlings/dlc/_shared.py +86 -1
- glitchlings/dlc/pytorch.py +166 -0
- glitchlings/dlc/pytorch_lightning.py +215 -0
- glitchlings/lexicon/__init__.py +10 -16
- glitchlings/lexicon/_cache.py +21 -15
- glitchlings/lexicon/data/default_vector_cache.json +80 -14
- glitchlings/lexicon/vector.py +94 -15
- glitchlings/lexicon/wordnet.py +66 -25
- glitchlings/main.py +21 -11
- glitchlings/zoo/__init__.py +5 -1
- glitchlings/zoo/_rate.py +114 -1
- glitchlings/zoo/_rust_extensions.py +143 -0
- glitchlings/zoo/adjax.py +5 -6
- 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 +61 -23
- glitchlings/zoo/jargoyle.py +50 -36
- glitchlings/zoo/redactyl.py +15 -13
- glitchlings/zoo/reduple.py +5 -6
- glitchlings/zoo/rushmore.py +5 -6
- glitchlings/zoo/scannequin.py +5 -6
- glitchlings/zoo/typogre.py +8 -6
- glitchlings/zoo/zeedub.py +8 -6
- {glitchlings-0.4.2.dist-info → glitchlings-0.4.4.dist-info}/METADATA +40 -4
- glitchlings-0.4.4.dist-info/RECORD +47 -0
- glitchlings/lexicon/graph.py +0 -282
- glitchlings-0.4.2.dist-info/RECORD +0 -42
- {glitchlings-0.4.2.dist-info → glitchlings-0.4.4.dist-info}/WHEEL +0 -0
- {glitchlings-0.4.2.dist-info → glitchlings-0.4.4.dist-info}/entry_points.txt +0 -0
- {glitchlings-0.4.2.dist-info → glitchlings-0.4.4.dist-info}/licenses/LICENSE +0 -0
- {glitchlings-0.4.2.dist-info → glitchlings-0.4.4.dist-info}/top_level.txt +0 -0
glitchlings/lexicon/wordnet.py
CHANGED
|
@@ -4,49 +4,74 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from importlib import import_module
|
|
6
6
|
from pathlib import Path
|
|
7
|
-
from
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
from typing import Any, Callable, Protocol, Sequence, cast
|
|
8
9
|
|
|
9
10
|
from ..compat import nltk as _nltk_dependency
|
|
10
11
|
from . import LexiconBackend
|
|
11
12
|
from ._cache import CacheSnapshot
|
|
12
13
|
|
|
13
|
-
nltk = _nltk_dependency.get() # type: ignore[assignment]
|
|
14
|
-
_NLTK_IMPORT_ERROR = _nltk_dependency.error
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
WordNetCorpusReader = Any
|
|
15
|
+
class _LemmaProtocol(Protocol):
|
|
16
|
+
def name(self) -> str:
|
|
17
|
+
...
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
|
|
20
|
+
class _SynsetProtocol(Protocol):
|
|
21
|
+
def lemmas(self) -> Sequence[_LemmaProtocol]:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _WordNetResource(Protocol):
|
|
26
|
+
def synsets(self, word: str, pos: str | None = None) -> Sequence[_SynsetProtocol]:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
def ensure_loaded(self) -> None:
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
WordNetCorpusReaderFactory = Callable[[Any, Any], _WordNetResource]
|
|
34
|
+
|
|
35
|
+
nltk: ModuleType | None = _nltk_dependency.get()
|
|
36
|
+
_NLTK_IMPORT_ERROR: ModuleNotFoundError | None = _nltk_dependency.error
|
|
37
|
+
|
|
38
|
+
WordNetCorpusReader: WordNetCorpusReaderFactory | None = None
|
|
39
|
+
find: Callable[[str], Any] | None = None
|
|
40
|
+
_WORDNET_MODULE: _WordNetResource | None = None
|
|
23
41
|
|
|
24
42
|
if nltk is not None: # pragma: no cover - guarded by import success
|
|
25
43
|
try:
|
|
26
44
|
corpus_reader_module = import_module("nltk.corpus.reader")
|
|
27
|
-
WordNetCorpusReader = corpus_reader_module.WordNetCorpusReader # type: ignore[assignment]
|
|
28
45
|
except ModuleNotFoundError as exc: # pragma: no cover - triggered when corpus missing
|
|
29
46
|
if _NLTK_IMPORT_ERROR is None:
|
|
30
|
-
_NLTK_IMPORT_ERROR = exc
|
|
47
|
+
_NLTK_IMPORT_ERROR = exc
|
|
31
48
|
else:
|
|
49
|
+
reader_candidate = getattr(corpus_reader_module, "WordNetCorpusReader", None)
|
|
50
|
+
if reader_candidate is not None:
|
|
51
|
+
WordNetCorpusReader = cast(WordNetCorpusReaderFactory, reader_candidate)
|
|
52
|
+
|
|
32
53
|
try:
|
|
33
54
|
data_module = import_module("nltk.data")
|
|
34
55
|
except ModuleNotFoundError as exc: # pragma: no cover - triggered when data missing
|
|
35
56
|
if _NLTK_IMPORT_ERROR is None:
|
|
36
|
-
_NLTK_IMPORT_ERROR = exc
|
|
57
|
+
_NLTK_IMPORT_ERROR = exc
|
|
37
58
|
else:
|
|
38
|
-
|
|
59
|
+
locator = getattr(data_module, "find", None)
|
|
60
|
+
if callable(locator):
|
|
61
|
+
find = cast(Callable[[str], Any], locator)
|
|
39
62
|
|
|
40
63
|
try:
|
|
41
|
-
|
|
64
|
+
module_candidate = import_module("nltk.corpus.wordnet")
|
|
42
65
|
except ModuleNotFoundError: # pragma: no cover - only hit on namespace packages
|
|
43
66
|
_WORDNET_MODULE = None
|
|
67
|
+
else:
|
|
68
|
+
_WORDNET_MODULE = cast(_WordNetResource, module_candidate)
|
|
44
69
|
else:
|
|
45
|
-
nltk = None
|
|
70
|
+
nltk = None
|
|
46
71
|
find = None
|
|
47
72
|
_WORDNET_MODULE = None
|
|
48
73
|
|
|
49
|
-
_WORDNET_HANDLE:
|
|
74
|
+
_WORDNET_HANDLE: _WordNetResource | None = _WORDNET_MODULE
|
|
50
75
|
_wordnet_ready = False
|
|
51
76
|
|
|
52
77
|
_VALID_POS: tuple[str, ...] = ("n", "v", "a", "r")
|
|
@@ -69,15 +94,22 @@ def dependencies_available() -> bool:
|
|
|
69
94
|
return nltk is not None and find is not None
|
|
70
95
|
|
|
71
96
|
|
|
72
|
-
def _load_wordnet_reader() ->
|
|
97
|
+
def _load_wordnet_reader() -> _WordNetResource:
|
|
73
98
|
"""Return a WordNet corpus reader from the downloaded corpus files."""
|
|
74
99
|
_require_nltk()
|
|
75
100
|
|
|
101
|
+
if WordNetCorpusReader is None:
|
|
102
|
+
raise RuntimeError("The NLTK WordNet corpus reader is unavailable.")
|
|
103
|
+
|
|
104
|
+
locator = find
|
|
105
|
+
if locator is None:
|
|
106
|
+
raise RuntimeError("The NLTK data locator is unavailable.")
|
|
107
|
+
|
|
76
108
|
try:
|
|
77
|
-
root =
|
|
109
|
+
root = locator("corpora/wordnet")
|
|
78
110
|
except LookupError:
|
|
79
111
|
try:
|
|
80
|
-
zip_root =
|
|
112
|
+
zip_root = locator("corpora/wordnet.zip")
|
|
81
113
|
except LookupError as exc:
|
|
82
114
|
raise RuntimeError(
|
|
83
115
|
"The NLTK WordNet corpus is not installed; run `nltk.download('wordnet')`."
|
|
@@ -87,18 +119,20 @@ def _load_wordnet_reader() -> WordNetCorpusReader:
|
|
|
87
119
|
return WordNetCorpusReader(root, None)
|
|
88
120
|
|
|
89
121
|
|
|
90
|
-
def _wordnet(force_refresh: bool = False) ->
|
|
122
|
+
def _wordnet(force_refresh: bool = False) -> _WordNetResource:
|
|
91
123
|
"""Retrieve the active WordNet handle, rebuilding it on demand."""
|
|
92
124
|
global _WORDNET_HANDLE
|
|
93
125
|
|
|
94
126
|
if force_refresh:
|
|
95
127
|
_WORDNET_HANDLE = _WORDNET_MODULE
|
|
96
128
|
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
cached = _WORDNET_HANDLE
|
|
130
|
+
if cached is not None:
|
|
131
|
+
return cached
|
|
99
132
|
|
|
100
|
-
|
|
101
|
-
|
|
133
|
+
resource = _load_wordnet_reader()
|
|
134
|
+
_WORDNET_HANDLE = resource
|
|
135
|
+
return resource
|
|
102
136
|
|
|
103
137
|
|
|
104
138
|
def ensure_wordnet() -> None:
|
|
@@ -110,11 +144,14 @@ def ensure_wordnet() -> None:
|
|
|
110
144
|
_require_nltk()
|
|
111
145
|
|
|
112
146
|
resource = _wordnet()
|
|
147
|
+
nltk_module = nltk
|
|
148
|
+
if nltk_module is None:
|
|
149
|
+
raise RuntimeError("The NLTK dependency is unexpectedly unavailable.")
|
|
113
150
|
|
|
114
151
|
try:
|
|
115
152
|
resource.ensure_loaded()
|
|
116
153
|
except LookupError:
|
|
117
|
-
|
|
154
|
+
nltk_module.download("wordnet", quiet=True)
|
|
118
155
|
try:
|
|
119
156
|
resource = _wordnet(force_refresh=True)
|
|
120
157
|
resource.ensure_loaded()
|
|
@@ -159,6 +196,7 @@ class WordNetLexicon(LexiconBackend):
|
|
|
159
196
|
"""Lexicon that retrieves synonyms from the NLTK WordNet corpus."""
|
|
160
197
|
|
|
161
198
|
def get_synonyms(self, word: str, pos: str | None = None, n: int = 5) -> list[str]:
|
|
199
|
+
"""Return up to ``n`` WordNet lemmas for ``word`` filtered by ``pos`` if provided."""
|
|
162
200
|
ensure_wordnet()
|
|
163
201
|
|
|
164
202
|
if pos is None:
|
|
@@ -173,15 +211,18 @@ class WordNetLexicon(LexiconBackend):
|
|
|
173
211
|
return self._deterministic_sample(synonyms, limit=n, word=word, pos=pos)
|
|
174
212
|
|
|
175
213
|
def supports_pos(self, pos: str | None) -> bool:
|
|
214
|
+
"""Return ``True`` when ``pos`` is unset or recognised by the WordNet corpus."""
|
|
176
215
|
if pos is None:
|
|
177
216
|
return True
|
|
178
217
|
return pos.lower() in _VALID_POS
|
|
179
218
|
|
|
180
219
|
@classmethod
|
|
181
220
|
def load_cache(cls, path: str | Path) -> CacheSnapshot:
|
|
221
|
+
"""WordNet lexicons do not persist caches; raising keeps the contract explicit."""
|
|
182
222
|
raise RuntimeError("WordNetLexicon does not persist or load caches.")
|
|
183
223
|
|
|
184
224
|
def save_cache(self, path: str | Path | None = None) -> Path | None:
|
|
225
|
+
"""WordNet lexicons do not persist caches; raising keeps the contract explicit."""
|
|
185
226
|
raise RuntimeError("WordNetLexicon does not persist or load caches.")
|
|
186
227
|
|
|
187
228
|
def __repr__(self) -> str: # pragma: no cover - trivial representation
|
glitchlings/main.py
CHANGED
|
@@ -5,7 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
import difflib
|
|
7
7
|
import sys
|
|
8
|
+
from collections.abc import Sequence
|
|
8
9
|
from pathlib import Path
|
|
10
|
+
from typing import cast
|
|
9
11
|
|
|
10
12
|
from . import SAMPLE_TEXT
|
|
11
13
|
from .config import DEFAULT_ATTACK_SEED, build_gaggle, load_attack_config
|
|
@@ -88,6 +90,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
88
90
|
|
|
89
91
|
|
|
90
92
|
def build_lexicon_parser() -> argparse.ArgumentParser:
|
|
93
|
+
"""Create the ``build-lexicon`` subcommand parser with vector cache options."""
|
|
91
94
|
builder = argparse.ArgumentParser(
|
|
92
95
|
prog="glitchlings build-lexicon",
|
|
93
96
|
description=(
|
|
@@ -179,21 +182,23 @@ def read_text(args: argparse.Namespace, parser: argparse.ArgumentParser) -> str:
|
|
|
179
182
|
SystemExit: Raised indirectly via ``parser.error`` on failure.
|
|
180
183
|
|
|
181
184
|
"""
|
|
182
|
-
|
|
185
|
+
file_path = cast(Path | None, getattr(args, "file", None))
|
|
186
|
+
if file_path is not None:
|
|
183
187
|
try:
|
|
184
|
-
return
|
|
188
|
+
return file_path.read_text(encoding="utf-8")
|
|
185
189
|
except OSError as exc:
|
|
186
|
-
filename = getattr(exc, "filename", None) or
|
|
190
|
+
filename = getattr(exc, "filename", None) or file_path
|
|
187
191
|
reason = exc.strerror or str(exc)
|
|
188
192
|
parser.error(f"Failed to read file {filename}: {reason}")
|
|
189
193
|
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
text_argument = cast(str | None, getattr(args, "text", None))
|
|
195
|
+
if text_argument:
|
|
196
|
+
return text_argument
|
|
192
197
|
|
|
193
198
|
if not sys.stdin.isatty():
|
|
194
199
|
return sys.stdin.read()
|
|
195
200
|
|
|
196
|
-
if args
|
|
201
|
+
if bool(getattr(args, "sample", False)):
|
|
197
202
|
return SAMPLE_TEXT
|
|
198
203
|
|
|
199
204
|
parser.error(
|
|
@@ -224,21 +229,23 @@ def summon_glitchlings(
|
|
|
224
229
|
|
|
225
230
|
return build_gaggle(config, seed_override=seed)
|
|
226
231
|
|
|
232
|
+
normalized: Sequence[str | Glitchling]
|
|
227
233
|
if names:
|
|
228
|
-
|
|
234
|
+
parsed: list[str | Glitchling] = []
|
|
229
235
|
for specification in names:
|
|
230
236
|
try:
|
|
231
|
-
|
|
237
|
+
parsed.append(parse_glitchling_spec(specification))
|
|
232
238
|
except ValueError as exc:
|
|
233
239
|
parser.error(str(exc))
|
|
234
240
|
raise AssertionError("parser.error should exit")
|
|
241
|
+
normalized = parsed
|
|
235
242
|
else:
|
|
236
|
-
normalized = DEFAULT_GLITCHLING_NAMES
|
|
243
|
+
normalized = list(DEFAULT_GLITCHLING_NAMES)
|
|
237
244
|
|
|
238
245
|
effective_seed = seed if seed is not None else DEFAULT_ATTACK_SEED
|
|
239
246
|
|
|
240
247
|
try:
|
|
241
|
-
return summon(normalized, seed=effective_seed)
|
|
248
|
+
return summon(list(normalized), seed=effective_seed)
|
|
242
249
|
except ValueError as exc:
|
|
243
250
|
parser.error(str(exc))
|
|
244
251
|
raise AssertionError("parser.error should exit")
|
|
@@ -285,7 +292,10 @@ def run_cli(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
|
|
285
292
|
config_path=args.config,
|
|
286
293
|
)
|
|
287
294
|
|
|
288
|
-
corrupted = gaggle(text)
|
|
295
|
+
corrupted = gaggle.corrupt(text)
|
|
296
|
+
if not isinstance(corrupted, str):
|
|
297
|
+
message = "Gaggle returned non-string output for string input"
|
|
298
|
+
raise TypeError(message)
|
|
289
299
|
|
|
290
300
|
if args.diff:
|
|
291
301
|
show_diff(text, corrupted)
|
glitchlings/zoo/__init__.py
CHANGED
|
@@ -4,6 +4,7 @@ import ast
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from .adjax import Adjax, adjax
|
|
7
|
+
from .apostrofae import Apostrofae, apostrofae
|
|
7
8
|
from .core import (
|
|
8
9
|
Gaggle,
|
|
9
10
|
Glitchling,
|
|
@@ -30,6 +31,8 @@ __all__ = [
|
|
|
30
31
|
"mim1c",
|
|
31
32
|
"Jargoyle",
|
|
32
33
|
"jargoyle",
|
|
34
|
+
"Apostrofae",
|
|
35
|
+
"apostrofae",
|
|
33
36
|
"Adjax",
|
|
34
37
|
"adjax",
|
|
35
38
|
"Reduple",
|
|
@@ -58,7 +61,7 @@ __all__ = [
|
|
|
58
61
|
|
|
59
62
|
_HAS_JARGOYLE = _jargoyle_available()
|
|
60
63
|
|
|
61
|
-
_BUILTIN_GLITCHLING_LIST: list[Glitchling] = [typogre, mim1c]
|
|
64
|
+
_BUILTIN_GLITCHLING_LIST: list[Glitchling] = [typogre, apostrofae, mim1c]
|
|
62
65
|
if _HAS_JARGOYLE:
|
|
63
66
|
_BUILTIN_GLITCHLING_LIST.append(jargoyle)
|
|
64
67
|
_BUILTIN_GLITCHLING_LIST.extend([adjax, reduple, rushmore, redactyl, scannequin, zeedub])
|
|
@@ -69,6 +72,7 @@ BUILTIN_GLITCHLINGS: dict[str, Glitchling] = {
|
|
|
69
72
|
|
|
70
73
|
_BUILTIN_GLITCHLING_TYPES: dict[str, type[Glitchling]] = {
|
|
71
74
|
typogre.name.lower(): Typogre,
|
|
75
|
+
apostrofae.name.lower(): Apostrofae,
|
|
72
76
|
mim1c.name.lower(): Mim1c,
|
|
73
77
|
adjax.name.lower(): Adjax,
|
|
74
78
|
reduple.name.lower(): Reduple,
|
glitchlings/zoo/_rate.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
"""Utilities for handling legacy parameter names across glitchling classes."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
3
7
|
|
|
4
8
|
def resolve_rate(
|
|
5
9
|
*,
|
|
@@ -8,11 +12,120 @@ def resolve_rate(
|
|
|
8
12
|
default: float,
|
|
9
13
|
legacy_name: str,
|
|
10
14
|
) -> float:
|
|
11
|
-
"""Return the effective rate while enforcing mutual exclusivity.
|
|
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
|
+
"""
|
|
12
57
|
if rate is not None and legacy_value is not None:
|
|
13
58
|
raise ValueError(f"Specify either 'rate' or '{legacy_name}', not both.")
|
|
59
|
+
|
|
14
60
|
if rate is not None:
|
|
15
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
|
+
|
|
16
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
|
+
)
|
|
17
129
|
return legacy_value
|
|
130
|
+
|
|
18
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
|
+
]
|
glitchlings/zoo/adjax.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import random
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any, cast
|
|
5
5
|
|
|
6
6
|
from ._rate import resolve_rate
|
|
7
|
+
from ._rust_extensions import get_rust_operation
|
|
7
8
|
from ._text_utils import split_preserving_whitespace, split_token_edges
|
|
8
9
|
from .core import AttackWave, Glitchling
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
except ImportError: # pragma: no cover - optional acceleration
|
|
13
|
-
_swap_adjacent_words_rust = None
|
|
11
|
+
# Load Rust-accelerated operation if available
|
|
12
|
+
_swap_adjacent_words_rust = get_rust_operation("swap_adjacent_words")
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def _python_swap_adjacent_words(
|
|
@@ -83,7 +82,7 @@ def swap_adjacent_words(
|
|
|
83
82
|
rng = random.Random(seed)
|
|
84
83
|
|
|
85
84
|
if _swap_adjacent_words_rust is not None:
|
|
86
|
-
return _swap_adjacent_words_rust(text, clamped_rate, rng)
|
|
85
|
+
return cast(str, _swap_adjacent_words_rust(text, clamped_rate, rng))
|
|
87
86
|
|
|
88
87
|
return _python_swap_adjacent_words(text, rate=clamped_rate, rng=rng)
|
|
89
88
|
|