From 836293504cab9eed5e8eaa0996a5922f777ed9d7 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Apr 2024 14:21:46 +0500 Subject: [PATCH] ifctester typing --- src/blenderbim/test/tool/test_document.py | 1 + src/ifctester/ifctester/facet.py | 11 +- src/ifctester/ifctester/ids.py | 14 +- src/ifctester/ifctester/reporter.py | 200 +++++++++++++++------- 4 files changed, 157 insertions(+), 69 deletions(-) diff --git a/src/blenderbim/test/tool/test_document.py b/src/blenderbim/test/tool/test_document.py index b458eedf1e..8b22ce19d0 100644 --- a/src/blenderbim/test/tool/test_document.py +++ b/src/blenderbim/test/tool/test_document.py @@ -18,6 +18,7 @@ import bpy import ifcopenshell +import ifcopenshell.api import blenderbim.core.tool import blenderbim.tool as tool from test.bim.bootstrap import NewFile diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 2048d1b307..867e06b36c 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -24,7 +24,7 @@ import ifcopenshell.util.element import ifcopenshell.util.classification from functools import lru_cache from xmlschema.validators import identities -from typing import Union, Optional, Any, Literal, TYPE_CHECKING +from typing import Union, Optional, Any, Literal, TYPE_CHECKING, TypedDict from logging import Logger if TYPE_CHECKING: @@ -61,12 +61,17 @@ def get_psets(element): Cardinality = Literal["required", "optional", "prohibited"] +class FacetFailure(TypedDict): + element: ifcopenshell.entity_instance + reason: str + + class Facet: cardinality: Cardinality def __init__(self, *parameters): self.status = None - self.failures = [] + self.failures: list[FacetFailure] = [] for i, name in enumerate(self.parameters): setattr(self, name.replace("@", ""), parameters[i]) @@ -105,7 +110,7 @@ class Facet: clause_type: str, specification: Optional[Specification] = None, requirement: Optional[Facet] = None, - ): + ) -> str: if clause_type == "applicability": templates = self.applicability_templates elif clause_type == "requirement": diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py index da71c87c96..4703b0445a 100644 --- a/src/ifctester/ifctester/ids.py +++ b/src/ifctester/ifctester/ids.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 os import datetime import ifcopenshell @@ -34,14 +35,19 @@ from .facet import ( get_pset, get_psets, Cardinality, + FacetFailure, ) -from typing import List, Optional, Union +from typing import List, Optional, Union, overload, Literal cwd = os.path.dirname(os.path.realpath(__file__)) schema = None -def open(filepath, validate=False): +@overload +def open(filepath: str, validate: Literal[False] = False) -> Ids: ... +@overload +def open(filepath: str, validate: Literal[True]) -> None: ... +def open(filepath: str, validate=False) -> Union[Ids, None]: if validate: get_schema().validate(filepath) return Ids().parse( @@ -265,11 +271,11 @@ class Specification: if self.maxOccurs != 0: # This is a required or optional specification if not is_pass: self.failed_entities.add(element) - facet.failures.append({"element": element, "reason": str(result)}) + facet.failures.append(FacetFailure(element=element, reason=str(result))) else: # This is a prohibited specification if is_pass: self.failed_entities.add(element) - facet.failures.append({"element": element, "reason": str(result)}) + facet.failures.append(FacetFailure(element=element, reason=str(result))) self.status = True for facet in self.requirements: diff --git a/src/ifctester/ifctester/reporter.py b/src/ifctester/ifctester/reporter.py index ea77e6a35f..c40979abb5 100644 --- a/src/ifctester/ifctester/reporter.py +++ b/src/ifctester/ifctester/reporter.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 os import sys import math @@ -24,12 +25,15 @@ import datetime import ifcopenshell import ifcopenshell.util.unit import ifcopenshell.util.element +from .ids import Specification, Ids +from .facet import Facet, FacetFailure +from typing import TypedDict, Union, Literal, Optional cwd = os.path.dirname(os.path.realpath(__file__)) class Reporter: - def __init__(self, ids): + def __init__(self, ids: Ids): self.ids = ids def report(self, ids): @@ -42,8 +46,79 @@ class Reporter: pass +ResultsPercent = Union[int, Literal["N/A"]] + + +class Results(TypedDict): + title: str + date: str + filepath: str + filename: str + specifications: list[ResultsSpecification] + status: bool + total_specifications: int + total_specifications: int + total_specifications_pass: int + total_specifications_fail: int + percent_specifications_pass: ResultsPercent + total_requirements: int + total_requirements_pass: int + total_requirements_fail: int + percent_requirements_pass: ResultsPercent + total_checks: int + total_checks_pass: int + total_checks_fail: int + percent_checks_pass: ResultsPercent + + +class ResultsSpecification(TypedDict): + name: str + description: str + instructions: str + status: bool + total_applicable: int + total_applicable_pass: int + total_applicable_fail: int + percent_applicable_pass: ResultsPercent + total_checks: int + total_checks_pass: int + total_checks_fail: int + percent_checks_pass: ResultsPercent + required: bool + applicability: list[str] + requirements: list[ResultsRequirement] + + +class ResultsRequirement(TypedDict): + description: str + status: bool + failed_entities: list[ResultsFailedEntity] + total_applicable: int + total_pass: int + total_fail: int + percent_pass: ResultsPercent + + +# use different syntax because of the "class" key +ResultsFailedEntity = TypedDict( + "ResultsFailedEntity", + { + "reason": str, + "element": str, + "element_type": str, + "class": str, + "predefined_type": str, + "name": Union[str, None], + "description": Union[str, None], + "id": int, + "global_id": Union[str, None], + "tag": Union[str, None], + }, +) + + class Console(Reporter): - def __init__(self, ids, use_colour=True): + def __init__(self, ids: Ids, use_colour=True): super().__init__(ids) self.use_colour = use_colour self.colours = { @@ -59,14 +134,14 @@ class Console(Reporter): "reverse": "\033[;7m", } - def report(self): + def report(self) -> None: self.set_style("bold", "blue") self.print(self.ids.info.get("title", "Untitled IDS")) for specification in self.ids.specifications: self.report_specification(specification) self.set_style("reset") - def report_specification(self, specification): + def report_specification(self, specification: Specification) -> None: if specification.status is True: self.set_style("bold", "green") self.print("[PASS] ", end="") @@ -113,7 +188,7 @@ class Console(Reporter): self.print(" " * 12 + f"... {len(requirement.failures)} in total ...") self.set_style("reset") - def report_reason(self, failure): + def report_reason(self, failure: FacetFailure) -> None: is_bold = False for substring in failure["reason"].split('"'): if is_bold: @@ -126,11 +201,11 @@ class Console(Reporter): self.print(" - " + str(failure["element"])) self.set_style("reset") - def set_style(self, *colours): + def set_style(self, *colours: str): if self.use_colour: sys.stdout.write("".join([self.colours[c] for c in colours])) - def print(self, txt, end=None): + def print(self, txt: str, end: Optional[str] = None): if end is not None: print(txt, end=end) else: @@ -138,14 +213,14 @@ class Console(Reporter): class Txt(Console): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids, use_colour=False) self.text = "" - def print(self, txt, end=None): + def print(self, txt: str, end: Optional[str] = None): self.text += txt + "\n" if end is None else txt - def to_string(self): + def to_string(self) -> None: print(self.text) def to_file(self, filepath: str) -> None: @@ -154,11 +229,11 @@ class Txt(Console): class Json(Reporter): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids) - self.results = {} + self.results = Results() - def report(self): + def report(self) -> Results: self.results["title"] = self.ids.info.get("title", "Untitled IDS") self.results["date"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.results["filepath"] = self.ids.filepath @@ -203,7 +278,7 @@ class Json(Reporter): ) return self.results - def report_specification(self, specification): + def report_specification(self, specification: Specification) -> ResultsSpecification: applicability = [a.to_string("applicability") for a in specification.applicability] total_applicable = len(specification.applicable_entities) total_checks = 0 @@ -216,57 +291,60 @@ class Json(Reporter): total_checks += total_applicable total_checks_pass += total_pass requirements.append( - { - "description": requirement.to_string("requirement", specification, requirement), - "status": requirement.status, - "failed_entities": self.report_failed_entities(requirement), - "total_applicable": total_applicable, - "total_pass": total_pass, - "total_fail": total_fail, - "percent_pass": percent_pass, - } + ResultsRequirement( + description=requirement.to_string("requirement", specification, requirement), + status=requirement.status, + failed_entities=self.report_failed_entities(requirement), + total_applicable=total_applicable, + total_pass=total_pass, + total_fail=total_fail, + percent_pass=percent_pass, + ) ) total_applicable_pass = total_applicable - len(specification.failed_entities) percent_applicable_pass = ( math.floor((total_applicable_pass / total_applicable) * 100) if total_applicable else "N/A" ) percent_checks_pass = math.floor((total_checks_pass / total_checks) * 100) if total_checks else "N/A" - return { - "name": specification.name, - "description": specification.description, - "instructions": specification.instructions, - "status": specification.status, - "total_applicable": total_applicable, - "total_applicable_pass": total_applicable_pass, - "total_applicable_fail": total_applicable - total_applicable_pass, - "percent_applicable_pass": percent_applicable_pass, - "total_checks": total_checks, - "total_checks_pass": total_checks_pass, - "total_checks_fail": total_checks - total_checks_pass, - "percent_checks_pass": percent_checks_pass, - "required": specification.minOccurs != 0, - "applicability": applicability, - "requirements": requirements, - } - def report_failed_entities(self, requirement): + return ResultsSpecification( + name=specification.name, + description=specification.description, + instructions=specification.instructions, + status=specification.status, + total_applicable=total_applicable, + total_applicable_pass=total_applicable_pass, + total_applicable_fail=total_applicable - total_applicable_pass, + percent_applicable_pass=percent_applicable_pass, + total_checks=total_checks, + total_checks_pass=total_checks_pass, + total_checks_fail=total_checks - total_checks_pass, + percent_checks_pass=percent_checks_pass, + required=specification.minOccurs != 0, + applicability=applicability, + requirements=requirements, + ) + + def report_failed_entities(self, requirement: Facet) -> list[ResultsFailedEntity]: return [ - { - "reason": f["reason"], - "element": str(f["element"]), - "element_type": str(ifcopenshell.util.element.get_type(f["element"])), - "class": f["element"].is_a(), - "predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]), - "name": getattr(f["element"], "Name", None), - "description": getattr(f["element"], "Description", None), - "id": f["element"].id(), - "global_id": getattr(f["element"], "GlobalId", None), - "tag": getattr(f["element"], "Tag", None), - } + ResultsFailedEntity( + { + "reason": f["reason"], + "element": str(f["element"]), + "element_type": str(ifcopenshell.util.element.get_type(f["element"])), + "class": f["element"].is_a(), + "predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]), + "name": getattr(f["element"], "Name", None), + "description": getattr(f["element"], "Description", None), + "id": f["element"].id(), + "global_id": getattr(f["element"], "GlobalId", None), + "tag": getattr(f["element"], "Tag", None), + } + ) for f in requirement.failures ] - def to_string(self): + def to_string(self) -> str: import json return json.dumps(self.results) @@ -279,11 +357,10 @@ class Json(Reporter): class Html(Json): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids) - self.results = {} - def report(self): + def report(self) -> None: super().report() entity_limit = 100 for spec in self.results["specifications"]: @@ -294,7 +371,7 @@ class Html(Json): requirement["total_entities"] = total requirement["total_omitted"] = total - entity_limit - def to_string(self): + def to_string(self) -> str: import pystache with open(os.path.join(cwd, "templates", "report.html"), "r") as file: @@ -309,7 +386,7 @@ class Html(Json): class Ods(Json): - def __init__(self, ids): + def __init__(self, ids: Ids): super().__init__(ids) self.colours = { "h": "cccccc", # Header @@ -317,7 +394,6 @@ class Ods(Json): "f": "fb5a3e", # Fail "t": "ffffff", # Regular text } - self.results = {} def to_file(self, filepath: str) -> None: from odf.opendocument import OpenDocumentSpreadsheet @@ -423,8 +499,8 @@ class Ods(Json): class Bcf(Json): - def report_failed_entities(self, requirement): - return [{"reason": f["reason"], "element": f["element"]} for f in requirement.failures] + def report_failed_entities(self, requirement: Facet) -> list[FacetFailure]: + return [FacetFailure(f) for f in requirement.failures] def to_file(self, filepath: str) -> None: import numpy as np