10C Inc 1: core modeling
Binds the modeling core the app builds every feature out of — 93 of its 138 symbols now resolve, up from 34. New modules: GeomAbs, Geom2d, Geom, TCollection, TColgp, TColStd, GProp, BRepGProp, Bnd, BRepBndLib, Adaptor3d, BRepAdaptor, GeomLProp, GeomAPI, BRepBuilderAPI, BOPAlgo, BRepAlgoAPI, BRepPrimAPI, GC, BRepMesh; gp and BRep_Tool completed. Three structural decisions: - BRepBuilderAPI_MakeShape carries Build/Shape/Generated/Modified/IsDeleted for every maker in the binding, so the booleans, the primitives and (later) the fillet builders all answer the app's duck-typed provenance layer through ordinary virtual dispatch. History lists come back copied, so they outlive the builder. - The executing two-argument BRepAlgoAPI constructors stay unbound; operands go in through SetArguments/SetTools. Section keeps Init1/Init2, which are plain setters. BOPAlgo moved up from Inc 2 — SetGlue needs its enum. - Adaptor3d is registered although the app never imports it: every method it calls on BRepAdaptor_Curve/Surface is a virtual declared there, so binding them once on the bases leaves mod_BRepAdaptor.cpp with just constructors. Gate: tests/test_inc1_modeling.py against reference values gen_fixtures.py now records from the stock wheel — measurements, per-face area/centroid in map order, mesh counts, and the boolean history map compared exactly, since that is the substrate the app's topological naming is built on. tools/sigdiff.py compares our bound surface against stock's, because a wrong nb::init<> is silent: MakePrism's five-argument form bound OCCT's semi-infinite gp_Dir overload (gp_Dir converts from gp_Vec), producing a valid solid of the wrong shape with the flags shifted along. The fixture digest caught it; sigdiff finds the class of bug directly, and now reports only one deliberate deviation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
This commit is contained in:
156
tools/sigdiff.py
Normal file
156
tools/sigdiff.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""Compare this binding's bound surface against the stock wheel's.
|
||||
|
||||
`inventory.py --check` answers "does the symbol exist"; this answers "does it
|
||||
mean the same thing". The gap between those two is where a binding does real
|
||||
damage: an `nb::init<...>` whose types differ from the constructor you meant
|
||||
can bind a *different overload* through an implicit conversion and build valid
|
||||
but wrong geometry, silently. That happened once — BRepPrimAPI_MakePrism's
|
||||
gp_Vec form resolving to the semi-infinite gp_Dir one, because gp_Dir converts
|
||||
from gp_Vec — and the fixture digests are what caught it. This finds the class
|
||||
of bug directly.
|
||||
|
||||
Two runs, then a diff:
|
||||
|
||||
# under the stock wheel, from the app checkout
|
||||
uv run --project backend python ../ocp/tools/sigdiff.py --dump stock.json
|
||||
# under ours
|
||||
make shell # then: /cache/venv/bin/python /io/tools/sigdiff.py --dump /io/ours.json
|
||||
python tools/sigdiff.py --compare stock.json ours.json
|
||||
|
||||
The comparison is one-sided on purpose: binding *fewer* overloads than stock is
|
||||
the normal state of this project, so only surface we expose that stock does not
|
||||
is reported. pybind11 spells the receiver as `self: Cls` and nanobind as plain
|
||||
`self`, so the leading class name is dropped before comparing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
|
||||
HERE = pathlib.Path(__file__).resolve().parent
|
||||
DEFAULT_INVENTORY = HERE.parent / "inventory.json"
|
||||
|
||||
#: Parameter annotations, as both wrappers write them: `name: Some.Type`.
|
||||
_ANNOTATION = re.compile(r":\s*([A-Za-z_][\w.]*)")
|
||||
|
||||
|
||||
def _signatures(owner, name: str) -> list[tuple[str, ...]]:
|
||||
"""Every overload of `name`, reduced to its parameter type names."""
|
||||
doc = getattr(getattr(owner, name, None), "__doc__", None)
|
||||
if not doc:
|
||||
return []
|
||||
out = set()
|
||||
for line in doc.splitlines():
|
||||
line = re.sub(r"^\d+\.\s*", "", line.strip())
|
||||
if "(" not in line or ")" not in line:
|
||||
continue
|
||||
inner = line[line.index("(") + 1 : line.rindex(")")]
|
||||
types = tuple(t.split(".")[-1] for t in _ANNOTATION.findall(inner))
|
||||
out.add(types)
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def dump(inventory: pathlib.Path, out: pathlib.Path) -> int:
|
||||
data = json.loads(inventory.read_text())
|
||||
result: dict = {}
|
||||
|
||||
for mod, symbols in data["modules"].items():
|
||||
try:
|
||||
module = importlib.import_module(f"OCP.{mod}")
|
||||
except ImportError:
|
||||
continue
|
||||
for name in symbols:
|
||||
obj = getattr(module, name, None)
|
||||
if obj is None:
|
||||
continue
|
||||
# OCCT namespaces (TopoDS, BRepGProp, ...) have no class to hang
|
||||
# their statics on: upstream exposes them as a module, this binding
|
||||
# as an empty carrier class. Both answer `TopoDS.Face_s(...)`, so
|
||||
# compare their members either way.
|
||||
if not isinstance(obj, (type, types.ModuleType)):
|
||||
continue
|
||||
# An nb::is_arithmetic() enum derives from int, so dir() carries
|
||||
# int's methods; pybind11's enum does not. Not surface we bound.
|
||||
inherited = set(dir(int)) if isinstance(obj, type) and issubclass(
|
||||
obj, int) else set()
|
||||
result[f"{mod}.{name}"] = {
|
||||
"init": _signatures(obj, "__init__"),
|
||||
"members": sorted(
|
||||
m for m in dir(obj)
|
||||
if not m.startswith("_") and m not in inherited
|
||||
),
|
||||
}
|
||||
|
||||
out.write_text(json.dumps(result, indent=1) + "\n")
|
||||
print(f"{len(result)} classes -> {out}")
|
||||
return 0
|
||||
|
||||
|
||||
def compare(stock_path: pathlib.Path, ours_path: pathlib.Path) -> int:
|
||||
stock = json.loads(stock_path.read_text())
|
||||
ours = json.loads(ours_path.read_text())
|
||||
problems = 0
|
||||
|
||||
def normalized(entry: dict, cls: str) -> set[tuple[str, ...]]:
|
||||
out = set()
|
||||
for sig in entry.get("init", []):
|
||||
sig = list(sig)
|
||||
if sig and sig[0] == cls: # pybind11's `self: Cls`
|
||||
sig = sig[1:]
|
||||
out.add(tuple(sig))
|
||||
return out
|
||||
|
||||
for key, mine in sorted(ours.items()):
|
||||
theirs = stock.get(key)
|
||||
if theirs is None:
|
||||
print(f"{key}: not present in the stock wheel")
|
||||
problems += 1
|
||||
continue
|
||||
|
||||
cls = key.split(".", 1)[1]
|
||||
extra_init = normalized(mine, cls) - normalized(theirs, cls)
|
||||
if extra_init:
|
||||
problems += 1
|
||||
print(f"{key}: constructor overloads stock does not have")
|
||||
for sig in sorted(extra_init):
|
||||
print(f" ours : ({', '.join(sig)})")
|
||||
for sig in sorted(normalized(theirs, cls)):
|
||||
print(f" stock: ({', '.join(sig)})")
|
||||
|
||||
extra_members = set(mine["members"]) - set(theirs["members"])
|
||||
if extra_members:
|
||||
problems += 1
|
||||
print(f"{key}: members stock does not have: {sorted(extra_members)}")
|
||||
|
||||
if problems:
|
||||
print(f"\n{problems} difference(s) — each is either a mis-bound overload "
|
||||
f"or a deliberate deviation worth a comment in its module")
|
||||
return 1
|
||||
print(f"{len(ours)} classes, no surface we expose that stock does not")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--dump", type=pathlib.Path, help="write this wheel's surface")
|
||||
ap.add_argument("--compare", nargs=2, type=pathlib.Path,
|
||||
metavar=("STOCK", "OURS"))
|
||||
ap.add_argument("--inventory", type=pathlib.Path, default=DEFAULT_INVENTORY)
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.dump:
|
||||
return dump(args.inventory, args.dump)
|
||||
if args.compare:
|
||||
return compare(*args.compare)
|
||||
ap.print_help()
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user