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