From 5810a012855033f30050bc650cf90906ae916e52 Mon Sep 17 00:00:00 2001 From: "Peter B. Johnson" Date: Sun, 20 Sep 2026 21:27:46 +0100 Subject: [PATCH] implement: Keep the pre-2.0 command line working as an alias (t47) --- CHANGELOG.md | 7 +++---- in2lambda/main.py | 11 +++++++++-- pyproject.toml | 2 +- tests/test_cli.py | 34 ++++++++++++++++++++++++---------- 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d717b7..df4bf4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,9 @@ # Changelog -## 2.0.0 +## 1.1.0 -- 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. -- `in2lambda FILE FILTER` exits with an error naming the command to run instead, rather than printing its usage and exiting successfully. -- 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. +- Converting a document is now `in2lambda convert FILE FILTER`, with the same options as before (`-o/--out`, `-a/--answers`). `in2lambda FILE FILTER` still converts the file, printing one line to stderr naming the `in2lambda convert` command to run instead, so scripts and Docker invocations written before this keep working. A first argument that is neither a command nor a file is refused with that same line, rather than printing the usage and exiting successfully. +- 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 command line either fails to import or runs `convert` whatever the arguments; 0.20.1 is the first version that works. - `in2lambda source add FILE` freezes a document: it converts .docx and .tex to markdown beside the file, and writes a `FILE.draft.json` beside it, holding the markdown's hash and every block in it with the lines it spans, so that another tool can quote the source by line range. The draft is named after the source, so a folder holding a term's worth of sheets holds a draft for each. `in2lambda source show` prints that markdown numbered with the block ids. Freezing a file that has changed since is refused unless `--start-over` says to discard the draft, and so is showing one, since its block ids would name lines they are not the ids of. Both need pandoc and the `convert` extra, as `convert` does. - A draft now holds a `log` of every command that changed it and a `fields` map of what those commands wrote, each field recording which layer wrote it (1 a spec, 2 a predicate, 3 a line range, 4 a literal), the source ranges it was copied from, whether it has been edited and by whom. `in2lambda draft mark ignore BLOCK` is the first such command, and `in2lambda draft replay` rebuilds the draft from the frozen markdown and the log, refusing unless what it builds is the draft that is there, byte for byte. A draft written before this has no `log` in it and is refused as one nothing here wrote; `in2lambda source add --start-over` freezes the document again. - A draft is filled in by `in2lambda draft question add`, `in2lambda draft part add QUESTION` and `in2lambda draft question solution QUESTION`. Each takes `--text` to copy the wording out of the frozen source, as a block id such as `b3` or as lines such as `s10:14`, or `--literal TEXT` where the source does not say it in a form the field can take, which records the field as edited and written by layer 4 rather than 3. Question and part numbers are worked out from the fields already written rather than given, so a replay arrives at the same ids. `in2lambda draft split block BLOCK AT` cuts a block the parser made one of two things into `b3a` and `b3b`, so that each half can be quoted on its own. A command writing a field that is already written, or quoting lines another field was taken from, is refused: the first naming the field, the second naming both. diff --git a/in2lambda/main.py b/in2lambda/main.py index 3452411..6c1f880 100644 --- a/in2lambda/main.py +++ b/in2lambda/main.py @@ -8,6 +8,7 @@ import getpass import importlib +import os import shlex import warnings from collections.abc import Callable # Rather than typing's, which beartype warns on. @@ -178,10 +179,10 @@ def runner( class _Cli(click.RichGroup): - """The in2lambda group, which says what to run when given the pre-2.0 command line.""" + """The in2lambda group, which says what to run when given the old command line.""" def resolve_command(self, ctx, args): # type: ignore[no-untyped-def] - """Fail with the new command line rather than click's handling of an unknown name. + """Run ``convert`` for the old form, and name it for a first argument that is neither. Click resolves a first argument starting with ``/`` or ``.`` by printing the group's help and exiting successfully, so `in2lambda /path/to/questions.tex @@ -189,6 +190,12 @@ def resolve_command(self, ctx, args): # type: ignore[no-untyped-def] """ # Shell completion resolves partial command lines, and must not raise. if not ctx.resilient_parsing and self.get_command(ctx, args[0]) is None: + if os.path.isfile(args[0]): + click.echo( + f"in2lambda FILE FILTER is the old form. Run: in2lambda convert {shlex.join(args)}", + err=True, + ) + return super().resolve_command(ctx, ["convert", *args]) raise click.UsageError( f"in2lambda no longer takes a file directly. Run: in2lambda convert {shlex.join(args)}" ) diff --git a/pyproject.toml b/pyproject.toml index 1dc30e3..911489e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "in2lambda" -version = "2.0.0" +version = "1.1.0" description = "Converts content ready for import into Lambda Feedback" authors = [] license = "MIT" diff --git a/tests/test_cli.py b/tests/test_cli.py index 35f3ea5..e43070b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,4 @@ -"""What the command line does with the current and the pre-2.0 form.""" +"""What the command line does with the current and the old form.""" import os import shutil @@ -25,10 +25,10 @@ def test_convert_writes_the_set(filters_dir: str, tmp_path) -> None: @pytest.mark.parametrize("path", ["example.tex", "./example.tex", "ABSOLUTE"]) -def test_old_form_fails_and_names_convert( +def test_old_form_converts_with_a_deprecation_line( path: str, filters_dir: str, monkeypatch, tmp_path ) -> None: - """The pre-2.0 form errors out whatever the file path looks like. + """The old form converts whatever the file path looks like, saying so on stderr. A path starting with ``/`` or ``.`` used to make click print the help and exit 0, so every script passing a full path appeared to succeed without converting anything. @@ -42,12 +42,12 @@ def test_old_form_fails_and_names_convert( result = CliRunner().invoke(cli, [path, "PartsSepSol"]) - assert result.exit_code != 0 - assert "in2lambda convert" in result.output - assert not (tmp_path / "out").exists() + assert result.exit_code == 0, result.output + assert "in2lambda FILE FILTER is the old form" in result.stderr + assert (tmp_path / "out" / "set.zip").exists() -def test_old_form_fails_when_run_as_the_installed_command( +def test_old_form_works_when_run_as_the_installed_command( filters_dir: str, tmp_path ) -> None: """The same holds for ``cli()``, which is what the installed command runs. @@ -70,9 +70,23 @@ def test_old_form_fails_when_run_as_the_installed_command( env={**os.environ, "COLUMNS": "200"}, ) - assert result.returncode != 0 - assert "in2lambda convert" in result.stdout + result.stderr - assert not (tmp_path / "out").exists() + assert result.returncode == 0, result.stdout + result.stderr + assert "in2lambda FILE FILTER is the old form" in result.stderr + assert "old form" not in result.stdout + assert (tmp_path / "out" / "set.zip").exists() + + +def test_first_argument_that_is_neither_still_names_convert( + monkeypatch, tmp_path +) -> None: + """A first argument that is neither a subcommand nor a file is refused.""" + monkeypatch.setenv("COLUMNS", "200") # So the message is not wrapped mid-sentence. + monkeypatch.chdir(tmp_path) + + result = CliRunner().invoke(cli, ["missing.tex", "PartsSepSol"]) + + assert result.exit_code != 0 + assert "in2lambda convert" in result.output def test_convert_leaves_only_what_it_writes(filters_dir: str, tmp_path) -> None: