- 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
5.3 KiB
Adding symbols
The routine task: the app needs an OCCT class this binding does not expose yet. Read design.md first if you are touching the machinery instead.
1. Find what is missing
python tools/inventory.py --emit # re-scan the app for OCP usage
python tools/inventory.py --check # what the wheel lacks, by module
python tools/inventory.py --methods --only BRepAdaptor # what to bind on each class
--check groups gaps by module, which is how increments are scoped. It only
sees symbols reached through an import (TopExp.MapShapes_s), so it answers
which classes to bind but not what to bind on them.
--methods answers the second question: it resolves variables assigned
straight from a constructor and reports the methods called on them, plus
chained calls as Klass.Outer() -> Inner (those constrain Outer's return
type — adaptor.Cylinder().Radius() is a requirement on gp_Cylinder). It is
a heuristic — it does not follow arguments, returns or attributes, and a local
reassigned from something else shows up as noise — so read it as a starting
surface, not a specification.
2. Write the module
One file per OCP module: src/modules/mod_<Name>.cpp.
#include "../common/occt_module.h" // brings in the handle caster and "_a"
#include "../common/occt_policies.h" // OCP_RETURN_COPY, OCP_NOGIL
#include <Some_Class.hxx>
void register_Some(nb::module_ &root) {
nb::module_ m = ocp_submodule(root, "Some");
nb::class_<Some_Class>(m, "Some_Class")
.def(nb::init<>())
.def("Value", &Some_Class::Value, "index"_a, OCP_RETURN_COPY)
.def("Build", &Some_Class::Build, OCP_NOGIL);
}
Declare and call register_Some in src/core.cpp. Registration order matters
only in that a base class must precede its derived classes.
3. The checklist
- Shape-returning API →
OCP_RETURN_COPY. Explorers, iterators, map lookups,Generated/Modifiedlists — anything handing out a reference into storage the caller does not own. - Static method →
OCP_DEF_S(cls, "Name", ...), which appends_s. Every static, without exception. - Transient (handle-managed) class → derive from
Standard_Transientin thenb::class_declaration and bind constructors withocp_new<T, Args...>(). Nevernb::init<>— see design.md. - Long kernel call →
OCP_NOGIL, but only if it cannot re-enter Python. - Executing constructor → banned only where a deferred
SetX/BuildAPI exists and the constructor duplicatesBuild(): theBRepAlgoAPI_*booleans and splitter. Bind their default constructor plus theSetX/Buildsequence. Classes that only compute in their constructor and have no deferred form —BRepMesh_IncrementalMesh,BRepCheck_Analyzer,GeomAPI_*,BRepClass3d_SolidClassifier,BRepExtrema_DistShapeShape,GCPnts_*,BRepBuilderAPI_Transform— bind exactly as stock does. - Enum →
nb::is_arithmetic()and.export_values(). Message_ProgressRangeparameters → omit them. The app never passes one (noOCP.Messageimport anywhere), and leaving them out keeps signatures small. Add the module if--checkever reports it.- Out-parameters stay out-parameters.
BRep_Tool.Triangulation_s(F, L)writes throughLbecause the app calls it that way; returning a tuple would be tidier and wrong.
When in doubt about a signature, ask the stock wheel rather than guessing:
cd ../app && .venv/bin/python -c "from OCP.BRep import BRep_Tool; print(BRep_Tool.Triangulation_s.__doc__)"
4. New toolkits
If the linker cannot find a symbol, the class lives in a toolkit not yet listed
in CMakeLists.txt (target_link_libraries(_OCP PRIVATE ...)). Add it there;
auditwheel bundles whatever the linker records, so nothing else changes.
5. Verify and ship
make dev # compile + tests
make test-asan # if you touched ownership or added transients
make wheel # bump the .devN in pyproject.toml first
make publish
tools/parity_venv.sh && python tools/inventory.py --check
The app's own tests cannot gate an individual increment. backend/tests/ conftest.py imports n3xd.main, which pulls in the whole app and therefore the
whole OCP surface, so every backend test fails at collection until the last
module is bound. Increments are gated here instead: tools/gen_fixtures.py
records reference values from the stock wheel (counts, Modified/Generated/
IsDeleted history maps, measured floats) into tests/data/manifest.json, and
tests/test_inc<N>_*.py reproduces the same constructions under our wheel.
Counts and history maps must match exactly; floats compare at rel 1e-9.
The app's full suite is the Inc 4 gate, run in the parity venv, alongside
pytest -m perf and backend/tools/rebuild_sweep.py --diff over the project
store.
Adding to n3xd_ocp instead
Anything that is not a faithful mirror of an upstream symbol belongs in
src/ext/ under the n3xd_ocp namespace: bulk array APIs, batched measurement,
anything GIL-free that upstream does not offer. OCP.* staying a
symbol-for-symbol drop-in is what makes parity testing meaningful, so keep
additive work out of it. Register leaf modules with
ocp_named_module("n3xd_ocp.<name>") and re-export them in
python/n3xd_ocp/__init__.py.