Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions src/pdealchemy/notebook_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",),
Expand Down Expand Up @@ -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

Expand Down
14 changes: 3 additions & 11 deletions src/pdealchemy/spec_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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]",
Expand Down
12 changes: 12 additions & 0 deletions src/pdealchemy/toml_rendering.py
Original file line number Diff line number Diff line change
@@ -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}"'
49 changes: 11 additions & 38 deletions tests/cli/test_spec_to_runtime_toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from collections.abc import Callable
from pathlib import Path

from typer.testing import CliRunner
Expand All @@ -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(
Expand All @@ -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)])

Expand Down
50 changes: 50 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
63 changes: 21 additions & 42 deletions tests/notebook/test_spec_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from collections.abc import Callable
from pathlib import Path

import pytest
Expand All @@ -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)

Expand All @@ -58,28 +28,37 @@ 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)

assert output_path == tmp_path / "black_scholes_pricing.toml"
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)
Loading