From 65cd5701c39ac0a1113d995455701b8f62435c5b Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Fri, 26 Jun 2026 16:51:16 -0500 Subject: [PATCH] Honor ApplicableOccurrence in assign_type class validation The class-pairing validation added in 10ee5aef4f rejected every typed annotation with "IfcTypeProduct cannot type IfcAnnotation ... (allowed occurrence classes: )". The check derived allowed occurrence classes solely from the buildingSMART implementer-agreement map, which has no entry for the abstract IfcTypeProduct that Bonsai uses for annotation types (IFC4 has no IfcAnnotationType). The intended occurrence class is declared in the type's ApplicableOccurrence attribute (e.g. "IfcAnnotation/TEXT"), the schema-defined mechanism for exactly this purpose. Augment the allow-list with the ApplicableOccurrence class, but only when its leading token resolves to a real entity in the schema so free-form text is not trusted blindly. Genuine mismatches (e.g. IfcWallType -> IfcWindow) are still rejected. Co-Authored-By: Claude Opus 4.8 --- .../ifcopenshell/api/type/assign_type.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py index 9dd01ee97e..3087811234 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py +++ b/src/ifcopenshell-python/ifcopenshell/api/type/assign_type.py @@ -193,15 +193,27 @@ class Usecase: # The EXPRESS schema has no WHERE rule pairing RelatingType / # RelatedObjects classes; the canonical class pairing per schema # is a buildingSMART implementer agreement, enforced here. - allowed_occurrences = ifcopenshell.util.type.get_applicable_entities( - relating_type.is_a(), schema=self.file.schema + allowed_occurrences = set( + ifcopenshell.util.type.get_applicable_entities(relating_type.is_a(), schema=self.file.schema) ) + # The implementer agreement map has no entry for the abstract + # IfcTypeProduct, which Bonsai uses for annotation types. The schema + # itself defines IfcTypeProduct.ApplicableOccurrence for exactly this + # purpose, so honor it when the leading class token is a valid entity. + if applicable_occurrence := getattr(relating_type, "ApplicableOccurrence", None): + occurrence_class = applicable_occurrence.split("/", 1)[0] + schema = ifcopenshell.schema_by_name(self.file.schema) + try: + schema.declaration_by_name(occurrence_class) + allowed_occurrences.add(occurrence_class) + except RuntimeError: + pass mismatched_classes = sorted({o.is_a() for o in related_objects if o.is_a() not in allowed_occurrences}) if mismatched_classes: raise TypeError( f"{relating_type.is_a()} cannot type {', '.join(mismatched_classes)} " f"in schema {self.file.schema} (allowed occurrence classes: " - f"{allowed_occurrences or ''})" + f"{', '.join(sorted(allowed_occurrences)) or ''})" ) ifc2x3 = self.file.schema == "IFC2X3"