Skip to content

Commit b07990b

Browse files
committed
Merge commit 'c2aac735a87d621211108c408fd7ae89413bad34' into wb/t33
2 parents b38e0d6 + c2aac73 commit b07990b

9 files changed

Lines changed: 149 additions & 19 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changelog
2+
3+
## 2.0.0
4+
5+
- Converting a document is now `in2lambda convert FILE FILTER`, with the same options as before (`-o/--out`, `-a/--answers`). Scripts and Docker invocations that run `in2lambda FILE FILTER` need the extra word.
6+
- `in2lambda FILE FILTER` exits with an error naming the command to run instead, rather than printing its usage and exiting successfully.
7+
- beartype is now `^0.22`. At 0.20.0 and below its import hook leaves `cli` a plain function rather than a group, so the new command line either fails to import or runs `convert` whatever the arguments; 0.20.1 is the first version that works.
8+
- The Python API is unchanged: `in2lambda.main.runner` and everything under `in2lambda.api` take the same arguments and return the same objects.

‎docs/source/contributing/new_version.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ name = "in2lambda"
1010
version = "0.2.1" # <-- This line here
1111
```
1212

13+
and add an entry to [`CHANGELOG.md`](https://github.com/lambda-feedback/in2lambda/blob/main/CHANGELOG.md) saying what changed for anyone using the command line or the Python API.
14+
1315
2) Create a new [tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) for the docker image to be built. For instance, to tag a version v1.2.3:
1416

1517
```shell

‎docs/source/quickstart.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ This can also be done through [pipx](https://pypa.github.io/pipx/).
4848

4949
## 2. Choose a Document
5050

51-
in2lambda takes in two arguments:
51+
`in2lambda convert` takes in two arguments:
5252

5353
- The path to a document.
5454
- A filter describing how to parse it.
@@ -58,7 +58,7 @@ A list of available filters can be found [here](filters/index).
5858
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):
5959

6060
```bash
61-
$ in2lambda questions.tex PartSolPartSol
61+
$ in2lambda convert questions.tex PartSolPartSol
6262
```
6363

6464
:::{note}
@@ -68,7 +68,7 @@ The filter name is case-insensitive. Don't worry about the capital letters.
6868
Another filter might be used if [the answers are in a separate file](filters/_autosummary/PartsSepSol):
6969

7070
```bash
71-
$ in2lambda questions.tex -a solutions.tex PartsSepSol
71+
$ in2lambda convert questions.tex -a solutions.tex PartsSepSol
7272
```
7373

7474
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: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import importlib
1010
import importlib.util
11+
import shlex
1112
import shutil
1213
import subprocess
1314
from typing import Optional
@@ -185,10 +186,34 @@ def runner(
185186
return set_obj
186187

187188

188-
@click.command(
189+
class _Cli(click.RichGroup):
190+
"""The in2lambda group, which says what to run when given the pre-2.0 command line."""
191+
192+
def resolve_command(self, ctx, args): # type: ignore[no-untyped-def]
193+
"""Fail with the new command line rather than click's handling of an unknown name.
194+
195+
Click resolves a first argument starting with ``/`` or ``.`` by printing the
196+
group's help and exiting successfully, so `in2lambda /path/to/questions.tex
197+
PartsSepSol` would look like it had worked while converting nothing.
198+
"""
199+
# Shell completion resolves partial command lines, and must not raise.
200+
if not ctx.resilient_parsing and self.get_command(ctx, args[0]) is None:
201+
raise click.UsageError(
202+
f"in2lambda no longer takes a file directly. Run: in2lambda convert {shlex.join(args)}"
203+
)
204+
return super().resolve_command(ctx, args)
205+
206+
207+
@click.group(
208+
cls=_Cli,
189209
no_args_is_help=True,
190210
epilog="See the docs at https://lambda-feedback.github.io/in2lambda/ for more details.",
191211
)
212+
def cli() -> None:
213+
"""Prepares content for import into Lambda Feedback."""
214+
215+
216+
@cli.command()
192217
@click.argument( # Use resolve_path to get absolute path
193218
"question_file", type=click.Path(exists=True, readable=True, resolve_path=True)
194219
)
@@ -214,7 +239,7 @@ def runner(
214239
help="File containing solutions for QUESTION_FILE.",
215240
type=click.Path(resolve_path=True, exists=True, dir_okay=False),
216241
)
217-
def cli(
242+
def convert(
218243
question_file: str, chosen_filter: str, output_dir: str, answer_file: Optional[str]
219244
) -> None:
220245
"""Takes in a QUESTION_FILE for a given SUBJECT and produces Lambda Feedback compatible json/zip files."""

‎poetry.lock‎

Lines changed: 11 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "in2lambda"
3-
version = "1.0.0"
3+
version = "2.0.0"
44
description = "Converts content ready for import into Lambda Feedback"
55
authors = []
66
license = "MIT"
@@ -25,7 +25,11 @@ documentation = "https://lambda-feedback.github.io/in2lambda"
2525
python = "^3.10"
2626
panflute = { version = "^2.3.1", optional = true }
2727
rich-click = "^1.7.4"
28-
beartype = "^0.17.2"
28+
# At 0.20.0 and below, the import hook in in2lambda/__init__.py leaves `cli` a plain
29+
# function: `@cli.command()` then raises AttributeError at import (0.19.0, 0.20.0), or
30+
# `in2lambda ...` runs convert's body whatever the arguments (0.18.5). 0.20.1 is the
31+
# first version that works; 0.22 is the series this is locked to and tested against.
32+
beartype = "^0.22"
2933

3034
[tool.poetry.extras]
3135
# Only needed to convert documents; the Python API works without it.

‎tests/test_cli.py‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""What the command line does with the current and the pre-2.0 form."""
2+
3+
import os
4+
import shutil
5+
import subprocess
6+
import sys
7+
8+
import pytest
9+
from click.shell_completion import ShellComplete
10+
from click.testing import CliRunner
11+
12+
from in2lambda.main import cli
13+
14+
15+
def test_convert_writes_the_set(filters_dir: str, tmp_path) -> None:
16+
"""`in2lambda convert` reaches the runner and produces the zipped set."""
17+
example = os.path.join(filters_dir, "PartsSepSol", "example.tex")
18+
19+
result = CliRunner().invoke(
20+
cli, ["convert", example, "PartsSepSol", "-o", str(tmp_path)]
21+
)
22+
23+
assert result.exit_code == 0, result.output
24+
assert (tmp_path / "set.zip").exists()
25+
26+
27+
@pytest.mark.parametrize("path", ["example.tex", "./example.tex", "ABSOLUTE"])
28+
def test_old_form_fails_and_names_convert(
29+
path: str, filters_dir: str, monkeypatch, tmp_path
30+
) -> None:
31+
"""The pre-2.0 form errors out whatever the file path looks like.
32+
33+
A path starting with ``/`` or ``.`` used to make click print the help and exit 0,
34+
so every script passing a full path appeared to succeed without converting anything.
35+
"""
36+
monkeypatch.setenv("COLUMNS", "200") # So the message is not wrapped mid-sentence.
37+
# Run from a directory of our own, so `out` appearing there is this command's doing.
38+
shutil.copy(os.path.join(filters_dir, "PartsSepSol", "example.tex"), tmp_path)
39+
monkeypatch.chdir(tmp_path)
40+
if path == "ABSOLUTE": # Only known once we're in the directory holding the file.
41+
path = os.path.abspath("example.tex")
42+
43+
result = CliRunner().invoke(cli, [path, "PartsSepSol"])
44+
45+
assert result.exit_code != 0
46+
assert "in2lambda convert" in result.output
47+
assert not (tmp_path / "out").exists()
48+
49+
50+
def test_old_form_fails_when_run_as_the_installed_command(
51+
filters_dir: str, tmp_path
52+
) -> None:
53+
"""The same holds for ``cli()``, which is what the installed command runs.
54+
55+
CliRunner calls ``cli.main`` instead, so it cannot see this path: on beartype
56+
0.18.5 the import hook left ``cli`` a plain function, and ``cli()`` ran
57+
``convert``'s body whatever the arguments.
58+
"""
59+
result = subprocess.run(
60+
[
61+
sys.executable,
62+
"-c",
63+
"from in2lambda.main import cli; cli()",
64+
os.path.join(filters_dir, "PartsSepSol", "example.tex"),
65+
"PartsSepSol",
66+
],
67+
capture_output=True,
68+
text=True,
69+
cwd=tmp_path,
70+
env={**os.environ, "COLUMNS": "200"},
71+
)
72+
73+
assert result.returncode != 0
74+
assert "in2lambda convert" in result.stdout + result.stderr
75+
assert not (tmp_path / "out").exists()
76+
77+
78+
def test_completing_the_old_form_offers_the_subcommand() -> None:
79+
"""Completion resolves half-typed command lines, so the guard must not fire there.
80+
81+
Without that exemption, `in2lambda ./questions.tex <TAB>` printed a traceback
82+
where the shell expected candidates.
83+
"""
84+
completions = ShellComplete(
85+
cli, {}, "in2lambda", "_IN2LAMBDA_COMPLETE"
86+
).get_completions(["./questions.tex"], "")
87+
88+
assert [candidate.value for candidate in completions] == ["convert"]

‎tests/test_conversion_tools.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ def test_cli_exits_with_message(filters_dir: str, monkeypatch, tmp_path) -> None
3939
monkeypatch.setitem(sys.modules, "panflute", None)
4040
example = os.path.join(filters_dir, "PartsSepSol", "example.tex")
4141

42-
result = CliRunner().invoke(cli, [example, "PartsSepSol", "-o", str(tmp_path)])
42+
result = CliRunner().invoke(
43+
cli, ["convert", example, "PartsSepSol", "-o", str(tmp_path)]
44+
)
4345

4446
assert result.exit_code != 0
4547
assert PANFLUTE_HINT in result.output

‎tests/test_runner.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def test_cli_reports_problems_and_exports_anyway(tmp_path) -> None:
7272
out_dir = tmp_path / "out"
7373

7474
result = CliRunner().invoke(
75-
cli, [str(question_file), "PartsOneSol", "-o", str(out_dir)]
75+
cli, ["convert", str(question_file), "PartsOneSol", "-o", str(out_dir)]
7676
)
7777

7878
assert result.exit_code == 0

0 commit comments

Comments
 (0)