glitchlings 0.4.1__tar.gz → 0.4.2__tar.gz

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 (83) hide show
  1. {glitchlings-0.4.1 → glitchlings-0.4.2}/PKG-INFO +67 -3
  2. {glitchlings-0.4.1 → glitchlings-0.4.2}/README.md +56 -2
  3. {glitchlings-0.4.1 → glitchlings-0.4.2}/pyproject.toml +74 -1
  4. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/src/text_buffer.rs +2 -8
  5. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/__init__.py +26 -17
  6. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/__main__.py +0 -1
  7. glitchlings-0.4.2/src/glitchlings/compat.py +215 -0
  8. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/config.py +136 -19
  9. glitchlings-0.4.2/src/glitchlings/dlc/_shared.py +68 -0
  10. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/dlc/huggingface.py +26 -41
  11. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/dlc/prime.py +64 -101
  12. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/lexicon/__init__.py +8 -19
  13. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/lexicon/_cache.py +0 -7
  14. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/lexicon/graph.py +4 -12
  15. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/lexicon/metrics.py +1 -8
  16. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/lexicon/vector.py +15 -34
  17. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/lexicon/wordnet.py +31 -32
  18. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/main.py +9 -13
  19. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/util/__init__.py +18 -4
  20. glitchlings-0.4.2/src/glitchlings/util/adapters.py +27 -0
  21. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/__init__.py +21 -14
  22. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/_ocr_confusions.py +1 -3
  23. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/_rate.py +1 -4
  24. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/_sampling.py +0 -1
  25. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/_text_utils.py +1 -5
  26. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/adjax.py +0 -2
  27. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/core.py +114 -75
  28. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/jargoyle.py +9 -14
  29. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/mim1c.py +11 -10
  30. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/redactyl.py +5 -8
  31. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/reduple.py +3 -1
  32. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/rushmore.py +2 -8
  33. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/scannequin.py +5 -4
  34. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/typogre.py +3 -7
  35. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/zeedub.py +2 -2
  36. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings.egg-info/PKG-INFO +67 -3
  37. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings.egg-info/SOURCES.txt +4 -24
  38. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings.egg-info/requires.txt +10 -0
  39. glitchlings-0.4.1/tests/test_benchmarks.py +0 -137
  40. glitchlings-0.4.1/tests/test_cli.py +0 -369
  41. glitchlings-0.4.1/tests/test_config.py +0 -196
  42. glitchlings-0.4.1/tests/test_dataset_corruption.py +0 -128
  43. glitchlings-0.4.1/tests/test_gaggle.py +0 -68
  44. glitchlings-0.4.1/tests/test_glitchling_core.py +0 -68
  45. glitchlings-0.4.1/tests/test_glitchlings_determinism.py +0 -103
  46. glitchlings-0.4.1/tests/test_graph_lexicon.py +0 -81
  47. glitchlings-0.4.1/tests/test_huggingface_dlc.py +0 -78
  48. glitchlings-0.4.1/tests/test_jargoyle.py +0 -209
  49. glitchlings-0.4.1/tests/test_keyboard_layouts.py +0 -42
  50. glitchlings-0.4.1/tests/test_lexicon_backends.py +0 -85
  51. glitchlings-0.4.1/tests/test_lexicon_config.py +0 -56
  52. glitchlings-0.4.1/tests/test_lexicon_metrics.py +0 -120
  53. glitchlings-0.4.1/tests/test_parameter_effects.py +0 -281
  54. glitchlings-0.4.1/tests/test_pipeline_operations.py +0 -95
  55. glitchlings-0.4.1/tests/test_prime_echo_chamber.py +0 -390
  56. glitchlings-0.4.1/tests/test_property_based.py +0 -150
  57. glitchlings-0.4.1/tests/test_rate_and_sampling.py +0 -51
  58. glitchlings-0.4.1/tests/test_rust_backed_glitchlings.py +0 -931
  59. glitchlings-0.4.1/tests/test_text_utils.py +0 -37
  60. glitchlings-0.4.1/tests/test_util.py +0 -35
  61. glitchlings-0.4.1/tests/test_vector_lexicon.py +0 -438
  62. {glitchlings-0.4.1 → glitchlings-0.4.2}/LICENSE +0 -0
  63. {glitchlings-0.4.1 → glitchlings-0.4.2}/MANIFEST.in +0 -0
  64. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/Cargo.lock +0 -0
  65. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/Cargo.toml +0 -0
  66. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/Cargo.toml +0 -0
  67. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/assets/ocr_confusions.tsv +0 -0
  68. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/build.rs +0 -0
  69. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/src/glitch_ops.rs +0 -0
  70. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/src/lib.rs +0 -0
  71. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/src/pipeline.rs +0 -0
  72. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/src/resources.rs +0 -0
  73. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/src/rng.rs +0 -0
  74. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/src/typogre.rs +0 -0
  75. {glitchlings-0.4.1 → glitchlings-0.4.2}/rust/zoo/src/zeedub.rs +0 -0
  76. {glitchlings-0.4.1 → glitchlings-0.4.2}/setup.cfg +0 -0
  77. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/config.toml +0 -0
  78. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/dlc/__init__.py +0 -0
  79. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/lexicon/data/default_vector_cache.json +0 -0
  80. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings/zoo/ocr_confusions.tsv +0 -0
  81. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings.egg-info/dependency_links.txt +0 -0
  82. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings.egg-info/entry_points.txt +0 -0
  83. {glitchlings-0.4.1 → glitchlings-0.4.2}/src/glitchlings.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glitchlings
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Monsters for your language games.
5
5
  Author: osoleve
6
6
  License: Apache License
@@ -239,6 +239,16 @@ Provides-Extra: dev
239
239
  Requires-Dist: pytest>=8.0.0; extra == "dev"
240
240
  Requires-Dist: hypothesis>=6.140.0; extra == "dev"
241
241
  Requires-Dist: numpy<=2.0,>=1.24; extra == "dev"
242
+ Requires-Dist: mkdocs>=1.6.0; extra == "dev"
243
+ Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "dev"
244
+ Requires-Dist: mkdocs-material>=9.5.0; extra == "dev"
245
+ Requires-Dist: mkdocstrings-python>=1.10.0; extra == "dev"
246
+ Requires-Dist: interrogate>=1.5.0; extra == "dev"
247
+ Requires-Dist: black>=24.4.0; extra == "dev"
248
+ Requires-Dist: isort>=5.13.0; extra == "dev"
249
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
250
+ Requires-Dist: mypy>=1.8.0; extra == "dev"
251
+ Requires-Dist: pre-commit>=3.8.0; extra == "dev"
242
252
  Dynamic: license-file
243
253
 
244
254
  #
@@ -338,10 +348,66 @@ They're horrible little gremlins, but they're not _unreasonable_.
338
348
 
339
349
  Keyboard warriors can challenge them directly via the `glitchlings` command:
340
350
 
351
+ <!-- BEGIN: CLI_USAGE -->
341
352
  ```bash
342
353
  # Discover which glitchlings are currently on the loose.
343
354
  glitchlings --list
355
+ ```
356
+
357
+ ```text
358
+ Typogre — scope: Character, order: early
359
+ Mim1c — scope: Character, order: last
360
+ Jargoyle — scope: Word, order: normal
361
+ Adjax — scope: Word, order: normal
362
+ Reduple — scope: Word, order: normal
363
+ Rushmore — scope: Word, order: normal
364
+ Redactyl — scope: Word, order: normal
365
+ Scannequin — scope: Character, order: late
366
+ Zeedub — scope: Character, order: last
367
+ ```
368
+
369
+ ```bash
370
+ # Review the full CLI contract.
371
+ glitchlings --help
372
+ ```
344
373
 
374
+ ```text
375
+ usage: glitchlings [-h] [-g SPEC] [-s SEED] [-f FILE] [--sample] [--diff]
376
+ [--list] [-c CONFIG]
377
+ [text]
378
+
379
+ Summon glitchlings to corrupt text. Provide input text as an argument, via
380
+ --file, or pipe it on stdin.
381
+
382
+ positional arguments:
383
+ text Text to corrupt. If omitted, stdin is used or --sample
384
+ provides fallback text.
385
+
386
+ options:
387
+ -h, --help show this help message and exit
388
+ -g SPEC, --glitchling SPEC
389
+ Glitchling to apply, optionally with parameters like
390
+ Typogre(rate=0.05). Repeat for multiples; defaults to
391
+ all built-ins.
392
+ -s SEED, --seed SEED Seed controlling deterministic corruption order
393
+ (default: 151).
394
+ -f FILE, --file FILE Read input text from a file instead of the command
395
+ line argument.
396
+ --sample Use the included SAMPLE_TEXT when no other input is
397
+ provided.
398
+ --diff Show a unified diff between the original and corrupted
399
+ text.
400
+ --list List available glitchlings and exit.
401
+ -c CONFIG, --config CONFIG
402
+ Load glitchlings from a YAML configuration file.
403
+ ```
404
+ <!-- END: CLI_USAGE -->
405
+
406
+ Run `python docs/build_cli_reference.py` whenever you tweak the CLI so the README stays in sync with the actual output. The script executes the commands above and replaces the block between the markers automatically.
407
+
408
+ Prefer inline tweaks? You can still configure glitchlings directly in the shell:
409
+
410
+ ```bash
345
411
  # Run Typogre against the contents of a file and inspect the diff.
346
412
  glitchlings -g typogre --file documents/report.txt --diff
347
413
 
@@ -355,8 +421,6 @@ echo "Beware LLM-written flavor-text" | glitchlings -g mim1c
355
421
  glitchlings --config experiments/chaos.yaml "Let slips the glitchlings of war"
356
422
  ```
357
423
 
358
- Use `--help` for a complete breakdown of available options, including support for parameterised glitchlings via `-g "Name(arg=value, ...)"` to mirror the Python API.
359
-
360
424
  Attack configurations live in plain YAML files so you can version-control experiments without touching code:
361
425
 
362
426
  ```yaml
@@ -95,10 +95,66 @@ They're horrible little gremlins, but they're not _unreasonable_.
95
95
 
96
96
  Keyboard warriors can challenge them directly via the `glitchlings` command:
97
97
 
98
+ <!-- BEGIN: CLI_USAGE -->
98
99
  ```bash
99
100
  # Discover which glitchlings are currently on the loose.
100
101
  glitchlings --list
102
+ ```
103
+
104
+ ```text
105
+ Typogre — scope: Character, order: early
106
+ Mim1c — scope: Character, order: last
107
+ Jargoyle — scope: Word, order: normal
108
+ Adjax — scope: Word, order: normal
109
+ Reduple — scope: Word, order: normal
110
+ Rushmore — scope: Word, order: normal
111
+ Redactyl — scope: Word, order: normal
112
+ Scannequin — scope: Character, order: late
113
+ Zeedub — scope: Character, order: last
114
+ ```
115
+
116
+ ```bash
117
+ # Review the full CLI contract.
118
+ glitchlings --help
119
+ ```
101
120
 
121
+ ```text
122
+ usage: glitchlings [-h] [-g SPEC] [-s SEED] [-f FILE] [--sample] [--diff]
123
+ [--list] [-c CONFIG]
124
+ [text]
125
+
126
+ Summon glitchlings to corrupt text. Provide input text as an argument, via
127
+ --file, or pipe it on stdin.
128
+
129
+ positional arguments:
130
+ text Text to corrupt. If omitted, stdin is used or --sample
131
+ provides fallback text.
132
+
133
+ options:
134
+ -h, --help show this help message and exit
135
+ -g SPEC, --glitchling SPEC
136
+ Glitchling to apply, optionally with parameters like
137
+ Typogre(rate=0.05). Repeat for multiples; defaults to
138
+ all built-ins.
139
+ -s SEED, --seed SEED Seed controlling deterministic corruption order
140
+ (default: 151).
141
+ -f FILE, --file FILE Read input text from a file instead of the command
142
+ line argument.
143
+ --sample Use the included SAMPLE_TEXT when no other input is
144
+ provided.
145
+ --diff Show a unified diff between the original and corrupted
146
+ text.
147
+ --list List available glitchlings and exit.
148
+ -c CONFIG, --config CONFIG
149
+ Load glitchlings from a YAML configuration file.
150
+ ```
151
+ <!-- END: CLI_USAGE -->
152
+
153
+ Run `python docs/build_cli_reference.py` whenever you tweak the CLI so the README stays in sync with the actual output. The script executes the commands above and replaces the block between the markers automatically.
154
+
155
+ Prefer inline tweaks? You can still configure glitchlings directly in the shell:
156
+
157
+ ```bash
102
158
  # Run Typogre against the contents of a file and inspect the diff.
103
159
  glitchlings -g typogre --file documents/report.txt --diff
104
160
 
@@ -112,8 +168,6 @@ echo "Beware LLM-written flavor-text" | glitchlings -g mim1c
112
168
  glitchlings --config experiments/chaos.yaml "Let slips the glitchlings of war"
113
169
  ```
114
170
 
115
- Use `--help` for a complete breakdown of available options, including support for parameterised glitchlings via `-g "Name(arg=value, ...)"` to mirror the Python API.
116
-
117
171
  Attack configurations live in plain YAML files so you can version-control experiments without touching code:
118
172
 
119
173
  ```yaml
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "glitchlings"
3
- version = "0.4.1"
3
+ version = "0.4.2"
4
4
  description = "Monsters for your language games."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -56,6 +56,16 @@ dev = [
56
56
  "pytest>=8.0.0",
57
57
  "hypothesis>=6.140.0",
58
58
  "numpy>=1.24,<=2.0",
59
+ "mkdocs>=1.6.0",
60
+ "mkdocstrings[python]>=0.24.0",
61
+ "mkdocs-material>=9.5.0",
62
+ "mkdocstrings-python>=1.10.0",
63
+ "interrogate>=1.5.0",
64
+ "black>=24.4.0",
65
+ "isort>=5.13.0",
66
+ "ruff>=0.6.0",
67
+ "mypy>=1.8.0",
68
+ "pre-commit>=3.8.0",
59
69
  ]
60
70
 
61
71
  [build-system]
@@ -85,3 +95,66 @@ debug = false
85
95
  pythonpath = [
86
96
  "src",
87
97
  ]
98
+
99
+ [tool.interrogate]
100
+ config = true
101
+ fail-under = 80
102
+ ignore-init-module = true
103
+ ignore-module = true
104
+ ignore-nested-functions = true
105
+ ignore-private = true
106
+ ignore-semiprivate = true
107
+ ignore-magic = true
108
+ ignore-property-decorators = false
109
+ color = true
110
+ quiet = false
111
+ exclude = [
112
+ "tests",
113
+ "docs",
114
+ "rust",
115
+ "benchmarks",
116
+ ]
117
+
118
+ [tool.black]
119
+ line-length = 100
120
+ target-version = ["py310"]
121
+
122
+ [tool.isort]
123
+ profile = "black"
124
+ line_length = 100
125
+
126
+ [tool.ruff]
127
+ target-version = "py310"
128
+ line-length = 100
129
+
130
+ [tool.ruff.lint]
131
+ select = ["E", "F", "I"]
132
+
133
+ [tool.mypy]
134
+ python_version = "3.10"
135
+ follow_imports = "skip"
136
+ ignore_missing_imports = true
137
+ enable_error_code = ["ignore-without-code"]
138
+
139
+ [[tool.mypy.overrides]]
140
+ module = [
141
+ "glitchlings.util.adapters",
142
+ "glitchlings.dlc._shared",
143
+ "glitchlings.dlc.huggingface",
144
+ "glitchlings.dlc.prime",
145
+ ]
146
+ strict = true
147
+
148
+ [[tool.mypy.overrides]]
149
+ module = [
150
+ "glitchlings.compat",
151
+ "glitchlings.config",
152
+ "glitchlings.lexicon",
153
+ "glitchlings.lexicon.*",
154
+ "glitchlings.zoo",
155
+ "glitchlings.zoo.*",
156
+ "glitchlings.main",
157
+ "glitchlings.__main__",
158
+ "glitchlings.__init__",
159
+ ]
160
+ ignore_errors = true
@@ -172,10 +172,7 @@ impl TextBuffer {
172
172
  }
173
173
 
174
174
  /// Replace multiple words in a single pass, avoiding repeated reindexing.
175
- pub fn replace_words_bulk<I>(
176
- &mut self,
177
- replacements: I,
178
- ) -> Result<(), TextBufferError>
175
+ pub fn replace_words_bulk<I>(&mut self, replacements: I) -> Result<(), TextBufferError>
179
176
  where
180
177
  I: IntoIterator<Item = (usize, String)>,
181
178
  {
@@ -435,10 +432,7 @@ mod tests {
435
432
  fn bulk_replace_words_updates_multiple_entries() {
436
433
  let mut buffer = TextBuffer::from_str("alpha beta gamma delta");
437
434
  buffer
438
- .replace_words_bulk(vec![
439
- (0, "delta".to_string()),
440
- (3, "alpha".to_string()),
441
- ])
435
+ .replace_words_bulk(vec![(0, "delta".to_string()), (3, "alpha".to_string())])
442
436
  .expect("bulk replace succeeds");
443
437
  assert_eq!(buffer.to_string(), "delta beta gamma alpha");
444
438
  let spans = buffer.spans();
@@ -1,29 +1,33 @@
1
+ from .config import AttackConfig, build_gaggle, load_attack_config
2
+ from .util import SAMPLE_TEXT
1
3
  from .zoo import (
2
- Typogre,
3
- typogre,
4
- Mim1c,
5
- mim1c,
6
- Jargoyle,
7
- jargoyle,
8
4
  Adjax,
9
- adjax,
5
+ Gaggle,
6
+ Glitchling,
7
+ Jargoyle,
8
+ Mim1c,
10
9
  Redactyl,
11
- redactyl,
12
10
  Reduple,
13
- reduple,
14
11
  Rushmore,
15
- rushmore,
16
12
  Scannequin,
17
- scannequin,
13
+ Typogre,
18
14
  Zeedub,
19
- zeedub,
20
- Glitchling,
21
- Gaggle,
15
+ adjax,
16
+ is_rust_pipeline_enabled,
17
+ is_rust_pipeline_supported,
18
+ jargoyle,
19
+ mim1c,
20
+ pipeline_feature_flag_enabled,
21
+ plan_glitchling_specs,
22
+ plan_glitchlings,
23
+ redactyl,
24
+ reduple,
25
+ rushmore,
26
+ scannequin,
22
27
  summon,
28
+ typogre,
29
+ zeedub,
23
30
  )
24
- from .config import AttackConfig, build_gaggle, load_attack_config
25
- from .util import SAMPLE_TEXT
26
-
27
31
 
28
32
  __all__ = [
29
33
  "Typogre",
@@ -47,6 +51,11 @@ __all__ = [
47
51
  "summon",
48
52
  "Glitchling",
49
53
  "Gaggle",
54
+ "plan_glitchlings",
55
+ "plan_glitchling_specs",
56
+ "is_rust_pipeline_enabled",
57
+ "is_rust_pipeline_supported",
58
+ "pipeline_feature_flag_enabled",
50
59
  "SAMPLE_TEXT",
51
60
  "AttackConfig",
52
61
  "build_gaggle",
@@ -4,6 +4,5 @@ import sys
4
4
 
5
5
  from .main import main
6
6
 
7
-
8
7
  if __name__ == "__main__":
9
8
  sys.exit(main())
@@ -0,0 +1,215 @@
1
+ """Compatibility helpers centralising optional dependency imports and extras."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from importlib import import_module, metadata
8
+ from types import ModuleType
9
+ from typing import Any, Iterable
10
+
11
+ try: # pragma: no cover - packaging is bundled with modern Python environments
12
+ from packaging.markers import default_environment
13
+ from packaging.requirements import Requirement
14
+ except ModuleNotFoundError: # pragma: no cover - fallback when packaging missing
15
+ Requirement = None # type: ignore[assignment]
16
+ default_environment = None # type: ignore[assignment]
17
+
18
+ _MISSING = object()
19
+
20
+
21
+ @dataclass
22
+ class OptionalDependency:
23
+ """Lazily import an optional dependency and retain the import error."""
24
+
25
+ module_name: str
26
+ _cached: ModuleType | object = _MISSING
27
+ _error: ModuleNotFoundError | None = None
28
+
29
+ def _attempt_import(self) -> ModuleType | None:
30
+ try:
31
+ module = import_module(self.module_name)
32
+ except ModuleNotFoundError as exc:
33
+ self._cached = None
34
+ self._error = exc
35
+ return None
36
+ else:
37
+ self._cached = module
38
+ self._error = None
39
+ return module
40
+
41
+ def get(self) -> ModuleType | None:
42
+ """Return the imported module or ``None`` when unavailable."""
43
+ if self._cached is _MISSING:
44
+ return self._attempt_import()
45
+ if self._cached is None:
46
+ return None
47
+ return self._cached
48
+
49
+ def load(self) -> ModuleType:
50
+ """Return the dependency, raising the original import error when absent."""
51
+ module = self.get()
52
+ if module is None:
53
+ error = self._error
54
+ if error is not None:
55
+ raise error
56
+ message = f"{self.module_name} is not installed"
57
+ raise ModuleNotFoundError(message)
58
+ return module
59
+
60
+ def require(self, message: str) -> ModuleType:
61
+ """Return the dependency or raise ``ModuleNotFoundError`` with ``message``."""
62
+ try:
63
+ return self.load()
64
+ except ModuleNotFoundError as exc:
65
+ raise ModuleNotFoundError(message) from exc
66
+
67
+ def available(self) -> bool:
68
+ """Return ``True`` when the dependency can be imported."""
69
+ return self.get() is not None
70
+
71
+ def reset(self) -> None:
72
+ """Forget any cached import result."""
73
+ self._cached = _MISSING
74
+ self._error = None
75
+
76
+ def attr(self, attribute: str) -> Any | None:
77
+ """Return ``attribute`` from the dependency when available."""
78
+ module = self.get()
79
+ if module is None:
80
+ return None
81
+ return getattr(module, attribute, None)
82
+
83
+ @property
84
+ def error(self) -> ModuleNotFoundError | None:
85
+ """Return the most recent ``ModuleNotFoundError`` (if any)."""
86
+ self.get()
87
+ return self._error
88
+
89
+
90
+ datasets = OptionalDependency("datasets")
91
+ verifiers = OptionalDependency("verifiers")
92
+ jellyfish = OptionalDependency("jellyfish")
93
+ jsonschema = OptionalDependency("jsonschema")
94
+ nltk = OptionalDependency("nltk")
95
+
96
+
97
+ def reset_optional_dependencies() -> None:
98
+ """Clear cached optional dependency imports (used by tests)."""
99
+ for dependency in (datasets, verifiers, jellyfish, jsonschema, nltk):
100
+ dependency.reset()
101
+
102
+
103
+ def get_datasets_dataset() -> Any | None:
104
+ """Return Hugging Face ``Dataset`` class when the dependency is installed."""
105
+ return datasets.attr("Dataset")
106
+
107
+
108
+ def require_datasets(message: str = "datasets is not installed") -> ModuleType:
109
+ """Ensure the Hugging Face datasets dependency is present."""
110
+ return datasets.require(message)
111
+
112
+
113
+ def require_verifiers(message: str = "verifiers is not installed") -> ModuleType:
114
+ """Ensure the verifiers dependency is present."""
115
+ return verifiers.require(message)
116
+
117
+
118
+ def require_jellyfish(message: str = "jellyfish is not installed") -> ModuleType:
119
+ """Ensure the jellyfish dependency is present."""
120
+ return jellyfish.require(message)
121
+
122
+
123
+ def get_installed_extras(
124
+ extras: Iterable[str] | None = None,
125
+ *,
126
+ distribution: str = "glitchlings",
127
+ ) -> dict[str, bool]:
128
+ """Return a mapping of optional extras to installation availability."""
129
+ try:
130
+ dist = metadata.distribution(distribution)
131
+ except metadata.PackageNotFoundError:
132
+ return {}
133
+
134
+ provided = {extra.lower() for extra in dist.metadata.get_all("Provides-Extra") or []}
135
+ targets = {extra.lower() for extra in extras} if extras is not None else provided
136
+ requirements = dist.requires or []
137
+ mapping: dict[str, set[str]] = {extra: set() for extra in provided}
138
+
139
+ for requirement in requirements:
140
+ names = _extras_from_requirement(requirement, provided)
141
+ if not names:
142
+ continue
143
+ req_name = _requirement_name(requirement)
144
+ for extra in names:
145
+ mapping.setdefault(extra, set()).add(req_name)
146
+
147
+ status: dict[str, bool] = {}
148
+ for extra in targets:
149
+ deps = mapping.get(extra)
150
+ if not deps:
151
+ status[extra] = False
152
+ continue
153
+ status[extra] = all(_distribution_installed(dep) for dep in deps)
154
+ return status
155
+
156
+
157
+ def _distribution_installed(name: str) -> bool:
158
+ try:
159
+ metadata.distribution(name)
160
+ except metadata.PackageNotFoundError:
161
+ return False
162
+ return True
163
+
164
+
165
+ _EXTRA_PATTERN = re.compile(r'extra\\s*==\\s*"(?P<extra>[^"]+)"')
166
+
167
+
168
+ def _extras_from_requirement(requirement: str, candidates: set[str]) -> set[str]:
169
+ if Requirement is not None and default_environment is not None:
170
+ req = Requirement(requirement)
171
+ if req.marker is None:
172
+ return set()
173
+ extras: set[str] = set()
174
+ for extra in candidates:
175
+ environment = default_environment()
176
+ environment["extra"] = extra
177
+ if req.marker.evaluate(environment):
178
+ extras.add(extra)
179
+ return extras
180
+
181
+ matches = set()
182
+ for match in _EXTRA_PATTERN.finditer(requirement):
183
+ extra = match.group("extra").lower()
184
+ if extra in candidates:
185
+ matches.add(extra)
186
+ return matches
187
+
188
+
189
+ def _requirement_name(requirement: str) -> str:
190
+ if Requirement is not None:
191
+ req = Requirement(requirement)
192
+ return req.name
193
+
194
+ candidate = requirement.split(";", 1)[0].strip()
195
+ for delimiter in ("[", "(", " ", "<", ">", "=", "!", "~"):
196
+ index = candidate.find(delimiter)
197
+ if index != -1:
198
+ return candidate[:index]
199
+ return candidate
200
+
201
+
202
+ __all__ = [
203
+ "OptionalDependency",
204
+ "datasets",
205
+ "verifiers",
206
+ "jellyfish",
207
+ "jsonschema",
208
+ "nltk",
209
+ "get_datasets_dataset",
210
+ "require_datasets",
211
+ "require_verifiers",
212
+ "require_jellyfish",
213
+ "get_installed_extras",
214
+ "reset_optional_dependencies",
215
+ ]