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 ifcopenshell
import ifcopenshell.api
import blenderbim.core.tool
import blenderbim.tool as tool
from test.bim.bootstrap import NewFile
+8 -3
View File
@@ -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":
+10 -4
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
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:
+138 -62
View File
@@ -16,6 +16,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcTester. If not, see <http://www.gnu.org/licenses/>.
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