glitchlings 0.4.3__cp311-cp311-macosx_11_0_universal2.whl → 0.4.5__cp311-cp311-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.

Files changed (39) hide show
  1. glitchlings/__init__.py +4 -0
  2. glitchlings/_zoo_rust.cpython-311-darwin.so +0 -0
  3. glitchlings/compat.py +2 -4
  4. glitchlings/config.py +2 -4
  5. glitchlings/data/__init__.py +1 -0
  6. glitchlings/data/hokey_assets.json +193 -0
  7. glitchlings/dlc/_shared.py +86 -1
  8. glitchlings/dlc/huggingface.py +6 -6
  9. glitchlings/dlc/prime.py +1 -1
  10. glitchlings/dlc/pytorch.py +9 -59
  11. glitchlings/dlc/pytorch_lightning.py +10 -34
  12. glitchlings/lexicon/__init__.py +5 -1
  13. glitchlings/lexicon/_cache.py +3 -5
  14. glitchlings/lexicon/vector.py +6 -5
  15. glitchlings/lexicon/wordnet.py +4 -8
  16. glitchlings/util/hokey_generator.py +144 -0
  17. glitchlings/util/stretch_locator.py +140 -0
  18. glitchlings/util/stretchability.py +375 -0
  19. glitchlings/zoo/__init__.py +5 -1
  20. glitchlings/zoo/_rate.py +114 -1
  21. glitchlings/zoo/_rust_extensions.py +143 -0
  22. glitchlings/zoo/adjax.py +3 -4
  23. glitchlings/zoo/apostrofae.py +3 -4
  24. glitchlings/zoo/core.py +21 -9
  25. glitchlings/zoo/hokey.py +173 -0
  26. glitchlings/zoo/jargoyle.py +6 -2
  27. glitchlings/zoo/redactyl.py +4 -5
  28. glitchlings/zoo/reduple.py +3 -4
  29. glitchlings/zoo/rushmore.py +3 -4
  30. glitchlings/zoo/scannequin.py +3 -4
  31. glitchlings/zoo/typogre.py +3 -4
  32. glitchlings/zoo/zeedub.py +3 -4
  33. {glitchlings-0.4.3.dist-info → glitchlings-0.4.5.dist-info}/METADATA +32 -8
  34. glitchlings-0.4.5.dist-info/RECORD +53 -0
  35. glitchlings-0.4.3.dist-info/RECORD +0 -46
  36. {glitchlings-0.4.3.dist-info → glitchlings-0.4.5.dist-info}/WHEEL +0 -0
  37. {glitchlings-0.4.3.dist-info → glitchlings-0.4.5.dist-info}/entry_points.txt +0 -0
  38. {glitchlings-0.4.3.dist-info → glitchlings-0.4.5.dist-info}/licenses/LICENSE +0 -0
  39. {glitchlings-0.4.3.dist-info → glitchlings-0.4.5.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@ from .core import (
14
14
  plan_glitchling_specs,
15
15
  plan_glitchlings,
16
16
  )
17
+ from .hokey import Hokey, hokey
17
18
  from .jargoyle import Jargoyle, jargoyle
18
19
  from .jargoyle import dependencies_available as _jargoyle_available
19
20
  from .mim1c import Mim1c, mim1c
@@ -33,6 +34,8 @@ __all__ = [
33
34
  "jargoyle",
34
35
  "Apostrofae",
35
36
  "apostrofae",
37
+ "Hokey",
38
+ "hokey",
36
39
  "Adjax",
37
40
  "adjax",
38
41
  "Reduple",
@@ -61,7 +64,7 @@ __all__ = [
61
64
 
62
65
  _HAS_JARGOYLE = _jargoyle_available()
63
66
 
64
- _BUILTIN_GLITCHLING_LIST: list[Glitchling] = [typogre, apostrofae, mim1c]
67
+ _BUILTIN_GLITCHLING_LIST: list[Glitchling] = [typogre, apostrofae, hokey, mim1c]
65
68
  if _HAS_JARGOYLE:
66
69
  _BUILTIN_GLITCHLING_LIST.append(jargoyle)
67
70
  _BUILTIN_GLITCHLING_LIST.extend([adjax, reduple, rushmore, redactyl, scannequin, zeedub])
@@ -73,6 +76,7 @@ BUILTIN_GLITCHLINGS: dict[str, Glitchling] = {
73
76
  _BUILTIN_GLITCHLING_TYPES: dict[str, type[Glitchling]] = {
74
77
  typogre.name.lower(): Typogre,
75
78
  apostrofae.name.lower(): Apostrofae,
79
+ hokey.name.lower(): Hokey,
76
80
  mim1c.name.lower(): Mim1c,
77
81
  adjax.name.lower(): Adjax,
78
82
  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
@@ -4,13 +4,12 @@ import random
4
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
- try:
11
- from glitchlings._zoo_rust import swap_adjacent_words as _swap_adjacent_words_rust
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(
@@ -8,12 +8,11 @@ from functools import cache
8
8
  from importlib import resources
9
9
  from typing import Any, Sequence, cast
10
10
 
11
+ from ._rust_extensions import get_rust_operation
11
12
  from .core import AttackOrder, AttackWave, Gaggle, Glitchling
12
13
 
13
- try: # pragma: no cover - compiled extension not present in pure-Python envs
14
- from glitchlings._zoo_rust import apostrofae as _apostrofae_rust
15
- except ImportError: # pragma: no cover - compiled extension not present
16
- _apostrofae_rust = None
14
+ # Load Rust-accelerated operation if available
15
+ _apostrofae_rust = get_rust_operation("apostrofae")
17
16
 
18
17
 
19
18
  @cache
glitchlings/zoo/core.py CHANGED
@@ -10,15 +10,13 @@ from hashlib import blake2s
10
10
  from typing import TYPE_CHECKING, Any, Callable, Protocol, TypedDict, TypeGuard, Union, cast
11
11
 
12
12
  from ..compat import get_datasets_dataset, require_datasets
13
+ from ._rust_extensions import get_rust_operation
13
14
 
14
15
  _DatasetsDataset = get_datasets_dataset()
15
16
 
16
- try: # pragma: no cover - optional dependency
17
- from glitchlings._zoo_rust import compose_glitchlings as _compose_glitchlings_rust
18
- from glitchlings._zoo_rust import plan_glitchlings as _plan_glitchlings_rust
19
- except ImportError: # pragma: no cover - compiled extension not present
20
- _compose_glitchlings_rust = None
21
- _plan_glitchlings_rust = None
17
+ # Load Rust-accelerated orchestration operations if available
18
+ _compose_glitchlings_rust = get_rust_operation("compose_glitchlings")
19
+ _plan_glitchlings_rust = get_rust_operation("plan_glitchlings")
22
20
 
23
21
 
24
22
  log = logging.getLogger(__name__)
@@ -135,7 +133,12 @@ def _plan_glitchlings_with_rust(
135
133
 
136
134
  try:
137
135
  plan = _plan_glitchlings_rust(specs, int(master_seed))
138
- except Exception: # pragma: no cover - defer to Python fallback on failure
136
+ except (
137
+ TypeError,
138
+ ValueError,
139
+ RuntimeError,
140
+ AttributeError,
141
+ ): # pragma: no cover - defer to Python fallback on failure
139
142
  log.debug("Rust orchestration planning failed; falling back to Python plan", exc_info=True)
140
143
  return None
141
144
 
@@ -537,10 +540,19 @@ class Gaggle(Glitchling):
537
540
  """Apply each glitchling to string input sequentially."""
538
541
  master_seed = self.seed
539
542
  descriptors = self._pipeline_descriptors()
540
- if master_seed is not None and descriptors is not None:
543
+ if (
544
+ master_seed is not None
545
+ and descriptors is not None
546
+ and _compose_glitchlings_rust is not None
547
+ ):
541
548
  try:
542
549
  return cast(str, _compose_glitchlings_rust(text, descriptors, master_seed))
543
- except Exception: # pragma: no cover - fall back to Python execution
550
+ except (
551
+ TypeError,
552
+ ValueError,
553
+ RuntimeError,
554
+ AttributeError,
555
+ ): # pragma: no cover - fall back to Python execution
544
556
  log.debug("Rust pipeline failed; falling back", exc_info=True)
545
557
 
546
558
  corrupted = text
@@ -0,0 +1,173 @@
1
+ """Hokey glitchling that performs expressive lengthening."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import random
6
+ from typing import Any, cast
7
+
8
+ from ..util.hokey_generator import HokeyConfig, HokeyGenerator, StretchEvent
9
+ from ..util.stretchability import StretchabilityAnalyzer
10
+ from ._rust_extensions import get_rust_operation
11
+ from .core import AttackOrder, AttackWave, Gaggle
12
+ from .core import Glitchling as GlitchlingBase
13
+
14
+ _hokey_rust = get_rust_operation("hokey")
15
+ _ANALYZER = StretchabilityAnalyzer()
16
+ _GENERATOR = HokeyGenerator(analyzer=_ANALYZER)
17
+
18
+
19
+ def _python_extend_vowels(
20
+ text: str,
21
+ *,
22
+ rate: float,
23
+ extension_min: int,
24
+ extension_max: int,
25
+ word_length_threshold: int,
26
+ base_p: float,
27
+ rng: random.Random,
28
+ return_trace: bool = False,
29
+ ) -> str | tuple[str, list[StretchEvent]]:
30
+ config = HokeyConfig(
31
+ rate=rate,
32
+ extension_min=extension_min,
33
+ extension_max=extension_max,
34
+ word_length_threshold=word_length_threshold,
35
+ base_p=base_p,
36
+ )
37
+ result, events = _GENERATOR.generate(text, rng=rng, config=config)
38
+ return (result, events) if return_trace else result
39
+
40
+
41
+ def extend_vowels(
42
+ text: str,
43
+ rate: float = 0.3,
44
+ extension_min: int = 2,
45
+ extension_max: int = 5,
46
+ word_length_threshold: int = 6,
47
+ seed: int | None = None,
48
+ rng: random.Random | None = None,
49
+ *,
50
+ return_trace: bool = False,
51
+ base_p: float | None = None,
52
+ ) -> str | tuple[str, list[StretchEvent]]:
53
+ """Extend expressive segments of words for emphasis.
54
+
55
+ Parameters
56
+ ----------
57
+ text : str
58
+ Input text to transform.
59
+ rate : float, optional
60
+ Global selection rate for candidate words.
61
+ extension_min : int, optional
62
+ Minimum number of extra repetitions for the stretch unit.
63
+ extension_max : int, optional
64
+ Maximum number of extra repetitions for the stretch unit.
65
+ word_length_threshold : int, optional
66
+ Preferred maximum alphabetic length; longer words are de-emphasised but not
67
+ excluded.
68
+ seed : int, optional
69
+ Deterministic seed when ``rng`` is not supplied.
70
+ rng : random.Random, optional
71
+ Random number generator to drive sampling.
72
+ return_trace : bool, optional
73
+ When ``True`` also return the stretch events for introspection.
74
+ base_p : float, optional
75
+ Base probability for the negative-binomial sampler (heavier tails for smaller
76
+ values). Defaults to ``0.45``.
77
+ """
78
+ if not text:
79
+ empty_trace: list[StretchEvent] = []
80
+ return (text, empty_trace) if return_trace else text
81
+
82
+ if rng is None:
83
+ rng = random.Random(seed)
84
+ base_probability = base_p if base_p is not None else 0.45
85
+
86
+ if return_trace or _hokey_rust is None:
87
+ return _python_extend_vowels(
88
+ text,
89
+ rate=rate,
90
+ extension_min=extension_min,
91
+ extension_max=extension_max,
92
+ word_length_threshold=word_length_threshold,
93
+ base_p=base_probability,
94
+ rng=rng,
95
+ return_trace=return_trace,
96
+ )
97
+
98
+ return cast(
99
+ str,
100
+ _hokey_rust(
101
+ text,
102
+ rate,
103
+ extension_min,
104
+ extension_max,
105
+ word_length_threshold,
106
+ base_probability,
107
+ rng,
108
+ ),
109
+ )
110
+
111
+
112
+ class Hokey(GlitchlingBase):
113
+ """Glitchling that stretches words using linguistic heuristics."""
114
+
115
+ seed: int | None
116
+
117
+ def __init__(
118
+ self,
119
+ *,
120
+ rate: float = 0.3,
121
+ extension_min: int = 2,
122
+ extension_max: int = 5,
123
+ word_length_threshold: int = 6,
124
+ base_p: float = 0.45,
125
+ seed: int | None = None,
126
+ ) -> None:
127
+ self._master_seed: int | None = seed
128
+
129
+ def _corruption_wrapper(text: str, **kwargs: Any) -> str:
130
+ result = extend_vowels(text, **kwargs)
131
+ return result if isinstance(result, str) else result[0]
132
+
133
+ super().__init__(
134
+ name="Hokey",
135
+ corruption_function=_corruption_wrapper,
136
+ scope=AttackWave.CHARACTER,
137
+ order=AttackOrder.FIRST,
138
+ seed=seed,
139
+ rate=rate,
140
+ extension_min=extension_min,
141
+ extension_max=extension_max,
142
+ word_length_threshold=word_length_threshold,
143
+ base_p=base_p,
144
+ )
145
+
146
+ def pipeline_operation(self) -> dict[str, Any] | None:
147
+ return {
148
+ "type": "hokey",
149
+ "rate": self.kwargs.get("rate", 0.3),
150
+ "extension_min": self.kwargs.get("extension_min", 2),
151
+ "extension_max": self.kwargs.get("extension_max", 5),
152
+ "word_length_threshold": self.kwargs.get("word_length_threshold", 6),
153
+ "base_p": self.kwargs.get("base_p", 0.45),
154
+ }
155
+
156
+ def reset_rng(self, seed: int | None = None) -> None:
157
+ if seed is not None:
158
+ self._master_seed = seed
159
+ super().reset_rng(seed)
160
+ if self.seed is None:
161
+ return
162
+ derived = Gaggle.derive_seed(int(seed), self.name, 0)
163
+ self.seed = int(derived)
164
+ self.rng = random.Random(self.seed)
165
+ self.kwargs["seed"] = self.seed
166
+ else:
167
+ super().reset_rng(None)
168
+
169
+
170
+ hokey = Hokey()
171
+
172
+
173
+ __all__ = ["Hokey", "hokey", "extend_vowels"]
@@ -14,7 +14,11 @@ _wordnet_module: ModuleType | None
14
14
 
15
15
  try: # pragma: no cover - optional WordNet dependency
16
16
  import glitchlings.lexicon.wordnet as _wordnet_module
17
- except Exception: # pragma: no cover - triggered when nltk unavailable
17
+ except (
18
+ ImportError,
19
+ ModuleNotFoundError,
20
+ AttributeError,
21
+ ): # pragma: no cover - triggered when nltk unavailable
18
22
  _wordnet_module = None
19
23
 
20
24
  _wordnet_runtime: ModuleType | None = _wordnet_module
@@ -49,7 +53,7 @@ def dependencies_available() -> bool:
49
53
  try:
50
54
  # Fall back to the configured default lexicon (typically the bundled vector cache).
51
55
  get_default_lexicon(seed=None)
52
- except Exception:
56
+ except (RuntimeError, ImportError, ModuleNotFoundError, AttributeError):
53
57
  return False
54
58
  return True
55
59
 
@@ -3,6 +3,7 @@ import re
3
3
  from typing import Any, cast
4
4
 
5
5
  from ._rate import resolve_rate
6
+ from ._rust_extensions import get_rust_operation
6
7
  from ._sampling import weighted_sample_without_replacement
7
8
  from ._text_utils import (
8
9
  WordToken,
@@ -13,11 +14,8 @@ from .core import AttackWave, Glitchling
13
14
 
14
15
  FULL_BLOCK = "█"
15
16
 
16
-
17
- try:
18
- from glitchlings._zoo_rust import redact_words as _redact_words_rust
19
- except ImportError: # pragma: no cover - compiled extension not present
20
- _redact_words_rust = None
17
+ # Load Rust-accelerated operation if available
18
+ _redact_words_rust = get_rust_operation("redact_words")
21
19
 
22
20
 
23
21
  def _python_redact_words(
@@ -119,6 +117,7 @@ def redact_words(
119
117
  use_rust = _redact_words_rust is not None and isinstance(merge_adjacent, bool)
120
118
 
121
119
  if use_rust:
120
+ assert _redact_words_rust is not None # Type narrowing for mypy
122
121
  return cast(
123
122
  str,
124
123
  _redact_words_rust(
@@ -2,13 +2,12 @@ import random
2
2
  from typing import Any, cast
3
3
 
4
4
  from ._rate import resolve_rate
5
+ from ._rust_extensions import get_rust_operation
5
6
  from ._text_utils import WordToken, collect_word_tokens, split_preserving_whitespace
6
7
  from .core import AttackWave, Glitchling
7
8
 
8
- try:
9
- from glitchlings._zoo_rust import reduplicate_words as _reduplicate_words_rust
10
- except ImportError: # pragma: no cover - compiled extension not present
11
- _reduplicate_words_rust = None
9
+ # Load Rust-accelerated operation if available
10
+ _reduplicate_words_rust = get_rust_operation("reduplicate_words")
12
11
 
13
12
 
14
13
  def _python_reduplicate_words(
@@ -4,13 +4,12 @@ import re
4
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 WordToken, collect_word_tokens, split_preserving_whitespace
8
9
  from .core import AttackWave, Glitchling
9
10
 
10
- try:
11
- from glitchlings._zoo_rust import delete_random_words as _delete_random_words_rust
12
- except ImportError: # pragma: no cover - compiled extension not present
13
- _delete_random_words_rust = None
11
+ # Load Rust-accelerated operation if available
12
+ _delete_random_words_rust = get_rust_operation("delete_random_words")
14
13
 
15
14
 
16
15
  def _python_delete_random_words(
@@ -4,12 +4,11 @@ from typing import Any, cast
4
4
 
5
5
  from ._ocr_confusions import load_confusion_table
6
6
  from ._rate import resolve_rate
7
+ from ._rust_extensions import get_rust_operation
7
8
  from .core import AttackOrder, AttackWave, Glitchling
8
9
 
9
- try:
10
- from glitchlings._zoo_rust import ocr_artifacts as _ocr_artifacts_rust
11
- except ImportError: # pragma: no cover - compiled extension not present
12
- _ocr_artifacts_rust = None
10
+ # Load Rust-accelerated operation if available
11
+ _ocr_artifacts_rust = get_rust_operation("ocr_artifacts")
13
12
 
14
13
 
15
14
  def _python_ocr_artifacts(
@@ -6,12 +6,11 @@ from typing import Any, Optional, cast
6
6
 
7
7
  from ..util import KEYNEIGHBORS
8
8
  from ._rate import resolve_rate
9
+ from ._rust_extensions import get_rust_operation
9
10
  from .core import AttackOrder, AttackWave, Glitchling
10
11
 
11
- try:
12
- from glitchlings._zoo_rust import fatfinger as _fatfinger_rust
13
- except ImportError: # pragma: no cover - compiled extension not present
14
- _fatfinger_rust = None
12
+ # Load Rust-accelerated operation if available
13
+ _fatfinger_rust = get_rust_operation("fatfinger")
15
14
 
16
15
 
17
16
  def _python_unichar(text: str, rng: random.Random) -> str: