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
157 lines
5.7 KiB
Python
157 lines
5.7 KiB
Python
"""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())
|