diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e3fcbb9..3ff1c2c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,11 +191,11 @@ jobs: # (avoids PF-007: CLI surface must actually run in CI, not skip silently). - name: Build mds CLI (CF-SM2 parity producer) run: cargo build -p mds-cli - # Install Python + the mdscript binding so CF-SM2 can compare the Python + # Install Python + the markdown_script binding so CF-SM2 can compare the Python # output as the fourth parity surface (avoids PF-007). pip uses the # maturin PEP 517 build backend declared in crates/mds-python/pyproject.toml; # no pre-installed maturin needed. MDS_PYTHON_BIN is set to the exact - # executable that owns the installed module so findPythonForMdscript() + # executable that owns the installed module so findPythonForMarkdownScript() # picks it up cross-platform (bin/ on Unix, Scripts/ on Windows). - uses: actions/setup-python@v5 with: @@ -327,8 +327,8 @@ jobs: run: ls dist/ && ls dist/ | grep -q 'cp311-abi3' || (echo "expected a cp311-abi3 wheel" && exit 1) - name: Install the built wheel (not editable) + import smoke run: | - python -m pip install --find-links dist --no-index mdscript - python -c "import mdscript; r = mdscript.compile('Hello {n}!', vars={'n': 'CI'}); print(r.kind, r.output)" + python -m pip install --find-links dist --no-index markdown-script + python -c "import markdown_script; r = markdown_script.compile('Hello {n}!', vars={'n': 'CI'}); print(r.kind, r.output)" # Run the suite against the INSTALLED wheel (mypy/pyright deselected — they # are covered in the develop job; parity CLI is built for the live check). - name: Build mds CLI (parity producer) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d30afd..d65d7601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,7 +100,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 public `mds-core` API: `format_str` / `format_str_with`. (#60) - **Native Python bindings** (`crates/mds-python`, PyO3 + maturin), to be distributed - as `mdscript` on PyPI. Seven functions — `compile`, `compile_file`, + as `markdown-script` on PyPI (importable as `markdown_script`). Seven functions — `compile`, `compile_file`, `compile_virtual`, `check`, `check_file`, `check_virtual`, and `scan_imports` — with idiomatic keyword-only signatures. Results are typed, frozen, and picklable (`CompileResult` / `Message` / `Span` / `CheckResult`), and failures raise a native @@ -158,9 +158,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 and WASM backends implement the full surface; `lintFile()` on the WASM backend uses `buildModulesMap` for `@import` resolution. - **Python** (`mdscript`): `lint()`, `lint_file()`, `lint_virtual()` with keyword-only + **Python** (`markdown_script`): `lint()`, `lint_file()`, `lint_virtual()` with keyword-only `rules` and `base_path` / `vars` options; `LintResult` with `.version`, `.truncated`, - `.files`, `.to_dict()`, `.to_json()`. Stubs shipped in `_mdscript.pyi` / `__init__.pyi`. + `.files`, `.to_dict()`, `.to_json()`. Stubs shipped in `_markdown_script.pyi` / `__init__.pyi`. **⚠ TypeScript interface implementers**: `MdsBaseBackend` gained `lint` and `lintVirtual` as required members; `MdsNodeBackend` gained `lintFile`. Code that @@ -186,7 +186,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **WASM** (`@mdscript/mds-wasm`): same `sourceMap`/`sourcesContent` options on `compile()`. - **Python** (`mdscript`): `compile()`, `compile_file()`, and `compile_virtual()` accept + **Python** (`markdown_script`): `compile()`, `compile_file()`, and `compile_virtual()` accept `source_map=True` and `sources_content=True` keyword arguments. Results expose a `.source_map` property (`dict | None`). @@ -528,6 +528,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 four tie-free sort sites switched to `sort_unstable`. CI-measured size: 836,126 bytes (Binaryen v129) against the 850,000-byte guard. +- **Python distribution renamed: `mdscript` → `markdown-script`; import as `markdown_script` (#292, ADR-012).** + The PyPI name `mdscript` is held by a genuine dormant 2021 project with a direct + topical collision (`top_level.txt` is exactly `mdscript`), and there is no PEP 541 + route for reclaiming it. Both the distribution name and the importable module name + must change together because they were identical before (`mdscript`/`mdscript`). The + private extension module is renamed from `_mdscript` to `_markdown_script` for + consistency with the new public package name. + + This rename is pre-publication: `publish = false` in `crates/mds-python/Cargo.toml` + and there is no PyPI publish step in `release.yml`, so there are zero existing PyPI + consumers. No deprecation shim or compat alias is provided. + + Migration: `pip install markdown-script` (hyphen), then `import markdown_script` (underscore). + ### Deprecated - **`mds::fix::apply_fixes` is deprecated in favor of `apply_fixes_incremental` (#209).** diff --git a/crates/mds-python/Cargo.toml b/crates/mds-python/Cargo.toml index 7983497d..ac4b1b0c 100644 --- a/crates/mds-python/Cargo.toml +++ b/crates/mds-python/Cargo.toml @@ -26,10 +26,10 @@ debug-panics = [] [lib] crate-type = ["cdylib"] -# The compiled module is imported as `mdscript._mdscript`; the Rust symbol is -# `PyInit__mdscript`, so the lib name must be `_mdscript` and match the -# `#[pymodule] fn _mdscript` in src/lib.rs. -name = "_mdscript" +# The compiled module is imported as `markdown_script._markdown_script`; the Rust symbol is +# `PyInit__markdown_script`, so the lib name must be `_markdown_script` and match the +# `#[pymodule] fn _markdown_script` in src/lib.rs. +name = "_markdown_script" # No Rust unit tests and no doctests: `abi3-py311` is always on and this is a # cdylib, so `cargo build/clippy/test --workspace` compile the extension without # linking libpython. All coverage is the pytest suite. `test = false` diff --git a/crates/mds-python/README.md b/crates/mds-python/README.md index 7536f44a..87f9e788 100644 --- a/crates/mds-python/README.md +++ b/crates/mds-python/README.md @@ -1,4 +1,4 @@ -# mdscript +# markdown-script Native **Python bindings** for [MDS (Markdown Script)](https://github.com/dean0x/mdscript) — a composable LLM prompt-template compiler. Compile `.mds` templates to Markdown or @@ -6,10 +6,10 @@ structured chat messages in-process, backed by the same Rust core as the MDS CLI the Node.js / WASM bindings. Output is byte-identical across every binding. ```bash -pip install mdscript +pip install markdown-script ``` -> **Not yet on PyPI** — publishing and the `mdscript` name registration are tracked in +> **Not yet on PyPI** — publishing and the `markdown-script` name registration are tracked in > [#132]. For now, build from source: `pip install ./crates/mds-python` (or `maturin > build -m crates/mds-python/Cargo.toml` to produce a wheel), with a Rust toolchain and > `python3` on `PATH`. Once published, wheels ship as `cp311-abi3` (CPython 3.11+, one @@ -20,24 +20,24 @@ pip install mdscript ## Quick start ```python -import mdscript +import markdown_script # Markdown template -r = mdscript.compile("Hello {{name}}!", vars={"name": "Alice"}) +r = markdown_script.compile("Hello {{name}}!", vars={"name": "Alice"}) assert r.kind == "markdown" assert r.output == "Hello Alice!" # @message template → structured messages -r = mdscript.compile("@message user:\nHi\n@end\n") +r = markdown_script.compile("@message user:\nHi\n@end\n") assert r.kind == "messages" assert r.messages[0].role == "user" assert r.output is None # inactive payload is None # Validate without rendering -mdscript.check("Hello {{name}}!", vars={"name": "Bob"}) +markdown_script.check("Hello {{name}}!", vars={"name": "Bob"}) # Compile a file (dependencies come back as absolute paths) -r = mdscript.compile_file("prompts/agent.mds") +r = markdown_script.compile_file("prompts/agent.mds") print(r.dependencies) ``` @@ -93,12 +93,12 @@ Results are frozen, comparable by value, intentionally unhashable, and picklable ### Errors -Every failure raises `mdscript.MdsError` (a subclass of `Exception`): +Every failure raises `markdown_script.MdsError` (a subclass of `Exception`): ```python try: - mdscript.compile("Hello {{undefined}}!") -except mdscript.MdsError as e: + markdown_script.compile("Hello {{undefined}}!") +except markdown_script.MdsError as e: print(e.code) # "mds::undefined_var" print(str(e)) # == e.message print(e.help) # hint, or None @@ -109,7 +109,7 @@ except mdscript.MdsError as e: ## Concurrency Compilation is synchronous, stateless CPU work and **releases the GIL**, so calls -parallelise across threads. For `asyncio`, offload with `asyncio.to_thread(mdscript.compile, src)`. +parallelise across threads. For `asyncio`, offload with `asyncio.to_thread(markdown_script.compile, src)`. The extension is also free-threading (`cp314t`) ready — result classes are frozen and the module declares `gil_used = false` — though a free-threaded wheel is not yet shipped. diff --git a/crates/mds-python/benchmarks/bench.py b/crates/mds-python/benchmarks/bench.py index 29cc705c..cfd40b15 100644 --- a/crates/mds-python/benchmarks/bench.py +++ b/crates/mds-python/benchmarks/bench.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""On-demand throughput + GIL-scaling benchmark for mdscript (stdlib only). +"""On-demand throughput + GIL-scaling benchmark for markdown_script (stdlib only). Not part of the gated test suite. Run directly: @@ -16,7 +16,7 @@ import threading import time -import mdscript +import markdown_script REPRESENTATIVE = "---\nname: Alice\n---\n@for i in items:\n- {{name}}: item {{i}}\n@end\n" ITEMS = list(range(200)) @@ -24,11 +24,11 @@ def bench_latency(iterations: int = 2000) -> None: - mdscript.compile(REPRESENTATIVE, vars=VARS) # warm up + markdown_script.compile(REPRESENTATIVE, vars=VARS) # warm up samples = [] for _ in range(iterations): t0 = time.perf_counter() - mdscript.compile(REPRESENTATIVE, vars=VARS) + markdown_script.compile(REPRESENTATIVE, vars=VARS) samples.append(time.perf_counter() - t0) samples.sort() p50 = statistics.median(samples) * 1e6 @@ -40,7 +40,7 @@ def bench_latency(iterations: int = 2000) -> None: def bench_gil_scaling(total: int = 4000) -> None: def run_n(n: int) -> None: for _ in range(n): - mdscript.compile(REPRESENTATIVE, vars=VARS) + markdown_script.compile(REPRESENTATIVE, vars=VARS) t0 = time.perf_counter() run_n(total) @@ -68,7 +68,7 @@ def run_n(n: int) -> None: def main() -> None: - print(f"mdscript {mdscript.__version__}\n") + print(f"markdown-script {markdown_script.__version__}\n") bench_latency() bench_gil_scaling() diff --git a/crates/mds-python/pyproject.toml b/crates/mds-python/pyproject.toml index 028cc7ab..02fcbb90 100644 --- a/crates/mds-python/pyproject.toml +++ b/crates/mds-python/pyproject.toml @@ -6,7 +6,7 @@ requires = ["maturin>=1.13.3,<1.14"] build-backend = "maturin" [project] -name = "mdscript" +name = "markdown-script" description = "Composable LLM prompt template compiler — native Python bindings for MDS (Markdown Script)" requires-python = ">=3.11" # PEP 639: an SPDX license expression as a plain string (NOT a `{ text = ... }` @@ -47,10 +47,10 @@ markers = [ ] [tool.maturin] -# Mixed Rust/Python layout: the pure-Python package lives under python/mdscript/ and -# the compiled extension is injected as the submodule `mdscript._mdscript`. +# Mixed Rust/Python layout: the pure-Python package lives under python/markdown_script/ +# and the compiled extension is injected as the submodule `markdown_script._markdown_script`. python-source = "python" -module-name = "mdscript._mdscript" +module-name = "markdown_script._markdown_script" # abi3-py311: emit a single `cp311-abi3` wheel usable on CPython 3.11+. Redundant with # the always-on workspace pyo3 feature, but stated explicitly for clarity. features = ["pyo3/abi3-py311"] diff --git a/crates/mds-python/python/mdscript/__init__.py b/crates/mds-python/python/markdown_script/__init__.py similarity index 79% rename from crates/mds-python/python/mdscript/__init__.py rename to crates/mds-python/python/markdown_script/__init__.py index e3664121..2a6412c9 100644 --- a/crates/mds-python/python/mdscript/__init__.py +++ b/crates/mds-python/python/markdown_script/__init__.py @@ -1,4 +1,4 @@ -"""mdscript — composable LLM prompt template compiler (native Python bindings). +"""markdown_script — composable LLM prompt template compiler (native Python bindings). Compile ``.mds`` templates to Markdown or structured chat messages in-process, via the same Rust core that powers the MDS CLI and Node.js/WASM bindings. Output is @@ -6,8 +6,8 @@ Example ------- ->>> import mdscript ->>> r = mdscript.compile("Hello {{name}}!", vars={"name": "Alice"}) +>>> import markdown_script +>>> r = markdown_script.compile("Hello {{name}}!", vars={"name": "Alice"}) >>> r.kind, r.output ('markdown', 'Hello Alice!') @@ -20,7 +20,7 @@ from importlib import metadata as _metadata -from ._mdscript import ( +from ._markdown_script import ( CheckResult, CompileResult, LintDiagnostic, @@ -41,13 +41,13 @@ scan_imports, ) -# The native exception is registered under the extension submodule `_mdscript`. -# Retag it (and it alone — the result classes already declare `module = "mdscript"`) -# to the public package so `pickle`, `repr`, and tracebacks resolve `mdscript.MdsError`. -MdsError.__module__ = "mdscript" +# The native exception is registered under the extension submodule `_markdown_script`. +# Retag it (and it alone — the result classes already declare `module = "markdown_script"`) +# to the public package so `pickle`, `repr`, and tracebacks resolve `markdown_script.MdsError`. +MdsError.__module__ = "markdown_script" try: - __version__ = _metadata.version("mdscript") + __version__ = _metadata.version("markdown-script") except _metadata.PackageNotFoundError: # pragma: no cover - source tree without an install __version__ = "0.0.0" diff --git a/crates/mds-python/python/markdown_script/__init__.pyi b/crates/mds-python/python/markdown_script/__init__.pyi new file mode 100644 index 00000000..ab2d5fe8 --- /dev/null +++ b/crates/mds-python/python/markdown_script/__init__.pyi @@ -0,0 +1,49 @@ +"""Public type surface for the ``markdown_script`` package. + +Everything is re-exported from the native ``._markdown_script`` extension; see +``_markdown_script.pyi`` for the full signatures. +""" + +from __future__ import annotations + +from ._markdown_script import CheckResult as CheckResult +from ._markdown_script import CompileResult as CompileResult +from ._markdown_script import LintDiagnostic as LintDiagnostic +from ._markdown_script import LintFileReport as LintFileReport +from ._markdown_script import LintResult as LintResult +from ._markdown_script import MdsError as MdsError +from ._markdown_script import Message as Message +from ._markdown_script import Span as Span +from ._markdown_script import check as check +from ._markdown_script import check_file as check_file +from ._markdown_script import check_virtual as check_virtual +from ._markdown_script import compile as compile +from ._markdown_script import compile_file as compile_file +from ._markdown_script import compile_virtual as compile_virtual +from ._markdown_script import lint as lint +from ._markdown_script import lint_file as lint_file +from ._markdown_script import lint_virtual as lint_virtual +from ._markdown_script import scan_imports as scan_imports + +__version__: str +__all__ = [ + "CheckResult", + "CompileResult", + "LintDiagnostic", + "LintFileReport", + "LintResult", + "MdsError", + "Message", + "Span", + "__version__", + "check", + "check_file", + "check_virtual", + "compile", + "compile_file", + "compile_virtual", + "lint", + "lint_file", + "lint_virtual", + "scan_imports", +] diff --git a/crates/mds-python/python/mdscript/_mdscript.pyi b/crates/mds-python/python/markdown_script/_markdown_script.pyi similarity index 99% rename from crates/mds-python/python/mdscript/_mdscript.pyi rename to crates/mds-python/python/markdown_script/_markdown_script.pyi index 449711c7..d9eb3aec 100644 --- a/crates/mds-python/python/mdscript/_mdscript.pyi +++ b/crates/mds-python/python/markdown_script/_markdown_script.pyi @@ -1,4 +1,4 @@ -"""Type stubs for the native ``mdscript._mdscript`` extension module. +"""Type stubs for the native ``markdown_script._markdown_script`` extension module. The runtime objects are implemented in Rust (PyO3). These stubs describe the public surface for ``mypy``/``pyright``. Result classes are frozen — their attributes are diff --git a/crates/mds-python/python/mdscript/py.typed b/crates/mds-python/python/markdown_script/py.typed similarity index 100% rename from crates/mds-python/python/mdscript/py.typed rename to crates/mds-python/python/markdown_script/py.typed diff --git a/crates/mds-python/python/mdscript/__init__.pyi b/crates/mds-python/python/mdscript/__init__.pyi deleted file mode 100644 index 4eae7ad5..00000000 --- a/crates/mds-python/python/mdscript/__init__.pyi +++ /dev/null @@ -1,49 +0,0 @@ -"""Public type surface for the ``mdscript`` package. - -Everything is re-exported from the native ``._mdscript`` extension; see -``_mdscript.pyi`` for the full signatures. -""" - -from __future__ import annotations - -from ._mdscript import CheckResult as CheckResult -from ._mdscript import CompileResult as CompileResult -from ._mdscript import LintDiagnostic as LintDiagnostic -from ._mdscript import LintFileReport as LintFileReport -from ._mdscript import LintResult as LintResult -from ._mdscript import MdsError as MdsError -from ._mdscript import Message as Message -from ._mdscript import Span as Span -from ._mdscript import check as check -from ._mdscript import check_file as check_file -from ._mdscript import check_virtual as check_virtual -from ._mdscript import compile as compile -from ._mdscript import compile_file as compile_file -from ._mdscript import compile_virtual as compile_virtual -from ._mdscript import lint as lint -from ._mdscript import lint_file as lint_file -from ._mdscript import lint_virtual as lint_virtual -from ._mdscript import scan_imports as scan_imports - -__version__: str -__all__ = [ - "CheckResult", - "CompileResult", - "LintDiagnostic", - "LintFileReport", - "LintResult", - "MdsError", - "Message", - "Span", - "__version__", - "check", - "check_file", - "check_virtual", - "compile", - "compile_file", - "compile_virtual", - "lint", - "lint_file", - "lint_virtual", - "scan_imports", -] diff --git a/crates/mds-python/src/lib.rs b/crates/mds-python/src/lib.rs index cee5b0e3..3097829a 100644 --- a/crates/mds-python/src/lib.rs +++ b/crates/mds-python/src/lib.rs @@ -1,7 +1,7 @@ //! Native Python bindings for the MDS compiler via PyO3. //! //! Exposes ten functions to Python as the native extension module -//! `mdscript._mdscript` (re-exported by the pure-Python `mdscript` package): +//! `markdown_script._markdown_script` (re-exported by the pure-Python `markdown_script` package): //! [`compile`], [`compile_file`], [`compile_virtual`], [`check`], [`check_file`], //! [`check_virtual`], [`scan_imports`], [`lint`], [`lint_file`], and //! [`lint_virtual`]. @@ -29,7 +29,7 @@ //! //! ## Error codes //! -//! Every failure raises [`MdsError`] (a native, catchable `mdscript.MdsError`) with a +//! Every failure raises [`MdsError`] (a native, catchable `markdown_script.MdsError`) with a //! `.code`. Codes originating in `mds-core` (e.g. `"mds::syntax"`) are defined by //! [`mds::MdsError`]. Three codes are **binding-only** — synthesised here: //! @@ -80,7 +80,7 @@ const MAX_MODULES_AGGREGATE_SIZE: usize = MAX_SOURCE_SIZE; // ── Native exception ─────────────────────────────────────────────────────────── create_exception!( - _mdscript, + _markdown_script, MdsError, PyException, "Raised for every MDS compilation failure.\n\n\ @@ -95,7 +95,7 @@ create_exception!( /// `offset`/`length` are byte offsets into the source; `line` is 1-indexed and /// `column` is the 1-indexed character (Unicode scalar) position, or `None` when /// the core could not resolve them. All values are Python `int`s — no truncation. -#[pyclass(frozen, eq, skip_from_py_object, module = "mdscript")] +#[pyclass(frozen, eq, skip_from_py_object, module = "markdown_script")] #[derive(Clone, PartialEq, Eq)] pub struct Span { #[pyo3(get)] @@ -168,7 +168,7 @@ impl Span { } /// A single chat message produced by a `@message`-bearing template. -#[pyclass(frozen, eq, skip_from_py_object, module = "mdscript")] +#[pyclass(frozen, eq, skip_from_py_object, module = "markdown_script")] #[derive(Clone, PartialEq, Eq)] pub struct Message { #[pyo3(get)] @@ -214,7 +214,7 @@ impl Message { } /// The result of [`check`], [`check_file`], or [`check_virtual`]. -#[pyclass(frozen, eq, skip_from_py_object, module = "mdscript")] +#[pyclass(frozen, eq, skip_from_py_object, module = "markdown_script")] #[derive(Clone, PartialEq, Eq)] pub struct CheckResult { #[pyo3(get)] @@ -259,7 +259,7 @@ impl CheckResult { /// Retains the canonical `to_canonical_json()` value as its single backing store; /// every typed getter and `to_dict()`/`to_json()` reads from it, so they can never /// diverge. `__eq__` is wire equality; the object is intentionally unhashable. -#[pyclass(frozen, eq, skip_from_py_object, module = "mdscript")] +#[pyclass(frozen, eq, skip_from_py_object, module = "markdown_script")] #[derive(Clone, PartialEq)] pub struct CompileResult { /// The canonical discriminated-union value — the single source of truth. @@ -419,7 +419,7 @@ impl CompileResult { /// the rule emits no hint, produces no source span, or carries no fix edits /// respectively. In the JSON wire format all three are JSON `null` (not /// absent keys) when `None`. -#[pyclass(frozen, eq, skip_from_py_object, module = "mdscript")] +#[pyclass(frozen, eq, skip_from_py_object, module = "markdown_script")] #[derive(Clone, PartialEq, Eq)] pub struct LintDiagnostic { #[pyo3(get)] @@ -639,7 +639,7 @@ type LintFileReportReduce<'py> = (Bound<'py, PyType>, (String, Vec) -> PyResult<()> { +fn _markdown_script(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("MdsError", m.py().get_type::())?; m.add_class::()?; m.add_class::()?; diff --git a/crates/mds-python/tests/conftest.py b/crates/mds-python/tests/conftest.py index ce83c464..524d569b 100644 --- a/crates/mds-python/tests/conftest.py +++ b/crates/mds-python/tests/conftest.py @@ -1,4 +1,4 @@ -"""Shared pytest fixtures for the mdscript binding suite.""" +"""Shared pytest fixtures for the markdown_script binding suite.""" from __future__ import annotations diff --git a/crates/mds-python/tests/test_concurrency.py b/crates/mds-python/tests/test_concurrency.py index edace220..d66e2447 100644 --- a/crates/mds-python/tests/test_concurrency.py +++ b/crates/mds-python/tests/test_concurrency.py @@ -25,7 +25,7 @@ import pytest -import mdscript as m +import markdown_script as m # A moderately expensive, deterministic compile (a few ms each). LOOP_SRC = "@for i in items:\nItem {{i}}: lorem ipsum dolor sit amet consectetur adipiscing\n@end\n" diff --git a/crates/mds-python/tests/test_contract.py b/crates/mds-python/tests/test_contract.py index 50f9c225..b65905a0 100644 --- a/crates/mds-python/tests/test_contract.py +++ b/crates/mds-python/tests/test_contract.py @@ -6,7 +6,7 @@ import pytest -import mdscript as m +import markdown_script as m MD = m.compile("Hello {{name}}!\n", vars={"name": "Alice"}) MSG = m.compile("@message user:\nHi\n@end\n") diff --git a/crates/mds-python/tests/test_errors.py b/crates/mds-python/tests/test_errors.py index 18146766..a2d932fa 100644 --- a/crates/mds-python/tests/test_errors.py +++ b/crates/mds-python/tests/test_errors.py @@ -6,7 +6,7 @@ import pytest -import mdscript as m +import markdown_script as m # ── E1/E2: MdsError type + structured fields ──────────────────────────────────── diff --git a/crates/mds-python/tests/test_functional.py b/crates/mds-python/tests/test_functional.py index 3cb7ca27..abe19ec9 100644 --- a/crates/mds-python/tests/test_functional.py +++ b/crates/mds-python/tests/test_functional.py @@ -6,7 +6,7 @@ import pytest -import mdscript as m +import markdown_script as m # ── compile: markdown vs messages, vars, base_path (F1–F4) ────────────────────── diff --git a/crates/mds-python/tests/test_limits.py b/crates/mds-python/tests/test_limits.py index f44ab498..45c81661 100644 --- a/crates/mds-python/tests/test_limits.py +++ b/crates/mds-python/tests/test_limits.py @@ -6,7 +6,7 @@ import pytest -import mdscript as m +import markdown_script as m MAX = 10 * 1024 * 1024 # MAX_SOURCE_SIZE (10 MiB) diff --git a/crates/mds-python/tests/test_lint.py b/crates/mds-python/tests/test_lint.py index f7bb97ef..d67af95f 100644 --- a/crates/mds-python/tests/test_lint.py +++ b/crates/mds-python/tests/test_lint.py @@ -7,7 +7,7 @@ import pytest -import mdscript as m +import markdown_script as m # ── Helpers ────────────────────────────────────────────────────────────────────── diff --git a/crates/mds-python/tests/test_parity.py b/crates/mds-python/tests/test_parity.py index 7f6159e0..e9223c6d 100644 --- a/crates/mds-python/tests/test_parity.py +++ b/crates/mds-python/tests/test_parity.py @@ -15,7 +15,7 @@ import pytest -import mdscript as m +import markdown_script as m from conftest import cli_build # (id, source, vars, expected canonical dict) — import-free so `dependencies == []`. diff --git a/crates/mds-python/tests/test_perf.py b/crates/mds-python/tests/test_perf.py index d97a91ae..af6f129b 100644 --- a/crates/mds-python/tests/test_perf.py +++ b/crates/mds-python/tests/test_perf.py @@ -14,7 +14,7 @@ import pytest -import mdscript as m +import markdown_script as m pytestmark = pytest.mark.perf diff --git a/crates/mds-python/tests/test_pickle.py b/crates/mds-python/tests/test_pickle.py index a909e523..cc00b12c 100644 --- a/crates/mds-python/tests/test_pickle.py +++ b/crates/mds-python/tests/test_pickle.py @@ -6,7 +6,7 @@ import pytest -import mdscript as m +import markdown_script as m RESULTS = [ m.compile("Hello {{n}}!\n", vars={"n": "A"}), @@ -87,7 +87,7 @@ def test_pk2_mdserror_round_trip_without_span() -> None: def _mp_worker(source: str) -> object: """Compile in a child process and return the (picklable) result.""" - import mdscript as _m + import markdown_script as _m return _m.compile(source, vars={"name": "MP"}) diff --git a/crates/mds-python/tests/test_source_map.py b/crates/mds-python/tests/test_source_map.py index 57cbec8a..67bf42b9 100644 --- a/crates/mds-python/tests/test_source_map.py +++ b/crates/mds-python/tests/test_source_map.py @@ -23,7 +23,7 @@ import pytest -import mdscript as m +import markdown_script as m from conftest import FIXTURES # Base64 alphabet used by VLQ mappings (excludes <, >, - per security constraint). diff --git a/crates/mds-python/tests/test_typing.py b/crates/mds-python/tests/test_typing.py index 36313903..2edb5c07 100644 --- a/crates/mds-python/tests/test_typing.py +++ b/crates/mds-python/tests/test_typing.py @@ -11,7 +11,7 @@ SAMPLE = Path(__file__).parent / "typecheck_sample.py" # Pyright project root: the mds-python package directory (contains pyrightconfig.json -# with extraPaths pointing to ./python so "import mdscript" resolves). +# with extraPaths pointing to ./python so "import markdown_script" resolves). _PYRIGHT_PROJECT = Path(__file__).parent.parent # Known, structurally-justified stub/runtime diffs `stubtest` cannot resolve @@ -21,19 +21,19 @@ # (src/lib.rs `mds_err_to_py`/`coded_error`), never as class-level descriptors, so # static introspection of the class object can never see them. _KNOWN_STUBTEST_DIFFS = ( - "mdscript.MdsError.code", - "mdscript.MdsError.message", - "mdscript.MdsError.help", - "mdscript.MdsError.span", + "markdown_script.MdsError.code", + "markdown_script.MdsError.message", + "markdown_script.MdsError.help", + "markdown_script.MdsError.span", ) def test_c6_py_typed_and_stubs_installed() -> None: - import mdscript + import markdown_script - pkg = Path(mdscript.__file__).parent + pkg = Path(markdown_script.__file__).parent assert (pkg / "py.typed").is_file(), "py.typed marker must ship in the package" - assert (pkg / "_mdscript.pyi").is_file(), "extension stub must ship" + assert (pkg / "_markdown_script.pyi").is_file(), "extension stub must ship" assert (pkg / "__init__.pyi").is_file(), "package stub must ship" @@ -74,7 +74,7 @@ def test_c6_stubtest_matches_runtime() -> None: sys.executable, "-m", "mypy.stubtest", - "mdscript", + "markdown_script", "--ignore-missing-stub", # runtime-only dunders (__repr__, __reduce__, …) ], capture_output=True, diff --git a/crates/mds-python/tests/typecheck_sample.py b/crates/mds-python/tests/typecheck_sample.py index 21c26971..1db48bf0 100644 --- a/crates/mds-python/tests/typecheck_sample.py +++ b/crates/mds-python/tests/typecheck_sample.py @@ -10,8 +10,8 @@ import pathlib from typing import Any -import mdscript -from mdscript import ( +import markdown_script +from markdown_script import ( CheckResult, CompileResult, LintDiagnostic, @@ -24,14 +24,14 @@ def render_markdown() -> str: - result: CompileResult = mdscript.compile("Hello {{name}}!", vars={"name": "Alice"}) + result: CompileResult = markdown_script.compile("Hello {{name}}!", vars={"name": "Alice"}) if result.output is not None: # narrow str | None -> str return result.output return "" def collect_roles() -> list[str]: - result = mdscript.compile("@message user:\nHi\n@end\n") + result = markdown_script.compile("@message user:\nHi\n@end\n") roles: list[str] = [] if result.messages is not None: for message in result.messages: @@ -41,26 +41,26 @@ def collect_roles() -> list[str]: def compile_from_file(path: pathlib.Path) -> CompileResult: - return mdscript.compile_file(path, vars={"count": 3}) + return markdown_script.compile_file(path, vars={"count": 3}) def compile_virtual_graph() -> CompileResult: modules: dict[str, str] = {"main.mds": "hi\n"} - return mdscript.compile_virtual(modules, "main.mds") + return markdown_script.compile_virtual(modules, "main.mds") def validate(source: str) -> list[str]: - check_result: CheckResult = mdscript.check(source, base_path="/tmp") + check_result: CheckResult = markdown_script.check(source, base_path="/tmp") return check_result.warnings def imports(source: str) -> list[str]: - return mdscript.scan_imports(source) + return markdown_script.scan_imports(source) def describe_error() -> str: try: - mdscript.compile("{{undef}}") + markdown_script.compile("{{undef}}") except MdsError as err: code: str = err.code span: Span | None = err.span @@ -73,7 +73,7 @@ def describe_error() -> str: def package_version() -> str: - return mdscript.__version__ + return markdown_script.__version__ # --------------------------------------------------------------------------- @@ -85,7 +85,7 @@ def package_version() -> str: def lint_source(source: str) -> int: """Lint inline source; return the number of files in the result.""" - result: LintResult = mdscript.lint(source) + result: LintResult = markdown_script.lint(source) version: int = result.version truncated: bool = result.truncated files: list[LintFileReport] = result.files @@ -95,7 +95,7 @@ def lint_source(source: str) -> int: def lint_typed_access(source: str) -> list[str]: """Demonstrate fully-typed attribute access on lint results (B6/F10).""" - result: LintResult = mdscript.lint(source) + result: LintResult = markdown_script.lint(source) # AC-224-1 / D8: the unknown-rule warning channel is typed as list[str], so a # consumer can read it without a cast or a `type: ignore`. lint_warnings: list[str] = result.lint_warnings @@ -120,7 +120,7 @@ def lint_typed_access(source: str) -> list[str]: def lint_source_with_options(source: str) -> str: """Lint source with all optional keyword arguments; return canonical JSON.""" rules: dict[str, str] = {"shadow-variable": "warn", "unused-variable": "off"} - result: LintResult = mdscript.lint( + result: LintResult = markdown_script.lint( source, base_path="/tmp", vars={"env": "ci"}, @@ -133,7 +133,7 @@ def lint_source_with_options(source: str) -> str: def lint_from_file(path: pathlib.Path) -> LintResult: """Lint a .mds file on disk and return the result.""" - return mdscript.lint_file( + return markdown_script.lint_file( path, vars={"count": 1}, rules={"unused-variable": "off"}, @@ -143,7 +143,7 @@ def lint_from_file(path: pathlib.Path) -> LintResult: def lint_virtual_graph() -> bool: """Lint a virtual module graph; return whether the result was truncated.""" modules: dict[str, str] = {"entry.mds": "Hello!\n"} - result: LintResult = mdscript.lint_virtual( + result: LintResult = markdown_script.lint_virtual( modules, "entry.mds", vars={"name": "world"}, diff --git a/examples/README.md b/examples/README.md index 8ddfa7d2..1a5fa714 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,7 +48,7 @@ mds lint examples/linting/config-demo/loop-shadow.mds | [`linting/`](linting/) | A deliberately-messy template that trips four lint rules; shows `mds lint` human and JSON output, `--fix` tiers (A auto-applies, B standalone-only, C report-only), `--diff` preview, exit-code semantics, and per-rule severity overrides via `mds.json` (`config-demo/` — enables `shadow-variable`, promotes `unused-variable` to error, silences `redundant-else`) | | [`source-maps/`](source-maps/) | Source Map v3 generation via `mds build --source-map` — sidecar map, `--inline` data-URI embed, and `--embed-sources` self-contained variant | | [`formatting/`](formatting/) | Auto-formatter demo (`mds fmt`) — write, `--check`, `--diff`, and stdin filter modes; what fmt normalizes (directive trailing whitespace, line endings, final newline) vs. preserves byte-for-byte (body text, `@message`/`@define` bodies); the safety gate that refuses any rewrite changing compiled output; exit codes | -| [`python/`](python/) | Native Python bindings (`mdscript`, built with PyO3) — compile strings and files with `mdscript.compile`/`compile_file`, generate Source Map v3 with `source_map=True`, handle `MdsError` (`.code`, `.help`, `.span`), and lint with `mdscript.lint`; requires a virtualenv + `maturin develop` (`source .venv/bin/activate`) | +| [`python/`](python/) | Native Python bindings (`markdown_script`, built with PyO3) — compile strings and files with `markdown_script.compile`/`compile_file`, generate Source Map v3 with `source_map=True`, handle `MdsError` (`.code`, `.help`, `.span`), and lint with `markdown_script.lint`; requires a virtualenv + `maturin develop` (`source .venv/bin/activate`) | Some examples take runtime variables — pass the accompanying `vars.json`: diff --git a/examples/python/README.md b/examples/python/README.md index b37487c0..ecb1915a 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -1,6 +1,6 @@ # Python bindings -`mdscript` is the native Python binding for the MDS compiler (built with PyO3). +`markdown_script` is the native Python binding for the MDS compiler (built with PyO3). It exposes the same compiler as the CLI and the JavaScript packages, including **Source Map v3** generation. @@ -16,7 +16,7 @@ pip install "maturin==1.13.3" pytest maturin develop -m crates/mds-python/Cargo.toml ``` -Once the venv is active, `import mdscript` works. +Once the venv is active, `import markdown_script` works. ## Run the demo @@ -30,17 +30,17 @@ python examples/python/demo.py 1. **Compile a string** to Markdown with runtime `vars`. 2. **Generate a source map** with `source_map=True` and read it back — plus `compile_file(..., sources_content=True)` to embed the original template text. -3. **Handle errors** — a failed compile raises `mdscript.MdsError`, which carries +3. **Handle errors** — a failed compile raises `markdown_script.MdsError`, which carries `.code`, `.help`, and a `.span` (`offset` / `length` / `line` / `column`). 4. **Lint** a template and inspect the structured findings. ## API quick reference ```python -import mdscript +import markdown_script # Compile a source string. Keyword-only options. -r = mdscript.compile( +r = markdown_script.compile( source, vars=None, # dict of runtime variables base_path=None, # directory for resolving @import in a string source @@ -58,14 +58,14 @@ r.to_dict() # plain dict; always includes "sourceMap": None when not request r.to_json() # JSON string; omits "sourceMap" key when absent (canonical wire format) # Compile a file, resolving @import relative to it. -mdscript.compile_file(path, vars=None, source_map=False, sources_content=False) +markdown_script.compile_file(path, vars=None, source_map=False, sources_content=False) # Validate without rendering. Passing source_map or sources_content raises # MdsError(code="mds::invalid_options") — source maps are a compile-only concept. -mdscript.check(source, vars=None, base_path=None) +markdown_script.check(source, vars=None, base_path=None) # Lint. LintResult.files returns typed LintFileReport objects (B6/F10). -lr = mdscript.lint(source, vars=None, base_path=None, rules=None) +lr = markdown_script.lint(source, vars=None, base_path=None, rules=None) lr.version, lr.truncated for report in lr.files: # list[LintFileReport] report.file # str — file key diff --git a/examples/python/demo.py b/examples/python/demo.py index 4b2128a2..b0b3998c 100644 --- a/examples/python/demo.py +++ b/examples/python/demo.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 -"""Runnable tour of the ``mdscript`` Python bindings (PyO3). +"""Runnable tour of the ``markdown_script`` Python bindings (PyO3). Demonstrates four things a real user needs: 1. Compiling a template to Markdown. 2. Generating a Source Map v3 with ``source_map=True`` and reading it back. -3. Handling compile errors via ``mdscript.MdsError`` (``.code`` / ``.help`` / ``.span``). +3. Handling compile errors via ``markdown_script.MdsError`` (``.code`` / ``.help`` / ``.span``). 4. Linting a template and inspecting the structured findings. Run it against the repo's virtualenv (see the README): @@ -17,7 +17,7 @@ import json import os -import mdscript +import markdown_script HERE = os.path.dirname(os.path.abspath(__file__)) # Reuse the two-source example from examples/source-maps/ for the file demo. @@ -30,7 +30,7 @@ def rule(title: str) -> None: # ── 1. Compile a string to Markdown ───────────────────────────────────────── rule("compile a string") -result = mdscript.compile( +result = markdown_script.compile( "# Hi {{name}}\n\n@for item in items:\n- {{item}}\n@end\n", vars={"name": "World", "items": ["alpha", "beta"]}, ) @@ -40,7 +40,7 @@ def rule(title: str) -> None: # ── 2. Compile with a Source Map v3 ───────────────────────────────────────── rule("compile a string with source_map=True") -mapped = mdscript.compile( +mapped = markdown_script.compile( "# Hi {{name}}\n\n@for item in items:\n- {{item}}\n@end\n", vars={"name": "World", "items": ["alpha", "beta"]}, source_map=True, @@ -55,7 +55,7 @@ def rule(title: str) -> None: # When not requested, the attribute is None. # to_dict() always includes "sourceMap": None (Python-idiomatic always-present); # to_json() omits the key (canonical wire format shared with other surfaces). -plain = mdscript.compile("# no map\n") +plain = markdown_script.compile("# no map\n") print("without source_map -> .source_map is None:", plain.source_map is None) d = plain.to_dict() print("to_dict has 'sourceMap' key:", "sourceMap" in d) @@ -64,7 +64,7 @@ def rule(title: str) -> None: # File compile resolves @import chains; sources become project-root-relative # (or basenames when no .git/.mdsroot marker is found above the file). rule("compile_file with an @import + embedded sources") -filemap = mdscript.compile_file(ANNOTATED, source_map=True, sources_content=True) +filemap = markdown_script.compile_file(ANNOTATED, source_map=True, sources_content=True) fsm = filemap.source_map print("sources:", fsm["sources"]) # entry template + imported _style.mds print("sourcesContent lengths:", [len(c) for c in fsm["sourcesContent"]]) @@ -73,16 +73,16 @@ def rule(title: str) -> None: # Requesting sources_content without source_map is rejected. rule("sources_content without source_map is an error") try: - mdscript.compile("x\n", sources_content=True) -except mdscript.MdsError as exc: + markdown_script.compile("x\n", sources_content=True) +except markdown_script.MdsError as exc: print("raised MdsError, code:", exc.code) # ── 3. Error handling via MdsError ────────────────────────────────────────── rule("error handling") try: - mdscript.compile("Hello {{missing}}!\n") -except mdscript.MdsError as exc: + markdown_script.compile("Hello {{missing}}!\n") +except markdown_script.MdsError as exc: print("code:", exc.code) print("help:", exc.help) span = exc.span @@ -93,7 +93,7 @@ def rule(title: str) -> None: # ── 4. Lint a template ────────────────────────────────────────────────────── rule("lint") -lint_result = mdscript.lint("---\nunused: 1\nused: hi\n---\n{{used}}\n") +lint_result = markdown_script.lint("---\nunused: 1\nused: hi\n---\n{{used}}\n") print("lint schema version:", lint_result.version, "truncated:", lint_result.truncated) # LintResult.files returns a list of LintFileReport objects (B6/F10 typed access). for report in lint_result.files: diff --git a/packages/mds/__test__/source-map.spec.mjs b/packages/mds/__test__/source-map.spec.mjs index cf1e1ff8..405786ea 100644 --- a/packages/mds/__test__/source-map.spec.mjs +++ b/packages/mds/__test__/source-map.spec.mjs @@ -56,7 +56,7 @@ function findMdsCli() { } /** - * Return the path to a Python interpreter that can import `mdscript`, or null. + * Return the path to a Python interpreter that can import `markdown_script`, or null. * * Resolution order: * 1. MDS_PYTHON_BIN env var (set by CI to the pip-managed interpreter). @@ -66,7 +66,7 @@ function findMdsCli() { * Returns null only when none of the above is found. In CI (process.env.CI) * the caller must treat null as a hard failure — see PF-007. */ -function findPythonForMdscript() { +function findPythonForMarkdownScript() { const envBin = process.env.MDS_PYTHON_BIN; if (envBin) { if (!existsSync(envBin)) throw new Error(`MDS_PYTHON_BIN=${envBin} does not exist`); @@ -758,18 +758,18 @@ describe('source maps — compileFile differential (CF-SM)', () => { const cliSources = cliMap.sources; // -- Surface 4: Python binding compile_file ------------------------------- - // Use the repo-local venv Python which has the mdscript module installed + // Use the repo-local venv Python which has the markdown_script module installed // by `maturin develop`, or the interpreter exported by MDS_PYTHON_BIN (CI). // In CI all four surfaces must run — a missing surface is a hard failure // (avoids PF-007: a gate that silently skips a surface reads as green). // Locally, a missing interpreter warns and skips the Python leg only. - const python = findPythonForMdscript(); + const python = findPythonForMarkdownScript(); let pySources = null; if (python == null) { if (process.env.CI) { throw new Error( 'CF-SM2: Python surface is required in CI but no interpreter was found. ' + - 'Set MDS_PYTHON_BIN to an interpreter that can import mdscript, or ' + + 'Set MDS_PYTHON_BIN to an interpreter that can import markdown_script, or ' + 'install the binding with `pip install ./crates/mds-python`. ' + 'A missing surface silently breaks the PF-007 cross-surface parity gate.', ); @@ -779,18 +779,18 @@ describe('source maps — compileFile differential (CF-SM)', () => { '(run `maturin develop` inside .venv to enable)', ); } else { - // Import mdscript from the Python environment directly (site-packages). + // Import markdown_script from the Python environment directly (site-packages). // Do NOT insert the source tree into sys.path: with `pip install`, the - // compiled extension (_mdscript.so) lands in site-packages, not in the + // compiled extension (_markdown_script.so) lands in site-packages, not in the // source crates/mds-python/python/ directory, so prepending the source - // path causes `from ._mdscript import` to fail (it finds __init__.py in + // path causes `from ._markdown_script import` to fail (it finds __init__.py in // the source tree but the .so is elsewhere). Importing from site-packages // works for both `pip install ./crates/mds-python` (CI) and - // `maturin develop` (local), since both make `mdscript` importable from + // `maturin develop` (local), since both make `markdown_script` importable from // the standard path. Pass entryPath as argv so no shell escaping is needed. const pyScript = [ 'import json, sys', - 'import mdscript as m', + 'import markdown_script as m', 'result = m.compile_file(sys.argv[1], source_map=True)', 'print(json.dumps(result.source_map["sources"]))', ].join('\n');