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"