ferrotherm 0.35.0

Thermodynamic computing in pure Rust: sparse energy-based models, chromatic block-Gibbs, parallel tempering, thermodynamic linear algebra, stochastic differentiable programs, a variational compiler onto device topologies, exact inference by variable elimination, planted instances with known optima, sampler certificates, and a first-class joules ledger. std-only, zero dependencies, wasm-clean, deterministic by seed.
Documentation
#!/usr/bin/env python3
"""Generate `python/ferrotherm/__init__.pyi` from the module itself.

WHY GENERATED AND NOT WRITTEN. A hand-kept stub is worse than no stub: it starts correct, drifts
one signature at a time, and every drift is a confident lie told to an editor, a type checker and
whatever model is writing code against this library. Nobody notices, because a wrong stub still
autocompletes. So the stub is derived from the runtime API, and `check-stubs.sh` regenerates it and
diffs -- which turns drift from a thing to remember into a thing that fails.

WHY A STUB AT ALL. This package binds a C ABI through ctypes. Without a stub an editor sees a module
of opaque callables: no parameter names, no types, no defaults, and no docstrings where a reader is
looking. That is the difference between a library a model can write against and one it has to guess
at, and guessing at a numeric API produces code that runs and is wrong.

    scripts/gen-stubs.py            write the stub
    scripts/gen-stubs.py --stdout   print it instead
"""
import inspect
import sys
from pathlib import Path

REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "python"))
import ferrotherm as ft  # noqa: E402


def render_annotation(a):
    """Annotations here are written as strings, so they come back as strings. Emit them verbatim."""
    if a is inspect.Signature.empty:
        return None
    if isinstance(a, str):
        return a
    if a is None or a is type(None):
        return "None"
    return getattr(a, "__name__", None) or str(a).replace("typing.", "")


def render_default(d):
    if d is inspect.Parameter.empty:
        return None
    if isinstance(d, str):
        return repr(d)
    if d is None or isinstance(d, (bool, int, float)):
        return repr(d)
    # A mutable or exotic default cannot be spelled reliably in a stub; `...` is the honest form and
    # is what PEP 484 asks for.
    return "..."


def render_signature(fn):
    try:
        sig = inspect.signature(fn)
    except (ValueError, TypeError):
        return "(*args, **kwargs)", None
    parts = []
    for name, p in sig.parameters.items():
        if p.kind is p.VAR_POSITIONAL:
            piece = "*" + name
        elif p.kind is p.VAR_KEYWORD:
            piece = "**" + name
        else:
            piece = name
        ann = render_annotation(p.annotation)
        if ann:
            piece += ": " + ann
        dflt = render_default(p.default)
        if dflt is not None:
            piece += (" = " if ann else "=") + dflt
        parts.append(piece)
    ret = render_annotation(sig.return_annotation)
    return "(" + ", ".join(parts) + ")", ret


# `inspect.getdoc` walks up to `object`, so a class that never wrote an `__init__` docstring inherits
# object's. Emitting it puts "Initialize self. See help(type(self))" on hover for half the API, which
# is worse than nothing: it looks like documentation and says only that documentation is absent.
INHERITED = {
    "Initialize self.  See help(type(self)) for accurate signature.",
    "Return repr(self).",
    "Return str(self).",
    "Return len(self).",
}


def summary(obj):
    """The first paragraph of the docstring: what an editor shows on hover."""
    doc = inspect.getdoc(obj)
    if not doc:
        return None
    out = []
    for line in doc.splitlines():
        if not line.strip():
            break
        out.append(line.strip())
    text = " ".join(out)
    if text in INHERITED:
        return None
    # A docstring holding a quote character would close the stub's own triple quote.
    return None if '"""' in text else text


def emit_callable(name, fn, indent, out, is_property=False):
    doc = summary(fn)
    if is_property:
        ann = render_annotation(getattr(fn, "__annotations__", {}).get("return", inspect.Signature.empty))
        out.append(f"{indent}@property")
        out.append(f"{indent}def {name}(self) -> {ann or 'Any'}:")
    else:
        sig, ret = render_signature(fn)
        out.append(f"{indent}def {name}{sig} -> {ret or 'Any'}:")
    if doc:
        out.append(f'{indent}    """{doc}"""')
        out.append(f"{indent}    ...")
    else:
        out.append(f"{indent}    ...")


def build():
    out = [
        "# GENERATED by scripts/gen-stubs.py -- do not edit.",
        "#",
        "# This package binds a C ABI through ctypes, so without this file an editor, a type checker",
        "# and any model writing code against ferrotherm see a module of opaque callables: no",
        "# parameter names, no types, no defaults. Guessing at a numeric API produces code that runs",
        "# and is wrong, which is the failure a stub exists to prevent.",
        "#",
        "# It is generated rather than written because a hand-kept stub drifts one signature at a",
        "# time, and every drift is a confident lie that still autocompletes. `check-stubs.sh`",
        "# regenerates and diffs.",
        "",
        "from typing import Any, Sequence",
        "",
        f'__version__: str',
        "",
    ]
    names = sorted(ft.__all__)
    classes = [n for n in names if inspect.isclass(getattr(ft, n))]
    funcs = [n for n in names if inspect.isfunction(getattr(ft, n))]
    other = [n for n in names if n not in classes and n not in funcs]

    for n in classes:
        c = getattr(ft, n)
        out.append(f"class {n}:")
        doc = summary(c)
        if doc:
            out.append(f'    """{doc}"""')
        members = []
        for m, v in sorted(vars(c).items()):
            if m.startswith("_") and m != "__init__":
                continue
            if isinstance(v, property):
                members.append((m, v.fget, True))
            elif inspect.isfunction(v):
                members.append((m, v, False))
        if not members:
            out.append("    ...")
        for m, v, is_prop in members:
            emit_callable(m, v, "    ", out, is_prop)
        out.append("")

    for n in funcs:
        emit_callable(n, getattr(ft, n), "", out, False)
        out.append("")

    for n in other:
        out.append(f"{n}: Any")
    if other:
        out.append("")
    return "\n".join(out).rstrip() + "\n"


if __name__ == "__main__":
    text = build()
    if "--stdout" in sys.argv:
        sys.stdout.write(text)
    else:
        (REPO / "python" / "ferrotherm" / "__init__.pyi").write_text(text)
        print(f"wrote python/ferrotherm/__init__.pyi ({len(text.splitlines())} lines)")