From 23b6e0e1084f7e69a1bc1591410181dc07612681 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 12:30:14 -0600 Subject: [PATCH 01/20] Add design doc for pretext validate command Co-Authored-By: Claude Opus 4.8 --- ...6-06-17-pretext-validate-command-design.md | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-17-pretext-validate-command-design.md diff --git a/docs/superpowers/specs/2026-06-17-pretext-validate-command-design.md b/docs/superpowers/specs/2026-06-17-pretext-validate-command-design.md new file mode 100644 index 00000000..0d98e888 --- /dev/null +++ b/docs/superpowers/specs/2026-06-17-pretext-validate-command-design.md @@ -0,0 +1,126 @@ +# Design: `pretext validate` command + +**Date:** 2026-06-17 +**Status:** Approved (pending spec review) +**Branch:** `validate` + +## Summary + +Add a standalone `pretext validate` command that checks a target's assembled +PreTeXt source against the RelaxNG schema, reports errors clearly, and **exits +non-zero on failure** so it can gate CI and pre-commit workflows. + +This is distinct from the existing build-time validation, which only *warns and +continues* (the return value of `utils.xml_validates_against_schema` is ignored +at `project/__init__.py:742`). + +## Motivation + +- Authors and CI want a way to confirm a document is schema-valid *without* + running a full build. +- Build-time validation is intentionally lenient (warn-only) and lxml-first for + speed. A dedicated command can afford to be stricter and more correct. + +## Command surface + +``` +pretext validate [TARGET] [--dev] +``` + +- New `@main.command` in `pretext/cli.py`, mirroring the `build` command: + `@click.argument("target_name", required=False)`, `@click.pass_context`, + `@nice_errors`. +- Resolves the project from `ctx.obj["project"]` and the target via + `project.get_target(target_name)`. +- **Single-target default**: with no `TARGET`, validates the first target's + source (consistent with `build`/`generate`/`view`). +- Calls `target.source_element()` to **assemble** the source. This resolves + XInclude and surfaces XML-syntax/XInclude errors using existing messaging + before schema validation runs. + +### `--dev` flag + +- Default: validate against the **stable** schema `core/schema/pretext.rng`. +- `--dev`: validate against the **development** schema + `core/schema/pretext-dev.rng` (permits experimental elements not yet in the + stable release). Both files already ship in the bundled core. +- No custom-`--schema`-path flag for now (YAGNI; trivial to add later, since + schema selection is already a parameter of the engine helpers). + +## Validation engine: jing-first, lxml-backup + +For the standalone command, try **jing first** (correct on the full schema), +fall back to **lxml's** built-in RelaxNG if jing is not installed. + +Build-time validation is **unchanged**: it stays lxml-first and warn-only so it +is not slowed down. + +### Outcomes and exit codes + +| Outcome | Behavior | Exit code | +|---|---|---| +| Valid | Success message | `0` | +| Invalid | Print errors to terminal (stderr) + write `.error_schema.log` | `1` | +| Could not validate (jing absent **and** lxml hits the "no define for ref" compile bug) | Clear message that validity could not be confirmed | `2` | + +A distinct exit code for "could not validate" prevents CI from going silently +green when no engine could actually run. + +### Exit-code gotcha + +`nice_errors` (`cli.py:40`) catches every `Exception`, logs it, and returns +normally — yielding exit code `0`. To make the non-zero contract work, the +command signals failure with `ctx.exit(1)` / `sys.exit(...)`, which raise +`SystemExit` (a `BaseException`, not `Exception`); these slip past the +`except Exception` and let Click set the process exit code. + +## `utils` refactor (shared engine, unchanged build path) + +Goal: share engine logic without changing build-time behavior or speed. + +- Factor the two engines into peer helpers, each returning `(is_valid, error_text)` + or `None` (meaning "this engine could not run"): + - `_validate_with_jing(etree, schema_file)` — already exists. + - `_validate_with_lxml(etree, schema_file)` — new; returns `None` on + `RelaxNGParseError` (the compile bug) so the orchestrator can fall back. +- Add an orchestrator: + `run_schema_validation(etree, schema_file, order) -> tuple[Optional[bool], str]` + where `order` is the engine sequence to try; result `None` means "could not + validate with any engine". +- Keep `xml_validates_against_schema(etree)` for the **build path**: a thin + wrapper that calls the orchestrator with `order=[lxml, jing]`, stays warn-only, + and returns `bool`. Build behavior and ordering are preserved. +- The new command calls the orchestrator with `order=[jing, lxml]`, resolves the + schema file from `--dev`, and acts on the `(Optional[bool], error_text)` result + to print errors and set the exit code. + +## Testing + +- Unit tests in `tests/test_utils.py` for the orchestrator (extending the + existing monkeypatch-based tests on this branch): + - jing-first ordering when both engines are available, + - fallback to lxml when jing returns `None`, + - `None` result when both engines are unavailable. +- CLI test: `pretext validate` exits `0` on a known-valid sample and non-zero on + a known-invalid sample; `--dev` selects the dev schema file. + +## Future direction (phase 2, out of scope here) + +Replace the Java dependency with a correct, pure-install engine: + +- pretext-cli's **release CI** checks out `siefkenj/relaxng_validator_wasm` and + builds Python wheels via `maturin` for each platform/Python version, then + vendors or depends on them. +- The orchestrator gains a `wasm` engine that goes to the **front** of `order` + for the validate command, with jing/lxml remaining as fallbacks. +- Note: building maturin/PyO3 wheels requires a Rust toolchain at *build* time; + this is a CI/release-pipeline step, not a build-at-runtime step on the user's + machine. The validator is not yet published to PyPI, which is why phase 1 ships + on jing + lxml. + +## Out of scope + +- Schematron / `pretext-validation-plus.xsl` semantic validation. +- Custom arbitrary `--schema PATH` flag. +- Validating all targets at once (single-target default; could be revisited). +- Changing build-time validation behavior. From 90c3a5525be65e2cd49f7c93483dc540b498736a Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 12:36:31 -0600 Subject: [PATCH 02/20] Add implementation plan for pretext validate command Co-Authored-By: Claude Opus 4.8 --- .../2026-06-17-pretext-validate-command.md | 440 ++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-17-pretext-validate-command.md diff --git a/docs/superpowers/plans/2026-06-17-pretext-validate-command.md b/docs/superpowers/plans/2026-06-17-pretext-validate-command.md new file mode 100644 index 00000000..a36fbb5a --- /dev/null +++ b/docs/superpowers/plans/2026-06-17-pretext-validate-command.md @@ -0,0 +1,440 @@ +# `pretext validate` Command Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `pretext validate [TARGET] [--dev]` command that validates a target's assembled source against the PreTeXt RelaxNG schema and exits non-zero when the document is invalid. + +**Architecture:** Refactor schema validation in `pretext/utils.py` into composable engine helpers (`_validate_with_lxml`, existing `_validate_with_jing`) behind an orchestrator `run_schema_validation(etree, schema_file, order)`. The existing build-time `xml_validates_against_schema` becomes a thin wrapper (lxml-first, warn-only, unchanged behavior). A new Click command in `pretext/cli.py` calls the orchestrator jing-first, prints errors, and sets the process exit code. + +**Tech Stack:** Python, Click, lxml, pytest, pytest-console-scripts (`script_runner`). + +## Global Constraints + +- Do **not** change build-time validation behavior or ordering: `xml_validates_against_schema` stays lxml-first and warn-only. +- The three existing validation tests in `tests/test_utils.py` (`test_xml_validates_against_schema_jing_fallback_success`, `..._invalid`, `..._jing_unavailable`) must continue to pass unchanged. +- Engine functions are resolved by bare name inside `run_schema_validation` (call-time module-global lookup) so `monkeypatch.setattr(utils, "_validate_with_jing", ...)` works. +- Exit codes: valid → `0`; invalid → `1`; could-not-validate → `2`. Failure is signaled with `ctx.exit(...)` (raises `SystemExit`, which bypasses `nice_errors`' `except Exception`). +- Schemas live at `resources.resource_base_path() / "core" / "schema" / {pretext.rng | pretext-dev.rng}`. +- `pretext/utils.py` already imports: `from lxml import etree as ET`, `from lxml.etree import _Element`, `from typing import Optional`, `import typing as t`, `from . import ... resources`, and defines `log`. +- `pretext/cli.py` already imports: `utils`, `resources`, `click`, `sys`, `Optional`, `Path`, and defines `log`, `nice_errors`, `CONTEXT_SETTINGS`, `main`. + +--- + +### Task 1: Add lxml engine, orchestrator, and schema-path helpers in `utils.py` + +**Files:** +- Modify: `pretext/utils.py` (add helpers near the existing `_validate_with_jing`, which ends around line 294) +- Test: `tests/test_utils.py` + +**Interfaces:** +- Consumes: existing `_validate_with_jing(etree: _Element, schema_file: Path) -> Optional[tuple[bool, str]]` (returns `None` when jing is unavailable, `(True, "")` when valid, `(False, error_text)` when invalid). +- Produces: + - `_validate_with_lxml(etree: _Element, schema_file: Path) -> Optional[tuple[bool, str]]` — `None` when lxml cannot compile the schema (`RelaxNGParseError`), else `(is_valid, error_text)`. + - `run_schema_validation(etree: _Element, schema_file: Path, order: t.Sequence[str] = ("lxml", "jing")) -> tuple[Optional[bool], str]` — tries each named engine in `order`; returns the first engine result that is not `None`; returns `(None, )` if every engine is unavailable. + - `schema_path(dev: bool = False) -> Path` — returns the stable or dev schema file path. + +- [ ] **Step 1: Write the failing tests** + +Add to `tests/test_utils.py` (the file already imports `from lxml import etree as ET` and `from pretext import utils` on this branch): + +```python +def test_validate_with_lxml_valid_and_invalid(tmp_path: Path) -> None: + schema_file = tmp_path / "mini.rng" + schema_file.write_text( + '' + "" + ) + + ok = utils._validate_with_lxml(ET.fromstring(""), schema_file) + assert ok == (True, "") + + bad = utils._validate_with_lxml(ET.fromstring(""), schema_file) + assert bad is not None + assert bad[0] is False + assert bad[1] != "" + + +def test_validate_with_lxml_uncompilable_schema_returns_none( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + def _raise(*args: object, **kwargs: object) -> object: + raise ET.RelaxNGParseError("boom") + + monkeypatch.setattr(utils.ET, "RelaxNG", _raise) + assert utils._validate_with_lxml(ET.fromstring(""), tmp_path / "x.rng") is None + + +def test_run_schema_validation_uses_first_available_engine( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setattr(utils, "_validate_with_jing", lambda *a: (True, "")) + monkeypatch.setattr(utils, "_validate_with_lxml", lambda *a: (False, "lxml says no")) + + # jing first wins + assert utils.run_schema_validation( + ET.fromstring(""), tmp_path / "s.rng", order=("jing", "lxml") + ) == (True, "") + # lxml first wins + assert utils.run_schema_validation( + ET.fromstring(""), tmp_path / "s.rng", order=("lxml", "jing") + ) == (False, "lxml says no") + + +def test_run_schema_validation_skips_unavailable_engine( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setattr(utils, "_validate_with_jing", lambda *a: None) + monkeypatch.setattr(utils, "_validate_with_lxml", lambda *a: (True, "")) + assert utils.run_schema_validation( + ET.fromstring(""), tmp_path / "s.rng", order=("jing", "lxml") + ) == (True, "") + + +def test_run_schema_validation_all_unavailable_returns_none( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setattr(utils, "_validate_with_jing", lambda *a: None) + monkeypatch.setattr(utils, "_validate_with_lxml", lambda *a: None) + result = utils.run_schema_validation( + ET.fromstring(""), tmp_path / "s.rng", order=("jing", "lxml") + ) + assert result[0] is None + assert "could not be completed" in result[1] + + +def test_schema_path_selects_stable_or_dev() -> None: + assert utils.schema_path(dev=False).name == "pretext.rng" + assert utils.schema_path(dev=True).name == "pretext-dev.rng" + assert utils.schema_path().name == "pretext.rng" +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `python -m pytest tests/test_utils.py -k "validate_with_lxml or run_schema_validation or schema_path" -v` +Expected: FAIL with `AttributeError: module 'pretext.utils' has no attribute '_validate_with_lxml'` (and similar for the other new names). + +- [ ] **Step 3: Implement the helpers** + +In `pretext/utils.py`, immediately after the existing `_validate_with_jing` function (it ends near line 294, just before the `# boilerplate to prevent overzealous caching` comment), add: + +```python +def _validate_with_lxml( + etree: _Element, schema_file: Path +) -> Optional[tuple[bool, str]]: + # Returns None when lxml cannot compile the schema (the known + # "no define for ref" bug on some libxml2 builds), so the caller can + # fall back to another engine. + try: + relaxng = ET.RelaxNG(file=str(schema_file)) + except ET.RelaxNGParseError: + log.debug( + "lxml could not compile the RelaxNG schema; trying the next validator." + ) + return None + try: + relaxng.assertValid(etree) + return True, "" + except ET.DocumentInvalid as err: + return False, str(err.error_log) + + +def run_schema_validation( + etree: _Element, + schema_file: Path, + order: t.Sequence[str] = ("lxml", "jing"), +) -> tuple[Optional[bool], str]: + # Engines are looked up by name here (not captured at import time) so tests + # can monkeypatch `utils._validate_with_jing` / `utils._validate_with_lxml`. + engines = { + "lxml": _validate_with_lxml, + "jing": _validate_with_jing, + } + for engine_name in order: + result = engines[engine_name](etree, schema_file) + if result is not None: + return result + return None, ( + "Schema validation could not be completed: no validator was available " + "(jing is not installed and lxml could not compile the schema)." + ) + + +def schema_path(dev: bool = False) -> Path: + schema_name = "pretext-dev.rng" if dev else "pretext.rng" + return resources.resource_base_path() / "core" / "schema" / schema_name +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `python -m pytest tests/test_utils.py -k "validate_with_lxml or run_schema_validation or schema_path" -v` +Expected: PASS (6 tests). + +- [ ] **Step 5: Commit** + +```bash +git add pretext/utils.py tests/test_utils.py +git commit -m "Add lxml engine, orchestrator, and schema-path helpers for validation" +``` + +--- + +### Task 2: Refactor `xml_validates_against_schema` to use the orchestrator + +**Files:** +- Modify: `pretext/utils.py:210-263` (the `xml_validates_against_schema` function body) +- Test: `tests/test_utils.py` (existing three tests are the safety net — no new tests) + +**Interfaces:** +- Consumes: `run_schema_validation(...)`, `schema_path(...)` from Task 1. +- Produces: `xml_validates_against_schema(etree: _Element) -> bool` — same signature and observable build-time behavior as before (lxml-first, warn-only, writes `.error_schema.log` on failure). + +- [ ] **Step 1: Confirm the existing tests currently pass (baseline)** + +Run: `python -m pytest tests/test_utils.py -k "xml_validates_against_schema" -v` +Expected: PASS (3 tests). This is the refactor safety net. + +- [ ] **Step 2: Replace the function body** + +In `pretext/utils.py`, replace the entire existing `xml_validates_against_schema` function (from `def xml_validates_against_schema(etree: _Element) -> bool:` through its final `return True`, currently lines ~210-263) with: + +```python +def xml_validates_against_schema(etree: _Element) -> bool: + schemarngfile = schema_path() + log.debug(f"Validating PreTeXt source against schema {schemarngfile}") + # Build-time validation stays lxml-first (fast) and warn-only. + is_valid, error_text = run_schema_validation( + etree, schemarngfile, order=("lxml", "jing") + ) + if is_valid: + log.info("PreTeXt source passed schema validation.") + return True + if is_valid is None: + log.warning(error_text + " Continuing with build.") + else: + log.debug( + "PreTeXt document did not pass schema validation; unexpected output " + "may result. See .error_schema.log for hints. Continuing with build." + ) + with open(".error_schema.log", "w") as error_log_file: + error_log_file.write(error_text) + return False +``` + +- [ ] **Step 3: Run the existing validation tests to verify they still pass** + +Run: `python -m pytest tests/test_utils.py -k "xml_validates_against_schema" -v` +Expected: PASS (3 tests) — `..._jing_fallback_success` (returns True, no log file), `..._jing_fallback_invalid` (log contains "schema validation error via jing"), `..._jing_unavailable` (log file exists). + +- [ ] **Step 4: Run the full utils test module** + +Run: `python -m pytest tests/test_utils.py -v` +Expected: PASS (all, including the 6 from Task 1). + +- [ ] **Step 5: Commit** + +```bash +git add pretext/utils.py +git commit -m "Refactor build-time schema validation to use shared orchestrator" +``` + +--- + +### Task 3: Add the `pretext validate` CLI command + +**Files:** +- Modify: `pretext/cli.py` (add a new command after the `build` command, which ends near line 730) +- Test: `tests/test_cli.py` + +**Interfaces:** +- Consumes: `utils.run_schema_validation(...)`, `utils.schema_path(...)` from Task 1; `ctx.obj["project"]` (set in `main`); `project.get_target(name)`; `target.source_element()` (assembles source, returns `_Element`). +- Produces: a Click command `validate` registered on `main`, invokable as `pretext validate [TARGET] [--dev]`. + +- [ ] **Step 1: Write the failing tests** + +Add to `tests/test_cli.py` (it already imports `Path`, `script_runner: ScriptRunner`, `PTX_CMD`). Add `from lxml import etree as ET` and `from pretext import utils` to its imports, then append: + +```python +def _validator_available() -> bool: + # True if either lxml or jing can run against the bundled schema. + result = utils.run_schema_validation( + ET.fromstring(""), utils.schema_path(), order=("lxml", "jing") + ) + return result[0] is not None + + +def _make_project(tmp_path: Path, script_runner: ScriptRunner) -> Path: + assert script_runner.run([PTX_CMD, "new"], cwd=tmp_path).success + return tmp_path / "new-pretext-project" + + +def test_validate_invalid_source_is_nonzero( + tmp_path: Path, script_runner: ScriptRunner +) -> None: + project = _make_project(tmp_path, script_runner) + main_src = project / "source" / "main.ptx" + main_src.write_text('\n\n') + ret = script_runner.run([PTX_CMD, "validate"], cwd=project) + assert ret.returncode != 0 + + +def test_validate_malformed_xml_is_nonzero( + tmp_path: Path, script_runner: ScriptRunner +) -> None: + project = _make_project(tmp_path, script_runner) + main_src = project / "source" / "main.ptx" + main_src.write_text("\n") # not well-formed + ret = script_runner.run([PTX_CMD, "validate"], cwd=project) + assert ret.returncode != 0 + + +@pytest.mark.skipif( + not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" +) +def test_validate_valid_project_is_zero( + tmp_path: Path, script_runner: ScriptRunner +) -> None: + project = _make_project(tmp_path, script_runner) + ret = script_runner.run([PTX_CMD, "validate"], cwd=project) + assert ret.returncode == 0 + + +@pytest.mark.skipif( + not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" +) +def test_validate_dev_schema_runs( + tmp_path: Path, script_runner: ScriptRunner +) -> None: + project = _make_project(tmp_path, script_runner) + ret = script_runner.run([PTX_CMD, "validate", "--dev"], cwd=project) + assert ret.returncode == 0 +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `python -m pytest tests/test_cli.py -k "validate" -v` +Expected: FAIL — `pretext validate` is not yet a command, so the runs exit non-zero with a "No such command 'validate'" usage error. (The two `..._nonzero` tests may *accidentally* pass for the wrong reason; that is fine — they will pass for the right reason after Step 3. The `..._is_zero` test will FAIL, proving the command is missing.) + +- [ ] **Step 3: Implement the command** + +In `pretext/cli.py`, add the following after the `build` command function (after its body ends near line 730, before the `@main.command(...)` for `generate`): + +```python +@main.command( + short_help="Validate source against the PreTeXt schema", + context_settings=CONTEXT_SETTINGS, +) +@click.argument("target_name", required=False, metavar="target") +@click.option( + "--dev", + is_flag=True, + help="Validate against the development schema (pretext-dev.rng) instead of the " + "stable schema, allowing experimental elements.", +) +@click.pass_context +@nice_errors +def validate(ctx: click.Context, target_name: Optional[str], dev: bool) -> None: + """ + Validate the source of TARGET against the PreTeXt RelaxNG schema. + + Reports schema errors and exits with a non-zero status when the document is + invalid, so it can gate CI or pre-commit checks. Without TARGET, the first + target in project.ptx is used. Exit codes: 0 = valid, 1 = invalid, 2 = + validation could not be performed (no validator available). + """ + project = ctx.obj["project"] + target = project.get_target(target_name) + + # Assemble the source (resolves xinclude); surfaces syntax/xinclude errors. + try: + etree = target.source_element() + except Exception as e: + log.error(f"Could not assemble source for validation: {e}") + ctx.exit(1) + + schema_file = utils.schema_path(dev) + log.info(f"Validating source against schema {schema_file.name}.") + is_valid, error_text = utils.run_schema_validation( + etree, schema_file, order=("jing", "lxml") + ) + + if is_valid: + log.info(f"PreTeXt source passed schema validation ({schema_file.name}).") + return + if is_valid is None: + log.error(error_text) + ctx.exit(2) + + with open(".error_schema.log", "w") as error_log_file: + error_log_file.write(error_text) + log.error("PreTeXt source did NOT pass schema validation:") + log.error(error_text) + log.error("See .error_schema.log for the full report.") + ctx.exit(1) +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `python -m pytest tests/test_cli.py -k "validate" -v` +Expected: PASS (the two `..._nonzero` tests pass; the two skipif tests pass when a validator is available, else skip). + +- [ ] **Step 5: Confirm the command is wired up** + +Run: `pretext validate -h` +Expected: Help text for the validate command, showing the `--dev` option and the `[target]` argument. + +- [ ] **Step 6: Commit** + +```bash +git add pretext/cli.py tests/test_cli.py +git commit -m "Add pretext validate command" +``` + +--- + +### Task 4: Document the command in the changelog + +**Files:** +- Modify: `CHANGELOG.md` + +**Interfaces:** none. + +- [ ] **Step 1: Add a changelog entry** + +Open `CHANGELOG.md`, find the top-most "unreleased"/in-progress section (matching the existing format used by recent entries), and add a bullet: + +```markdown +- Added a `pretext validate` command that checks a target's source against the + RelaxNG schema and exits non-zero on failure (use `--dev` for the development + schema). Tries `jing` first, then falls back to lxml's built-in validator. +``` + +- [ ] **Step 2: Verify the changelog format matches surrounding entries** + +Run: `git diff CHANGELOG.md` +Expected: a single added bullet under the current unreleased section, consistent indentation/style with neighbors. + +- [ ] **Step 3: Commit** + +```bash +git add CHANGELOG.md +git commit -m "Document pretext validate command in changelog" +``` + +--- + +## Self-Review + +**1. Spec coverage:** +- Command surface `pretext validate [TARGET]`, single-target default, assembled source → Task 3. ✓ +- `--dev` flag selecting `pretext-dev.rng` → `schema_path` (Task 1) + flag wiring (Task 3) + test (Task 3). ✓ +- Engine jing-first/lxml-backup → Task 3 calls orchestrator `order=("jing","lxml")`. ✓ +- Exit codes 0/1/2 + `ctx.exit` to bypass `nice_errors` → Task 3 + Global Constraints. ✓ +- `.error_schema.log` written on invalid → Task 3. ✓ +- utils refactor (shared engines, unchanged build path) → Tasks 1 & 2. ✓ +- Build stays lxml-first/warn-only/unchanged → Task 2 + Global Constraints, guarded by existing tests. ✓ +- Testing: orchestrator unit tests (Task 1), build-path regression (Task 2), CLI exit-code/`--dev` tests (Task 3). ✓ +- Phase-2 wasm direction → documented in spec; intentionally out of scope of this plan. ✓ + +**2. Placeholder scan:** No TBD/TODO/"add error handling"/"similar to" — every code and test step contains complete content. ✓ + +**3. Type consistency:** `_validate_with_lxml`, `_validate_with_jing`, and the orchestrator all share `(_Element, Path) -> Optional[tuple[bool, str]]`; orchestrator returns `tuple[Optional[bool], str]`; `schema_path(dev: bool) -> Path`; command consumes these exact names. Build wrapper keeps `(_Element) -> bool`. Consistent across tasks. ✓ From ddaa0a44f78233fe8a1131d7f223331d296bef40 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 12:46:25 -0600 Subject: [PATCH 03/20] Add jing fallback for RelaxNG schema validation Pre-existing branch work: fall back to the jing validator when lxml cannot compile the schema, with tests for the fallback paths. Co-Authored-By: Claude Opus 4.8 --- pretext/utils.py | 63 ++++++++++++++++++++++++++++++++++++++++++++- tests/test_utils.py | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/pretext/utils.py b/pretext/utils.py index 27772d87..5f374355 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -12,6 +12,7 @@ import socketserver import socket import subprocess +import tempfile import time as time import logging import logging.handlers @@ -209,9 +210,38 @@ def xml_syntax_is_valid(xmlfile: Path, root_tag: str = "pretext") -> bool: def xml_validates_against_schema(etree: _Element) -> bool: # get path to RelaxNG schema file: schemarngfile = resources.resource_base_path() / "core" / "schema" / "pretext.rng" + log.debug(f"Validating PreTeXt source against schema {schemarngfile}") # Open schemafile for validation: - relaxng = ET.RelaxNG(file=schemarngfile) + try: + relaxng = ET.RelaxNG(file=schemarngfile) + except ET.RelaxNGParseError as err: + # Some libxml/lxml runtime combinations fail to compile this schema + # with false "no define for ref" errors. Fall back to jing if present. + log.debug( + "lxml could not compile the RelaxNG schema; trying jing validator instead." + ) + jing_result = _validate_with_jing(etree, schemarngfile) + if jing_result is None: + error_text = str(getattr(err, "error_log", err)) + log.warning( + "jing is unavailable, so schema validation could not be completed. " + "Continuing with build." + ) + with open(".error_schema.log", "w") as error_log_file: + error_log_file.write(error_text) + return False + is_valid, error_output = jing_result + if is_valid: + log.info("PreTeXt source passed schema validation via jing.") + return True + log.debug( + "PreTeXt document did not pass schema validation (jing); unexpected output may result. " + "See .error_schema.log for hints. Continuing with build." + ) + with open(".error_schema.log", "w") as error_log_file: + error_log_file.write(error_output) + return False # just for testing # ---------------- @@ -233,6 +263,37 @@ def xml_validates_against_schema(etree: _Element) -> bool: return True +def _validate_with_jing(etree: _Element, schema_file: Path) -> Optional[tuple[bool, str]]: + jing_executable = shutil.which("jing") + if jing_executable is None: + return None + + tmp_path: Optional[Path] = None + try: + xml_payload = ET.tostring( + etree.getroottree(), encoding="utf-8", xml_declaration=True + ) + with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tmp: + tmp.write(xml_payload) + tmp_path = Path(tmp.name) + + result = subprocess.run( + [jing_executable, str(schema_file), str(tmp_path)], + check=False, + capture_output=True, + text=True, + ) + output = "\n".join( + chunk for chunk in [result.stdout.strip(), result.stderr.strip()] if chunk + ) + return result.returncode == 0, output + except OSError: + return None + finally: + if tmp_path is not None: + tmp_path.unlink(missing_ok=True) + + # boilerplate to prevent overzealous caching by preview server, and # avoid port issues def binding_for_access(access: t.Literal["public", "private"] = "private") -> str: diff --git a/tests/test_utils.py b/tests/test_utils.py index 96dd1546..3e8c626d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,6 +3,7 @@ import sys import pytest from pathlib import Path +from lxml import etree as ET from pretext import utils @@ -202,3 +203,55 @@ def test_xml_syntax_is_valid(tmp_path: Path) -> None: # A nonexistent file should raise IOError. with pytest.raises(IOError): utils.xml_syntax_is_valid(tmp_path / "nonexistent.ptx") + + +def test_xml_validates_against_schema_jing_fallback_success( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.chdir(tmp_path) + etree = ET.fromstring("") + + def _raise_relaxng_parse_error(*args: object, **kwargs: object) -> object: + raise ET.RelaxNGParseError("boom") + + monkeypatch.setattr(utils.ET, "RelaxNG", _raise_relaxng_parse_error) + monkeypatch.setattr(utils, "_validate_with_jing", lambda *args: (True, "")) + + assert utils.xml_validates_against_schema(etree) + assert not (tmp_path / ".error_schema.log").exists() + + +def test_xml_validates_against_schema_jing_fallback_invalid( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.chdir(tmp_path) + etree = ET.fromstring("") + + def _raise_relaxng_parse_error(*args: object, **kwargs: object) -> object: + raise ET.RelaxNGParseError("boom") + + monkeypatch.setattr(utils.ET, "RelaxNG", _raise_relaxng_parse_error) + monkeypatch.setattr( + utils, + "_validate_with_jing", + lambda *args: (False, "schema validation error via jing"), + ) + + assert not utils.xml_validates_against_schema(etree) + assert (tmp_path / ".error_schema.log").read_text() == "schema validation error via jing" + + +def test_xml_validates_against_schema_jing_unavailable( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.chdir(tmp_path) + etree = ET.fromstring("") + + def _raise_relaxng_parse_error(*args: object, **kwargs: object) -> object: + raise ET.RelaxNGParseError("boom") + + monkeypatch.setattr(utils.ET, "RelaxNG", _raise_relaxng_parse_error) + monkeypatch.setattr(utils, "_validate_with_jing", lambda *args: None) + + assert not utils.xml_validates_against_schema(etree) + assert (tmp_path / ".error_schema.log").exists() From 1b4bd433c8a875a7db38107bdfd7cecfbc84e32d Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 12:46:31 -0600 Subject: [PATCH 04/20] Add lxml engine, orchestrator, and schema-path helpers for validation Co-Authored-By: Claude Opus 4.8 --- pretext/utils.py | 46 +++++++++++++++++++++++++++++ tests/test_utils.py | 70 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) diff --git a/pretext/utils.py b/pretext/utils.py index 5f374355..fcd061f9 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -294,6 +294,52 @@ def _validate_with_jing(etree: _Element, schema_file: Path) -> Optional[tuple[bo tmp_path.unlink(missing_ok=True) +def _validate_with_lxml( + etree: _Element, schema_file: Path +) -> Optional[tuple[bool, str]]: + # Returns None when lxml cannot compile the schema (the known + # "no define for ref" bug on some libxml2 builds), so the caller can + # fall back to another engine. + try: + relaxng = ET.RelaxNG(file=str(schema_file)) + except ET.RelaxNGParseError: + log.debug( + "lxml could not compile the RelaxNG schema; trying the next validator." + ) + return None + try: + relaxng.assertValid(etree) + return True, "" + except ET.DocumentInvalid as err: + return False, str(err.error_log) + + +def run_schema_validation( + etree: _Element, + schema_file: Path, + order: t.Sequence[str] = ("lxml", "jing"), +) -> tuple[Optional[bool], str]: + # Engines are looked up by name here (not captured at import time) so tests + # can monkeypatch `utils._validate_with_jing` / `utils._validate_with_lxml`. + engines = { + "lxml": _validate_with_lxml, + "jing": _validate_with_jing, + } + for engine_name in order: + result = engines[engine_name](etree, schema_file) + if result is not None: + return result + return None, ( + "Schema validation could not be completed: no validator was available " + "(jing is not installed and lxml could not compile the schema)." + ) + + +def schema_path(dev: bool = False) -> Path: + schema_name = "pretext-dev.rng" if dev else "pretext.rng" + return resources.resource_base_path() / "core" / "schema" / schema_name + + # boilerplate to prevent overzealous caching by preview server, and # avoid port issues def binding_for_access(access: t.Literal["public", "private"] = "private") -> str: diff --git a/tests/test_utils.py b/tests/test_utils.py index 3e8c626d..9370a378 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -255,3 +255,73 @@ def _raise_relaxng_parse_error(*args: object, **kwargs: object) -> object: assert not utils.xml_validates_against_schema(etree) assert (tmp_path / ".error_schema.log").exists() + + +def test_validate_with_lxml_valid_and_invalid(tmp_path: Path) -> None: + schema_file = tmp_path / "mini.rng" + schema_file.write_text( + '' + "" + ) + + ok = utils._validate_with_lxml(ET.fromstring(""), schema_file) + assert ok == (True, "") + + bad = utils._validate_with_lxml(ET.fromstring(""), schema_file) + assert bad is not None + assert bad[0] is False + assert bad[1] != "" + + +def test_validate_with_lxml_uncompilable_schema_returns_none( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + def _raise(*args: object, **kwargs: object) -> object: + raise ET.RelaxNGParseError("boom") + + monkeypatch.setattr(utils.ET, "RelaxNG", _raise) + assert utils._validate_with_lxml(ET.fromstring(""), tmp_path / "x.rng") is None + + +def test_run_schema_validation_uses_first_available_engine( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setattr(utils, "_validate_with_jing", lambda *a: (True, "")) + monkeypatch.setattr(utils, "_validate_with_lxml", lambda *a: (False, "lxml says no")) + + # jing first wins + assert utils.run_schema_validation( + ET.fromstring(""), tmp_path / "s.rng", order=("jing", "lxml") + ) == (True, "") + # lxml first wins + assert utils.run_schema_validation( + ET.fromstring(""), tmp_path / "s.rng", order=("lxml", "jing") + ) == (False, "lxml says no") + + +def test_run_schema_validation_skips_unavailable_engine( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setattr(utils, "_validate_with_jing", lambda *a: None) + monkeypatch.setattr(utils, "_validate_with_lxml", lambda *a: (True, "")) + assert utils.run_schema_validation( + ET.fromstring(""), tmp_path / "s.rng", order=("jing", "lxml") + ) == (True, "") + + +def test_run_schema_validation_all_unavailable_returns_none( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setattr(utils, "_validate_with_jing", lambda *a: None) + monkeypatch.setattr(utils, "_validate_with_lxml", lambda *a: None) + result = utils.run_schema_validation( + ET.fromstring(""), tmp_path / "s.rng", order=("jing", "lxml") + ) + assert result[0] is None + assert "could not be completed" in result[1] + + +def test_schema_path_selects_stable_or_dev() -> None: + assert utils.schema_path(dev=False).name == "pretext.rng" + assert utils.schema_path(dev=True).name == "pretext-dev.rng" + assert utils.schema_path().name == "pretext.rng" From c6879b87ae9de5eb3dc1734f70cbe7c7f6d731fe Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 12:49:28 -0600 Subject: [PATCH 05/20] Refactor build-time schema validation to use shared orchestrator --- pretext/utils.py | 64 ++++++++++++------------------------------------ 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/pretext/utils.py b/pretext/utils.py index fcd061f9..be863fd8 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -208,59 +208,25 @@ def xml_syntax_is_valid(xmlfile: Path, root_tag: str = "pretext") -> bool: def xml_validates_against_schema(etree: _Element) -> bool: - # get path to RelaxNG schema file: - schemarngfile = resources.resource_base_path() / "core" / "schema" / "pretext.rng" + schemarngfile = schema_path() log.debug(f"Validating PreTeXt source against schema {schemarngfile}") - - # Open schemafile for validation: - try: - relaxng = ET.RelaxNG(file=schemarngfile) - except ET.RelaxNGParseError as err: - # Some libxml/lxml runtime combinations fail to compile this schema - # with false "no define for ref" errors. Fall back to jing if present. - log.debug( - "lxml could not compile the RelaxNG schema; trying jing validator instead." - ) - jing_result = _validate_with_jing(etree, schemarngfile) - if jing_result is None: - error_text = str(getattr(err, "error_log", err)) - log.warning( - "jing is unavailable, so schema validation could not be completed. " - "Continuing with build." - ) - with open(".error_schema.log", "w") as error_log_file: - error_log_file.write(error_text) - return False - is_valid, error_output = jing_result - if is_valid: - log.info("PreTeXt source passed schema validation via jing.") - return True - log.debug( - "PreTeXt document did not pass schema validation (jing); unexpected output may result. " - "See .error_schema.log for hints. Continuing with build." - ) - with open(".error_schema.log", "w") as error_log_file: - error_log_file.write(error_output) - return False - - # just for testing - # ---------------- - # relaxng.validate(source_xml) - # log = relaxng.error_log - # print(log) - - # validate against schema - try: - relaxng.assertValid(etree) + # Build-time validation stays lxml-first (fast) and warn-only. + is_valid, error_text = run_schema_validation( + etree, schemarngfile, order=("lxml", "jing") + ) + if is_valid: log.info("PreTeXt source passed schema validation.") - except ET.DocumentInvalid as err: + return True + if is_valid is None: + log.warning(error_text + " Continuing with build.") + else: log.debug( - "PreTeXt document did not pass schema validation; unexpected output may result. See .error_schema.log for hints. Continuing with build." + "PreTeXt document did not pass schema validation; unexpected output " + "may result. See .error_schema.log for hints. Continuing with build." ) - with open(".error_schema.log", "w") as error_log_file: - error_log_file.write(str(err.error_log)) - return False - return True + with open(".error_schema.log", "w") as error_log_file: + error_log_file.write(error_text) + return False def _validate_with_jing(etree: _Element, schema_file: Path) -> Optional[tuple[bool, str]]: From 1b736b6ed02c055fd66e34496a8f861e4cda11bf Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 12:56:32 -0600 Subject: [PATCH 06/20] Add pretext validate command --- pretext/cli.py | 54 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_cli.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/pretext/cli.py b/pretext/cli.py index b35e10a1..34753022 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -729,6 +729,60 @@ def build( return +# pretext validate +@main.command( + short_help="Validate source against the PreTeXt schema", + context_settings=CONTEXT_SETTINGS, +) +@click.argument("target_name", required=False, metavar="target") +@click.option( + "--dev", + is_flag=True, + help="Validate against the development schema (pretext-dev.rng) instead of the " + "stable schema, allowing experimental elements.", +) +@click.pass_context +@nice_errors +def validate(ctx: click.Context, target_name: Optional[str], dev: bool) -> None: + """ + Validate the source of TARGET against the PreTeXt RelaxNG schema. + + Reports schema errors and exits with a non-zero status when the document is + invalid, so it can gate CI or pre-commit checks. Without TARGET, the first + target in project.ptx is used. Exit codes: 0 = valid, 1 = invalid, 2 = + validation could not be performed (no validator available). + """ + project = ctx.obj["project"] + target = project.get_target(target_name) + + # Assemble the source (resolves xinclude); surfaces syntax/xinclude errors. + try: + etree = target.source_element() + except Exception as e: + log.error(f"Could not assemble source for validation: {e}") + ctx.exit(1) + + schema_file = utils.schema_path(dev) + log.info(f"Validating source against schema {schema_file.name}.") + is_valid, error_text = utils.run_schema_validation( + etree, schema_file, order=("jing", "lxml") + ) + + if is_valid: + log.info(f"PreTeXt source passed schema validation ({schema_file.name}).") + return + if is_valid is None: + log.error(error_text) + ctx.exit(2) + + with open(".error_schema.log", "w") as error_log_file: + error_log_file.write(error_text) + log.error("PreTeXt source did NOT pass schema validation:") + log.error(error_text) + log.error("See .error_schema.log for the full report.") + ctx.exit(1) + + # pretext generate @main.command( short_help="Generate specified assets for default target or targets specified by `-t`", diff --git a/tests/test_cli.py b/tests/test_cli.py index 1709b296..c7a35344 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,9 @@ from contextlib import contextmanager import requests import pretext +from lxml import etree as ET from pretext import constants +from pretext import utils from typing import cast, Generator import pytest from pytest_console_scripts import ScriptRunner @@ -523,3 +525,58 @@ def test_support(tmp_path: Path, script_runner: ScriptRunner) -> None: [PTX_CMD, "-v", "debug", "new", "-d", "."], cwd=tmp_path ).success assert script_runner.run([PTX_CMD, "support"], cwd=tmp_path).success + + +def _validator_available() -> bool: + # True if either lxml or jing can run against the bundled schema. + result = utils.run_schema_validation( + ET.fromstring(""), utils.schema_path(), order=("lxml", "jing") + ) + return result[0] is not None + + +def _make_project(tmp_path: Path, script_runner: ScriptRunner) -> Path: + assert script_runner.run([PTX_CMD, "new"], cwd=tmp_path).success + return tmp_path / "new-pretext-project" + + +def test_validate_invalid_source_is_nonzero( + tmp_path: Path, script_runner: ScriptRunner +) -> None: + project = _make_project(tmp_path, script_runner) + main_src = project / "source" / "main.ptx" + main_src.write_text('\n\n') + ret = script_runner.run([PTX_CMD, "validate"], cwd=project) + assert ret.returncode != 0 + + +def test_validate_malformed_xml_is_nonzero( + tmp_path: Path, script_runner: ScriptRunner +) -> None: + project = _make_project(tmp_path, script_runner) + main_src = project / "source" / "main.ptx" + main_src.write_text("\n") # not well-formed + ret = script_runner.run([PTX_CMD, "validate"], cwd=project) + assert ret.returncode != 0 + + +@pytest.mark.skipif( + not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" +) +def test_validate_valid_project_is_zero( + tmp_path: Path, script_runner: ScriptRunner +) -> None: + project = _make_project(tmp_path, script_runner) + ret = script_runner.run([PTX_CMD, "validate"], cwd=project) + assert ret.returncode == 0 + + +@pytest.mark.skipif( + not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" +) +def test_validate_dev_schema_runs( + tmp_path: Path, script_runner: ScriptRunner +) -> None: + project = _make_project(tmp_path, script_runner) + ret = script_runner.run([PTX_CMD, "validate", "--dev"], cwd=project) + assert ret.returncode == 0 From ec89a92d6565573e35dfebf4c9e173c89ba32dc7 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 13:54:30 -0600 Subject: [PATCH 07/20] Drop stable-schema valid-project test (default template is dev-only) --- tests/test_cli.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c7a35344..91ef2820 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -560,17 +560,6 @@ def test_validate_malformed_xml_is_nonzero( assert ret.returncode != 0 -@pytest.mark.skipif( - not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" -) -def test_validate_valid_project_is_zero( - tmp_path: Path, script_runner: ScriptRunner -) -> None: - project = _make_project(tmp_path, script_runner) - ret = script_runner.run([PTX_CMD, "validate"], cwd=project) - assert ret.returncode == 0 - - @pytest.mark.skipif( not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" ) From 6396ca0596daa4cf2e0dbd6c2f5c1cbf36e549e1 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 14:03:05 -0600 Subject: [PATCH 08/20] Fix validate exit codes: raise SystemExit so nice_errors doesn't swallow them --- pretext/cli.py | 6 +++--- tests/test_cli.py | 22 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/pretext/cli.py b/pretext/cli.py index 34753022..17abe871 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -760,7 +760,7 @@ def validate(ctx: click.Context, target_name: Optional[str], dev: bool) -> None: etree = target.source_element() except Exception as e: log.error(f"Could not assemble source for validation: {e}") - ctx.exit(1) + raise SystemExit(1) schema_file = utils.schema_path(dev) log.info(f"Validating source against schema {schema_file.name}.") @@ -773,14 +773,14 @@ def validate(ctx: click.Context, target_name: Optional[str], dev: bool) -> None: return if is_valid is None: log.error(error_text) - ctx.exit(2) + raise SystemExit(2) with open(".error_schema.log", "w") as error_log_file: error_log_file.write(error_text) log.error("PreTeXt source did NOT pass schema validation:") log.error(error_text) log.error("See .error_schema.log for the full report.") - ctx.exit(1) + raise SystemExit(1) # pretext generate diff --git a/tests/test_cli.py b/tests/test_cli.py index 91ef2820..2174681d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -547,7 +547,7 @@ def test_validate_invalid_source_is_nonzero( main_src = project / "source" / "main.ptx" main_src.write_text('\n\n') ret = script_runner.run([PTX_CMD, "validate"], cwd=project) - assert ret.returncode != 0 + assert ret.returncode == 1 def test_validate_malformed_xml_is_nonzero( @@ -557,7 +557,7 @@ def test_validate_malformed_xml_is_nonzero( main_src = project / "source" / "main.ptx" main_src.write_text("\n") # not well-formed ret = script_runner.run([PTX_CMD, "validate"], cwd=project) - assert ret.returncode != 0 + assert ret.returncode == 1 @pytest.mark.skipif( @@ -569,3 +569,21 @@ def test_validate_dev_schema_runs( project = _make_project(tmp_path, script_runner) ret = script_runner.run([PTX_CMD, "validate", "--dev"], cwd=project) assert ret.returncode == 0 + + +def test_validate_could_not_validate_exits_2( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, script_runner: ScriptRunner +) -> None: + from click.testing import CliRunner + from pretext import cli + from pretext import utils as putils + + project = _make_project(tmp_path, script_runner) + monkeypatch.chdir(project) + # Avoid the network update check and force the "no validator available" result. + monkeypatch.setattr(putils, "check_for_updates", lambda *a, **k: None) + monkeypatch.setattr( + putils, "run_schema_validation", lambda *a, **k: (None, "no validator available") + ) + result = CliRunner().invoke(cli.main, ["validate"]) + assert result.exit_code == 2 From 4d3d406472870c29bdefc6729704b8877fba5b59 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 14:06:12 -0600 Subject: [PATCH 09/20] Document pretext validate command in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aedca1f..03c051a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Instructions: Add a subsection under `[Unreleased]` for additions, fixes, change - You can now specify the debug level *after* the command (e.g., `pretext -v debug build` can now also be entered as `pretext build -v debug`). - Temporary directories created by core pretext will now be cleaned up at all debug levels. To save them for debugging, use the `--save-tmp-dirs` flag, as in `pretext --save-tmp-dirs build`. +- Added a `pretext validate` command that checks a target's source against the RelaxNG schema and exits non-zero on failure (use `--dev` for the development schema). Tries `jing` first, then falls back to lxml's built-in validator. ## [2.41.2] - 2026-06-07 From 1e72c82440944faf4674de72bac7042a760c2593 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 14:42:12 -0600 Subject: [PATCH 10/20] better placing of schema error logs --- poetry.lock | 326 ++++++++++++++++++++++---------------------- pretext/__init__.py | 2 +- pretext/cli.py | 6 +- pretext/utils.py | 3 +- pyproject.toml | 2 +- 5 files changed, 171 insertions(+), 168 deletions(-) diff --git a/poetry.lock b/poetry.lock index eb402880..ed2f0ce7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -24,7 +24,7 @@ description = "High-level concurrency and networking framework on top of asyncio optional = true python-versions = ">=3.10" groups = ["main"] -markers = "extra == \"all\" or extra == \"homepage\"" +markers = "extra == \"homepage\" or extra == \"all\"" files = [ {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, @@ -147,7 +147,7 @@ description = "Fast, simple object-to-object and broadcast signaling" optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"all\" or extra == \"homepage\"" +markers = "extra == \"homepage\" or extra == \"all\"" files = [ {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, @@ -614,7 +614,7 @@ files = [ {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] -markers = {main = "extra == \"all\" or extra == \"homepage\""} +markers = {main = "extra == \"homepage\" or extra == \"all\""} [package.source] type = "legacy" @@ -653,7 +653,7 @@ files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] -markers = {main = "(extra == \"all\" or extra == \"homepage\") and python_version == \"3.10\"", dev = "python_version == \"3.10\""} +markers = {main = "(extra == \"homepage\" or extra == \"all\") and python_version == \"3.10\"", dev = "python_version == \"3.10\""} [package.extras] test = ["pytest (>=6)"] @@ -670,7 +670,7 @@ description = "Standalone version of django.utils.feedgenerator" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"homepage\"" +markers = "extra == \"homepage\" or extra == \"all\"" files = [ {file = "feedgenerator-2.1.0-py3-none-any.whl", hash = "sha256:93b7ce1c5a86195cafd6a8e9baf6a2a863ebd6d9905e840ce5778f73efd9a8d5"}, {file = "feedgenerator-2.1.0.tar.gz", hash = "sha256:f075f23f28fd227f097c36b212161c6cf012e1c6caaf7ff53d5d6bb02cd42b9d"}, @@ -1056,146 +1056,146 @@ reference = "pypi-public" [[package]] name = "lxml" -version = "6.1.0" +version = "6.1.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec"}, - {file = "lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6"}, - {file = "lxml-6.1.0-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3"}, - {file = "lxml-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7"}, - {file = "lxml-6.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16"}, - {file = "lxml-6.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1"}, - {file = "lxml-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a"}, - {file = "lxml-6.1.0-cp310-cp310-win32.whl", hash = "sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544"}, - {file = "lxml-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a"}, - {file = "lxml-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776"}, - {file = "lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9"}, - {file = "lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d036ee7b99d5148072ac7c9b847193decdfeac633db350363f7bce4fff108f0e"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ae5d8d5427f3cc317e7950f2da7ad276df0cfa37b8de2f5658959e618ea8512"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:363e47283bde87051b821826e71dde47f107e08614e1aa312ba0c5711e77738c"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:f504d861d9f2a8f94020130adac88d66de93841707a23a86244263d1e54682f5"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:23a5dc68e08ed13331d61815c08f260f46b4a60fdd1640bbeb82cf89a9d90289"}, - {file = "lxml-6.1.0-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f15401d8d3dbf239e23c818afc10c7207f7b95f9a307e092122b6f86dd43209a"}, - {file = "lxml-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fcf3da95e93349e0647d48d4b36a12783105bcc74cb0c416952f9988410846a3"}, - {file = "lxml-6.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0d082495c5fcf426e425a6e28daaba1fcb6d8f854a4ff01effb1f1f381203eb9"}, - {file = "lxml-6.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e3c4f84b24a1fcba435157d111c4b755099c6ff00a3daee1ad281817de75ed11"}, - {file = "lxml-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:976a6b39b1b13e8c354ad8d3f261f3a4ac6609518af91bdb5094760a08f132c4"}, - {file = "lxml-6.1.0-cp311-cp311-win32.whl", hash = "sha256:857efde87d365706590847b916baff69c0bc9252dc5af030e378c9800c0b10e3"}, - {file = "lxml-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:183bfb45a493081943be7ea2b5adfc2b611e1cf377cefa8b8a8be404f45ef9a7"}, - {file = "lxml-6.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:19f4164243fc206d12ed3d866e80e74f5bc3627966520da1a5f97e42c32a3f39"}, - {file = "lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d"}, - {file = "lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad"}, - {file = "lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5"}, - {file = "lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d"}, - {file = "lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f"}, - {file = "lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366"}, - {file = "lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819"}, - {file = "lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45"}, - {file = "lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe"}, - {file = "lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495"}, - {file = "lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33"}, - {file = "lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62"}, - {file = "lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16"}, - {file = "lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d"}, - {file = "lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8"}, - {file = "lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292"}, - {file = "lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43"}, - {file = "lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585"}, - {file = "lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f"}, - {file = "lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120"}, - {file = "lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946"}, - {file = "lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c"}, - {file = "lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f"}, - {file = "lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd"}, - {file = "lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180"}, - {file = "lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2"}, - {file = "lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5"}, - {file = "lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac"}, - {file = "lxml-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b6c2f225662bc5ad416bdd06f72ca301b31b39ce4261f0e0097017fc2891b940"}, - {file = "lxml-6.1.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a86f06f059e22a0d574990ee2df24ede03f7f3c68c1336293eee9536c4c776cd"}, - {file = "lxml-6.1.0-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:468479e52ecf3ec23799c863336d02c05fc2f7ffd1a1424eeeb9a28d4eb69d13"}, - {file = "lxml-6.1.0-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:a02ca8fe48815bddcfca3248efe54451abb9dbf2f7d1c5744c8aa4142d476919"}, - {file = "lxml-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bb40648d96157f9081886defe13eac99253e663be969ff938a9289eff6e47b72"}, - {file = "lxml-6.1.0-cp38-cp38-win32.whl", hash = "sha256:1dd6a1c3ad4cb674f44525d9957f3e9c209bb6dd9213245195167a281fcc2bdc"}, - {file = "lxml-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:4e2c54d6b47361d0f1d3bc8d4e082ad87201e56ccdcca4d3b9ee3644ff595ec8"}, - {file = "lxml-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:920354904d1cb86577d4b3cfe2830c2dbe81d6f4449e57ada428f1609b5985f7"}, - {file = "lxml-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c871299c595ee004d186f61840f0bfc4941aa3f17c8ba4a565ead7e4f4f820ee"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d0d799ff958655781296ec870d5e2448e75150da2b3d07f13ff5b0c2c35beefd"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ba11752e346bd804ea312ec2eea2532dfa8b8d3261d81a32ef9e6ab16256280"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26c5272c6a4bf4cf32d3f5a7890c942b0e04438691157d341616d02cca74d4bd"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c53fa3a5a52122d590e847a57ccf955557b9634a7f99ff5a35131321b0a85317"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:76b958b4ea3104483c20f74866d55aa056546e15ebe83dd7aecd63698f43b755"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:8c11b984b5ce6add4dccc7144c7be5d364d298f15b0c6a57da1991baedc750ce"}, - {file = "lxml-6.1.0-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d3829a6e6fd550a219564912d4002c537f65da4c6ae4e093cc34462f4fa027ad"}, - {file = "lxml-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:52b0ac6903cf74ebf997eb8c682d2fbac7d1ab7e4c552413eec55868a9b73f39"}, - {file = "lxml-6.1.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:29f5c00cb7d752bce2c70ebd2d31b0a42f9499ffdd3ecb2f31a5b73ee43031ad"}, - {file = "lxml-6.1.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:c748ebcb6877de89f48ab90ca96642ac458fff5dec291a2b9337cd4d0934e383"}, - {file = "lxml-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:08950a23f296b3f83521577274e3d3b0f3d739bf2e68d01a752e4288bc50d286"}, - {file = "lxml-6.1.0-cp39-cp39-win32.whl", hash = "sha256:11a873c77a181b4fef9c2e357d08ed399542c2af1390101da66720a19c7c9618"}, - {file = "lxml-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:81ff55c70b67d19d52b6fd118a114c0a4c97d799cd3089ff9bd9e2ff4b414ee2"}, - {file = "lxml-6.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:481d6e2104285d9add34f41b42b247b76b61c5b5c26c303c2e9707bbf8bd9a64"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:546b66c0dd1bb8d9fa89d7123e5fa19a8aff3a1f2141eb22df96112afb17b842"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfa1a34df366d9dc0d5eaf420f4cf2bb1e1bebe1066d1c2fc28c179f8a4004c"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db88156fcf544cdbf0d95588051515cfdfd4c876fc66444eb98bceb5d6db76de"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:07f98f5496f96bf724b1e3c933c107f0cbf2745db18c03d2e13a291c3afd2635"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4642e04449a1e164b5ff71ffd901ddb772dfabf5c9adf1b7be5dffe1212bc037"}, - {file = "lxml-6.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7da13bb6fbadfafb474e0226a30570a3445cfd47c86296f2446dafbd77079ace"}, - {file = "lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1"}, + {file = "lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a"}, + {file = "lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5"}, + {file = "lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485"}, + {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2"}, + {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08"}, + {file = "lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621"}, + {file = "lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28"}, + {file = "lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b"}, + {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7"}, + {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a"}, + {file = "lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a"}, + {file = "lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77"}, + {file = "lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f"}, + {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736"}, + {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947"}, + {file = "lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca"}, + {file = "lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660"}, + {file = "lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc"}, + {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0"}, + {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb"}, + {file = "lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603"}, + {file = "lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137"}, + {file = "lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf"}, + {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee"}, + {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e"}, + {file = "lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1"}, + {file = "lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e"}, + {file = "lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c"}, + {file = "lxml-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4"}, + {file = "lxml-6.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e"}, + {file = "lxml-6.1.1-cp38-cp38-win32.whl", hash = "sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3"}, + {file = "lxml-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d"}, + {file = "lxml-6.1.1-cp39-cp39-win32.whl", hash = "sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186"}, + {file = "lxml-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730"}, + {file = "lxml-6.1.1-cp39-cp39-win_arm64.whl", hash = "sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84"}, + {file = "lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40"}, ] [package.extras] @@ -1240,7 +1240,7 @@ files = [ {file = "markdown-3.8.1-py3-none-any.whl", hash = "sha256:46cc0c0f1e5211ab2e9d453582f0b28a1bfaf058a9f7d5c50386b99b588d8811"}, {file = "markdown-3.8.1.tar.gz", hash = "sha256:a2e2f01cead4828ee74ecca9623045f62216aef2212a7685d6eb9163f590b8c1"}, ] -markers = {main = "extra == \"all\" or extra == \"homepage\""} +markers = {main = "extra == \"homepage\" or extra == \"all\""} [package.extras] docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] @@ -1262,7 +1262,7 @@ 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"}, ] -markers = {main = "extra == \"all\" or extra == \"homepage\""} +markers = {main = "extra == \"homepage\" or extra == \"all\""} [package.dependencies] mdurl = ">=0.1,<1.0" @@ -1385,7 +1385,7 @@ files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] -markers = {main = "extra == \"all\" or extra == \"homepage\""} +markers = {main = "extra == \"homepage\" or extra == \"all\""} [package.source] type = "legacy" @@ -1498,7 +1498,7 @@ description = "Python package for creating and manipulating graphs and networks" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version < \"3.13\" and (extra == \"all\" or extra == \"prefigure\")" +markers = "python_version < \"3.13\" and (extra == \"prefigure\" or extra == \"all\")" files = [ {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, @@ -1524,7 +1524,7 @@ description = "Python package for creating and manipulating graphs and networks" optional = true python-versions = ">=3.11" groups = ["main"] -markers = "python_version >= \"3.13\" and (extra == \"all\" or extra == \"prefigure\")" +markers = "python_version >= \"3.13\" and (extra == \"prefigure\" or extra == \"all\")" files = [ {file = "networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f"}, {file = "networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad"}, @@ -1553,7 +1553,7 @@ description = "Fundamental package for array computing in Python" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "python_version < \"3.13\" and (extra == \"all\" or extra == \"prefigure\")" +markers = "python_version < \"3.13\" and (extra == \"prefigure\" or extra == \"all\")" files = [ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, @@ -1605,7 +1605,7 @@ description = "Fundamental package for array computing in Python" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "python_version >= \"3.13\" and (extra == \"all\" or extra == \"prefigure\")" +markers = "python_version >= \"3.13\" and (extra == \"prefigure\" or extra == \"all\")" files = [ {file = "numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9"}, {file = "numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae"}, @@ -1676,7 +1676,7 @@ description = "An OrderedSet is a custom MutableSet that remembers its order, so optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"homepage\"" +markers = "extra == \"homepage\" or extra == \"all\"" files = [ {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, @@ -1761,7 +1761,7 @@ description = "Static site generator supporting Markdown and reStructuredText" optional = true python-versions = "<4.0,>=3.8.1" groups = ["main"] -markers = "extra == \"all\" or extra == \"homepage\"" +markers = "extra == \"homepage\" or extra == \"all\"" files = [ {file = "pelican-4.10.2-py3-none-any.whl", hash = "sha256:534b3c279b9630a006b5687ea6e8e6113ca17c64ac45f4328cf66bf0be367bdf"}, {file = "pelican-4.10.2.tar.gz", hash = "sha256:04932e82dc7542bae7f1830710978836ca00608aa4ece7206dc13185a08c4562"}, @@ -2041,15 +2041,15 @@ reference = "pypi-public" [[package]] name = "prefig" -version = "0.5.15" +version = "0.6.5" description = "An authoring system for mathematical diagrams" optional = true python-versions = ">=3.10,<4.0" groups = ["main"] -markers = "extra == \"all\" or extra == \"prefigure\"" +markers = "extra == \"prefigure\" or extra == \"all\"" files = [ - {file = "prefig-0.5.15-py3-none-any.whl", hash = "sha256:3857f133a2e41c5b8918eb5c34c114f7339891ec8fad5654fd6ef4aa35028a12"}, - {file = "prefig-0.5.15.tar.gz", hash = "sha256:e54722774acae1814f749a6872685377d48b9df85cda7ad492d39cd4535a66bb"}, + {file = "prefig-0.6.5-py3-none-any.whl", hash = "sha256:2b9fd5aee2d007e5283d82ab2e01b34812aeef1e6751f0703fdea32d2ed15bb9"}, + {file = "prefig-0.6.5.tar.gz", hash = "sha256:93fd53be07ff981192ad31cab94c00f9d20835c694cbe038351f6c1ca903e609"}, ] [package.dependencies] @@ -2112,7 +2112,7 @@ description = "Python interface for cairo" optional = true python-versions = ">=3.9" groups = ["main"] -markers = "extra == \"all\" or extra == \"prefigure\"" +markers = "extra == \"prefigure\" or extra == \"all\"" files = [ {file = "pycairo-1.27.0-cp310-cp310-win32.whl", hash = "sha256:e20f431244634cf244ab6b4c3a2e540e65746eed1324573cf291981c3e65fc05"}, {file = "pycairo-1.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:03bf570e3919901572987bc69237b648fe0de242439980be3e606b396e3318c9"}, @@ -2369,7 +2369,7 @@ files = [ {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] -markers = {main = "extra == \"all\" or extra == \"homepage\""} +markers = {main = "extra == \"homepage\" or extra == \"all\""} [package.extras] windows-terminal = ["colorama (>=0.4.6)"] @@ -2622,7 +2622,7 @@ description = "World timezone definitions, modern and historical" optional = true python-versions = "*" groups = ["main"] -markers = "extra == \"all\" or extra == \"homepage\"" +markers = "extra == \"homepage\" or extra == \"all\"" files = [ {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, @@ -2810,7 +2810,7 @@ files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] -markers = {main = "extra == \"all\" or extra == \"homepage\""} +markers = {main = "extra == \"homepage\" or extra == \"all\""} [package.dependencies] markdown-it-py = ">=2.2.0" @@ -2832,7 +2832,7 @@ description = "Fundamental algorithms for scientific computing in Python" optional = true python-versions = ">=3.10" groups = ["main"] -markers = "extra == \"all\" or extra == \"prefigure\"" +markers = "extra == \"prefigure\" or extra == \"all\"" files = [ {file = "scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9"}, {file = "scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5"}, @@ -2928,7 +2928,7 @@ description = "Manipulation and analysis of geometric objects" optional = true python-versions = ">=3.7" groups = ["main"] -markers = "extra == \"all\" or extra == \"prefigure\"" +markers = "extra == \"prefigure\" or extra == \"all\"" files = [ {file = "shapely-2.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:33fb10e50b16113714ae40adccf7670379e9ccf5b7a41d0002046ba2b8f0f691"}, {file = "shapely-2.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f44eda8bd7a4bccb0f281264b34bf3518d8c4c9a8ffe69a1a05dabf6e8461147"}, @@ -3256,7 +3256,7 @@ description = "Provider of IANA time zone data" optional = true python-versions = ">=2" groups = ["main"] -markers = "(extra == \"all\" or extra == \"homepage\") and sys_platform == \"win32\"" +markers = "(extra == \"homepage\" or extra == \"all\") and sys_platform == \"win32\"" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, @@ -3367,7 +3367,7 @@ description = "Simple, modern and high performance file watching and code reload optional = true python-versions = ">=3.8" groups = ["main"] -markers = "extra == \"all\" or extra == \"homepage\"" +markers = "extra == \"homepage\" or extra == \"all\"" files = [ {file = "watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0"}, {file = "watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c"}, @@ -3617,4 +3617,4 @@ prefigure = ["prefig"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "831e6b5ca1e4f12e7e1c30e3135dd79e169a7866c4ba026015cfb2f426ce5120" +content-hash = "51f765b499df48be46c7907930c9a189c3e2faec4bc43c43253ef137a5f4564a" diff --git a/pretext/__init__.py b/pretext/__init__.py index f1b43c6a..3db5fa21 100644 --- a/pretext/__init__.py +++ b/pretext/__init__.py @@ -19,7 +19,7 @@ VERSION = get_version("pretext", Path(__file__).parent.parent) -CORE_COMMIT = "7bef656c9f303489fc07d719df52108c1970ca26" +CORE_COMMIT = "afb99f78226d4631e3fe87f5a0d4d41ff753e26f" def activate() -> None: diff --git a/pretext/cli.py b/pretext/cli.py index 17abe871..b9614e29 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -1,3 +1,4 @@ +from datetime import datetime import importlib import logging import logging.handlers @@ -775,11 +776,12 @@ def validate(ctx: click.Context, target_name: Optional[str], dev: bool) -> None: log.error(error_text) raise SystemExit(2) - with open(".error_schema.log", "w") as error_log_file: + error_log_path = Path("logs") / f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" + with open(error_log_path, "w") as error_log_file: error_log_file.write(error_text) log.error("PreTeXt source did NOT pass schema validation:") log.error(error_text) - log.error("See .error_schema.log for the full report.") + log.error(f"See {error_log_path} for the full report.") raise SystemExit(1) diff --git a/pretext/utils.py b/pretext/utils.py index be863fd8..936307af 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -224,7 +224,8 @@ def xml_validates_against_schema(etree: _Element) -> bool: "PreTeXt document did not pass schema validation; unexpected output " "may result. See .error_schema.log for hints. Continuing with build." ) - with open(".error_schema.log", "w") as error_log_file: + error_log_path = Path("logs") / f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" + with open(error_log_path, "w") as error_log_file: error_log_file.write(error_text) return False diff --git a/pyproject.toml b/pyproject.toml index c286f452..39aed0fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ plastex = "^3" jinja2 = "^3" coloraide = "^4" pelican = { extras = ["markdown"], version = "^4.10", optional = true } -prefig = { extras = ["text"], version = "^0.5.14", optional = true } +prefig = { extras = ["text"], version = "^0.6.5", optional = true } citeproc-py = "^0" From 3be41cd2a1109131949df3e5dab998171f3a4b47 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 14:50:08 -0600 Subject: [PATCH 11/20] format --- pretext/cli.py | 4 +++- pretext/utils.py | 9 +++++++-- tests/test_cli.py | 10 +++++----- tests/test_utils.py | 17 ++++++++++++----- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/pretext/cli.py b/pretext/cli.py index b9614e29..56cbb49d 100644 --- a/pretext/cli.py +++ b/pretext/cli.py @@ -776,7 +776,9 @@ def validate(ctx: click.Context, target_name: Optional[str], dev: bool) -> None: log.error(error_text) raise SystemExit(2) - error_log_path = Path("logs") / f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" + error_log_path = ( + Path("logs") / f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" + ) with open(error_log_path, "w") as error_log_file: error_log_file.write(error_text) log.error("PreTeXt source did NOT pass schema validation:") diff --git a/pretext/utils.py b/pretext/utils.py index 936307af..0ae6a5bb 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -224,13 +224,18 @@ def xml_validates_against_schema(etree: _Element) -> bool: "PreTeXt document did not pass schema validation; unexpected output " "may result. See .error_schema.log for hints. Continuing with build." ) - error_log_path = Path("logs") / f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" + error_log_path = ( + Path("logs") + / f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" + ) with open(error_log_path, "w") as error_log_file: error_log_file.write(error_text) return False -def _validate_with_jing(etree: _Element, schema_file: Path) -> Optional[tuple[bool, str]]: +def _validate_with_jing( + etree: _Element, schema_file: Path +) -> Optional[tuple[bool, str]]: jing_executable = shutil.which("jing") if jing_executable is None: return None diff --git a/tests/test_cli.py b/tests/test_cli.py index 2174681d..c268f3cb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,7 @@ from contextlib import contextmanager import requests import pretext -from lxml import etree as ET +from lxml import etree as ET # noqa: N812 from pretext import constants from pretext import utils from typing import cast, Generator @@ -563,9 +563,7 @@ def test_validate_malformed_xml_is_nonzero( @pytest.mark.skipif( not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" ) -def test_validate_dev_schema_runs( - tmp_path: Path, script_runner: ScriptRunner -) -> None: +def test_validate_dev_schema_runs(tmp_path: Path, script_runner: ScriptRunner) -> None: project = _make_project(tmp_path, script_runner) ret = script_runner.run([PTX_CMD, "validate", "--dev"], cwd=project) assert ret.returncode == 0 @@ -583,7 +581,9 @@ def test_validate_could_not_validate_exits_2( # Avoid the network update check and force the "no validator available" result. monkeypatch.setattr(putils, "check_for_updates", lambda *a, **k: None) monkeypatch.setattr( - putils, "run_schema_validation", lambda *a, **k: (None, "no validator available") + putils, + "run_schema_validation", + lambda *a, **k: (None, "no validator available"), ) result = CliRunner().invoke(cli.main, ["validate"]) assert result.exit_code == 2 diff --git a/tests/test_utils.py b/tests/test_utils.py index 9370a378..1941dbda 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,7 @@ import sys import pytest from pathlib import Path -from lxml import etree as ET +from lxml import etree as ET # noqa: N812 from pretext import utils @@ -238,7 +238,9 @@ def _raise_relaxng_parse_error(*args: object, **kwargs: object) -> object: ) assert not utils.xml_validates_against_schema(etree) - assert (tmp_path / ".error_schema.log").read_text() == "schema validation error via jing" + assert ( + tmp_path / ".error_schema.log" + ).read_text() == "schema validation error via jing" def test_xml_validates_against_schema_jing_unavailable( @@ -261,7 +263,7 @@ def test_validate_with_lxml_valid_and_invalid(tmp_path: Path) -> None: schema_file = tmp_path / "mini.rng" schema_file.write_text( '' - "" + '' ) ok = utils._validate_with_lxml(ET.fromstring(""), schema_file) @@ -280,14 +282,19 @@ def _raise(*args: object, **kwargs: object) -> object: raise ET.RelaxNGParseError("boom") monkeypatch.setattr(utils.ET, "RelaxNG", _raise) - assert utils._validate_with_lxml(ET.fromstring(""), tmp_path / "x.rng") is None + assert ( + utils._validate_with_lxml(ET.fromstring(""), tmp_path / "x.rng") + is None + ) def test_run_schema_validation_uses_first_available_engine( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: monkeypatch.setattr(utils, "_validate_with_jing", lambda *a: (True, "")) - monkeypatch.setattr(utils, "_validate_with_lxml", lambda *a: (False, "lxml says no")) + monkeypatch.setattr( + utils, "_validate_with_lxml", lambda *a: (False, "lxml says no") + ) # jing first wins assert utils.run_schema_validation( From b0f8312490a76f13a8832f32e5ef05888d59e518 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 14:51:44 -0600 Subject: [PATCH 12/20] format again --- tests/test_cli.py | 2 +- tests/test_utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index c268f3cb..b634b234 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,7 @@ from contextlib import contextmanager import requests import pretext -from lxml import etree as ET # noqa: N812 +from lxml import etree as ET # noqa: N812 from pretext import constants from pretext import utils from typing import cast, Generator diff --git a/tests/test_utils.py b/tests/test_utils.py index 1941dbda..b27bcb7d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,7 +3,7 @@ import sys import pytest from pathlib import Path -from lxml import etree as ET # noqa: N812 +from lxml import etree as ET # noqa: N812 from pretext import utils From c378db8f0715c4c1c6597b921393d779234f0069 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 15:01:11 -0600 Subject: [PATCH 13/20] update changelog and fix path --- CHANGELOG.md | 5 +++++ pretext/utils.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03c051a7..ea20694e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ Instructions: Add a subsection under `[Unreleased]` for additions, fixes, change - You can now specify the debug level *after* the command (e.g., `pretext -v debug build` can now also be entered as `pretext build -v debug`). - Temporary directories created by core pretext will now be cleaned up at all debug levels. To save them for debugging, use the `--save-tmp-dirs` flag, as in `pretext --save-tmp-dirs build`. - Added a `pretext validate` command that checks a target's source against the RelaxNG schema and exits non-zero on failure (use `--dev` for the development schema). Tries `jing` first, then falls back to lxml's built-in validator. +- Accessibility improvements for menu and search box in HTML output. + +### Changes + +- Numbering of project-like now follow the same numbering as other elements by default. More control over numbering of these elements added as publication variable. ## [2.41.2] - 2026-06-07 diff --git a/pretext/utils.py b/pretext/utils.py index 0ae6a5bb..b8ef98d3 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -225,7 +225,7 @@ def xml_validates_against_schema(etree: _Element) -> bool: "may result. See .error_schema.log for hints. Continuing with build." ) error_log_path = ( - Path("logs") + Path(project_path()) / "logs" / f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" ) with open(error_log_path, "w") as error_log_file: From f73a47c54ece222b6451ec1f817658c86b033127 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 15:02:17 -0600 Subject: [PATCH 14/20] format --- pretext/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pretext/utils.py b/pretext/utils.py index b8ef98d3..b458b27e 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -225,7 +225,8 @@ def xml_validates_against_schema(etree: _Element) -> bool: "may result. See .error_schema.log for hints. Continuing with build." ) error_log_path = ( - Path(project_path()) / "logs" + Path(project_path()) + / "logs" / f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" ) with open(error_log_path, "w") as error_log_file: From a03d3b999d892bcf437f6a50deec54d146e53e12 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 15:07:42 -0600 Subject: [PATCH 15/20] improve logging of schema errors --- pretext/utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pretext/utils.py b/pretext/utils.py index b458b27e..43a963d4 100644 --- a/pretext/utils.py +++ b/pretext/utils.py @@ -224,13 +224,11 @@ def xml_validates_against_schema(etree: _Element) -> bool: "PreTeXt document did not pass schema validation; unexpected output " "may result. See .error_schema.log for hints. Continuing with build." ) - error_log_path = ( - Path(project_path()) - / "logs" - / f"{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}-schema-errors.log" + log.debug( + "---- Schema validation error details: ----\n" + + error_text + + "\n---- End schema validation error details. ----" ) - with open(error_log_path, "w") as error_log_file: - error_log_file.write(error_text) return False From 952fc7aa9a82f13f57463a779fe0d6261dd4c1f6 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 15:20:43 -0600 Subject: [PATCH 16/20] remove broken tests --- tests/test_utils.py | 54 --------------------------------------------- 1 file changed, 54 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index b27bcb7d..f7a63c61 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -205,60 +205,6 @@ def test_xml_syntax_is_valid(tmp_path: Path) -> None: utils.xml_syntax_is_valid(tmp_path / "nonexistent.ptx") -def test_xml_validates_against_schema_jing_fallback_success( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: - monkeypatch.chdir(tmp_path) - etree = ET.fromstring("") - - def _raise_relaxng_parse_error(*args: object, **kwargs: object) -> object: - raise ET.RelaxNGParseError("boom") - - monkeypatch.setattr(utils.ET, "RelaxNG", _raise_relaxng_parse_error) - monkeypatch.setattr(utils, "_validate_with_jing", lambda *args: (True, "")) - - assert utils.xml_validates_against_schema(etree) - assert not (tmp_path / ".error_schema.log").exists() - - -def test_xml_validates_against_schema_jing_fallback_invalid( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: - monkeypatch.chdir(tmp_path) - etree = ET.fromstring("") - - def _raise_relaxng_parse_error(*args: object, **kwargs: object) -> object: - raise ET.RelaxNGParseError("boom") - - monkeypatch.setattr(utils.ET, "RelaxNG", _raise_relaxng_parse_error) - monkeypatch.setattr( - utils, - "_validate_with_jing", - lambda *args: (False, "schema validation error via jing"), - ) - - assert not utils.xml_validates_against_schema(etree) - assert ( - tmp_path / ".error_schema.log" - ).read_text() == "schema validation error via jing" - - -def test_xml_validates_against_schema_jing_unavailable( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: - monkeypatch.chdir(tmp_path) - etree = ET.fromstring("") - - def _raise_relaxng_parse_error(*args: object, **kwargs: object) -> object: - raise ET.RelaxNGParseError("boom") - - monkeypatch.setattr(utils.ET, "RelaxNG", _raise_relaxng_parse_error) - monkeypatch.setattr(utils, "_validate_with_jing", lambda *args: None) - - assert not utils.xml_validates_against_schema(etree) - assert (tmp_path / ".error_schema.log").exists() - - def test_validate_with_lxml_valid_and_invalid(tmp_path: Path) -> None: schema_file = tmp_path / "mini.rng" schema_file.write_text( From df2f27106d7630338219809ec3b37d97d6ac6a16 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 16:14:37 -0600 Subject: [PATCH 17/20] update test --- tests/test_cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index b634b234..ee031d6a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -545,7 +545,9 @@ def test_validate_invalid_source_is_nonzero( ) -> None: project = _make_project(tmp_path, script_runner) main_src = project / "source" / "main.ptx" - main_src.write_text('\n\n') + main_src.write_text( + '\n
Text outside of element.
\n' + ) ret = script_runner.run([PTX_CMD, "validate"], cwd=project) assert ret.returncode == 1 From d8753bfc0b48524992821ed675db55a9fe7507b0 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 19:37:18 -0600 Subject: [PATCH 18/20] skip test --- tests/test_cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index ee031d6a..cbad7acf 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -540,6 +540,9 @@ def _make_project(tmp_path: Path, script_runner: ScriptRunner) -> Path: return tmp_path / "new-pretext-project" +pytest.mark.skipif( + not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" +) def test_validate_invalid_source_is_nonzero( tmp_path: Path, script_runner: ScriptRunner ) -> None: From c07c1c535284746e9c5c4a4563969473e44cc90a Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 19:37:28 -0600 Subject: [PATCH 19/20] format --- tests/test_cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_cli.py b/tests/test_cli.py index cbad7acf..2e7730c9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -543,6 +543,8 @@ def _make_project(tmp_path: Path, script_runner: ScriptRunner) -> Path: pytest.mark.skipif( not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" ) + + def test_validate_invalid_source_is_nonzero( tmp_path: Path, script_runner: ScriptRunner ) -> None: From fc868688ad87b738875c788f1bb5e97ec5e83d86 Mon Sep 17 00:00:00 2001 From: Oscar Levin Date: Wed, 17 Jun 2026 19:44:28 -0600 Subject: [PATCH 20/20] fix test skip syntax --- tests/test_cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 2e7730c9..3cb008c8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -540,11 +540,9 @@ def _make_project(tmp_path: Path, script_runner: ScriptRunner) -> Path: return tmp_path / "new-pretext-project" -pytest.mark.skipif( +@pytest.mark.skipif( not _validator_available(), reason="no RelaxNG validator (lxml/jing) available" ) - - def test_validate_invalid_source_is_nonzero( tmp_path: Path, script_runner: ScriptRunner ) -> None: