mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-10-03 21:38:12 +00:00
ifctester typing
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user