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
def render_annotation(a):
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)
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
INHERITED = {
"Initialize self. See help(type(self)) for accurate signature.",
"Return repr(self).",
"Return str(self).",
"Return len(self).",
}
def summary(obj):
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
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)")