Housekeeping ahead of the Inc 1-4 coverage work
- inventory.py --methods: report the instance methods the app calls per class, plus chained calls that constrain a return type. --check says which classes to bind; this says what to bind on them, which is what writing ~38 module TUs needs. - Drop StlAPI from the inventory: StlAPI_Writer has no app call site (the only use was a test fixture, now on the app's own STL writer). 138 symbols / 47 modules. - adding-symbols.md: scope the executing-constructor ban to the BRepAlgoAPI booleans, which are the only classes with a deferred Set*/Build form — BRepMesh_IncrementalMesh, GeomAPI_*, BRepCheck_Analyzer and friends compute in their constructor by design and bind as stock. Replace the per-increment app-test guidance: backend/tests/conftest.py imports n3xd.main, so no app test can collect until the last module is bound. Increments gate on stock-recorded fixtures here; the app suite is the Inc 4 gate. - parity_venv.sh: run the ocp suite in the swapped venv (it imports only OCP/n3xd_ocp, so it works throughout). - Fix a stale macro name in occt_handle.h (ocp_new, not OCP_TRANSIENT_NEW) and drop the unused ocp_transient_class helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DfriM8XUkn7uYf5Dwe2xo6
This commit is contained in:
@@ -80,6 +80,91 @@ def scan(app_root: pathlib.Path) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def methods(app_root: pathlib.Path, only: str | None) -> int:
|
||||
"""Report the *instance* methods the app calls on each OCP class.
|
||||
|
||||
`scan` only sees names reached through an import, which is enough to know
|
||||
*which* classes to bind but not *what* to bind on them. This fills that
|
||||
gap well enough to write a module in one pass instead of discovering the
|
||||
surface one AttributeError at a time: variables assigned straight from a
|
||||
constructor carry their class through the file, so `a = BRepAdaptor_Surface(f)`
|
||||
followed by `a.GetType()` is resolved.
|
||||
|
||||
Chained calls are reported separately as `Klass.Outer() -> Inner`, because
|
||||
what they constrain is the *return* type — `adaptor.Cylinder().Radius()`
|
||||
says gp_Cylinder needs `Radius`, not that BRepAdaptor_Surface does.
|
||||
|
||||
Heuristic by construction: it does not follow arguments, returns or
|
||||
attributes, so treat a quiet class as "look again", not "nothing needed".
|
||||
"""
|
||||
direct: dict[str, set[str]] = collections.defaultdict(set)
|
||||
chained: dict[str, set[str]] = collections.defaultdict(set)
|
||||
|
||||
for path in sorted(app_root.rglob("*.py")):
|
||||
if "__pycache__" in path.parts:
|
||||
continue
|
||||
try:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
continue
|
||||
|
||||
classes = {
|
||||
alias.asname or alias.name: alias.name
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom)
|
||||
and node.module
|
||||
and (node.module == "OCP" or node.module.startswith("OCP."))
|
||||
for alias in node.names
|
||||
}
|
||||
if not classes:
|
||||
continue
|
||||
|
||||
# local variable -> OCP class, from `v = Klass(...)`
|
||||
env: dict[str, str] = {}
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Assign) and isinstance(node.value, ast.Call):
|
||||
fn = node.value.func
|
||||
cls = classes.get(getattr(fn, "id", ""))
|
||||
if cls:
|
||||
for tgt in node.targets:
|
||||
if isinstance(tgt, ast.Name):
|
||||
env[tgt.id] = cls
|
||||
|
||||
def owner(node: ast.expr) -> str | None:
|
||||
"""The OCP class an expression evaluates to, when knowable."""
|
||||
if isinstance(node, ast.Name):
|
||||
return env.get(node.id)
|
||||
if isinstance(node, ast.Call):
|
||||
return classes.get(getattr(node.func, "id", ""))
|
||||
return None
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Attribute):
|
||||
continue
|
||||
cls = owner(node.value)
|
||||
if cls:
|
||||
direct[cls].add(node.attr)
|
||||
continue
|
||||
# <known>.Outer().Inner — constrains Outer's return type
|
||||
inner = node.value
|
||||
if isinstance(inner, ast.Call) and isinstance(inner.func, ast.Attribute):
|
||||
cls = owner(inner.func.value)
|
||||
if cls:
|
||||
chained[f"{cls}.{inner.func.attr}()"].add(node.attr)
|
||||
|
||||
def dump(title: str, data: dict[str, set[str]]) -> None:
|
||||
keys = [k for k in sorted(data) if only is None or only in k]
|
||||
if not keys:
|
||||
return
|
||||
print(f"\n{title}")
|
||||
for key in keys:
|
||||
print(f" {key}: {', '.join(sorted(data[key]))}")
|
||||
|
||||
dump("instance methods, by class:", direct)
|
||||
dump("chained calls (constrain the return type):", chained)
|
||||
return 0
|
||||
|
||||
|
||||
def emit(app_root: pathlib.Path, out: pathlib.Path) -> int:
|
||||
data = scan(app_root)
|
||||
out.write_text(json.dumps(data, indent=2) + "\n")
|
||||
@@ -130,10 +215,18 @@ def main() -> int:
|
||||
ap.add_argument("--emit", action="store_true", help="rewrite the inventory")
|
||||
ap.add_argument("--check", action="store_true",
|
||||
help="compare the installed OCP against the inventory")
|
||||
ap.add_argument("--methods", action="store_true",
|
||||
help="report the instance methods the app calls per class")
|
||||
ap.add_argument("--only", help="with --methods: substring filter on the class")
|
||||
ap.add_argument("--app", type=pathlib.Path, default=DEFAULT_APP)
|
||||
ap.add_argument("--inventory", type=pathlib.Path, default=DEFAULT_INVENTORY)
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.methods:
|
||||
if not args.app.is_dir():
|
||||
print(f"app sources not found: {args.app}", file=sys.stderr)
|
||||
return 2
|
||||
return methods(args.app, args.only)
|
||||
if args.emit:
|
||||
if not args.app.is_dir():
|
||||
print(f"app sources not found: {args.app}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user