mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-16 18:44:47 +00:00
ifctester typing
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user