From 209c44db8360fece58a2e0252d86ea873b42889e Mon Sep 17 00:00:00 2001 From: Petru Conduraru Date: Thu, 9 Jul 2026 13:24:46 +0300 Subject: [PATCH] Selector: negate list comparisons as an aggregate #8129 compare() recursed into list values passing the negated comparison through, so != meant "at least one item differs" and both = and != matched the same elements on any multi-valued property (e.g. an enumerated property with two values selected). Strip the negation for the per-item comparison and negate the aggregate instead, so != means "no item equals" and stays the complement of =. The same applies to !*=. Co-Authored-By: Claude Fable 5 --- src/ifcopenshell-python/ifcopenshell/util/selector.py | 8 +++++++- src/ifcopenshell-python/test/util/test_selector.py | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/ifcopenshell-python/ifcopenshell/util/selector.py b/src/ifcopenshell-python/ifcopenshell/util/selector.py index f83d3cd3d5..da86b69a91 100644 --- a/src/ifcopenshell-python/ifcopenshell/util/selector.py +++ b/src/ifcopenshell-python/ifcopenshell/util/selector.py @@ -1242,7 +1242,13 @@ class FacetTransformer(lark.Transformer): def compare(self, element_value, comparison, value) -> bool: if isinstance(element_value, (list, tuple)): - return any(self.compare(ev, comparison, value) for ev in element_value) + # Match if any item does, negating the aggregate rather than each + # item, so that e.g. != means "no item equals" and stays the + # complement of = (#8129). + result = any(self.compare(ev, comparison.lstrip("!"), value) for ev in element_value) + if comparison.startswith("!"): + return not result + return result elif isinstance(value, str): try: if isinstance(element_value, int): diff --git a/src/ifcopenshell-python/test/util/test_selector.py b/src/ifcopenshell-python/test/util/test_selector.py index 5ee2267269..af80141e81 100644 --- a/src/ifcopenshell-python/test/util/test_selector.py +++ b/src/ifcopenshell-python/test/util/test_selector.py @@ -279,6 +279,12 @@ class TestFilterElements(test.bootstrap.IFC4): pset = ifcopenshell.api.pset.add_pset(self.file, product=element, name="Pset_WallCommon") ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Status": ["New"]}) assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status=New") == {element} + # On multi-valued properties, != means "no value equals" and stays the + # complement of = (#8129). + ifcopenshell.api.pset.edit_pset(self.file, pset=pset, properties={"Status": ["New", "Demolish"]}) + assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status=New") == {element} + assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status!=New") == {element2} + assert subject.filter_elements(self.file, "IfcWall, Pset_WallCommon.Status!=Temporary") == {element, element2} def test_selecting_by_property_with_comparisons(self): element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")