Compare entity instances in one file by identity again

entity_instance.file is a property backed by a fresh SWIG wrapper on every
access, and ifcopenshell::file had no __eq__, so `self.file != other.file`
in entity_instance.__eq__ compared two throwaway wrappers and was always
true - even for an instance against itself. Every entity comparison
therefore took the deep get_info() branch, making distinct but structurally
identical instances compare equal and leaving the final `return False`
unreachable. Bonsai's TestAddRepresentationItemToShapeAspect showed this as
two separate IfcShapeAspects being treated as one, so the stale aspect was
never removed.

Restore the file_pointer() pair that was commented out on both
ifcopenshell::file and express::Base - IfcParseWrapper.i already described
it as the way to "trace file ownership of instances on the python side" -
and give file the __eq__/__hash__ it was missing. The express::Base one
needs $self->file() now that file_ lives on instance_data. This also
repairs rocksdb_lazy_instance.__eq__, which already called file_pointer().

EXPRESS `=` is value comparison and `:=:` is instance comparison, but
rule_compiler emits `==` for both (see the @todo on process_rel_op), and
derived attributes build their operands in the shared global file, so rules
compare same-file instances and need value semantics. Restore those for the
duration of rule execution with settings.compare_instances_by_value,
alongside the existing unpack_non_aggregate_inverses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-08-05 11:10:11 +10:00
parent d1027c5877
commit 38e0e0e297
6 changed files with 69 additions and 7 deletions
@@ -90,6 +90,12 @@ def run(f: ifcopenshell.file, logger: Logger) -> None:
orig = ifcopenshell.settings.unpack_non_aggregate_inverses
ifcopenshell.settings.unpack_non_aggregate_inverses = True
# Rules are transpiled with EXPRESS `=` emitted as Python `==`, so `==` has
# to mean value comparison for the duration. See the @todo on
# rule_compiler.process_rel_op.
orig_compare = ifcopenshell.settings.compare_instances_by_value
ifcopenshell.settings.compare_instances_by_value = True
fn = os.path.join(os.path.dirname(__file__), "rules", f"{f.schema_identifier}.py")
try:
source = open(fn, "r").read()
@@ -272,6 +278,7 @@ def run(f: ifcopenshell.file, logger: Logger) -> None:
)
ifcopenshell.settings.unpack_non_aggregate_inverses = orig
ifcopenshell.settings.compare_instances_by_value = orig_compare
if __name__ == "__main__":