From 34181b41ff2e66686fbc54b8ab72ef806012e25c Mon Sep 17 00:00:00 2001 From: Michael Booth Date: Sun, 19 Apr 2026 07:26:33 +1000 Subject: [PATCH] Deduplicate TOML rendering and spec test fixtures Co-Authored-By: Oz --- src/pdealchemy/notebook_spec.py | 12 +---- src/pdealchemy/spec_bridge.py | 14 ++---- src/pdealchemy/toml_rendering.py | 12 +++++ tests/cli/test_spec_to_runtime_toml.py | 49 +++++--------------- tests/conftest.py | 50 ++++++++++++++++++++ tests/notebook/test_spec_bridge.py | 63 +++++++++----------------- 6 files changed, 99 insertions(+), 101 deletions(-) create mode 100644 src/pdealchemy/toml_rendering.py create mode 100644 tests/conftest.py diff --git a/src/pdealchemy/notebook_spec.py b/src/pdealchemy/notebook_spec.py index e9c4897..5e0b991 100644 --- a/src/pdealchemy/notebook_spec.py +++ b/src/pdealchemy/notebook_spec.py @@ -8,6 +8,7 @@ from pathlib import Path from pdealchemy.exceptions import ConfigError +from pdealchemy.toml_rendering import render_toml_string _CELL_SECTION_MAP: dict[str, tuple[str, ...]] = { "instrument": ("instrument",), @@ -118,20 +119,11 @@ def _looks_like_path(value: str) -> bool: return value.startswith(".") -def _render_toml_string(value: str) -> str: - """Render a TOML string with support for multiline content.""" - if "\n" in value: - escaped = value.replace("'''", "\\'\\'\\'") - return f"'''\n{escaped}\n'''" - escaped = value.replace("\\", "\\\\").replace('"', '\\"') - return f'"{escaped}"' - - def _render_section(table_path: Iterable[str], values: dict[str, str]) -> list[str]: """Render one TOML table section.""" lines = [f"[{'.'.join(table_path)}]"] for key, value in values.items(): - lines.append(f"{key} = {_render_toml_string(value)}") + lines.append(f"{key} = {render_toml_string(value)}") lines.append("") return lines diff --git a/src/pdealchemy/spec_bridge.py b/src/pdealchemy/spec_bridge.py index b1b8c7d..53dd56b 100644 --- a/src/pdealchemy/spec_bridge.py +++ b/src/pdealchemy/spec_bridge.py @@ -8,6 +8,7 @@ from typing import cast from pdealchemy.exceptions import ConfigError +from pdealchemy.toml_rendering import render_toml_string _BASELINE_SDE_FILE = "library/sde/black_scholes_geometric_brownian_motion.md" _BASELINE_PDE_FILE = "library/pde/black_scholes.md" @@ -39,15 +40,6 @@ class BlackScholesBridgeDefaults: monte_carlo_antithetic: bool = True -def _render_toml_string(value: str) -> str: - """Render a TOML string with support for multiline content.""" - if "\n" in value: - escaped = value.replace("'''", "\\'\\'\\'") - return f"'''\n{escaped}\n'''" - escaped = value.replace("\\", "\\\\").replace('"', '\\"') - return f'"{escaped}"' - - def _render_toml_float(value: float) -> str: """Render a float with stable TOML-friendly formatting.""" rendered = f"{value:.12g}" @@ -249,8 +241,8 @@ def spec_to_runtime_toml_content( f"# Generated from {spec_toml_path.name} by pdealchemy spec-to-runtime-toml", "", "[metadata]", - f"name = {_render_toml_string(runtime_name)}", - f"description = {_render_toml_string(' '.join(runtime_description_parts))}", + f"name = {render_toml_string(runtime_name)}", + f"description = {render_toml_string(' '.join(runtime_description_parts))}", 'tags = ["notebook-bridge", "black-scholes", "vanilla"]', "", "[process]", diff --git a/src/pdealchemy/toml_rendering.py b/src/pdealchemy/toml_rendering.py new file mode 100644 index 0000000..1d3b8f8 --- /dev/null +++ b/src/pdealchemy/toml_rendering.py @@ -0,0 +1,12 @@ +"""Shared helpers for deterministic TOML value rendering.""" + +from __future__ import annotations + + +def render_toml_string(value: str) -> str: + """Render a TOML string with support for multiline content.""" + if "\n" in value: + escaped = value.replace("'''", "\\'\\'\\'") + return f"'''\n{escaped}\n'''" + escaped = value.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped}"' diff --git a/tests/cli/test_spec_to_runtime_toml.py b/tests/cli/test_spec_to_runtime_toml.py index 56898bc..ef58685 100644 --- a/tests/cli/test_spec_to_runtime_toml.py +++ b/tests/cli/test_spec_to_runtime_toml.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable from pathlib import Path from typer.testing import CliRunner @@ -11,43 +12,12 @@ runner = CliRunner() -def _write_spec_toml(path: Path, *, payoff_file: str = "library/payoff/vanilla_call.md") -> None: - path.write_text( - "\n".join( - [ - "[metadata]", - 'name = "Black-Scholes European Call — Specification"', - "", - "[instrument]", - 'description = "European vanilla call option in AUD."', - 'markdown = "European Call"', - "", - "[mathematics.sde]", - 'equation_file = "library/sde/black_scholes_geometric_brownian_motion.md"', - "", - "[mathematics.operator]", - 'equation_file = "library/pde/black_scholes.md"', - "", - "[payoff]", - f'equation_file = "{payoff_file}"', - "", - "[numerics]", - 'markdown_file = "library/discretisation/crank_nicolson_standard.md"', - "", - "[data.rates]", - 'equation_file = "library/data/rates_flat.md"', - "", - "[data.volatility]", - 'equation_file = "library/data/volatility_constant.md"', - ] - ), - encoding="utf-8", - ) - - -def test_spec_to_runtime_toml_command_generates_runtime_file(tmp_path: Path) -> None: +def test_spec_to_runtime_toml_command_generates_runtime_file( + tmp_path: Path, + write_spec_toml: Callable[..., None], +) -> None: spec_path = tmp_path / "spec.toml" - _write_spec_toml(spec_path) + write_spec_toml(spec_path) runtime_path = tmp_path / "runtime.toml" result = runner.invoke( @@ -68,9 +38,12 @@ def test_spec_to_runtime_toml_command_generates_runtime_file(tmp_path: Path) -> assert "[numerics]" in rendered -def test_spec_to_runtime_toml_command_rejects_unsupported_baseline(tmp_path: Path) -> None: +def test_spec_to_runtime_toml_command_rejects_unsupported_baseline( + tmp_path: Path, + write_spec_toml: Callable[..., None], +) -> None: spec_path = tmp_path / "spec.toml" - _write_spec_toml(spec_path, payoff_file="library/payoff/custom_payoff.md") + write_spec_toml(spec_path, payoff_file="library/payoff/custom_payoff.md") result = runner.invoke(app, ["spec-to-runtime-toml", str(spec_path)]) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7f8ac43 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,50 @@ +"""Shared pytest fixtures for test inputs and builders.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + +_DEFAULT_PAYOFF_FILE = "library/payoff/vanilla_call.md" + + +def _write_spec_toml(path: Path, *, payoff_file: str = _DEFAULT_PAYOFF_FILE) -> None: + path.write_text( + "\n".join( + [ + "[metadata]", + 'name = "Black-Scholes European Call — Specification"', + "", + "[instrument]", + 'description = "European vanilla call option in AUD."', + 'markdown = "European Call"', + "", + "[mathematics.sde]", + 'equation_file = "library/sde/black_scholes_geometric_brownian_motion.md"', + "", + "[mathematics.operator]", + 'equation_file = "library/pde/black_scholes.md"', + "", + "[payoff]", + f'equation_file = "{payoff_file}"', + "", + "[numerics]", + 'markdown_file = "library/discretisation/crank_nicolson_standard.md"', + "", + "[data.rates]", + 'equation_file = "library/data/rates_flat.md"', + "", + "[data.volatility]", + 'equation_file = "library/data/volatility_constant.md"', + ] + ), + encoding="utf-8", + ) + + +@pytest.fixture +def write_spec_toml() -> Callable[..., None]: + """Provide a reusable Black-Scholes specification TOML writer.""" + return _write_spec_toml diff --git a/tests/notebook/test_spec_bridge.py b/tests/notebook/test_spec_bridge.py index 47d4ce4..ff1eded 100644 --- a/tests/notebook/test_spec_bridge.py +++ b/tests/notebook/test_spec_bridge.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable from pathlib import Path import pytest @@ -10,43 +11,12 @@ from pdealchemy.spec_bridge import spec_to_runtime_toml_content, spec_to_runtime_toml_file -def _write_spec_toml(path: Path, *, payoff_file: str = "library/payoff/vanilla_call.md") -> None: - path.write_text( - "\n".join( - [ - "[metadata]", - 'name = "Black-Scholes European Call — Specification"', - "", - "[instrument]", - 'description = "European vanilla call option in AUD."', - 'markdown = "European Call"', - "", - "[mathematics.sde]", - 'equation_file = "library/sde/black_scholes_geometric_brownian_motion.md"', - "", - "[mathematics.operator]", - 'equation_file = "library/pde/black_scholes.md"', - "", - "[payoff]", - f'equation_file = "{payoff_file}"', - "", - "[numerics]", - 'markdown_file = "library/discretisation/crank_nicolson_standard.md"', - "", - "[data.rates]", - 'equation_file = "library/data/rates_flat.md"', - "", - "[data.volatility]", - 'equation_file = "library/data/volatility_constant.md"', - ] - ), - encoding="utf-8", - ) - - -def test_spec_to_runtime_toml_content_renders_runtime_shape(tmp_path: Path) -> None: +def test_spec_to_runtime_toml_content_renders_runtime_shape( + tmp_path: Path, + write_spec_toml: Callable[..., None], +) -> None: spec_path = tmp_path / "spec.toml" - _write_spec_toml(spec_path) + write_spec_toml(spec_path) rendered = spec_to_runtime_toml_content(spec_path) @@ -58,18 +28,24 @@ def test_spec_to_runtime_toml_content_renders_runtime_shape(tmp_path: Path) -> N assert "[numerics.grid]" in rendered -def test_spec_to_runtime_toml_file_writes_default_pricing_suffix(tmp_path: Path) -> None: +def test_spec_to_runtime_toml_file_writes_default_pricing_suffix( + tmp_path: Path, + write_spec_toml: Callable[..., None], +) -> None: spec_path = tmp_path / "spec_black_scholes.toml" - _write_spec_toml(spec_path) + write_spec_toml(spec_path) output_path = spec_to_runtime_toml_file(spec_path, overwrite=True) assert output_path == tmp_path / "spec_black_scholes.pricing.toml" assert output_path.exists() -def test_spec_to_runtime_toml_file_rewrites_blueprint_suffix(tmp_path: Path) -> None: +def test_spec_to_runtime_toml_file_rewrites_blueprint_suffix( + tmp_path: Path, + write_spec_toml: Callable[..., None], +) -> None: spec_path = tmp_path / "black_scholes_blueprint.toml" - _write_spec_toml(spec_path) + write_spec_toml(spec_path) output_path = spec_to_runtime_toml_file(spec_path, overwrite=True) @@ -77,9 +53,12 @@ def test_spec_to_runtime_toml_file_rewrites_blueprint_suffix(tmp_path: Path) -> assert output_path.exists() -def test_spec_to_runtime_toml_rejects_unsupported_payoff_mapping(tmp_path: Path) -> None: +def test_spec_to_runtime_toml_rejects_unsupported_payoff_mapping( + tmp_path: Path, + write_spec_toml: Callable[..., None], +) -> None: spec_path = tmp_path / "spec.toml" - _write_spec_toml(spec_path, payoff_file="library/payoff/custom_payoff.md") + write_spec_toml(spec_path, payoff_file="library/payoff/custom_payoff.md") with pytest.raises(ConfigError, match="Unsupported baseline mapping for \\[payoff\\]"): _ = spec_to_runtime_toml_content(spec_path)