mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-21 12:13:43 +00:00
ifcwrap: build the Python wrapper with SWIG -fastproxy -fastdispatch
-fastproxy binds each wrapped method directly to its compiled function instead of going through a generated Python def, and -fastdispatch shortens overload dispatch. Reading is_a(), id() and an attribute of every one of TXG's 918k instances takes 2.15 s instead of 2.36 s (-8%); the tight loop over 133 walls reading GlobalId, Name and is_a() twenty times over 9 ms instead of 11 ms. validate_stub compared the stub against the wrapper's def signatures by parsing both files; a fast-proxy wrapper has assignments instead of defs. It now rebuilds the signature from the compiled function's autodoc docstring (the C++ prototype) with the rule SWIG itself uses: one prototype whose defaults are Python literals becomes named parameters, several prototypes or a non-literal default such as an enum become *args. Checked against every def of a wrapper built without -fastproxy: 773 of 773 signatures rebuild identically. The autodoc feature moves above the SWIG library includes so the iterator and container classes carry prototypes too. This commit was written by an AI coding tool and has not been verified by a human. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013wcN7XquTfUi4vsKQ4KchL
This commit is contained in:
@@ -27,6 +27,9 @@ Things we do check:
|
||||
|
||||
import ast
|
||||
import difflib
|
||||
import importlib
|
||||
import re
|
||||
import types
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
@@ -52,6 +55,79 @@ def format_diff(lines: list[str]) -> None:
|
||||
SubnameType = Union[str, tuple[str, str]]
|
||||
|
||||
|
||||
def _wrapper_function_doc(name: str) -> Union[str, None]:
|
||||
"""Docstring of a function of the compiled module, ``_ifcopenshell_wrapper.<name>``, or ``None``.
|
||||
|
||||
A wrapper built with SWIG's ``-fastproxy`` binds methods as
|
||||
``name = _swig_new_instance_method(_ifcopenshell_wrapper.cls_name)`` instead of
|
||||
a ``def``, so the signature has to come from the compiled function, whose
|
||||
docstring SWIG's autodoc feature fills with the C++ prototype(s).
|
||||
"""
|
||||
# The compiled module has no stub of its own, hence the dynamic import.
|
||||
compiled = importlib.import_module("ifcopenshell._ifcopenshell_wrapper")
|
||||
|
||||
function = getattr(compiled, name, None)
|
||||
if not isinstance(function, types.BuiltinFunctionType):
|
||||
return None
|
||||
return function.__doc__
|
||||
|
||||
|
||||
def _split_prototype_args(text: str) -> list[str]:
|
||||
items: list[str] = []
|
||||
depth = 0
|
||||
current = ""
|
||||
for char in text:
|
||||
if char in "(<[":
|
||||
depth += 1
|
||||
elif char in ")>]":
|
||||
depth -= 1
|
||||
if char == "," and depth == 0:
|
||||
items.append(current.strip())
|
||||
current = ""
|
||||
else:
|
||||
current += char
|
||||
if current.strip():
|
||||
items.append(current.strip())
|
||||
return items
|
||||
|
||||
|
||||
def signature_from_docstring(name: str, doc: Union[str, None], is_method: bool) -> Union[str, None]:
|
||||
"""Rebuild the proxy ``def`` signature SWIG would have generated from an autodoc docstring.
|
||||
|
||||
One prototype whose defaults are all Python literals becomes named
|
||||
parameters; several prototypes (overloads), or a default SWIG cannot
|
||||
express as a literal such as an enum, become ``*args``, which is what SWIG
|
||||
does. Checked against every ``def`` of a wrapper built without
|
||||
``-fastproxy``: 773 of 773 signatures rebuild identically.
|
||||
"""
|
||||
# A method's compiled function is documented as `cls_name(...)`, sometimes as `name(...)`.
|
||||
short = name.split("_", 1)[1] if is_method and "_" in name else name
|
||||
prefixes = tuple(f"{prefix}(" for prefix in {name, short})
|
||||
lines = [line.strip() for line in (doc or "").split("\n") if line.strip().startswith(prefixes)]
|
||||
if not lines:
|
||||
return None
|
||||
fallback = "self, *args" if is_method else "*args"
|
||||
if len(lines) > 1:
|
||||
return f"def {name}({fallback}): ..."
|
||||
match = re.match(r"[A-Za-z0-9_]+\((.*)\)(?: -> .*)?$", lines[0])
|
||||
assert match
|
||||
args: list[str] = []
|
||||
for item in _split_prototype_args(match.group(1)):
|
||||
default: Union[str, None] = None
|
||||
if "=" in item:
|
||||
item, default = item.rsplit("=", 1)
|
||||
arg_name = item.split()[-1].lstrip("&*")
|
||||
if default is None:
|
||||
args.append(arg_name)
|
||||
continue
|
||||
try:
|
||||
ast.literal_eval(default.strip())
|
||||
except (ValueError, SyntaxError):
|
||||
return f"def {name}({fallback}): ..."
|
||||
args.append(f"{arg_name}={ast.unparse(ast.parse(default.strip(), mode='eval'))}")
|
||||
return f"def {name}({', '.join(args)}): ..."
|
||||
|
||||
|
||||
def get_function_node_name(node: ast.FunctionDef) -> Union[SubnameType, None]:
|
||||
"""
|
||||
:return: Function node name as ``SubnameType`` or ``None``, if function wasn't processed and can be skipped.
|
||||
@@ -119,7 +195,7 @@ def get_names_tree(tree: ast.Module) -> dict[str, set[SubnameType]]:
|
||||
continue
|
||||
subname_ = target.id
|
||||
|
||||
if subname_.startswith(("_", "thisown")):
|
||||
if subname_.startswith(("_", "thisown")) and subname_ != "_is":
|
||||
continue
|
||||
|
||||
value = subnode.value
|
||||
@@ -131,6 +207,21 @@ def get_names_tree(tree: ast.Module) -> dict[str, set[SubnameType]]:
|
||||
# - `matrix = property(matrix_getter, matrix_setter)`
|
||||
# - `operation_str = staticmethod(operation_str)`
|
||||
func = value.func
|
||||
if isinstance(func, ast.Name) and func.id in (
|
||||
"_swig_new_instance_method",
|
||||
"_swig_new_static_method",
|
||||
):
|
||||
# -fastproxy: `name = _swig_new_instance_method(_ifcopenshell_wrapper.cls_name)`.
|
||||
(bound,) = value.args
|
||||
assert isinstance(bound, ast.Attribute) and isinstance(bound.value, ast.Name)
|
||||
is_static = func.id == "_swig_new_static_method"
|
||||
rebuilt = signature_from_docstring(
|
||||
bound.attr, _wrapper_function_doc(bound.attr), not is_static
|
||||
)
|
||||
assert rebuilt is not None, bound.attr
|
||||
rebuilt = rebuilt.replace(f"def {bound.attr}(", f"def {subname_}(", 1)
|
||||
subnames.add(("@staticmethod", rebuilt) if is_static else rebuilt)
|
||||
continue
|
||||
if not isinstance(func, ast.Name) or ((func_id := func.id) not in ("property", "staticmethod")):
|
||||
continue
|
||||
args = [arg.id for arg in value.args if isinstance(arg, ast.Name)]
|
||||
@@ -203,6 +294,19 @@ def get_names_tree(tree: ast.Module) -> dict[str, set[SubnameType]]:
|
||||
if not len(targets) == 1 or not isinstance(target := targets[0], ast.Name):
|
||||
continue
|
||||
node_name = target.id
|
||||
if node_name.startswith("_"):
|
||||
continue
|
||||
value = node.value
|
||||
if (
|
||||
isinstance(value, ast.Attribute)
|
||||
and isinstance(value.value, ast.Name)
|
||||
and value.value.id == "_ifcopenshell_wrapper"
|
||||
and not node_name.startswith("_")
|
||||
):
|
||||
# -fastproxy: a module function is `name = _ifcopenshell_wrapper.name`.
|
||||
rebuilt = signature_from_docstring(value.attr, _wrapper_function_doc(value.attr), False)
|
||||
if rebuilt is not None:
|
||||
node_name = rebuilt.replace(f"def {value.attr}(", f"def {node_name}(", 1)
|
||||
|
||||
elif isinstance(node, ast.AnnAssign):
|
||||
target = node.target
|
||||
|
||||
@@ -110,7 +110,7 @@ ENDIF()
|
||||
set(_python_libs_version "${Python_VERSION_MAJOR}${Python_VERSION_MINOR}")
|
||||
INCLUDE_DIRECTORIES(BEFORE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
SET(CMAKE_SWIG_FLAGS ${SWIG_DEFINES})
|
||||
SET(CMAKE_SWIG_FLAGS ${SWIG_DEFINES} -fastproxy -fastdispatch)
|
||||
|
||||
set_source_files_properties(IfcPython.i PROPERTIES CPLUSPLUS ON)
|
||||
# Rebuild on changes in other .i files.
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
// TODO add '# pragma warning(pop)' to the very end of the file
|
||||
%}
|
||||
|
||||
// Before the SWIG library includes, so their classes (iterators, std::vector and
|
||||
// std::array wrappers) get prototype docstrings too; validate_stub reads them.
|
||||
%feature("autodoc", "1");
|
||||
|
||||
%include "stdint.i"
|
||||
%include "std_array.i"
|
||||
%include "std_vector.i"
|
||||
@@ -269,7 +273,6 @@
|
||||
%}
|
||||
|
||||
// Create docstrings for generated python code.
|
||||
%feature("autodoc", "1");
|
||||
|
||||
%include "utils/type_conversion.i"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user