From dfefb5377c6412cc18c4a2f3b2c25c4524626265 Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Sat, 11 Jul 2026 20:34:38 +0300 Subject: [PATCH] ifcopenshell: attach owning file to geom.tree select results #3189 tree.select and tree.select_box wrapped their C++ results as entity_instance(e) with no file, so wrapped_data.file was None. That forces entity_instance.__eq__ down the slow recursive get_info_2 attribute comparison instead of the fast step-id path, and drops the gc-keepalive the file reference provides. The underlying instances carry a valid file_pointer(), so resolve the owning file via file.from_pointer() and attach it to each result. Verified with the compiled wrapper on basic.ifc: before, wrapped_data.file is None and equality takes the slow path (2 get_info_2 calls); after, the file is attached, equality/hash use the fast path (0 slow calls), for both select and select_box. Generated with the assistance of an AI coding tool. Co-Authored-By: Claude Opus 4.8 --- src/ifcopenshell-python/ifcopenshell/geom/main.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/geom/main.py b/src/ifcopenshell-python/ifcopenshell/geom/main.py index fc66264b95..26b34491b7 100644 --- a/src/ifcopenshell-python/ifcopenshell/geom/main.py +++ b/src/ifcopenshell-python/ifcopenshell/geom/main.py @@ -370,6 +370,17 @@ CLASH_TYPE_ITEMS = ("protrusion", "pierce", "collision", "clearance") class tree(ifcopenshell_wrapper.tree): + @staticmethod + def _wrap(e: ifcopenshell_wrapper.entity_instance) -> entity_instance: + inst = entity_instance(e) + # #3189 attach the owning file so that equality and hashing of + # selected instances use the fast step-id based path instead of + # falling back to a slow recursive attribute comparison. + ptr = e.file_pointer() + if ptr: + inst.wrapped_data.file = file.from_pointer(ptr) + return inst + def __init__(self, file: Optional[file] = None, settings: Optional[settings] = None): args = [self] if file is not None: @@ -412,7 +423,7 @@ class tree(ifcopenshell_wrapper.tree): args.append(kwargs.get("completely_within", False)) if "extend" in kwargs: args.append(kwargs["extend"]) - return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select(*args)] + return [self._wrap(e) for e in ifcopenshell_wrapper.tree.select(*args)] def select_box(self, value, **kwargs) -> list[entity_instance]: def unwrap(value): @@ -427,7 +438,7 @@ class tree(ifcopenshell_wrapper.tree): args.append(kwargs.get("completely_within", False)) if "extend" in kwargs: args.append(kwargs.get("extend", -1.0e-5)) - return [entity_instance(e) for e in ifcopenshell_wrapper.tree.select_box(*args)] + return [self._wrap(e) for e in ifcopenshell_wrapper.tree.select_box(*args)] def clash_intersection_many( self,