diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 55ef2913d1..2048d1b307 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with IfcTester. If not, see . +from __future__ import annotations import re import builtins import ifcopenshell.util.unit @@ -23,7 +24,11 @@ import ifcopenshell.util.element import ifcopenshell.util.classification from functools import lru_cache from xmlschema.validators import identities -from typing import List, Union +from typing import Union, Optional, Any, Literal, TYPE_CHECKING +from logging import Logger + +if TYPE_CHECKING: + from .ids import Specification def cast_to_value(from_value, to_value): @@ -53,14 +58,19 @@ def get_psets(element): return ifcopenshell.util.element.get_psets(element) +Cardinality = Literal["required", "optional", "prohibited"] + + class Facet: + cardinality: Cardinality + def __init__(self, *parameters): self.status = None self.failures = [] for i, name in enumerate(self.parameters): setattr(self, name.replace("@", ""), parameters[i]) - def asdict(self, clause_type): + def asdict(self, clause_type: str) -> dict[str, Any]: results = {} for name in self.parameters: value = getattr(self, name.replace("@", "")) @@ -86,11 +96,16 @@ class Facet: return self def filter( - self, ifc_file: ifcopenshell.file, elements: List[ifcopenshell.entity_instance] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: list[ifcopenshell.entity_instance] + ) -> list[ifcopenshell.entity_instance]: return [e for e in elements if self(e)] - def to_string(self, clause_type, specification=None, requirement=None): + def to_string( + self, + clause_type: str, + specification: Optional[Specification] = None, + requirement: Optional[Facet] = None, + ): if clause_type == "applicability": templates = self.applicability_templates elif clause_type == "requirement": @@ -114,7 +129,7 @@ class Facet: if total_replacements == total_variables: return template - def to_ids_value(self, parameter): + def to_ids_value(self, parameter: Union[str, Restriction, list]) -> dict[str, Any]: if isinstance(parameter, str): parameter_dict = {"simpleValue": parameter} elif isinstance(parameter, Restriction): @@ -129,9 +144,12 @@ class Facet: raise Exception(str(parameter) + " was not able to be converted into 'Parameter_dict'") return parameter_dict - def get_usage(self): + def get_usage(self) -> Cardinality: return self.cardinality + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> Result: + raise NotImplementedError + class Entity(Facet): def __init__(self, name="IFCWALL", predefinedType=None, instructions=None): @@ -150,7 +168,9 @@ class Entity(Facet): ] super().__init__(name, predefinedType, instructions) - def filter(self, ifc_file, elements): + def filter( + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] = None + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) @@ -173,7 +193,7 @@ class Entity(Facet): return [r for r in results if self(r)] return results - def __call__(self, inst, logger=None): + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> EntityResult: is_pass = inst.is_a().upper() == self.name reason = None @@ -191,7 +211,7 @@ class Entity(Facet): class Attribute(Facet): - def __init__(self, name="Name", value=None, cardinality="required", instructions=None): + def __init__(self, name="Name", value=None, cardinality: Cardinality = "required", instructions=None): self.parameters = ["name", "value", "@cardinality", "@instructions"] self.applicability_templates = [ "Data where the {name} is {value}", @@ -208,8 +228,8 @@ class Attribute(Facet): super().__init__(name, value, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) @@ -235,7 +255,7 @@ class Attribute(Facet): return results - def __call__(self, inst, logger=None): + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> AttributeResult: if self.cardinality == "optional": return AttributeResult(True) @@ -323,7 +343,7 @@ class Attribute(Facet): class Classification(Facet): - def __init__(self, value=None, system=None, uri=None, cardinality="required", instructions=None): + def __init__(self, value=None, system=None, uri=None, cardinality: Cardinality = "required", instructions=None): self.parameters = ["value", "system", "@uri", "@cardinality", "@instructions"] self.applicability_templates = [ "Data having a {system} reference of {value}", @@ -344,13 +364,13 @@ class Classification(Facet): super().__init__(value, system, uri, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) return ifc_file.by_type("IfcObjectDefinition") - def __call__(self, inst, logger=None): + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> ClassificationResult: if self.cardinality == "optional": return ClassificationResult(True) # Is this really the correct behaviour? @@ -389,7 +409,7 @@ class PartOf(Facet): name="IFCWALL", predefinedType=None, relation=None, - cardinality="required", + cardinality: Cardinality = "required", instructions=None, ): self.parameters = ["name", "predefinedType", "@relation", "@cardinality", "@instructions"] @@ -408,13 +428,13 @@ class PartOf(Facet): super().__init__(name, predefinedType, relation, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) return list(ifc_file) # Lazy - def asdict(self, clause_type): + def asdict(self, clause_type: str) -> dict[str, Any]: results = super().asdict(clause_type) entity = {} if "name" in results: @@ -433,7 +453,7 @@ class PartOf(Facet): del xml["entity"] return super().parse(xml) - def __call__(self, inst, logger=None): + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> PartOfResult: reason = None if not self.relation: is_pass = False @@ -587,7 +607,7 @@ class Property(Facet): value=None, dataType=None, uri=None, - cardinality="required", + cardinality: Cardinality = "required", instructions=None, ): self.parameters = [ @@ -614,8 +634,8 @@ class Property(Facet): super().__init__(propertySet, baseName, value, dataType, uri, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) if ifc_file.schema == "IFC2X3": @@ -626,7 +646,7 @@ class Property(Facet): + ifc_file.by_type("IfcProfileDef") ) - def __call__(self, inst, logger=None): + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> PropertyResult: if self.cardinality == "optional": return PropertyResult(True) @@ -864,7 +884,7 @@ class Property(Facet): class Material(Facet): - def __init__(self, value=None, uri=None, cardinality="required", instructions=None): + def __init__(self, value=None, uri=None, cardinality: Cardinality = "required", instructions=None): self.parameters = ["value", "@uri", "@cardinality", "@instructions"] self.applicability_templates = [ "All data with a {value} material", @@ -881,13 +901,13 @@ class Material(Facet): super().__init__(value, uri, cardinality, instructions) def filter( - self, ifc_file: ifcopenshell.file, elements: Union[ifcopenshell.entity_instance, None] - ) -> List[ifcopenshell.entity_instance]: + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] + ) -> list[ifcopenshell.entity_instance]: if isinstance(elements, list): return super().filter(ifc_file, elements) return ifc_file.by_type("IfcObjectDefinition") - def __call__(self, inst, logger=None): + def __call__(self, inst: ifcopenshell.entity_instance, logger: Optional[Logger] = None) -> MaterialResult: if self.cardinality == "optional": return MaterialResult(True) @@ -965,7 +985,7 @@ class Restriction: self.options[key] = [v["@value"] for v in value] return self - def asdict(self): + def asdict(self) -> dict[str, Any]: result = {"@base": "xs:" + self.base} for constraint, value in self.options.items(): value = [value] if not isinstance(value, list) else value diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py index e92d6fdcbc..98062eb872 100644 --- a/src/ifctester/ifctester/ids.py +++ b/src/ifctester/ifctester/ids.py @@ -18,6 +18,7 @@ import os import datetime +import ifcopenshell from xmlschema import XMLSchema from xmlschema import etree_tostring from xml.etree import ElementTree as ET @@ -32,8 +33,9 @@ from .facet import ( Restriction, get_pset, get_psets, + Cardinality, ) -from typing import List, Set +from typing import List, Optional, Union cwd = os.path.dirname(os.path.realpath(__file__)) schema = None @@ -57,7 +59,7 @@ def get_schema(): class Ids: def __init__( self, - title="Untitled", + title: Optional[str] = "Untitled", copyright=None, version=None, description=None, @@ -131,7 +133,7 @@ class Ids: ET.ElementTree(get_schema().encode(self.asdict())).write(filepath, encoding="utf-8", xml_declaration=True) return get_schema().is_valid(filepath) - def validate(self, ifc_file, filter_version=False, filepath=None): + def validate(self, ifc_file: ifcopenshell.file, filter_version=False, filepath: Optional[str] = None) -> None: if filepath: self.filepath = filepath self.filename = os.path.basename(filepath) @@ -158,14 +160,14 @@ class Specification: self.name = name or "Unnamed" self.applicability: List[Facet] = [] self.requirements: List[Facet] = [] - self.minOccurs = minOccurs - self.maxOccurs = maxOccurs + self.minOccurs: Union[int, str] = minOccurs + self.maxOccurs: Union[int, str] = maxOccurs self.ifcVersion = ifcVersion self.identifier = identifier self.description = description self.instructions = instructions - self.applicable_entities: List[Entity] = [] + self.applicable_entities: list[ifcopenshell.entity_instance] = [] self.status = None def asdict(self): @@ -229,13 +231,13 @@ class Specification: def reset_status(self): self.applicable_entities.clear() - self.failed_entities: Set[Entity] = set() + self.failed_entities: set[ifcopenshell.entity_instance] = set() for facet in self.requirements: facet.status = None facet.failures.clear() self.status = None - def validate(self, ifc_file, filter_version=False): + def validate(self, ifc_file: ifcopenshell.file, filter_version=False) -> None: if filter_version and ifc_file.schema not in self.ifcVersion: return @@ -286,7 +288,7 @@ class Specification: if self.applicable_entities and not self.requirements: self.status = False - def get_usage(self): + def get_usage(self) -> Cardinality: if self.minOccurs != 0: return "required" elif self.minOccurs == 0 and self.maxOccurs != 0: diff --git a/src/ifctester/test/test_ids.py b/src/ifctester/test/test_ids.py index 78e26b4512..52fcaafb7a 100644 --- a/src/ifctester/test/test_ids.py +++ b/src/ifctester/test/test_ids.py @@ -21,9 +21,17 @@ import pytest import xmlschema import ifcopenshell from ifctester import ids +from typing import Optional -def run(name, ids, ifc, expected, applicable_entities=None, failed_entities=None): +def run( + name: str, + ids: ids.Ids, + ifc: ifcopenshell.file, + expected: bool, + applicable_entities: Optional[list[ifcopenshell.entity_instance]] = None, + failed_entities: Optional[list[ifcopenshell.entity_instance]] = None, +): ids.validate(ifc) all_applicable = set() all_failures = set()