Skip to content

Commit 5a8414f

Browse files
m-messerclaude
andcommitted
refactor!: make in2lambda a command group (convert)
BREAKING CHANGE: file conversion moves from `in2lambda <file> <filter>` to `in2lambda convert <file> <filter>`. `cli` is now a click.group so further subcommands (a wizard, etc.) can be added without overloading the top-level command. The former command body is unchanged, just relocated to `convert`; `runner()` is untouched. - pyproject `[tool.poetry.scripts]` and docs/source/reference/command-line.rst still point at `in2lambda.main:cli` - no change needed there, sphinx-click renders the subcommand automatically. - quickstart examples updated to `in2lambda convert ...` with a note. - tests/test_cli.py added (CliRunner) covering help, output files, case-insensitive filter names, and unknown-filter rejection. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017VXb8aZqgFBjoeuuddjW6r
1 parent dfff12f commit 5a8414f

3 files changed

Lines changed: 75 additions & 7 deletions

File tree

docs/source/quickstart.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,23 +56,29 @@ A list of available filters can be found [here](filters/index).
5656
For instance, the following takes in `questions.tex` and uses a filter that expects [each part to be directly followed by the solution](filters/_autosummary/PartSolPartSol):
5757

5858
```bash
59-
$ in2lambda questions.tex PartSolPartSol
59+
$ in2lambda convert questions.tex PartSolPartSol
6060
```
6161

62+
:::{important}
63+
File conversion lives under the `convert` subcommand (`in2lambda convert ...`,
64+
not `in2lambda ...`). This leaves room for other subcommands, such as one that
65+
turns unstructured documents into markdown.
66+
:::
67+
6268
:::{note}
6369
The filter name is case-insensitive. Don't worry about the capital letters.
6470
:::
6571

6672
Another filter might be used if [the answers are in a separate file](filters/_autosummary/PartsSepSol):
6773

6874
```bash
69-
$ in2lambda questions.tex -a solutions.tex PartsSepSol
75+
$ in2lambda convert questions.tex -a solutions.tex PartsSepSol
7076
```
7177

7278
If you would rather write the questions yourself, the [`Markdown` filter](filters/_autosummary/Markdown) reads a plain markdown file where `#` starts a question, `##` starts a part, and `## Solution` gives a worked solution:
7379

7480
```bash
75-
$ in2lambda questions.md Markdown
81+
$ in2lambda convert questions.md Markdown
7682
```
7783

7884
By default, this generates an `out` directory in the same place that the command was run in. It contains the zipped question files.

in2lambda/main.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,15 @@ def runner(
171171
return set_obj
172172

173173

174-
@click.command(
174+
@click.group(
175175
no_args_is_help=True,
176176
epilog="See the docs at https://lambda-feedback.github.io/in2lambda/ for more details.",
177177
)
178+
def cli() -> None:
179+
"""Convert documents into Lambda Feedback compatible question sets."""
180+
181+
182+
@cli.command(no_args_is_help=True)
178183
@click.argument( # Use resolve_path to get absolute path
179184
"question_file", type=click.Path(exists=True, readable=True, resolve_path=True)
180185
)
@@ -207,11 +212,11 @@ def runner(
207212
help="File containing solutions for QUESTION_FILE.",
208213
type=click.Path(resolve_path=True, exists=True, dir_okay=False),
209214
)
210-
def cli(
215+
def convert(
211216
question_file: str, chosen_filter: str, output_dir: str, answer_file: Optional[str]
212217
) -> None:
213-
"""Takes in a QUESTION_FILE for a given SUBJECT and produces Lambda Feedback compatible json/zip files."""
214-
# main() is made separate from click() so that it can be easily imported as part of a library.
218+
"""Take a QUESTION_FILE and CHOSEN_FILTER and produce Lambda Feedback json/zip files."""
219+
# Kept separate from runner() so runner() can be imported as part of the library.
215220
runner(question_file, chosen_filter, output_dir, answer_file)
216221

217222

tests/test_cli.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Tests for the ``in2lambda`` command-line interface (the ``convert`` subcommand)."""
2+
3+
import os
4+
5+
from click.testing import CliRunner
6+
7+
from in2lambda.main import cli
8+
9+
10+
def test_bare_invocation_shows_usage() -> None:
11+
result = CliRunner().invoke(cli, [])
12+
assert "Usage:" in result.output
13+
assert "convert" in result.output
14+
15+
16+
def test_help_lists_the_convert_command() -> None:
17+
result = CliRunner().invoke(cli, ["--help"])
18+
assert result.exit_code == 0
19+
assert "convert" in result.output
20+
21+
22+
def test_convert_writes_output_files(filters_dir: str, tmp_path) -> None:
23+
example = os.path.join(filters_dir, "PartsSepSol", "example.tex")
24+
out_dir = tmp_path / "out"
25+
26+
result = CliRunner().invoke(
27+
cli, ["convert", example, "PartsSepSol", "-o", str(out_dir)]
28+
)
29+
30+
assert result.exit_code == 0, result.output
31+
assert (out_dir / "set").is_dir()
32+
assert (out_dir / "set.zip").is_file()
33+
34+
35+
def test_convert_accepts_case_insensitive_filter_and_markdown(
36+
filters_dir: str, tmp_path
37+
) -> None:
38+
example = os.path.join(filters_dir, "Markdown", "example.md")
39+
out_dir = tmp_path / "out"
40+
41+
result = CliRunner().invoke(
42+
cli, ["convert", example, "markdown", "-o", str(out_dir)]
43+
)
44+
45+
assert result.exit_code == 0, result.output
46+
assert (out_dir / "set" / "set_set.json").is_file()
47+
48+
49+
def test_convert_rejects_unknown_filter(filters_dir: str, tmp_path) -> None:
50+
example = os.path.join(filters_dir, "PartsSepSol", "example.tex")
51+
52+
result = CliRunner().invoke(
53+
cli, ["convert", example, "NotAFilter", "-o", str(tmp_path / "out")]
54+
)
55+
56+
assert result.exit_code != 0
57+
assert "NotAFilter" in result.output

0 commit comments

Comments
 (0)