Skip to content

Commit 437e343

Browse files
Export the solution environment convert's own example uses (t52)
Export the solution environment convert's own example uses
2 parents 7b39999 + 333559a commit 437e343

6 files changed

Lines changed: 37 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- An export now names its images by the file names `media/` holds them under. Every markdown image reference a question holds — in the question's text, a part's text, a worked solution, a final answer or an answer box's wording — is rewritten to the file name the image was carried under, so a document writing `![](figures/train.png)` exports as `![](train.png)` beside `media/train.png`, and Lambda Feedback finds the figure where it looks for one. A file two questions use is carried once. Where two different files have the same name, in2lambda names the second as Lambda Feedback's own exports name an image, `question_001_<Title>_0001.png`. Reading an export back is unchanged, because an export already names its images this way.
2121
- A draft can freeze more than one document, which is how a sheet written as a question file and a separate solutions file is drafted. `in2lambda source add questions.docx solutions.docx` freezes them as source 1 and source 2 of the one `questions.draft.json`, and `in2lambda source add solutions.docx --draft questions.docx` adds a file to an existing draft as its next source. Every block id and line range of a source after the first carries that source's number — `2/b3`, `2/s10:14` — and `1/b3` names the block `b3` names. A field quoted from a source records which source it came from, so that the same line number in two documents is two places. `in2lambda source show` prints each source under its number and its name. `in2lambda spec run` runs the spec over every source: the first source is laid out as the spec's `layout` says, and in any source after it the `question` selector picks out the marker written above each question's solutions while every other match is a solution, paired onto the questions and parts of the first source as `in2lambda convert -a` pairs an answers file. A draft now holds `sources`, a list of `{source, hash, blocks}` in the order they were frozen, in place of those three keys at the top level, so a draft written before this release is refused as a draft in2lambda did not write; `in2lambda source add --start-over` freezes the document again. Four changes to the Python API break existing scripts: `in2lambda.source.add` takes a list of files and the draft to freeze them into; `in2lambda.source.frozen` returns the markdown of every source and `in2lambda.draft.apply` takes the markdown of every source, in place of one; `in2lambda.spec.fields` takes one `(blocks, markdown)` pair per source in place of its `elements` and `markdown` arguments; and `in2lambda.spec.Field` carries the number of the source its ranges are lines of, which every caller constructing a `Field` must pass.
2222
- Every command that works on a draft takes `--draft`, naming either the draft or the source it was frozen from: `in2lambda source show`, each `in2lambda draft` command, `in2lambda spec run`, `in2lambda validate`, `in2lambda build` and `in2lambda render`. Left off, each command uses the one draft in the current directory, and where the directory holds more than one draft, the command is refused, naming them. `in2lambda spec run` resolves its SPEC from the draft's directory. The Python functions behind those commands take the draft's path in place of a directory: `in2lambda.source.frozen`, `in2lambda.source.show`, `in2lambda.draft.execute`, `in2lambda.draft.replay`, `in2lambda.draft.spec_command`, `in2lambda.draft.report.validate`, `in2lambda.draft.export.build` and `in2lambda.draft.export.render`. `in2lambda.source.draft_of` returns the path of a document's draft, and `in2lambda.source.find` resolves `--draft` for the command line.
23+
- `in2lambda convert FILE PartsOneSol` now exports the worked solution a document writes in a `solution` environment. Pandoc writes that environment as a Div whose classes hold `solution`, and the filter recognised only a Div whose first block reads `Solution`, so a document using the environment exported every question with an empty worked solution. A Div whose first block reads `Solution` is still recognised.
2324
- Importing `in2lambda.katex_convert` no longer writes a file called `log` into the working directory. That module reports what it changed in an expression to the `in2lambda.katex_convert` logger, which is silent unless the application configures logging.
2425
- `in2lambda convert` now reads a .docx that holds an image. in2lambda looks in the document for the directories a `\graphicspath` names, and read the document as UTF-8 text to find them. A .docx is a zip file, so converting a Word document holding a figure raised `UnicodeDecodeError`. in2lambda now reads a document that is not UTF-8 text as naming no directory, which is what a .docx names.
2526
- The rest of the Python API is unchanged: `in2lambda.main.runner` and everything under `in2lambda.api` take the same arguments and return the same objects.

‎in2lambda/filters/PartsOneSol/filter.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,14 @@ def pandoc_filter(
4949
for item in elem.content:
5050
set.current_question.add_part_text(item)
5151

52-
# Solution is in a Div with nested content being "Solution"
52+
# Pandoc writes a LaTeX environment it has no block for as a Div whose classes
53+
# hold the environment's name, so \begin{solution} becomes a Div classed
54+
# "solution". Some documents instead write the word Solution as the first block
55+
# of the Div; the filter accepts both.
5356
case pf.Div:
54-
if pf.stringify(elem.content[0].content) == "Solution":
55-
set.current_question.add_solution(
56-
pf.stringify(elem) # [len("Solution") :] - For Jon Rackham
57-
)
57+
if "solution" in elem.classes or (
58+
elem.content and pf.stringify(elem.content[0].content) == "Solution"
59+
):
60+
set.current_question.add_solution(pf.stringify(elem))
5861

5962
return None

‎in2lambda/main.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def runner(
103103
>>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsSepSol/example.tex", "PartsSepSol") # doctest: +ELLIPSIS
104104
Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution='', answer='', response_areas=[]), ...], images=[], main_text='This is a sample question\n\n'), ...])
105105
>>> runner(f"{os.path.dirname(in2lambda.__file__)}/filters/PartsOneSol/example.tex", "PartsOneSol") # doctest: +ELLIPSIS
106-
Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution='', answer='', response_areas=[]), ...], images=[...], main_text='Here is some preliminary question information that might be useful.'), ...])
106+
Set(_name='set', _description='', _finalAnswerVisibility='OPEN_WITH_WARNINGS', _workedSolutionVisibility='OPEN_WITH_WARNINGS', _structuredTutorialVisibility='OPEN', questions=[Question(title='', parts=[Part(text=..., worked_solution='This is the final answer...', answer='', response_areas=[]), ...], images=[...], main_text='Here is some preliminary question information that might be useful.'), ...])
107107
"""
108108
_require_conversion_tools()
109109
import panflute as pf
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
1-
Question 1 "", part (a), worked solution: the draft says 'This is the final answer. It contains the solutions for all parts without breaking them up. $$ 1+1 = 2 $$ The solution is copied across all parts.' and convert says '' # t52: the PartsOneSol filter drops every solution
21
Question 1 "", part (b), text: the draft says "The filter still works even if there aren't any parts" and convert says 'The filter still works even if there aren’t any parts' # smart quotes: pandoc's LaTeX reader writes ’ where its commonmark_x writer writes '
3-
Question 1 "", part (b), worked solution: the draft says 'This is the final answer. It contains the solutions for all parts without breaking them up. $$ 1+1 = 2 $$ The solution is copied across all parts.' and convert says '' # t52: the PartsOneSol filter drops every solution
4-
Question 2 "", part (a): the draft wrote this part and convert did not # t52: the PartsOneSol filter drops every solution
2+
Question 2 "", part (a), worked solution: the draft says "And here's the solution" and convert says 'And here’s the solution' # smart quotes: pandoc's LaTeX reader writes ’ where its commonmark_x writer writes '

‎tests/fixtures/against_convert/README.md‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ A folder with no such file is a document the two routes say the same thing about
4242
Finding a difference the file does not list fails the test, and so does agreeing where it
4343
lists one: closing a ticket below means deleting the lines it names.
4444

45-
- **t52** - the `PartsOneSol` filter matches a solution environment only when the Div's
46-
first element stringifies to `Solution`, which pandoc writes for no solution environment,
47-
so convert drops every worked solution in that layout. The draft writes both solutions
48-
of `PartsOneSol/example.tex`, and the second question's solution is a part of its own
49-
that convert has nothing to match.
5045
- **t53** - no draft command writes a `qN.pM.solution`, and no selector reaches inside the
5146
top-level `\item` that `PartPartSolSol/example.tex` and `PartSolPartSol/example.tex`
5247
nest each part's solution in, so those lines are marked ignore and the draft answers

‎tests/test_runner.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,32 @@ def test_runner_returns_populated_set(filter_name: str, filters_dir: str) -> Non
3737
assert question.main_text or question.parts
3838

3939

40+
@pytest.mark.parametrize("filter_name", builtin_filters())
41+
def test_runner_exports_the_examples_solutions(
42+
filter_name: str, filters_dir: str
43+
) -> None:
44+
"""A solution written in an example is a worked solution in the exported set.
45+
46+
Each example that writes a ``solution`` environment answers every question in it, so
47+
every part carries a worked solution. An example added later that leaves a question
48+
unanswered needs a test of its own rather than a looser assertion here.
49+
"""
50+
example = _example(filters_dir, filter_name)
51+
with open(example) as source:
52+
written_solutions = "\\begin{solution}" in source.read()
53+
if not written_solutions:
54+
pytest.skip(f"{filter_name}'s example writes no solution environment")
55+
56+
result = runner(example, filter_name)
57+
58+
for number, question in enumerate(result.questions, start=1):
59+
assert question.parts, f"question {number} has no parts to answer"
60+
for index, part in enumerate(question.parts, start=1):
61+
assert (
62+
part.worked_solution.strip()
63+
), f"question {number} part {index} has no worked solution"
64+
65+
4066
@pytest.mark.parametrize("filter_name", builtin_filters())
4167
def test_runner_writes_importable_json(
4268
filter_name: str, filters_dir: str, tmp_path

0 commit comments

Comments
 (0)