ifctester typing

This commit is contained in:
Andrej730
2024-04-16 14:21:46 +05:00
parent b458aea6b9
commit 836293504c
4 changed files with 157 additions and 69 deletions
@@ -18,6 +18,7 @@
import bpy import bpy
import ifcopenshell import ifcopenshell
import ifcopenshell.api
import blenderbim.core.tool import blenderbim.core.tool
import blenderbim.tool as tool import blenderbim.tool as tool
from test.bim.bootstrap import NewFile from test.bim.bootstrap import NewFile
+8 -3
View File
@@ -24,7 +24,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.classification import ifcopenshell.util.classification
from functools import lru_cache from functools import lru_cache
from xmlschema.validators import identities 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 from logging import Logger
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -61,12 +61,17 @@ def get_psets(element):
Cardinality = Literal["required", "optional", "prohibited"] Cardinality = Literal["required", "optional", "prohibited"]
class FacetFailure(TypedDict):
element: ifcopenshell.entity_instance
reason: str
class Facet: class Facet:
cardinality: Cardinality cardinality: Cardinality
def __init__(self, *parameters): def __init__(self, *parameters):
self.status = None self.status = None
self.failures = [] self.failures: list[FacetFailure] = []
for i, name in enumerate(self.parameters): for i, name in enumerate(self.parameters):
setattr(self, name.replace("@", ""), parameters[i]) setattr(self, name.replace("@", ""), parameters[i])
@@ -105,7 +110,7 @@ class Facet:
clause_type: str, clause_type: str,
specification: Optional[Specification] = None, specification: Optional[Specification] = None,
requirement: Optional[Facet] = None, requirement: Optional[Facet] = None,
): ) -> str:
if clause_type == "applicability": if clause_type == "applicability":
templates = self.applicability_templates templates = self.applicability_templates
elif clause_type == "requirement": elif clause_type == "requirement":
+10 -4
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>. # along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os import os
import datetime import datetime
import ifcopenshell import ifcopenshell
@@ -34,14 +35,19 @@ from .facet import (
get_pset, get_pset,
get_psets, get_psets,
Cardinality, Cardinality,
FacetFailure,
) )
from typing import List, Optional, Union from typing import List, Optional, Union, overload, Literal
cwd = os.path.dirname(os.path.realpath(__file__)) cwd = os.path.dirname(os.path.realpath(__file__))
schema = None 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: if validate:
get_schema().validate(filepath) get_schema().validate(filepath)
return Ids().parse( return Ids().parse(
@@ -265,11 +271,11 @@ class Specification:
if self.maxOccurs != 0: # This is a required or optional specification if self.maxOccurs != 0: # This is a required or optional specification
if not is_pass: if not is_pass:
self.failed_entities.add(element) 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 else: # This is a prohibited specification
if is_pass: if is_pass:
self.failed_entities.add(element) 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 self.status = True
for facet in self.requirements: for facet in self.requirements:
+138 -62
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>. # along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations
import os import os
import sys import sys
import math import math
@@ -24,12 +25,15 @@ import datetime
import ifcopenshell import ifcopenshell
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcopenshell.util.element 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__)) cwd = os.path.dirname(os.path.realpath(__file__))
class Reporter: class Reporter:
def __init__(self, ids): def __init__(self, ids: Ids):
self.ids = ids self.ids = ids
def report(self, ids): def report(self, ids):
@@ -42,8 +46,79 @@ class Reporter:
pass 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): class Console(Reporter):
def __init__(self, ids, use_colour=True): def __init__(self, ids: Ids, use_colour=True):
super().__init__(ids) super().__init__(ids)
self.use_colour = use_colour self.use_colour = use_colour
self.colours = { self.colours = {
@@ -59,14 +134,14 @@ class Console(Reporter):
"reverse": "\033[;7m", "reverse": "\033[;7m",
} }
def report(self): def report(self) -> None:
self.set_style("bold", "blue") self.set_style("bold", "blue")
self.print(self.ids.info.get("title", "Untitled IDS")) self.print(self.ids.info.get("title", "Untitled IDS"))
for specification in self.ids.specifications: for specification in self.ids.specifications:
self.report_specification(specification) self.report_specification(specification)
self.set_style("reset") self.set_style("reset")
def report_specification(self, specification): def report_specification(self, specification: Specification) -> None:
if specification.status is True: if specification.status is True:
self.set_style("bold", "green") self.set_style("bold", "green")
self.print("[PASS] ", end="") self.print("[PASS] ", end="")
@@ -113,7 +188,7 @@ class Console(Reporter):
self.print(" " * 12 + f"... {len(requirement.failures)} in total ...") self.print(" " * 12 + f"... {len(requirement.failures)} in total ...")
self.set_style("reset") self.set_style("reset")
def report_reason(self, failure): def report_reason(self, failure: FacetFailure) -> None:
is_bold = False is_bold = False
for substring in failure["reason"].split('"'): for substring in failure["reason"].split('"'):
if is_bold: if is_bold:
@@ -126,11 +201,11 @@ class Console(Reporter):
self.print(" - " + str(failure["element"])) self.print(" - " + str(failure["element"]))
self.set_style("reset") self.set_style("reset")
def set_style(self, *colours): def set_style(self, *colours: str):
if self.use_colour: if self.use_colour:
sys.stdout.write("".join([self.colours[c] for c in colours])) 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: if end is not None:
print(txt, end=end) print(txt, end=end)
else: else:
@@ -138,14 +213,14 @@ class Console(Reporter):
class Txt(Console): class Txt(Console):
def __init__(self, ids): def __init__(self, ids: Ids):
super().__init__(ids, use_colour=False) super().__init__(ids, use_colour=False)
self.text = "" 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 self.text += txt + "\n" if end is None else txt
def to_string(self): def to_string(self) -> None:
print(self.text) print(self.text)
def to_file(self, filepath: str) -> None: def to_file(self, filepath: str) -> None:
@@ -154,11 +229,11 @@ class Txt(Console):
class Json(Reporter): class Json(Reporter):
def __init__(self, ids): def __init__(self, ids: Ids):
super().__init__(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["title"] = self.ids.info.get("title", "Untitled IDS")
self.results["date"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.results["date"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.results["filepath"] = self.ids.filepath self.results["filepath"] = self.ids.filepath
@@ -203,7 +278,7 @@ class Json(Reporter):
) )
return self.results 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] applicability = [a.to_string("applicability") for a in specification.applicability]
total_applicable = len(specification.applicable_entities) total_applicable = len(specification.applicable_entities)
total_checks = 0 total_checks = 0
@@ -216,57 +291,60 @@ class Json(Reporter):
total_checks += total_applicable total_checks += total_applicable
total_checks_pass += total_pass total_checks_pass += total_pass
requirements.append( requirements.append(
{ ResultsRequirement(
"description": requirement.to_string("requirement", specification, requirement), description=requirement.to_string("requirement", specification, requirement),
"status": requirement.status, status=requirement.status,
"failed_entities": self.report_failed_entities(requirement), failed_entities=self.report_failed_entities(requirement),
"total_applicable": total_applicable, total_applicable=total_applicable,
"total_pass": total_pass, total_pass=total_pass,
"total_fail": total_fail, total_fail=total_fail,
"percent_pass": percent_pass, percent_pass=percent_pass,
} )
) )
total_applicable_pass = total_applicable - len(specification.failed_entities) total_applicable_pass = total_applicable - len(specification.failed_entities)
percent_applicable_pass = ( percent_applicable_pass = (
math.floor((total_applicable_pass / total_applicable) * 100) if total_applicable else "N/A" 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" 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 [ return [
{ ResultsFailedEntity(
"reason": f["reason"], {
"element": str(f["element"]), "reason": f["reason"],
"element_type": str(ifcopenshell.util.element.get_type(f["element"])), "element": str(f["element"]),
"class": f["element"].is_a(), "element_type": str(ifcopenshell.util.element.get_type(f["element"])),
"predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]), "class": f["element"].is_a(),
"name": getattr(f["element"], "Name", None), "predefined_type": ifcopenshell.util.element.get_predefined_type(f["element"]),
"description": getattr(f["element"], "Description", None), "name": getattr(f["element"], "Name", None),
"id": f["element"].id(), "description": getattr(f["element"], "Description", None),
"global_id": getattr(f["element"], "GlobalId", None), "id": f["element"].id(),
"tag": getattr(f["element"], "Tag", None), "global_id": getattr(f["element"], "GlobalId", None),
} "tag": getattr(f["element"], "Tag", None),
}
)
for f in requirement.failures for f in requirement.failures
] ]
def to_string(self): def to_string(self) -> str:
import json import json
return json.dumps(self.results) return json.dumps(self.results)
@@ -279,11 +357,10 @@ class Json(Reporter):
class Html(Json): class Html(Json):
def __init__(self, ids): def __init__(self, ids: Ids):
super().__init__(ids) super().__init__(ids)
self.results = {}
def report(self): def report(self) -> None:
super().report() super().report()
entity_limit = 100 entity_limit = 100
for spec in self.results["specifications"]: for spec in self.results["specifications"]:
@@ -294,7 +371,7 @@ class Html(Json):
requirement["total_entities"] = total requirement["total_entities"] = total
requirement["total_omitted"] = total - entity_limit requirement["total_omitted"] = total - entity_limit
def to_string(self): def to_string(self) -> str:
import pystache import pystache
with open(os.path.join(cwd, "templates", "report.html"), "r") as file: with open(os.path.join(cwd, "templates", "report.html"), "r") as file:
@@ -309,7 +386,7 @@ class Html(Json):
class Ods(Json): class Ods(Json):
def __init__(self, ids): def __init__(self, ids: Ids):
super().__init__(ids) super().__init__(ids)
self.colours = { self.colours = {
"h": "cccccc", # Header "h": "cccccc", # Header
@@ -317,7 +394,6 @@ class Ods(Json):
"f": "fb5a3e", # Fail "f": "fb5a3e", # Fail
"t": "ffffff", # Regular text "t": "ffffff", # Regular text
} }
self.results = {}
def to_file(self, filepath: str) -> None: def to_file(self, filepath: str) -> None:
from odf.opendocument import OpenDocumentSpreadsheet from odf.opendocument import OpenDocumentSpreadsheet
@@ -423,8 +499,8 @@ class Ods(Json):
class Bcf(Json): class Bcf(Json):
def report_failed_entities(self, requirement): def report_failed_entities(self, requirement: Facet) -> list[FacetFailure]:
return [{"reason": f["reason"], "element": f["element"]} for f in requirement.failures] return [FacetFailure(f) for f in requirement.failures]
def to_file(self, filepath: str) -> None: def to_file(self, filepath: str) -> None:
import numpy as np import numpy as np