sync_stub

Sync ifcopenshell_wrapper.pyi with the compiled ifcopenshell_wrapper.py by editing only the entries that are unambiguous to add or remove - never by regenerating the file wholesale.

Auto-applies:
  • top-level symbols (classes, functions, constants) present in the wrapper but missing from the stub -> inserted, alphabetically among neighbouring tracked entries. A brand-new class is rendered with its full member set (there’s no existing curated body to preserve).

  • top-level symbols in the stub that no longer exist in the wrapper at all -> removed.

  • for a class whose own declaration (name + bases) matches on both sides and whose body isn’t a single-line …: plain members (methods, bare attributes - not @property/@staticmethod wrappers) present in the wrapper’s class but missing from the stub’s -> inserted; members in the stub’s class no longer present on the wrapper’s -> removed.

Never auto-applies - reported instead, left completely untouched:
  • a top-level symbol whose declaration differs between stub and wrapper under the same name (e.g. a function’s parameter list, or a class’s base classes).

  • __init__, in every case (present on both sides with a different signature, or missing from one side entirely) - this is almost always where hand-curated constructor signatures live, since SWIG always emits generic *args for overloaded C++ constructors.

  • a class member that differs under the same name but isn’t a plain def/attribute, or any member of a class whose own declaration didn’t match.

  • anything that only looks addable/removable in the narrow view this tool parses but is actually still present on the other side in a form it doesn’t parse (chiefly property()/staticmethod() wrapper assignments, and the raw getter/setter method(s) such a wrapper consumes) - checked against validate_stub.py’s own fuller canonicalisation before anything is added or removed, so this never causes data loss.

  • any class where, after the safe edits above, its member set still doesn’t fully match the wrapper’s (typically for the property/ staticmethod reason above) - reported so a human can look, using validate_stub.py’s own diff for that class.

This is exactly where a hand-curated stub is expected to diverge from raw SWIG output on purpose - a mechanical tool can’t tell that apart from real drift, so it leaves those lines untouched and reports them for a human to judge instead of guessing.

Everything else in the file - the license header, comments, blank lines, docstrings, curated signatures, import order - is left byte-for-byte untouched: this script tracks a line cursor through the original source and only ever substitutes the exact line ranges of the entries it’s confident about, it never rewrites the file from scratch.

Usage:

python sync_stub.py # dry run: prints a report, writes nothing python sync_stub.py –write # applies the safe add/remove edits in place

Module Contents

class sync_stub.Entry
canonical: validate_stub.SubnameType
identity: str
is_init: bool = False
node: ast.stmt
recursable: bool = False
class sync_stub.Report
added: list[str] = []
changed: list[str] = []
removed: list[str] = []
residual: list[str] = []
sync_stub.identity_of(value: validate_stub.SubnameType) → str

Bare identity name for a get_names_tree()-style canonical value or top-level key, so entries can be matched by name even when their full signature differs (or when one side’s form - e.g. a property()-wrapped Assign - isn’t something iter_simple_entries() looks at directly).

sync_stub.iter_simple_entries(body: list) → list[Entry]

The subset of a module’s or class’s statements this tool is willing to reason about for auto-add/auto-remove: classes, plain function defs (including directly @decorated ones), and plain (non-property()/ staticmethod()-wrapped) assignments. Everything else - imports, bare docstrings/expressions, private names, trivial __init__(self), a property()/staticmethod() wrapper assignment itself, and the raw getter/setter method(s) it wraps (SWIG emits both, e.g. a calc_surface_area_ method alongside surface_area = property(calc_surface_area_)) - is left alone entirely by never being reported as an Entry at all.

sync_stub.main() → None
sync_stub.node_lines(source_lines: list[str], node: ast.stmt) → list[str]
sync_stub.render_entry(canonical: validate_stub.SubnameType, indent: str) → list[str]

Render a canonical name/signature value as stub text, matching generate_stub.py’s own per-line rendering so a freshly-inserted entry parses back to the same canonical value get_names_tree() would compute.

sync_stub.render_new_top_level(canonical: str, indent: str, wrapper_tree: dict) → list[str]

Render a brand-new top-level entry. For a class this can’t just be the header line - there’s no existing curated body to preserve, so the whole class is rendered fresh from the wrapper’s true canonical subname set (the same full set validate_stub.py itself would compute) - it’s all new content either way, same as generate_stub.py would produce.

sync_stub.splice_body(nodes: list[ast.stmt], source_lines: list[str], stub_tree: dict, wrapper_tree: dict, wrapper_class_bodies: dict[str, list], wrapper_entries_by_id: dict[str, Entry], indent: str, report: Report, scope: str, start_line: int, scope_end_line: int, class_key: str | None = None) → list[str]
sync_stub.LICENSE_HEADER_START = '# IfcOpenShell - IFC toolkit and geometry engine'