glitchlings 0.4.3__cp310-cp310-manylinux_2_28_x86_64.whl → 0.4.4__cp310-cp310-manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of glitchlings might be problematic. Click here for more details.

@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  from collections.abc import Callable, Sequence
6
6
  from typing import Any
7
7
 
8
+ from ..zoo.core import Gaggle, _is_transcript
9
+
8
10
 
9
11
  def resolve_environment(
10
12
  env: Any,
@@ -65,4 +67,87 @@ def resolve_columns(dataset: Any, columns: Sequence[str] | None) -> list[str]:
65
67
  raise ValueError("Unable to determine which dataset columns to corrupt.")
66
68
 
67
69
 
68
- __all__ = ["resolve_columns", "resolve_environment"]
70
+ def normalise_column_spec(
71
+ columns: str | int | Sequence[str | int] | None,
72
+ ) -> list[str | int] | None:
73
+ """Normalise a column specification into a list of keys or indices.
74
+
75
+ Args:
76
+ columns: Column specification as a single value, sequence of values, or None.
77
+
78
+ Returns:
79
+ A list of column identifiers, or None if input was None.
80
+
81
+ Raises:
82
+ ValueError: If an empty sequence is provided.
83
+ """
84
+ if columns is None:
85
+ return None
86
+
87
+ if isinstance(columns, (str, int)):
88
+ return [columns]
89
+
90
+ normalised = list(columns)
91
+ if not normalised:
92
+ raise ValueError("At least one column must be specified")
93
+ return normalised
94
+
95
+
96
+ def is_textual_candidate(value: Any) -> bool:
97
+ """Return ``True`` when ``value`` looks like text that glitchlings can corrupt.
98
+
99
+ Args:
100
+ value: The value to check for textual content.
101
+
102
+ Returns:
103
+ True if the value appears to be textual content.
104
+ """
105
+ if isinstance(value, str):
106
+ return True
107
+
108
+ if _is_transcript(value, allow_empty=False, require_all_content=True):
109
+ return True
110
+
111
+ if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray, str)):
112
+ if not value:
113
+ return False
114
+ if all(isinstance(item, str) for item in value):
115
+ return True
116
+ if _is_transcript(list(value), allow_empty=False, require_all_content=True):
117
+ return True
118
+
119
+ return False
120
+
121
+
122
+ def corrupt_text_value(value: Any, gaggle: Gaggle) -> Any:
123
+ """Return ``value`` with glitchlings applied when possible.
124
+
125
+ Args:
126
+ value: The value to corrupt (string, transcript, or sequence of strings).
127
+ gaggle: The gaggle of glitchlings to apply.
128
+
129
+ Returns:
130
+ The corrupted value, preserving the original type where possible.
131
+ """
132
+ if isinstance(value, str):
133
+ return gaggle.corrupt(value)
134
+
135
+ if _is_transcript(value, allow_empty=True):
136
+ return gaggle.corrupt(value)
137
+
138
+ if isinstance(value, list) and value and all(isinstance(item, str) for item in value):
139
+ return [gaggle.corrupt(item) for item in value]
140
+
141
+ if isinstance(value, tuple) and value and all(isinstance(item, str) for item in value):
142
+ return tuple(gaggle.corrupt(item) for item in value)
143
+
144
+ return value
145
+
146
+
147
+ __all__ = [
148
+ "corrupt_text_value",
149
+ "is_textual_candidate",
150
+ "normalise_column_spec",
151
+ "resolve_columns",
152
+ "resolve_environment",
153
+ ]
@@ -9,63 +9,13 @@ from ..compat import get_torch_dataloader, require_torch
9
9
  from ..compat import torch as _torch_dependency
10
10
  from ..util.adapters import coerce_gaggle
11
11
  from ..zoo import Gaggle, Glitchling
12
- from ..zoo.core import _is_transcript
13
-
14
-
15
- def _normalise_columns(columns: str | int | Sequence[str | int] | None) -> list[str | int] | None:
16
- """Normalise a column specification into a list of keys or indices."""
17
- if columns is None:
18
- return None
19
-
20
- if isinstance(columns, (str, int)):
21
- return [columns]
22
-
23
- normalised = list(columns)
24
- if not normalised:
25
- raise ValueError("At least one column must be specified")
26
- return normalised
27
-
28
-
29
- def _is_textual_candidate(value: Any) -> bool:
30
- """Return ``True`` when ``value`` looks like text that glitchlings can corrupt."""
31
- if isinstance(value, str):
32
- return True
33
-
34
- if _is_transcript(value, allow_empty=False, require_all_content=True):
35
- return True
36
-
37
- if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray, str)):
38
- if not value:
39
- return False
40
- if all(isinstance(item, str) for item in value):
41
- return True
42
- if _is_transcript(list(value), allow_empty=False, require_all_content=True):
43
- return True
44
-
45
- return False
46
-
47
-
48
- def _corrupt_text(value: Any, gaggle: Gaggle) -> Any:
49
- """Return ``value`` with glitchlings applied when possible."""
50
- if isinstance(value, str):
51
- return gaggle.corrupt(value)
52
-
53
- if _is_transcript(value, allow_empty=True):
54
- return gaggle.corrupt(value)
55
-
56
- if isinstance(value, list) and value and all(isinstance(item, str) for item in value):
57
- return [gaggle.corrupt(item) for item in value]
58
-
59
- if isinstance(value, tuple) and value and all(isinstance(item, str) for item in value):
60
- return tuple(gaggle.corrupt(item) for item in value)
61
-
62
- return value
12
+ from ._shared import corrupt_text_value, is_textual_candidate, normalise_column_spec
63
13
 
64
14
 
65
15
  def _apply_to_batch(batch: Any, targets: list[str | int] | None, gaggle: Gaggle) -> Any:
66
16
  """Return ``batch`` with glitchlings applied to the specified ``targets``."""
67
17
  if targets is None:
68
- return _corrupt_text(batch, gaggle)
18
+ return corrupt_text_value(batch, gaggle)
69
19
 
70
20
  if isinstance(batch, Mapping):
71
21
  mutated = cast(MutableMapping[str, Any], dict(batch))
@@ -74,7 +24,7 @@ def _apply_to_batch(batch: Any, targets: list[str | int] | None, gaggle: Gaggle)
74
24
  raise TypeError("Mapping batches require string column names")
75
25
  if key not in mutated:
76
26
  raise ValueError(f"Column '{key}' not found in DataLoader batch")
77
- mutated[key] = _corrupt_text(mutated[key], gaggle)
27
+ mutated[key] = corrupt_text_value(mutated[key], gaggle)
78
28
  return mutated
79
29
 
80
30
  if isinstance(batch, Sequence) and not isinstance(batch, (bytes, bytearray, str)):
@@ -83,7 +33,7 @@ def _apply_to_batch(batch: Any, targets: list[str | int] | None, gaggle: Gaggle)
83
33
  if not isinstance(index, int):
84
34
  raise TypeError("Sequence batches require integer column indices")
85
35
  try:
86
- mutated_sequence[index] = _corrupt_text(mutated_sequence[index], gaggle)
36
+ mutated_sequence[index] = corrupt_text_value(mutated_sequence[index], gaggle)
87
37
  except IndexError as exc: # pragma: no cover - defensive
88
38
  raise IndexError("Column index out of range for DataLoader batch") from exc
89
39
  if isinstance(batch, tuple):
@@ -96,20 +46,20 @@ def _apply_to_batch(batch: Any, targets: list[str | int] | None, gaggle: Gaggle)
96
46
  def _infer_targets(batch: Any) -> list[str | int] | None:
97
47
  """Infer which fields should be glitched from a representative ``batch``."""
98
48
  if isinstance(batch, Mapping):
99
- inferred = [key for key, value in batch.items() if _is_textual_candidate(value)]
49
+ inferred = [key for key, value in batch.items() if is_textual_candidate(value)]
100
50
  if inferred:
101
51
  return inferred
102
52
  raise ValueError("Unable to infer which mapping columns contain text")
103
53
 
104
54
  if isinstance(batch, Sequence) and not isinstance(batch, (bytes, bytearray, str)):
105
55
  inferred_indices: list[str | int] = [
106
- idx for idx, value in enumerate(batch) if _is_textual_candidate(value)
56
+ idx for idx, value in enumerate(batch) if is_textual_candidate(value)
107
57
  ]
108
58
  if inferred_indices:
109
59
  return inferred_indices
110
60
  raise ValueError("Unable to infer which sequence indices contain text")
111
61
 
112
- if _is_textual_candidate(batch):
62
+ if is_textual_candidate(batch):
113
63
  return None
114
64
 
115
65
  raise TypeError("Unsupported DataLoader batch type for glitching")
@@ -184,7 +134,7 @@ def _ensure_dataloader_class() -> type[Any]:
184
134
  ) -> _GlitchedDataLoader:
185
135
  """Return a lazily glitched view of the loader's batches."""
186
136
  gaggle = coerce_gaggle(glitchlings, seed=seed)
187
- normalised = _normalise_columns(columns)
137
+ normalised = normalise_column_spec(columns)
188
138
  return _GlitchedDataLoader(self, gaggle, columns=normalised)
189
139
 
190
140
  setattr(dataloader_cls, "glitch", glitch)
@@ -8,29 +8,7 @@ from typing import Any, cast
8
8
  from ..compat import get_pytorch_lightning_datamodule, require_pytorch_lightning
9
9
  from ..util.adapters import coerce_gaggle
10
10
  from ..zoo import Gaggle, Glitchling
11
- from ..zoo.core import _is_transcript
12
-
13
-
14
- def _normalise_columns(column: str | Sequence[str]) -> list[str]:
15
- """Normalise a column specification to a list."""
16
- if isinstance(column, str):
17
- return [column]
18
-
19
- normalised = list(column)
20
- if not normalised:
21
- raise ValueError("At least one column must be specified")
22
- return normalised
23
-
24
-
25
- def _glitch_value(value: Any, gaggle: Gaggle) -> Any:
26
- """Apply glitchlings to a value when it contains textual content."""
27
- if isinstance(value, str) or _is_transcript(value, allow_empty=False, require_all_content=True):
28
- return gaggle.corrupt(value)
29
-
30
- if isinstance(value, Sequence) and value and all(isinstance(item, str) for item in value):
31
- return [gaggle.corrupt(item) for item in value]
32
-
33
- return value
11
+ from ._shared import corrupt_text_value, normalise_column_spec
34
12
 
35
13
 
36
14
  def _glitch_batch(batch: Any, columns: list[str], gaggle: Gaggle) -> Any:
@@ -49,7 +27,7 @@ def _glitch_batch(batch: Any, columns: list[str], gaggle: Gaggle) -> Any:
49
27
  raise ValueError(f"Columns not found in batch: {missing_str}")
50
28
 
51
29
  for column in columns:
52
- mutated[column] = _glitch_value(mutated[column], gaggle)
30
+ mutated[column] = corrupt_text_value(mutated[column], gaggle)
53
31
 
54
32
  return mutated
55
33
 
@@ -111,9 +89,13 @@ def _glitch_datamodule(
111
89
  ) -> Any:
112
90
  """Return a proxy that applies glitchlings to batches from the datamodule."""
113
91
 
114
- columns = _normalise_columns(column)
92
+ columns = normalise_column_spec(column)
93
+ if columns is None: # pragma: no cover - defensive
94
+ raise ValueError("At least one column must be specified")
95
+ # Lightning datamodules only support string column names (mapping keys)
96
+ columns_str = cast(list[str], columns)
115
97
  gaggle = coerce_gaggle(glitchlings, seed=seed)
116
- return _GlitchedLightningDataModule(datamodule, columns, gaggle)
98
+ return _GlitchedLightningDataModule(datamodule, columns_str, gaggle)
117
99
 
118
100
 
119
101
  class _GlitchedLightningDataModule:
@@ -102,7 +102,11 @@ from .vector import VectorLexicon, build_vector_cache # noqa: E402
102
102
  _WordNetLexicon: type[LexiconBackend] | None
103
103
  try: # pragma: no cover - optional dependency
104
104
  from .wordnet import WordNetLexicon as _WordNetLexicon
105
- except Exception: # pragma: no cover - triggered when nltk unavailable
105
+ except (
106
+ ImportError,
107
+ ModuleNotFoundError,
108
+ AttributeError,
109
+ ): # pragma: no cover - triggered when nltk unavailable
106
110
  _WordNetLexicon = None
107
111
 
108
112
  WordNetLexicon: type[LexiconBackend] | None = _WordNetLexicon
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
@@ -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:
glitchlings/zoo/zeedub.py CHANGED
@@ -6,12 +6,11 @@ from collections.abc import Sequence
6
6
  from typing import Any, cast
7
7
 
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 inject_zero_widths as _inject_zero_widths_rust
13
- except ImportError: # pragma: no cover - compiled extension not present
14
- _inject_zero_widths_rust = None
12
+ # Load Rust-accelerated operation if available
13
+ _inject_zero_widths_rust = get_rust_operation("inject_zero_widths")
15
14
 
16
15
  _DEFAULT_ZERO_WIDTH_CHARACTERS: tuple[str, ...] = (
17
16
  "\u200b", # ZERO WIDTH SPACE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glitchlings
3
- Version: 0.4.3
3
+ Version: 0.4.4
4
4
  Summary: Monsters for your language games.
5
5
  Author: osoleve
6
6
  License: Apache License
@@ -217,6 +217,7 @@ Classifier: Programming Language :: Python :: 3
217
217
  Classifier: Programming Language :: Python :: 3.10
218
218
  Classifier: Programming Language :: Python :: 3.11
219
219
  Classifier: Programming Language :: Python :: 3.12
220
+ Classifier: Programming Language :: Python :: 3.13
220
221
  Classifier: Programming Language :: Rust
221
222
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
222
223
  Classifier: Topic :: Software Development :: Testing
@@ -237,9 +238,10 @@ Requires-Dist: mkdocs-material>=9.5.0; extra == "all"
237
238
  Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "all"
238
239
  Requires-Dist: mkdocstrings-python>=1.10.0; extra == "all"
239
240
  Requires-Dist: mypy>=1.8.0; extra == "all"
240
- Requires-Dist: numpy<=2.0,>=1.24; extra == "all"
241
+ Requires-Dist: numpy<3.0,>=1.24; extra == "all"
241
242
  Requires-Dist: pre-commit>=3.8.0; extra == "all"
242
243
  Requires-Dist: pytest>=8.0.0; extra == "all"
244
+ Requires-Dist: pytest-cov>=4.1.0; extra == "all"
243
245
  Requires-Dist: ruff>=0.6.0; extra == "all"
244
246
  Requires-Dist: verifiers>=0.1.3.post0; extra == "all"
245
247
  Provides-Extra: hf
@@ -247,7 +249,7 @@ Requires-Dist: datasets>=4.0.0; extra == "hf"
247
249
  Provides-Extra: lightning
248
250
  Requires-Dist: pytorch_lightning>=2.0.0; extra == "lightning"
249
251
  Provides-Extra: vectors
250
- Requires-Dist: numpy<=2.0,>=1.24; extra == "vectors"
252
+ Requires-Dist: numpy<3.0,>=1.24; extra == "vectors"
251
253
  Requires-Dist: spacy>=3.7.2; extra == "vectors"
252
254
  Requires-Dist: gensim>=4.3.2; extra == "vectors"
253
255
  Provides-Extra: st
@@ -259,8 +261,9 @@ Provides-Extra: torch
259
261
  Requires-Dist: torch>=2.0.0; extra == "torch"
260
262
  Provides-Extra: dev
261
263
  Requires-Dist: pytest>=8.0.0; extra == "dev"
264
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
262
265
  Requires-Dist: hypothesis>=6.140.0; extra == "dev"
263
- Requires-Dist: numpy<=2.0,>=1.24; extra == "dev"
266
+ Requires-Dist: numpy<3.0,>=1.24; extra == "dev"
264
267
  Requires-Dist: mkdocs>=1.6.0; extra == "dev"
265
268
  Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "dev"
266
269
  Requires-Dist: mkdocs-material>=9.5.0; extra == "dev"
@@ -1,17 +1,17 @@
1
1
  glitchlings/__init__.py,sha256=bkyRgzjC8ssidEO9UL9VpbYXQxTV1Hz3VAPOIqd9uMg,1182
2
2
  glitchlings/__main__.py,sha256=f-P4jiVBd7ZpS6QxRpa_6SJgOG03UhZhcWasMDRWLs8,120
3
- glitchlings/_zoo_rust.cpython-310-x86_64-linux-gnu.so,sha256=q_e2Rp5VFO_uKkpCWfepa8whqnhzb9wDGAm3fmYtJN0,3398104
3
+ glitchlings/_zoo_rust.cpython-310-x86_64-linux-gnu.so,sha256=9OJBoKN9JRtHsfVnBlJMsGWHnYaODzMX5c1e49Mt6ig,3397688
4
4
  glitchlings/compat.py,sha256=T_5Ia8yCzZvsMdicZ2TCcOgDO53_AjNGkSXWTR_qEnA,8908
5
5
  glitchlings/config.py,sha256=ofxDMkoMg4j51CFube54aca1Ky9y_ZeVktXpeUEdWmA,12953
6
6
  glitchlings/config.toml,sha256=04-Y_JCdQU68SRmwk2qZqrH_bbX4jEH9uh7URtxdIHA,99
7
7
  glitchlings/main.py,sha256=uw8VbDgxov1m-wYHPDl2dP5ItpLB4ZHpb0ChJXzcL0o,10623
8
8
  glitchlings/dlc/__init__.py,sha256=qlY4nuagy4AAWuPMwmuhwK2m36ktp-qkeiIxC7OXg34,305
9
- glitchlings/dlc/_shared.py,sha256=EFSnush3rjjaf4La5QfVaf_KEp0U_l_3-q4PKx0A6NQ,1972
9
+ glitchlings/dlc/_shared.py,sha256=OmEjJmSs1pQ7j1ggR_H8D8RDp5E1ZqOnzSIxyqRE1aE,4407
10
10
  glitchlings/dlc/huggingface.py,sha256=9lW7TnTHA_bXyo4Is8pymZchrB9BIL1bMCP2p7LCMtg,2576
11
11
  glitchlings/dlc/prime.py,sha256=qGFI1d4BiOEIgQZ5v9QnlbYx4J4q-vNlh5tWZng11xs,8607
12
- glitchlings/dlc/pytorch.py,sha256=tfHEDsDAOUnEvImFgRMjqC7Ig_aNVO8suXKpv24C2cA,7823
13
- glitchlings/dlc/pytorch_lightning.py,sha256=Om45BHYx8tMoUwYOOTk5B5A5AIjNkh58V37OC2IBFxE,8553
14
- glitchlings/lexicon/__init__.py,sha256=PLuu63iX6GSRypGI4DxiN_U-QmqmDobk1Xb7B5IrsZg,5951
12
+ glitchlings/dlc/pytorch.py,sha256=QaiIYyQ3koy2-enhUI9WY3SIMRX65gmsnjDvCsf8xbg,6233
13
+ glitchlings/dlc/pytorch_lightning.py,sha256=Ls7Xh5Mg643Tyk3KvCMq_MsB4vvekfUUZOhE0z4K22c,8074
14
+ glitchlings/lexicon/__init__.py,sha256=ooEPcAJhCI2Nw5z8OsQ0EtVpKBfiTrU0-AQJq8Zn2nQ,6007
15
15
  glitchlings/lexicon/_cache.py,sha256=aWSUb5Ex162dr3HouO2Ic2O8ck3ViEFWs8-XMLKMeJ0,4086
16
16
  glitchlings/lexicon/metrics.py,sha256=VBFfFpxjiEwZtK-jS55H8xP7MTC_0OjY8lQ5zSQ9aTY,4572
17
17
  glitchlings/lexicon/vector.py,sha256=yWf-vlN2OEHnTCPu7tgDnJbhm47cmhdrTtjR0RZKkUM,22530
@@ -21,26 +21,27 @@ glitchlings/util/__init__.py,sha256=vc3EAY8ehRjbOiryFdaqvvljXcyNGtZSPiEp9ok1vVw,
21
21
  glitchlings/util/adapters.py,sha256=psxQFYSFmh1u7NuqtIrKwQP5FOhOrZoxZzc7X7DDi9U,693
22
22
  glitchlings/zoo/__init__.py,sha256=1dWZPCTXuh5J7WdCxHX7ZX9bNd8bakzYndxQRhF43i8,5243
23
23
  glitchlings/zoo/_ocr_confusions.py,sha256=Ju2_avXiwsr1p8zWFUTOzMxJ8vT5PpYobuGIn4L_sqI,1204
24
- glitchlings/zoo/_rate.py,sha256=Vb1_5HAzrqr9eAh_zzngSV-d0zI264zcYspnT3VHPkE,504
24
+ glitchlings/zoo/_rate.py,sha256=tkIlXHewE8s9w1jpCw8ZzkVN31690FAnvTM_R3dCIpY,3579
25
+ glitchlings/zoo/_rust_extensions.py,sha256=Bsd0kiPB1rUn5x3k7ykydFuk2YSvXS9CQGPRlE5XzXY,4211
25
26
  glitchlings/zoo/_sampling.py,sha256=KrWyUSsYXghlvktS5hQBO0bPqywEEyA49A2qDWInB7Q,1586
26
27
  glitchlings/zoo/_text_utils.py,sha256=fS5L_eq-foBbBdiv4ymI8-O0D0csc3yDekHpX8bqfV4,2754
27
- glitchlings/zoo/adjax.py,sha256=TABKGQOwpyj_5czSoN8tPyEinwp8oZHKOBfU78ae9n0,3545
28
- glitchlings/zoo/apostrofae.py,sha256=m2-VPO-ahp0zAEJTHPItXMwnpD9D8bQIjVyyIRzj46k,3922
29
- glitchlings/zoo/core.py,sha256=3IHYEo8f2K7q4EbSZBYPb4MQXUVoMPm6B0IgsjiWNXk,20493
30
- glitchlings/zoo/jargoyle.py,sha256=zGXi6WFSzYA_44UXvyK0aj18CMFHIFL4eQeijEHfZl4,11568
28
+ glitchlings/zoo/adjax.py,sha256=XT5kKqPOUPgKSDOcR__HBnv4OXtBKee40GuNNmm1GYI,3518
29
+ glitchlings/zoo/apostrofae.py,sha256=qjpfnxdPWXMNzZnSD7UMfvHyzGKa7TLsvUhMsIvjwj8,3822
30
+ glitchlings/zoo/core.py,sha256=dRzUTmhOswDV0hWcaD-Sx7rZdPlrszn7C_1G2xd4ECk,20675
31
+ glitchlings/zoo/jargoyle.py,sha256=2TGU_z8gILwQ-lyZEqvmsrLupxqb8ydlDiwcp-O6WwY,11679
31
32
  glitchlings/zoo/mim1c.py,sha256=-fgodKWZq--Xw8L2t1EqNbsh48bwX5jZxmiXdoaQShI,3437
32
33
  glitchlings/zoo/ocr_confusions.tsv,sha256=KhtR7vJDTITpfTSGa-I7RHr6CK7LkGi2KjdhEWipI6o,183
33
- glitchlings/zoo/redactyl.py,sha256=9Rtgkg87LnGt47DHKsD8XW25gtg9pv2aXvrFv46XOTQ,5516
34
- glitchlings/zoo/reduple.py,sha256=ttHha3Yl0SRzEyAx9SfENbJRO_WhmJYL8ow5LGKn248,4258
35
- glitchlings/zoo/rushmore.py,sha256=R6dgt4HSvkt31foazNmUhO4wL9PHpjh_7pzJ8vQPgO0,4322
36
- glitchlings/zoo/scannequin.py,sha256=AQ7JPIxLiPFy4fDV6MgO4OFo34dMShc7sipStUaCG40,4900
37
- glitchlings/zoo/typogre.py,sha256=AuAtx-KyWrk-zX3uuxjkvjiduLyDwGJNW7XYktnsuos,6712
38
- glitchlings/zoo/zeedub.py,sha256=3VneZOEeL98Ek1VnZQI4V2o1alv41vvMzZXrKc9Lt1s,4875
34
+ glitchlings/zoo/redactyl.py,sha256=eWn7JC81BXkp2bSinwrBfU3jXukcUGDVkaa6BcGvte4,5559
35
+ glitchlings/zoo/reduple.py,sha256=zSc1N_-tz9Kl7CDMrdZKgCuW3Bxp_-g6axadAa6AszM,4224
36
+ glitchlings/zoo/rushmore.py,sha256=k429trwNPcWJHEOIoeGsdKBzJNL4Fxz9KRqX3Ro9u_0,4286
37
+ glitchlings/zoo/scannequin.py,sha256=GfjLYWWp-jdnOBmdg7gt5wQnobY8jWQHScB5EMgo6HE,4870
38
+ glitchlings/zoo/typogre.py,sha256=BQotNL-gn4PXQI9j63d2w9mQ4X6ZJKSJ4de-GN-gmUI,6686
39
+ glitchlings/zoo/zeedub.py,sha256=aNnjZGeTmMqA2WjgtGh7Fgl9pUQo3AZ2B-tYs2ZFOQE,4840
39
40
  glitchlings/zoo/assets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
41
  glitchlings/zoo/assets/apostrofae_pairs.json,sha256=bfjSEaMTI_axGNJ93nI431KXU0IVp7ayO42gGcMgL6U,521
41
- glitchlings-0.4.3.dist-info/METADATA,sha256=NV0-8T4jx5R2Eswhib6B29vAeoiXBdIDBFOu6KrzqdM,32242
42
- glitchlings-0.4.3.dist-info/WHEEL,sha256=yzF9ixp0XVYLhnovZSdud9vspTPdVe52BzwI7Tv3jTM,113
43
- glitchlings-0.4.3.dist-info/entry_points.txt,sha256=kGOwuAsjFDLtztLisaXtOouq9wFVMOJg5FzaAkg-Hto,54
44
- glitchlings-0.4.3.dist-info/top_level.txt,sha256=VHFNBrLjtDwPCYXbGKi6o17Eueedi81eNbR3hBOoST0,12
45
- glitchlings-0.4.3.dist-info/RECORD,,
46
- glitchlings-0.4.3.dist-info/licenses/LICENSE,sha256=YCvGip-LoaRyu6h0nPo71q6eHEkzUpsE11psDJOIRkw,11337
42
+ glitchlings-0.4.4.dist-info/METADATA,sha256=onpPJTtANv13MyyvYS930OWiJb_Ipxvje5sTrNmNPQw,32388
43
+ glitchlings-0.4.4.dist-info/WHEEL,sha256=yzF9ixp0XVYLhnovZSdud9vspTPdVe52BzwI7Tv3jTM,113
44
+ glitchlings-0.4.4.dist-info/entry_points.txt,sha256=kGOwuAsjFDLtztLisaXtOouq9wFVMOJg5FzaAkg-Hto,54
45
+ glitchlings-0.4.4.dist-info/top_level.txt,sha256=VHFNBrLjtDwPCYXbGKi6o17Eueedi81eNbR3hBOoST0,12
46
+ glitchlings-0.4.4.dist-info/RECORD,,
47
+ glitchlings-0.4.4.dist-info/licenses/LICENSE,sha256=YCvGip-LoaRyu6h0nPo71q6eHEkzUpsE11psDJOIRkw,11337