hoodscript 1.3.0__tar.gz → 1.3.1__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.
Files changed (146) hide show
  1. {hoodscript-1.3.0 → hoodscript-1.3.1}/CHANGELOG.md +25 -0
  2. {hoodscript-1.3.0 → hoodscript-1.3.1}/PKG-INFO +1 -1
  3. {hoodscript-1.3.0 → hoodscript-1.3.1}/pyproject.toml +3 -0
  4. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodarcade/cli.py +17 -4
  5. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodfarm/cli.py +18 -5
  6. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/_version.py +2 -2
  7. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/cli.py +18 -10
  8. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/kernel/kernel.py +4 -1
  9. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_cli.py +14 -0
  10. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_kernel.py +14 -0
  11. hoodscript-1.3.1/tests/test_wheel_contents.py +34 -0
  12. {hoodscript-1.3.0 → hoodscript-1.3.1}/.gitignore +0 -0
  13. {hoodscript-1.3.0 → hoodscript-1.3.1}/CODE_OF_CONDUCT.md +0 -0
  14. {hoodscript-1.3.0 → hoodscript-1.3.1}/CONTRIBUTING.md +0 -0
  15. {hoodscript-1.3.0 → hoodscript-1.3.1}/LICENSE +0 -0
  16. {hoodscript-1.3.0 → hoodscript-1.3.1}/README.md +0 -0
  17. {hoodscript-1.3.0 → hoodscript-1.3.1}/SECURITY.md +0 -0
  18. {hoodscript-1.3.0 → hoodscript-1.3.1}/SPRINT_PLAN.md +0 -0
  19. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/adr/0001-token-pipeline-over-parser.md +0 -0
  20. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/adr/0002-keyword-reconciliation.md +0 -0
  21. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/adr/0003-inclusive-ranges.md +0 -0
  22. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/adr/0004-no-regional-dialect-overlays.md +0 -0
  23. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/adr/README.md +0 -0
  24. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/archive/HOODSCRIPT_COMPLETE_MASTER_SYSTEM.md +0 -0
  25. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/archive/HOODSCRIPT_FULL_ENGINEERING_SUITE.md +0 -0
  26. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/archive/HOODSCRIPT_UNSPRED_FEATURES_ROADMAP.md +0 -0
  27. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/curriculum.md +0 -0
  28. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/grammar.ebnf +0 -0
  29. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/keywords.md +0 -0
  30. {hoodscript-1.3.0 → hoodscript-1.3.1}/docs/onboarding.md +0 -0
  31. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/quickstart.ipynb +0 -0
  32. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/beatlab/stage1.hs +0 -0
  33. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/beatlab/stage2.hs +0 -0
  34. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/beatlab/stage3.hs +0 -0
  35. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/beatlab/stage4.hs +0 -0
  36. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/beatlab/stage5.hs +0 -0
  37. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/cookout/stage1.hs +0 -0
  38. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/cookout/stage2.hs +0 -0
  39. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/cookout/stage3.hs +0 -0
  40. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/cookout/stage4.hs +0 -0
  41. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/cookout/stage5.hs +0 -0
  42. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/dropday/stage1.hs +0 -0
  43. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/dropday/stage2.hs +0 -0
  44. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/dropday/stage3.hs +0 -0
  45. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/dropday/stage4.hs +0 -0
  46. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/dropday/stage5.hs +0 -0
  47. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/level1.hs +0 -0
  48. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/level2.hs +0 -0
  49. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/level3.hs +0 -0
  50. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/level4.hs +0 -0
  51. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/level5.hs +0 -0
  52. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/level6.hs +0 -0
  53. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/level7.hs +0 -0
  54. {hoodscript-1.3.0 → hoodscript-1.3.1}/examples/solutions/level8.hs +0 -0
  55. {hoodscript-1.3.0 → hoodscript-1.3.1}/out.txt +0 -0
  56. {hoodscript-1.3.0 → hoodscript-1.3.1}/playground/app.js +0 -0
  57. {hoodscript-1.3.0 → hoodscript-1.3.1}/playground/bundle.js +0 -0
  58. {hoodscript-1.3.0 → hoodscript-1.3.1}/playground/examples.js +0 -0
  59. {hoodscript-1.3.0 → hoodscript-1.3.1}/playground/hoodscript-latest-py3-none-any.whl +0 -0
  60. {hoodscript-1.3.0 → hoodscript-1.3.1}/playground/index.html +0 -0
  61. {hoodscript-1.3.0 → hoodscript-1.3.1}/playground/style.css +0 -0
  62. {hoodscript-1.3.0 → hoodscript-1.3.1}/playground/worker.js +0 -0
  63. {hoodscript-1.3.0 → hoodscript-1.3.1}/scripts/build_playground.py +0 -0
  64. {hoodscript-1.3.0 → hoodscript-1.3.1}/scripts/gen_ebnf.py +0 -0
  65. {hoodscript-1.3.0 → hoodscript-1.3.1}/scripts/gen_keywords.py +0 -0
  66. {hoodscript-1.3.0 → hoodscript-1.3.1}/scripts/hooks/pre-push +0 -0
  67. {hoodscript-1.3.0 → hoodscript-1.3.1}/scripts/install-hooks.sh +0 -0
  68. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodarcade/__init__.py +0 -0
  69. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodarcade/core/runtime.py +0 -0
  70. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodarcade/core/ui.py +0 -0
  71. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodarcade/games/beatlab.py +0 -0
  72. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodarcade/games/cookout.py +0 -0
  73. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodarcade/games/dropday.py +0 -0
  74. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodfarm/__init__.py +0 -0
  75. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodfarm/drone.py +0 -0
  76. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodfarm/grid.py +0 -0
  77. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodfarm/levels.py +0 -0
  78. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodfarm/renderer.py +0 -0
  79. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodfarm/runner.py +0 -0
  80. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodlib/__init__.py +0 -0
  81. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodlib/disk.py +0 -0
  82. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/__init__.py +0 -0
  83. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/__main__.py +0 -0
  84. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/cache.py +0 -0
  85. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/constants.py +0 -0
  86. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/debugger.py +0 -0
  87. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/errors/__init__.py +0 -0
  88. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/errors/catalog.py +0 -0
  89. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/errors/renderer.py +0 -0
  90. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/errors/translator.py +0 -0
  91. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/formatter.py +0 -0
  92. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/importer.py +0 -0
  93. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/kernel/__init__.py +0 -0
  94. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/kernel/__main__.py +0 -0
  95. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/kernel/installer.py +0 -0
  96. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/kernel/magics.py +0 -0
  97. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/linter.py +0 -0
  98. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/lsp.py +0 -0
  99. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/migrator.py +0 -0
  100. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/patterns.py +0 -0
  101. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/repl.py +0 -0
  102. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/sandbox.py +0 -0
  103. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/stubs.py +0 -0
  104. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/traceback_handler.py +0 -0
  105. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/transpiler.py +0 -0
  106. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/tutor.py +0 -0
  107. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/hoodscript/zen.py +0 -0
  108. {hoodscript-1.3.0 → hoodscript-1.3.1}/src/vibe.py +0 -0
  109. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/conftest.py +0 -0
  110. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/deep_nesting.hs +0 -0
  111. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/fork_bomb.hs +0 -0
  112. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/huge_literal.hs +0 -0
  113. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/import_escapes.hs +0 -0
  114. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/infinite_recursion.hs +0 -0
  115. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/open_file.hs +0 -0
  116. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/pathological_indent.hs +0 -0
  117. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/runaway_output.hs +0 -0
  118. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/corpus/runaway_time.hs +0 -0
  119. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/test_corpus.py +0 -0
  120. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/test_exec_sites.py +0 -0
  121. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/test_fuzz.py +0 -0
  122. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/security/test_sandbox.py +0 -0
  123. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_arcade.py +0 -0
  124. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_arcade_gates.py +0 -0
  125. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_conformance.py +0 -0
  126. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_debugger.py +0 -0
  127. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_docs.py +0 -0
  128. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_errors.py +0 -0
  129. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_fmt.py +0 -0
  130. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_fuzz_ebnf.py +0 -0
  131. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_game_engine.py +0 -0
  132. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_grammar_canonical.py +0 -0
  133. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_grammar_ebnf.py +0 -0
  134. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_hoodlib_disk.py +0 -0
  135. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_importer.py +0 -0
  136. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_kernel_zmq.py +0 -0
  137. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_keywords_doc.py +0 -0
  138. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_learn.py +0 -0
  139. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_lint.py +0 -0
  140. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_lsp_features.py +0 -0
  141. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_patterns.py +0 -0
  142. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_playground.py +0 -0
  143. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_release.py +0 -0
  144. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_repl.py +0 -0
  145. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_transpiler.py +0 -0
  146. {hoodscript-1.3.0 → hoodscript-1.3.1}/tests/test_vscode_extension.py +0 -0
@@ -5,6 +5,31 @@ Versioning: the keyword set is the public API; changing it is at least a minor b
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [1.3.1] — 2026-09-12
9
+
10
+ Patch release: what the fresh-machine check of 1.3.0 found, plus two audit
11
+ leftovers. No keyword or API changes.
12
+
13
+ ### Fixed
14
+
15
+ - Fresh-machine check of v1.3.0 (`pip install "hoodscript[all]"` in a clean
16
+ venv, `docs/onboarding.md` §2–§11 verbatim): everything passed except
17
+ `hoodfarm test` and `hoodarcade test`, which reported "No Sol" for every
18
+ level and **exited 0** — the reference solutions were not in the wheel and a
19
+ missing solution counted as a pass. The solutions now ship
20
+ (`hoodscript/data/solutions`, found by both CLIs after the repo checkout), a
21
+ missing one is a red `MISSING` row and a non-zero exit, and
22
+ `tests/test_wheel_contents.py` builds the wheel and asserts the runtime
23
+ data (curriculum, `vibe.py`, solutions) is inside it.
24
+
25
+ - `hoodscript --sandbox run f.hs` now works (the flags may come before or after
26
+ `run`; `run` is recognised as the first positional, not only as `argv[0]`).
27
+ - Jupyter: `%hood_tb` is honoured by the error card — the Python traceback
28
+ section opens by default and the plain-text fallback carries it too.
29
+ - MANUAL §16 documents that `hoodscript fmt` writes `ain't nobody x` back as
30
+ `ain't x` (same Python, one canonical spelling).
31
+ - `cli.py` had the `arcade` dispatch block twice; the copy is gone.
32
+
8
33
  ## [1.3.0] — 2026-09-12
9
34
 
10
35
  Three features that were on `main` but not on PyPI: the two games and the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hoodscript
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Summary: A production-grade Python 3 conlang based on AAVE with 1:1 AST parity.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -61,6 +61,9 @@ packages = ["src/hoodscript", "src/hoodlib", "src/hoodfarm", "src/hoodarcade"]
61
61
  "docs/curriculum.md" = "hoodscript/data/curriculum.md"
62
62
  # `import vibe` — the Zen of HoodScript, like `import this`.
63
63
  "src/vibe.py" = "vibe.py"
64
+ # `hoodfarm test` / `hoodarcade test` benchmark the reference solutions; a pip
65
+ # install has no examples/ tree, so ship them (found via hoodscript/data/solutions).
66
+ "examples/solutions" = "hoodscript/data/solutions"
64
67
 
65
68
  [tool.hatch.build.targets.sdist]
66
69
  # The nested hoodscript/ prototype is reference material, not part of the distribution.
@@ -191,11 +191,23 @@ def cmd_run(args: argparse.Namespace, console: Console, ui: ArcadeUI, progress:
191
191
  return run_stage_session(game_id, stage_id, file_path, console, ui, progress, once=True)
192
192
 
193
193
 
194
+ def solutions_dir() -> Path:
195
+ """Where the reference solutions live: the repo's examples/solutions when run
196
+ from a checkout, else the copy shipped in the wheel (hoodscript/data/solutions)."""
197
+ import hoodscript
198
+ for cand in (
199
+ Path("examples/solutions"),
200
+ Path(__file__).resolve().parents[2] / "examples" / "solutions",
201
+ Path(hoodscript.__file__).resolve().parent / "data" / "solutions",
202
+ ):
203
+ if cand.is_dir():
204
+ return cand
205
+ return Path("examples/solutions")
206
+
207
+
194
208
  def cmd_test(args: argparse.Namespace, console: Console) -> int:
195
209
  """Run automated benchmarks against reference solutions."""
196
- solutions_root = Path("examples/solutions")
197
- if not solutions_root.exists():
198
- solutions_root = Path(__file__).resolve().parents[2] / "examples" / "solutions"
210
+ solutions_root = solutions_dir()
199
211
 
200
212
  target_games = [args.game.lower()] if args.game else list(GAMES_MAP.keys())
201
213
  failures = 0
@@ -220,12 +232,13 @@ def cmd_test(args: argparse.Namespace, console: Console) -> int:
220
232
  sol_file = solutions_root / game_id / f"stage{stg_id}.hs"
221
233
 
222
234
  if not sol_file.exists():
235
+ failures += 1 # a missing solution is not a pass
223
236
  table.add_row(
224
237
  game_id.capitalize(),
225
238
  str(stg_id),
226
239
  stage.title,
227
240
  ", ".join(stage.concepts),
228
- "[dim]No Sol[/dim]",
241
+ f"[bold red]MISSING[/bold red] {sol_file}",
229
242
  )
230
243
  continue
231
244
 
@@ -128,11 +128,23 @@ def cmd_run(args: argparse.Namespace, console: Console) -> int:
128
128
  return 0 if won else 1
129
129
 
130
130
 
131
+ def solutions_dir() -> Path:
132
+ """Where the reference solutions live: the repo's examples/solutions when run
133
+ from a checkout, else the copy shipped in the wheel (hoodscript/data/solutions)."""
134
+ import hoodscript
135
+ for cand in (
136
+ Path("examples/solutions"),
137
+ Path(__file__).resolve().parents[2] / "examples" / "solutions",
138
+ Path(hoodscript.__file__).resolve().parent / "data" / "solutions",
139
+ ):
140
+ if cand.is_dir():
141
+ return cand
142
+ return Path("examples/solutions")
143
+
144
+
131
145
  def cmd_test(args: argparse.Namespace, console: Console) -> int:
132
146
  """Run automated benchmarks against solutions."""
133
- solutions_dir = Path("examples/solutions")
134
- if not solutions_dir.exists():
135
- solutions_dir = Path(__file__).resolve().parents[2] / "examples" / "solutions"
147
+ solutions = solutions_dir()
136
148
 
137
149
  target_levels = [args.level] if args.level else sorted(ALL_LEVELS.keys())
138
150
  failures = 0
@@ -152,9 +164,10 @@ def cmd_test(args: argparse.Namespace, console: Console) -> int:
152
164
 
153
165
  for lvl_id in target_levels:
154
166
  level = ALL_LEVELS[lvl_id]
155
- sol_file = solutions_dir / f"level{lvl_id}.hs"
167
+ sol_file = solutions / f"level{lvl_id}.hs"
156
168
  if not sol_file.exists():
157
- table.add_row(str(lvl_id), level.title, "-", "-", "-", "[dim]No Sol[/dim]")
169
+ failures += 1 # a missing solution is not a pass
170
+ table.add_row(str(lvl_id), level.title, "-", "-", "-", f"[bold red]MISSING[/bold red] {sol_file}")
158
171
  continue
159
172
 
160
173
  code = sol_file.read_text(encoding="utf-8")
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.3.0'
22
- __version_tuple__ = version_tuple = (1, 3, 0)
21
+ __version__ = version = '1.3.1'
22
+ __version_tuple__ = version_tuple = (1, 3, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -380,6 +380,20 @@ def kernel_command(args: list[str]) -> int:
380
380
  return 0
381
381
 
382
382
 
383
+ _VALUED = ("--max-seconds", "--max-output", "--max-depth")
384
+
385
+
386
+ def _strip_run(argv):
387
+ i = 0
388
+ while i < len(argv):
389
+ a = argv[i]
390
+ if a in _VALUED: i += 2; continue
391
+ if a.startswith("-") and a != "-": i += 1; continue
392
+ if a == "run": return argv[:i] + argv[i + 1:]
393
+ return argv
394
+ return argv
395
+
396
+
383
397
  def main(argv=None) -> int:
384
398
  if argv is None:
385
399
  argv = sys.argv[1:]
@@ -449,13 +463,6 @@ def main(argv=None) -> int:
449
463
  sys.stderr.write("The arcade needs rich. Install it: pip install \"hoodscript[arcade]\"\n")
450
464
  return 2
451
465
  return arcade_main(argv[1:])
452
- if cmd == "arcade":
453
- try:
454
- from hoodarcade.cli import main as arcade_main
455
- except ImportError:
456
- sys.stderr.write("The arcade needs rich. Install it: pip install \"hoodscript[arcade]\"\n")
457
- return 2
458
- return arcade_main(argv[1:])
459
466
  if cmd == "build":
460
467
  # build <file> transpiles to stdout (alias for -c)
461
468
  if len(argv) < 2:
@@ -467,9 +474,10 @@ def main(argv=None) -> int:
467
474
  sys.stdout.write(transpile(src, filepath) + "\n")
468
475
  return 0
469
476
 
470
- # If user ran `hoodscript run ...`
471
- if cmd == "run":
472
- argv = argv[1:]
477
+ # `hoodscript run f.hs` and `hoodscript --sandbox run f.hs` both work: `run` is
478
+ # the first positional, wherever the flags landed. `_VALUED` are the options
479
+ # that take an argument, so their value is not mistaken for the positional.
480
+ argv = _strip_run(argv)
473
481
 
474
482
  # Fallback to standard execution parser (supports `hoodscript <file>`, `-c`, `--sandbox`, etc.)
475
483
  parser = argparse.ArgumentParser(prog="hoodscript")
@@ -109,8 +109,9 @@ class HoodScriptKernel(Kernel):
109
109
  )
110
110
 
111
111
  if raw_tb_escaped:
112
+ opened = " open" if self.show_raw_traceback else "" # %hood_tb expands the traceback by default
112
113
  card += f"""
113
- <details style="margin-top: 8px; font-size: 12px; color: #9ca3af;">
114
+ <details{opened} style="margin-top: 8px; font-size: 12px; color: #9ca3af;">
114
115
  <summary style="cursor: pointer; user-select: none;">Show Python Traceback</summary>
115
116
  <pre style="background: rgba(0,0,0,0.4); padding: 8px; border-radius: 4px;
116
117
  color: #f87171; overflow-x: auto; font-family: monospace;
@@ -126,6 +127,8 @@ class HoodScriptKernel(Kernel):
126
127
  return
127
128
 
128
129
  plain_text = render_plain(diag) if diag else str(exc)
130
+ if self.show_raw_traceback and raw_tb:
131
+ plain_text += "\n\n--- Python traceback (%hood_tb) ---\n" + raw_tb
129
132
  html_card = self._render_html_card(diag, raw_tb)
130
133
 
131
134
  content = {
@@ -86,6 +86,20 @@ def test_cli_run(tmp_path):
86
86
  assert "from run" in res2.stdout.strip()
87
87
 
88
88
 
89
+ def test_cli_run_accepts_flags_before_the_word_run(tmp_path):
90
+ """`hoodscript --sandbox run f.hs` and `hoodscript run --sandbox f.hs` are the same command."""
91
+ from hoodscript.cli import _strip_run
92
+ assert _strip_run(["run", "f.hs"]) == ["f.hs"]
93
+ assert _strip_run(["--sandbox", "run", "f.hs"]) == ["--sandbox", "f.hs"]
94
+ assert _strip_run(["--max-seconds", "5", "run", "f.hs"]) == ["--max-seconds", "5", "f.hs"]
95
+ assert _strip_run(["run.hs"]) == ["run.hs"] # a file called run.hs is a file
96
+ assert _strip_run(["f.hs", "run"]) == ["f.hs", "run"] # only the first positional
97
+ src = tmp_path / "prog.hs"
98
+ src.write_text('f = open("/dev/null")\n', encoding="utf-8")
99
+ res = run_cli("--sandbox", "run", str(src))
100
+ assert res.returncode != 0 and "NoAccessTrip" in res.stderr
101
+
102
+
89
103
  def test_cli_fmt_and_lint(tmp_path):
90
104
  src = tmp_path / "test.hs"
91
105
  src.write_text("bet f( ):\n unused = 1\n dip 2\n", encoding="utf-8")
@@ -105,6 +105,20 @@ def test_kernel_magic_traceback_toggle(mock_kernel):
105
105
  res = mock_kernel.do_execute("%hood_tb", silent=False)
106
106
  assert res["status"] == "ok"
107
107
  assert mock_kernel.show_raw_traceback is True
108
+ # The toggle is honoured by the card: the traceback section opens by default
109
+ # and the plain-text fallback carries the raw traceback too.
110
+ mock_kernel.send_response.reset_mock()
111
+ mock_kernel.do_execute("throw BadValueTrip('x')", silent=False)
112
+ shown = [c for c in mock_kernel.send_response.call_args_list if c.args[1] == "display_data"]
113
+ assert shown, "no diagnostic was displayed"
114
+ data = shown[-1].args[2]["data"]
115
+ assert "<details open" in data["text/html"]
116
+ assert "--- Python traceback (%hood_tb) ---" in data["text/plain"]
117
+ mock_kernel.do_execute("%hood_tb", silent=False) # back off
118
+ mock_kernel.send_response.reset_mock()
119
+ mock_kernel.do_execute("throw BadValueTrip('x')", silent=False)
120
+ shown = [c for c in mock_kernel.send_response.call_args_list if c.args[1] == "display_data"]
121
+ assert "<details open" not in shown[-1].args[2]["data"]["text/html"]
108
122
 
109
123
 
110
124
  def test_kernel_do_complete(mock_kernel):
@@ -0,0 +1,34 @@
1
+ """The wheel is what users install; the repo is what tests run against. Things the
2
+ CLI reads at runtime (curriculum, `import vibe`, the games' reference solutions)
3
+ are force-included by pyproject — this test builds the wheel and checks they are
4
+ in it, because a fresh-machine check of v1.3.0 found `hoodfarm test` reporting
5
+ "No Sol" for every level and exiting 0."""
6
+
7
+ import subprocess
8
+ import sys
9
+ import zipfile
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ pytest.importorskip("hatchling")
16
+
17
+ REQUIRED = [
18
+ "hoodscript/data/curriculum.md",
19
+ "vibe.py",
20
+ "hoodscript/data/solutions/level1.hs",
21
+ "hoodscript/data/solutions/level8.hs",
22
+ "hoodscript/data/solutions/cookout/stage1.hs",
23
+ "hoodscript/data/solutions/beatlab/stage5.hs",
24
+ "hoodscript/data/solutions/dropday/stage3.hs",
25
+ ]
26
+
27
+
28
+ def test_wheel_ships_the_runtime_data(tmp_path):
29
+ subprocess.run([sys.executable, "-m", "hatchling", "build", "-t", "wheel", "-d", str(tmp_path)],
30
+ cwd=ROOT, check=True, capture_output=True)
31
+ wheel = next(tmp_path.glob("*.whl"))
32
+ names = set(zipfile.ZipFile(wheel).namelist())
33
+ missing = [n for n in REQUIRED if n not in names]
34
+ assert not missing, f"not in the wheel: {missing}"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes