From dfff12ffe6589bf7b4f118143e3c274004757f53 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Mon, 31 Aug 2026 11:39:36 +0100 Subject: [PATCH 1/4] feat: add Markdown filter for hand-authored question sets A new "Markdown" filter parses a plain #/## markdown document into the Set/Question/Part model: - `#` -> new question (heading is the title) - `##` -> new part - `## Solution` -> worked solution for the current part (or the whole question if it has no parts) - `-a answers.md` -> `#` advances to the next question, body blocks become worked solutions This is the intermediate contract the wizard will emit and hand back for review. runner() now runs in2lambda.validation.check_markdown over any markdown input and echoes a warning per math-delimiter problem (non-fatal). docs/source/filters.py learns to document a filter whose example is example.md (shown inline) instead of example.tex (rendered to a PDF), so the new filter is picked up by the existing autosummary generation. Structure adapted from the Markdown2Lambda branch; the elaborate \st/\fa/\ws/*** syntax there is dropped since the Set/Question/Part model only has question text, part text and one worked solution per part. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017VXb8aZqgFBjoeuuddjW6r --- docs/source/filters.py | 46 +++++++--- docs/source/quickstart.md | 6 ++ in2lambda/filters/Markdown/__init__.py | 1 + in2lambda/filters/Markdown/example.md | 35 ++++++++ in2lambda/filters/Markdown/filter.py | 119 +++++++++++++++++++++++++ in2lambda/main.py | 13 +++ in2lambda/validation/__init__.py | 2 +- in2lambda/validation/delimiters.py | 2 +- tests/test_markdown_filter.py | 72 +++++++++++++++ 9 files changed, 282 insertions(+), 14 deletions(-) create mode 100644 in2lambda/filters/Markdown/__init__.py create mode 100644 in2lambda/filters/Markdown/example.md create mode 100644 in2lambda/filters/Markdown/filter.py create mode 100644 tests/test_markdown_filter.py diff --git a/docs/source/filters.py b/docs/source/filters.py index 2002768..3879d61 100644 --- a/docs/source/filters.py +++ b/docs/source/filters.py @@ -37,20 +37,31 @@ def generate_filters_docs(): If absolute were needed: f"{os.path.dirname(filter_module.__file__)}/filename" """ filter_file = f"{relative_directory}/filter.py" - tex_file = f"{relative_directory}/example.tex" + + # Filters ship either a LaTeX example (rendered to an embedded PDF) or, + # for the plain-markdown filter, a markdown example shown inline. + source_directory = Path(os.path.dirname(filter_module.__file__)) + if (source_directory / "example.tex").is_file(): + example_file = f"{relative_directory}/example.tex" + example_language = "LaTeX" + example_is_latex = True + else: + example_file = f"{relative_directory}/example.md" + example_language = "markdown" + example_is_latex = False # Different path likely needed since GitHub Actions builds with dirhtml builder. # This is relative to the auto-generated filter file. pdf_file = f"../../{'../' if os.getenv('GITHUB_ACTIONS') == 'true' else './'}{static_pdf_directory}/{filter_name}.pdf" - if shutil.which("pdflatex"): + if example_is_latex and shutil.which("pdflatex"): subprocess.run( [ "pdflatex", f"-output-directory={static_pdf_directory}", f"-jobname={filter_name}", "-interaction=nonstopmode", - tex_file, + example_file, ], check=True, ) @@ -58,6 +69,25 @@ def generate_filters_docs(): if not os.path.exists(f"{static_pdf_directory}/{filter_name}.pdf"): raise RuntimeError("PDF output not found") + if example_is_latex: + example_rst = f"""\ +A PDF which this filter parses correctly is shown below: + +.. dropdown:: ๐Ÿ“„ LaTeX Code + + .. literalinclude:: {example_file} + :language: {example_language} + +:pdfembed:`src: {pdf_file}, height:700, width:100%, align:middle` +""" + else: + example_rst = f"""\ +A markdown document which this filter parses correctly is shown below: + +.. literalinclude:: {example_file} + :language: {example_language} +""" + rst_content = f"""\ {filter_name} {'*' * len(filter_name)} @@ -67,15 +97,7 @@ def generate_filters_docs(): Minimal Example ---------------- -A PDF which this filter parses correctly is shown below: - -.. dropdown:: ๐Ÿ“„ LaTeX Code - - .. literalinclude:: {tex_file} - :language: LaTeX - -:pdfembed:`src: {pdf_file}, height:700, width:100%, align:middle` - +{example_rst} .. dropdown:: ๐Ÿ Python Filter .. literalinclude:: {filter_file} diff --git a/docs/source/quickstart.md b/docs/source/quickstart.md index 34fd175..194f94e 100644 --- a/docs/source/quickstart.md +++ b/docs/source/quickstart.md @@ -69,6 +69,12 @@ Another filter might be used if [the answers are in a separate file](filters/_au $ in2lambda questions.tex -a solutions.tex PartsSepSol ``` +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: + +```bash +$ in2lambda questions.md Markdown +``` + By default, this generates an `out` directory in the same place that the command was run in. It contains the zipped question files. Check the [command line tool reference](reference/command-line) for more information. diff --git a/in2lambda/filters/Markdown/__init__.py b/in2lambda/filters/Markdown/__init__.py new file mode 100644 index 0000000..adcf12c --- /dev/null +++ b/in2lambda/filters/Markdown/__init__.py @@ -0,0 +1 @@ +"""Filter for question sets hand-authored (or wizard-generated) in plain markdown.""" diff --git a/in2lambda/filters/Markdown/example.md b/in2lambda/filters/Markdown/example.md new file mode 100644 index 0000000..6492f79 --- /dev/null +++ b/in2lambda/filters/Markdown/example.md @@ -0,0 +1,35 @@ +# Projectile motion + +A ball is thrown horizontally from a height of $h = 20\,\text{m}$ with speed +$v_0 = 15\,\text{m/s}$. Take $g = 9.8\,\text{m/s}^2$. + +## Time of flight + +How long does the ball take to reach the ground? + +## Solution + +Vertical motion is independent of the horizontal throw: + +$$ +h = \frac{1}{2} g t^2 \implies t = \sqrt{\frac{2h}{g}} +$$ + +So $t \approx 2.0\,\text{s}$. + +## Horizontal range + +How far from the launch point does the ball land? + +## Solution + +$x = v_0 t \approx 30\,\text{m}$. + +# Newton's second law + +State Newton's second law of motion and give its equation. + +## Solution + +The net force on a body equals the rate of change of its momentum; for constant +mass this is $F = m a$. diff --git a/in2lambda/filters/Markdown/filter.py b/in2lambda/filters/Markdown/filter.py new file mode 100644 index 0000000..21fec5b --- /dev/null +++ b/in2lambda/filters/Markdown/filter.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +r"""Questions written directly in markdown, with a ``#``/``##`` structure. + +The document is a flat sequence of headings and body blocks: + +* A level-1 heading (``#``) starts a **new question**. Its text becomes the + question title; the blocks that follow it (until the next heading) become the + top-level question text. +* A level-2 heading (``##``) whose text is not ``Solution`` starts a **new part** + of the current question. The blocks that follow become the part text. +* A level-2 heading (``##``) whose text is ``Solution`` (case-insensitive) marks + the blocks that follow as the **worked solution** for the current part, or for + the whole question if it has no parts yet. + +When a separate answers file is supplied via ``-a``, a level-1 heading advances +to the next question and every body block is added as a worked solution +(:meth:`~in2lambda.api.question.Question.add_solution` spreads it across the +question's parts). + +This is the format the ``in2lambda wizard`` command emits, and the validator in +:mod:`in2lambda.validation` checks it before conversion. +""" + +from typing import Optional + +import panflute as pf + +from in2lambda.api.part import Part +from in2lambda.api.set import Set +from in2lambda.filters.markdown import filter + +_SOLUTION_HEADING = "solution" + + +class _State: + """Where the next body block should go, tracked while walking one document.""" + + def __init__(self) -> None: + self.target = "main" # "main" | "part" | "solution" + self.part: Optional[Part] = None + + +def _state_for(doc: pf.Doc) -> _State: + """Return the parser state for ``doc``, resetting it when a new document starts. + + panflute has no per-run hook, so state is kept on the function object and + refreshed whenever the document object identity changes (e.g. the question + file followed by a separate answers file). + """ + if getattr(pandoc_filter, "_doc", None) is not doc: + pandoc_filter._doc = doc + pandoc_filter._state = _State() + return pandoc_filter._state + + +def _append(current: str, addition: str) -> str: + """Join two blocks of text with a blank line, ignoring empty additions.""" + addition = addition.strip() + if not addition: + return current + return f"{current}\n\n{addition}" if current else addition + + +@filter +def pandoc_filter( + elem: pf.Element, + doc: pf.elements.Doc, + set: Set, + parsing_answers: bool, +) -> Optional[pf.Str]: + """Turn a ``#``/``##`` markdown document into questions, parts and solutions. + + Args: + elem: The current element being processed. + doc: The Pandoc document container. + set: The Python API used to store the parsed result. + parsing_answers: Whether an answers-only document is being parsed. + + Returns: + Always ``None`` - this filter records into ``set`` rather than rewriting + the AST (inline rewriting is handled by the shared markdown decorator). + """ + # Only act on top-level blocks; inline elements are handled by @filter. + if not isinstance(elem, pf.Block) or not isinstance(elem.parent, pf.Doc): + return None + + state = _state_for(doc) + is_heading = isinstance(elem, pf.Header) + text = pf.stringify(elem).strip() + + if parsing_answers: + if is_heading and elem.level == 1: + set.increment_current_question() + elif not is_heading and text: + set.current_question.add_solution(text) + return None + + if is_heading and elem.level == 1: + set.add_question(title=text) + state.target, state.part = "main", None + elif is_heading and elem.level == 2 and text.lower() == _SOLUTION_HEADING: + if state.part is None: + state.part = Part() + set.current_question.parts.append(state.part) + state.target = "solution" + elif is_heading and elem.level == 2: + state.part = Part() + set.current_question.parts.append(state.part) + state.target = "part" + elif not is_heading and text: + if state.target == "main": + set.current_question.main_text = text + elif state.target == "part" and state.part is not None: + state.part.text = _append(state.part.text, text) + elif state.target == "solution" and state.part is not None: + state.part.worked_solution = _append(state.part.worked_solution, text) + + return None diff --git a/in2lambda/main.py b/in2lambda/main.py index d5df1e2..0aeb664 100644 --- a/in2lambda/main.py +++ b/in2lambda/main.py @@ -16,6 +16,13 @@ import in2lambda.filters from in2lambda.api.set import Set +from in2lambda.validation import check_markdown + + +def _warn_markdown_issues(text: str, source: str) -> None: + """Echo a warning for each math-delimiter problem found in a markdown source.""" + for problem in check_markdown(text): + click.echo(f"Warning: {source}: {problem.value}") def docx_to_md(docx_file: str) -> str: @@ -120,6 +127,9 @@ def runner( input_format = file_type(question_file) + if input_format == "markdown": + _warn_markdown_issues(text, question_file) + # Parse the Pandoc AST using the relevant panflute filter. pf.run_filter( filter_module.pandoc_filter, @@ -141,6 +151,9 @@ def runner( answer_text = file.read() answer_format = file_type(answer_file) + if answer_format == "markdown": + _warn_markdown_issues(answer_text, answer_file) + pf.run_filter( filter_module.pandoc_filter, doc=pf.convert_text( diff --git a/in2lambda/validation/__init__.py b/in2lambda/validation/__init__.py index 2cdfba6..f7aad90 100644 --- a/in2lambda/validation/__init__.py +++ b/in2lambda/validation/__init__.py @@ -1,4 +1,4 @@ -"""Pre-flight checks for the ``#``/``##`` markdown that flows through in2lambda. +"""Pre-flight checks for the markdown that flows through in2lambda. The markdown produced by the wizard (and hand-written by users) is the shared contract between the wizard, the ``Markdown`` filter and Lambda Feedback. These diff --git a/in2lambda/validation/delimiters.py b/in2lambda/validation/delimiters.py index 6824b99..7d6521c 100644 --- a/in2lambda/validation/delimiters.py +++ b/in2lambda/validation/delimiters.py @@ -1,4 +1,4 @@ -"""Checks that ``$ ... $`` and ``$$ ... $$`` math delimiters are balanced and placed correctly. +"""Check that inline and display math delimiters are balanced and placed correctly. KaTeX (and Lambda Feedback) expect inline math wrapped in single dollar signs on one line, and display math wrapped in ``$$`` that each sit alone on their own diff --git a/tests/test_markdown_filter.py b/tests/test_markdown_filter.py new file mode 100644 index 0000000..0b78f20 --- /dev/null +++ b/tests/test_markdown_filter.py @@ -0,0 +1,72 @@ +"""Tests for the ``Markdown`` filter (hand-authored ``#``/``##`` question sets).""" + +import json +import os + +from in2lambda.main import runner + + +def _example(filters_dir: str) -> str: + return os.path.join(filters_dir, "Markdown", "example.md") + + +def test_example_parses_into_questions_parts_and_solutions(filters_dir: str) -> None: + result = runner(_example(filters_dir), "Markdown") + + assert [q.title for q in result.questions] == [ + "Projectile motion", + "Newtonโ€™s second law", + ] + + projectile = result.questions[0] + assert projectile.main_text.startswith("A ball is thrown horizontally") + assert [p.text for p in projectile.parts] == [ + "How long does the ball take to reach the ground?", + "How far from the launch point does the ball land?", + ] + assert projectile.parts[0].worked_solution.startswith("Vertical motion is") + assert "v_0 t" in projectile.parts[1].worked_solution + + # A question with no ``##`` parts keeps its solution on a single empty part. + newton = result.questions[1] + assert newton.parts[0].text == "" + assert "F = m a" in newton.parts[0].worked_solution + + +def test_markdown_filter_writes_importable_json(filters_dir: str, tmp_path) -> None: + out_dir = tmp_path / "out" + runner(_example(filters_dir), "Markdown", str(out_dir)) + + question_files = sorted((out_dir / "set").glob("question_*.json")) + assert len(question_files) == 2 + first = json.loads(question_files[0].read_text()) + assert first["title"] == "Projectile motion" + assert ( + first["parts"][0]["content"] + == "How long does the ball take to reach the ground?" + ) + assert first["parts"][0]["workedSolution"]["content"].startswith( + "Vertical motion is" + ) + + +def test_bad_math_delimiters_warn_but_do_not_fail(tmp_path, capsys) -> None: + bad = tmp_path / "bad.md" + bad.write_text("# Q\n\nText with a stray $ sign and no closing delimiter") + + result = runner(str(bad), "Markdown") + + assert result.questions[0].title == "Q" + assert "unclosed inline" in capsys.readouterr().out + + +def test_separate_answers_file_fills_worked_solutions(tmp_path) -> None: + questions = tmp_path / "q.md" + questions.write_text("# Q1\n\nFirst question.\n\n# Q2\n\nSecond question.\n") + answers = tmp_path / "a.md" + answers.write_text("# Q1\n\nAnswer to one.\n\n# Q2\n\nAnswer to two.\n") + + result = runner(str(questions), "Markdown", answer_file=str(answers)) + + assert result.questions[0].parts[0].worked_solution == "Answer to one." + assert result.questions[1].parts[0].worked_solution == "Answer to two." From 921ec12a137b196a2a4f5181e64cfe6703262543 Mon Sep 17 00:00:00 2001 From: Marcus Messer <12846590+m-messer@users.noreply.github.com> Date: Tue, 15 Sep 2026 09:55:30 +0100 Subject: [PATCH 2/4] test: add pytest suite alongside doctests (#21) Adds a real tests/ suite so the project no longer relies on doctests alone: - tests/test_runner.py runs each built-in filter over its own example.tex end to end, asserting on the returned Set and on the JSON/ZIP written to disk. - [tool.pytest.ini_options] collects both tests/ and the package doctests, so a bare `pytest` covers everything. - CI: `black .` -> `black --check .` (no longer silently reformats), and isort/pydocstyle now also cover tests/. Applies black to two pre-existing files (visibility_status.py, json_convert.py) that were not clean under `black --check`. Claude-Session: https://claude.ai/code/session_017VXb8aZqgFBjoeuuddjW6r Co-authored-by: Claude Sonnet 5 --- .github/workflows/test.yml | 8 ++-- in2lambda/api/visibility_status.py | 1 + in2lambda/json_convert/json_convert.py | 5 ++- pyproject.toml | 5 +++ tests/conftest.py | 16 ++++++++ tests/test_runner.py | 57 ++++++++++++++++++++++++++ 6 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_runner.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0288e3..d2e798f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,11 +24,11 @@ jobs: uses: r-lib/actions/setup-pandoc@v2 - name: Linting Checks run: | - poetry run black . - poetry run isort --check-only in2lambda docs - poetry run pydocstyle --convention=google in2lambda + poetry run black --check . + poetry run isort --check-only in2lambda docs tests + poetry run pydocstyle --convention=google in2lambda tests - name: pytest - run: poetry run pytest --cov-report=xml:coverage.xml --cov=in2lambda --doctest-modules in2lambda + run: poetry run pytest --cov-report=xml:coverage.xml --cov=in2lambda - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/in2lambda/api/visibility_status.py b/in2lambda/api/visibility_status.py index 541c97d..c0295dd 100644 --- a/in2lambda/api/visibility_status.py +++ b/in2lambda/api/visibility_status.py @@ -2,6 +2,7 @@ from enum import Enum + class VisibilityStatus(Enum): """Enum representing the visibility status of a question or set.""" diff --git a/in2lambda/json_convert/json_convert.py b/in2lambda/json_convert/json_convert.py index 73b49eb..dd85f23 100644 --- a/in2lambda/json_convert/json_convert.py +++ b/in2lambda/json_convert/json_convert.py @@ -98,7 +98,10 @@ def converter( # Output file filename = ( - "question_" + str(i).zfill(3) + "_" + re.sub(r'[^\w\-_.]', '_', output['title'].strip()) + "question_" + + str(i).zfill(3) + + "_" + + re.sub(r"[^\w\-_.]", "_", output["title"].strip()) ) # write questions into directory diff --git a/pyproject.toml b/pyproject.toml index 7929d25..e1b4022 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,11 @@ ignore_missing_imports = true [tool.isort] profile = "black" +[tool.pytest.ini_options] +# Collect both the unit tests in tests/ and the doctests embedded in the package. +testpaths = ["tests", "in2lambda"] +addopts = "--doctest-modules" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..095b418 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +"""Shared pytest fixtures for the in2lambda test suite.""" + +import os + +import pytest + +import in2lambda + + +@pytest.fixture(scope="session") +def filters_dir() -> str: + """Absolute path to the packaged ``filters`` directory. + + Each filter ships a self-contained ``example.tex`` used by the end-to-end tests. + """ + return os.path.join(os.path.dirname(in2lambda.__file__), "filters") diff --git a/tests/test_runner.py b/tests/test_runner.py new file mode 100644 index 0000000..df31429 --- /dev/null +++ b/tests/test_runner.py @@ -0,0 +1,57 @@ +"""End-to-end tests for :func:`in2lambda.main.runner` across the built-in filters. + +Each built-in filter ships a self-contained ``example.tex`` that exercises the +document structure it targets. These tests run every filter over its own example +and check both the in-memory :class:`~in2lambda.api.set.Set` and the JSON/ZIP +files written to disk. +""" + +import json +import os + +import pytest + +from in2lambda.api.set import Set +from in2lambda.main import runner + +BUILTIN_FILTERS = ["PartsSepSol", "PartsOneSol", "PartPartSolSol", "PartSolPartSol"] + + +@pytest.mark.parametrize("filter_name", BUILTIN_FILTERS) +def test_runner_returns_populated_set(filter_name: str, filters_dir: str) -> None: + """Every filter turns its example into a Set with at least one usable question.""" + result = runner(os.path.join(filters_dir, filter_name, "example.tex"), filter_name) + + assert isinstance(result, Set) + assert result.questions, f"{filter_name} produced no questions" + for question in result.questions: + # A question is only useful if it has top-level text or at least one part. + assert question.main_text or question.parts + + +@pytest.mark.parametrize("filter_name", BUILTIN_FILTERS) +def test_runner_writes_importable_json( + filter_name: str, filters_dir: str, tmp_path +) -> None: + """Passing an output directory produces the Lambda Feedback set/ dir and zip.""" + out_dir = tmp_path / "out" + result = runner( + os.path.join(filters_dir, filter_name, "example.tex"), + filter_name, + str(out_dir), + ) + + set_dir = out_dir / "set" + assert set_dir.is_dir() + assert (out_dir / "set.zip").is_file() + + set_json = json.loads((set_dir / "set_set.json").read_text()) + assert set_json["name"] == "set" + + question_files = sorted(set_dir.glob("question_*.json")) + assert len(question_files) == len(result.questions) + for question_file in question_files: + question_json = json.loads(question_file.read_text()) + assert question_json["title"] + assert "masterContent" in question_json + assert "parts" in question_json From 3fdccbc846478eb730db3721c94d8475d36d8ad9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 10:00:04 +0100 Subject: [PATCH 3/4] build(deps): bump idna from 3.7 to 3.15 (#28) Bumps [idna](https://github.com/kjd/idna) from 3.7 to 3.15. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md) - [Commits](https://github.com/kjd/idna/compare/v3.7...v3.15) --- updated-dependencies: - dependency-name: idna dependency-version: '3.15' dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- poetry.lock | 109 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index b2c6799..0886a88 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -6,6 +6,7 @@ version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, @@ -17,6 +18,7 @@ version = "2.14.0" description = "Internationalization utilities" optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, @@ -31,6 +33,7 @@ version = "0.17.2" description = "Unbearably fast runtime type checking in pure Python." optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "beartype-0.17.2-py3-none-any.whl", hash = "sha256:c22b21e1f785cfcf5c4d3d13070f532b6243a3ad67e68d2298ff08d539847dce"}, {file = "beartype-0.17.2.tar.gz", hash = "sha256:e911e1ae7de4bccd15745f7643609d8732f64de5c2fb844e89cbbed1c5a8d495"}, @@ -38,9 +41,9 @@ files = [ [package.extras] all = ["typing-extensions (>=3.10.0.0)"] -dev = ["autoapi (>=0.9.0)", "coverage (>=5.5)", "equinox", "mypy (>=0.800)", "numpy", "pandera", "pydata-sphinx-theme (<=0.7.2)", "pytest (>=4.0.0)", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)"] +dev = ["autoapi (>=0.9.0)", "coverage (>=5.5)", "equinox", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pydata-sphinx-theme (<=0.7.2)", "pytest (>=4.0.0)", "sphinx (>=4.2.0,<6.0.0)", "sphinx ; python_version >= \"3.8.0\"", "sphinxext-opengraph (>=0.7.5)", "tox (>=3.20.1)", "typing-extensions (>=3.10.0.0)"] doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"] -test-tox = ["equinox", "mypy (>=0.800)", "numpy", "pandera", "pytest (>=4.0.0)", "sphinx", "typing-extensions (>=3.10.0.0)"] +test-tox = ["equinox", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "numpy ; sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera", "pytest (>=4.0.0)", "sphinx ; python_version >= \"3.8.0\"", "typing-extensions (>=3.10.0.0)"] test-tox-coverage = ["coverage (>=5.5)"] [[package]] @@ -49,6 +52,7 @@ version = "4.12.3" description = "Screen-scraping library" optional = false python-versions = ">=3.6.0" +groups = ["docs"] files = [ {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, @@ -70,6 +74,7 @@ version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, @@ -106,7 +111,7 @@ typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] @@ -116,6 +121,7 @@ version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "docs"] files = [ {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, @@ -127,6 +133,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -138,6 +145,7 @@ version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main", "docs"] files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, @@ -237,6 +245,7 @@ version = "8.1.7" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev", "docs"] files = [ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, @@ -251,10 +260,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev", "docs"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\"", docs = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "coverage" @@ -262,6 +273,7 @@ version = "7.4.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, @@ -321,7 +333,7 @@ files = [ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "distlib" @@ -329,6 +341,7 @@ version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, @@ -340,6 +353,7 @@ version = "0.20.1" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, @@ -351,6 +365,8 @@ version = "1.2.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, @@ -365,6 +381,7 @@ version = "3.13.1" description = "A platform independent file lock." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, @@ -373,7 +390,7 @@ files = [ [package.extras] docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] +typing = ["typing-extensions (>=4.8) ; python_version < \"3.11\""] [[package]] name = "furo" @@ -381,6 +398,7 @@ version = "2024.1.29" description = "A clean customisable Sphinx documentation theme." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "furo-2024.1.29-py3-none-any.whl", hash = "sha256:3548be2cef45a32f8cdc0272d415fcb3e5fa6a0eb4ddfe21df3ecf1fe45a13cf"}, {file = "furo-2024.1.29.tar.gz", hash = "sha256:4d6b2fe3f10a6e36eb9cc24c1e7beb38d7a23fc7b3c382867503b7fcac8a1e02"}, @@ -398,6 +416,7 @@ version = "2.5.35" description = "File identification library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, @@ -408,21 +427,26 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.7" +version = "3.15" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" +groups = ["main", "docs"] files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8"}, + {file = "idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc"}, ] +[package.extras] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["docs"] files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -434,6 +458,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -445,6 +470,7 @@ version = "5.13.2" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, @@ -459,6 +485,7 @@ version = "3.1.4" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, @@ -476,6 +503,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -500,6 +528,7 @@ version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, @@ -569,6 +598,7 @@ version = "0.4.0" description = "Collection of plugins for markdown-it-py" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mdit_py_plugins-0.4.0-py3-none-any.whl", hash = "sha256:b51b3bb70691f57f974e257e367107857a93b36f322a9e6d44ca5bf28ec2def9"}, {file = "mdit_py_plugins-0.4.0.tar.gz", hash = "sha256:d8ab27e9aed6c38aa716819fedfde15ca275715955f8a185a8e1cf90fb1d2c1b"}, @@ -588,6 +618,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main", "docs"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -599,6 +630,7 @@ version = "1.9.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, @@ -646,6 +678,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -657,6 +690,7 @@ version = "2.0.0" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "myst_parser-2.0.0-py3-none-any.whl", hash = "sha256:7c36344ae39c8e740dad7fdabf5aa6fc4897a813083c6cc9990044eb93656b14"}, {file = "myst_parser-2.0.0.tar.gz", hash = "sha256:ea929a67a6a0b1683cdbe19b8d2e724cd7643f8aa3e7bb18dd65beac3483bead"}, @@ -673,7 +707,7 @@ sphinx = ">=6,<8" [package.extras] code-style = ["pre-commit (>=3.0,<4.0)"] linkify = ["linkify-it-py (>=2.0,<3.0)"] -rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.8.2,<0.9.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +rtd = ["ipython", "pydata-sphinx-theme (==0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.8.2,<0.9.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] @@ -683,6 +717,7 @@ version = "1.8.0" description = "Node.js virtual environment builder" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +groups = ["dev"] files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, @@ -697,6 +732,7 @@ version = "24.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["dev", "docs"] files = [ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, @@ -708,6 +744,7 @@ version = "2.3.1" description = "Pythonic Pandoc filters" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "panflute-2.3.1-py3-none-any.whl", hash = "sha256:e44afd875b7b17ffebbbe58282849df06d9f1b20a45a2f933cd51bdcf4e89130"}, {file = "panflute-2.3.1.tar.gz", hash = "sha256:5f1bd02a34ef3982ee025ec5b58fb3a6eedfc31d994b8ae39d8dc9915a2d8f1f"}, @@ -728,6 +765,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -739,6 +777,7 @@ version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, @@ -754,6 +793,7 @@ version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, @@ -769,6 +809,7 @@ version = "3.6.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, @@ -787,6 +828,7 @@ version = "6.3.0" description = "Python docstring style checker" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, @@ -796,7 +838,7 @@ files = [ snowballstemmer = ">=2.2.0" [package.extras] -toml = ["tomli (>=1.2.3)"] +toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""] [[package]] name = "pygments" @@ -804,13 +846,14 @@ version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" +groups = ["main", "docs"] files = [ {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] -plugins = ["importlib-metadata"] +plugins = ["importlib-metadata ; python_version < \"3.8\""] windows-terminal = ["colorama (>=0.4.6)"] [[package]] @@ -819,6 +862,7 @@ version = "8.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, @@ -841,6 +885,7 @@ version = "4.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, @@ -859,6 +904,7 @@ version = "1.0.0" description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, @@ -878,6 +924,7 @@ version = "6.0.1" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.6" +groups = ["main", "dev", "docs"] files = [ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, @@ -938,6 +985,7 @@ version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" +groups = ["main", "docs"] files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, @@ -959,6 +1007,7 @@ version = "13.7.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" +groups = ["main"] files = [ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, @@ -977,6 +1026,7 @@ version = "1.7.4" description = "Format click help output nicely with rich" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "rich-click-1.7.4.tar.gz", hash = "sha256:7ce5de8e4dc0333aec946113529b3eeb349f2e5d2fafee96b9edf8ee36a01395"}, {file = "rich_click-1.7.4-py3-none-any.whl", hash = "sha256:e363655475c60fec5a3e16a1eb618118ed79e666c365a36006b107c17c93ac4e"}, @@ -996,6 +1046,7 @@ version = "69.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] files = [ {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, @@ -1003,7 +1054,7 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -1012,6 +1063,7 @@ version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" +groups = ["dev", "docs"] files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, @@ -1023,6 +1075,7 @@ version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, @@ -1034,6 +1087,7 @@ version = "7.4.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, @@ -1069,6 +1123,7 @@ version = "1.0.0b2" description = "A modern skeleton for Sphinx themes." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, @@ -1086,6 +1141,7 @@ version = "5.1.0" description = "Sphinx extension that automatically documents click applications" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "sphinx-click-5.1.0.tar.gz", hash = "sha256:6812c2db62d3fae71a4addbe5a8a0a16c97eb491f3cd63fe34b4ed7e07236f33"}, {file = "sphinx_click-5.1.0-py3-none-any.whl", hash = "sha256:ae97557a4e9ec646045089326c3b90e026c58a45e083b8f35f17d5d6558d08a0"}, @@ -1102,6 +1158,7 @@ version = "0.5.2" description = "Add a copy button to each of your code cells." optional = false python-versions = ">=3.7" +groups = ["docs"] files = [ {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, @@ -1120,6 +1177,7 @@ version = "0.5.0" description = "A sphinx extension for designing beautiful, view size responsive web components." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "sphinx_design-0.5.0-py3-none-any.whl", hash = "sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e"}, {file = "sphinx_design-0.5.0.tar.gz", hash = "sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00"}, @@ -1143,6 +1201,7 @@ version = "0.3.2" description = "Toggle page content and collapse admonitions in Sphinx." optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "sphinx-togglebutton-0.3.2.tar.gz", hash = "sha256:ab0c8b366427b01e4c89802d5d078472c427fa6e9d12d521c34fa0442559dc7a"}, {file = "sphinx_togglebutton-0.3.2-py3-none-any.whl", hash = "sha256:9647ba7874b7d1e2d43413d8497153a85edc6ac95a3fea9a75ef9c1e08aaae2b"}, @@ -1163,6 +1222,7 @@ version = "1.0.8" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_applehelp-1.0.8-py3-none-any.whl", hash = "sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"}, {file = "sphinxcontrib_applehelp-1.0.8.tar.gz", hash = "sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619"}, @@ -1179,6 +1239,7 @@ version = "1.0.6" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_devhelp-1.0.6-py3-none-any.whl", hash = "sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f"}, {file = "sphinxcontrib_devhelp-1.0.6.tar.gz", hash = "sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"}, @@ -1195,6 +1256,7 @@ version = "2.0.5" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"}, {file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"}, @@ -1211,6 +1273,7 @@ version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" +groups = ["docs"] files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -1225,6 +1288,7 @@ version = "0.1" description = "Sphinx extension to embedd a pdf files webpages" optional = false python-versions = "*" +groups = ["docs"] files = [] develop = false @@ -1243,6 +1307,7 @@ version = "0.17" description = "Sphinx extension to include program output" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +groups = ["docs"] files = [ {file = "sphinxcontrib-programoutput-0.17.tar.gz", hash = "sha256:300ee9b8caee8355d25cc74b4d1c7efd12e608d2ad165e3141d31e6fbc152b7f"}, {file = "sphinxcontrib_programoutput-0.17-py2.py3-none-any.whl", hash = "sha256:0ef1c1d9159dbe7103077748214305eb4e0138e861feb71c0c346afc5fe97f84"}, @@ -1257,6 +1322,7 @@ version = "1.0.7" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"}, {file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"}, @@ -1273,6 +1339,7 @@ version = "1.1.10" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" +groups = ["docs"] files = [ {file = "sphinxcontrib_serializinghtml-1.1.10-py3-none-any.whl", hash = "sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7"}, {file = "sphinxcontrib_serializinghtml-1.1.10.tar.gz", hash = "sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"}, @@ -1289,6 +1356,7 @@ version = "0.9.1" description = "Sphinx Extension to enable OGP support" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "sphinxext-opengraph-0.9.1.tar.gz", hash = "sha256:dd2868a1e7c9497977fbbf44cc0844a42af39ca65fe1bb0272518af225d06fc5"}, {file = "sphinxext_opengraph-0.9.1-py3-none-any.whl", hash = "sha256:b3b230cc6a5b5189139df937f0d9c7b23c7c204493b22646273687969dcb760e"}, @@ -1303,6 +1371,7 @@ version = "2.4.0" description = "ANSI color formatting for output in terminal" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, @@ -1317,10 +1386,12 @@ version = "2.0.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.7" +groups = ["dev", "docs"] files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +markers = {dev = "python_full_version <= \"3.11.0a6\"", docs = "python_version == \"3.10\""} [[package]] name = "typing-extensions" @@ -1328,6 +1399,7 @@ version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, @@ -1339,13 +1411,14 @@ version = "2.2.1" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" +groups = ["main", "docs"] files = [ {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1356,6 +1429,7 @@ version = "20.25.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, @@ -1368,7 +1442,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "wheel" @@ -1376,6 +1450,7 @@ version = "0.43.0" description = "A built-package format for Python" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81"}, {file = "wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85"}, @@ -1385,6 +1460,6 @@ files = [ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" content-hash = "1936b6f3f9ddf7bd069e1fba61aa4e60e990e82aef7c4fb3c3653fd9f53fbbc8" From 33a3d2ef52c4e5a276d56315a5c87bfc631e3aa7 Mon Sep 17 00:00:00 2001 From: Marcus Messer Date: Tue, 15 Sep 2026 15:55:12 +0100 Subject: [PATCH 4/4] feat: support horizontal rules and enhanced Markdown serialization - Preserve `---` horizontal rules in Markdown question sets as solution step separators. - Improve Markdown processing of inline elements, allowing lists, tables, and other markup to remain intact. - Replace `pf.Str` with `pf.RawInline` for better fidelity when serializing inline Markdown elements (e.g., bold/italic text, images, math expressions). - Update test cases to verify new behavior with horizontal rules and enhanced content parsing. --- in2lambda/filters/Markdown/example.md | 7 ++++++- in2lambda/filters/Markdown/filter.py | 23 +++++++++++++++++++---- in2lambda/filters/markdown.py | 17 +++++++++-------- tests/test_markdown_filter.py | 18 +++++++++++------- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/in2lambda/filters/Markdown/example.md b/in2lambda/filters/Markdown/example.md index 6492f79..0ec4a3c 100644 --- a/in2lambda/filters/Markdown/example.md +++ b/in2lambda/filters/Markdown/example.md @@ -5,7 +5,10 @@ $v_0 = 15\,\text{m/s}$. Take $g = 9.8\,\text{m/s}^2$. ## Time of flight -How long does the ball take to reach the ground? +How long does the ball take to reach the ground, using: + +- the vertical motion equation +- the given height and gravity ## Solution @@ -15,6 +18,8 @@ $$ h = \frac{1}{2} g t^2 \implies t = \sqrt{\frac{2h}{g}} $$ +--- + So $t \approx 2.0\,\text{s}$. ## Horizontal range diff --git a/in2lambda/filters/Markdown/filter.py b/in2lambda/filters/Markdown/filter.py index 21fec5b..660cb82 100644 --- a/in2lambda/filters/Markdown/filter.py +++ b/in2lambda/filters/Markdown/filter.py @@ -68,7 +68,7 @@ def pandoc_filter( doc: pf.elements.Doc, set: Set, parsing_answers: bool, -) -> Optional[pf.Str]: +) -> Optional[pf.Inline]: """Turn a ``#``/``##`` markdown document into questions, parts and solutions. Args: @@ -87,12 +87,27 @@ def pandoc_filter( state = _state_for(doc) is_heading = isinstance(elem, pf.Header) - text = pf.stringify(elem).strip() + is_rule = isinstance(elem, pf.HorizontalRule) + + if is_heading: + text = pf.stringify(elem).strip() + elif is_rule: + # HorizontalRule blocks (``---``) stringify to nothing, so they're matched + # separately and kept as literal text: in a Lambda Feedback worked solution + # they mark the boundary between the steps a student clicks through. + text = "---" + else: + # Serialized back to markdown (rather than flattened with pf.stringify) so + # that lists, tables and other markup survive into the question/part/ + # solution text verbatim. + text = pf.convert_text( + elem, input_format="panflute", output_format="markdown" + ).strip() if parsing_answers: if is_heading and elem.level == 1: set.increment_current_question() - elif not is_heading and text: + elif (is_rule or not is_heading) and text: set.current_question.add_solution(text) return None @@ -108,7 +123,7 @@ def pandoc_filter( state.part = Part() set.current_question.parts.append(state.part) state.target = "part" - elif not is_heading and text: + elif (is_rule or not is_heading) and text: if state.target == "main": set.current_question.main_text = text elif state.target == "part" and state.part is not None: diff --git a/in2lambda/filters/markdown.py b/in2lambda/filters/markdown.py index 789aabd..a3b1421 100644 --- a/in2lambda/filters/markdown.py +++ b/in2lambda/filters/markdown.py @@ -118,11 +118,11 @@ def image_path(image_name: str, tex_file: str) -> Optional[str]: def filter( func: Callable[ [pf.Element, pf.elements.Doc, Set, bool], - Optional[pf.Str], + Optional[pf.Inline], ] ) -> Callable[ [pf.Element, pf.elements.Doc, Set, str, bool], - Optional[pf.Str], + Optional[pf.Inline], ]: """Python decorator to make generic LaTeX elements markdown readable. @@ -139,7 +139,7 @@ def markdown_converter( set: Set, tex_file: str, parsing_answers: bool, - ) -> Optional[pf.Str]: + ) -> Optional[pf.Inline]: """Handles LaTeX elements within the filter, before calling the original function. N.B. tex_file is required to determine where the relative image directory is. @@ -163,10 +163,11 @@ def markdown_converter( expression = latex_to_katex(elem.text) except Exception: expression = elem.text - return pf.Str( + return pf.RawInline( f"${expression}$" if elem.format == "InlineMath" - else f"\n\n$$\n{expression}\n$$\n\n" + else f"\n\n$$\n{expression}\n$$\n\n", + format="markdown", ) case pf.Image: @@ -176,13 +177,13 @@ def markdown_converter( echo(f"Warning: Couldn't find {elem.url}") else: set.current_question.images.append(path) - return pf.Str(f"![pictureTag]({elem.url})") + return pf.RawInline(f"![pictureTag]({elem.url})", format="markdown") case pf.Strong: - return pf.Str(f"**{pf.stringify(elem)}**") + return pf.RawInline(f"**{pf.stringify(elem)}**", format="markdown") case pf.Emph: - return pf.Str(f"*{pf.stringify(elem)}*") + return pf.RawInline(f"*{pf.stringify(elem)}*", format="markdown") # Replace siunitx no-break space with narrow no-break space # This should be the space between the number and the units diff --git a/tests/test_markdown_filter.py b/tests/test_markdown_filter.py index 0b78f20..50ca445 100644 --- a/tests/test_markdown_filter.py +++ b/tests/test_markdown_filter.py @@ -20,11 +20,14 @@ def test_example_parses_into_questions_parts_and_solutions(filters_dir: str) -> projectile = result.questions[0] assert projectile.main_text.startswith("A ball is thrown horizontally") - assert [p.text for p in projectile.parts] == [ - "How long does the ball take to reach the ground?", - "How far from the launch point does the ball land?", - ] + assert projectile.parts[0].text == ( + "How long does the ball take to reach the ground, using:\n\n" + "- the vertical motion equation\n- the given height and gravity" + ) + assert projectile.parts[1].text == "How far from the launch point does the ball land?" assert projectile.parts[0].worked_solution.startswith("Vertical motion is") + # The ``---`` separator survives as a literal step boundary. + assert "\n\n---\n\n" in projectile.parts[0].worked_solution assert "v_0 t" in projectile.parts[1].worked_solution # A question with no ``##`` parts keeps its solution on a single empty part. @@ -41,13 +44,14 @@ def test_markdown_filter_writes_importable_json(filters_dir: str, tmp_path) -> N assert len(question_files) == 2 first = json.loads(question_files[0].read_text()) assert first["title"] == "Projectile motion" - assert ( - first["parts"][0]["content"] - == "How long does the ball take to reach the ground?" + assert first["parts"][0]["content"].startswith( + "How long does the ball take to reach the ground, using:" ) + assert "- the vertical motion equation" in first["parts"][0]["content"] assert first["parts"][0]["workedSolution"]["content"].startswith( "Vertical motion is" ) + assert "\n\n---\n\n" in first["parts"][0]["workedSolution"]["content"] def test_bad_math_delimiters_warn_but_do_not_fail(tmp_path, capsys) -> None: