From a1e0b8858ec4aa0d0fa09b4875bbfbf716d63931 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Thu, 9 May 2024 13:37:57 +1000 Subject: [PATCH] Track last actions and friendlier error reporting when something breaks --- src/blenderbim/blenderbim/__init__.py | 43 +++++++++++++------ src/blenderbim/blenderbim/bim/ifc.py | 29 ++++++++----- .../blenderbim/bim/module/debug/operator.py | 14 +++--- .../ifcopenshell/api/__init__.py | 9 ++-- src/ifctester/ifctester/facet.py | 7 ++- src/ifctester/ifctester/ids.py | 5 ++- 6 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/blenderbim/blenderbim/__init__.py b/src/blenderbim/blenderbim/__init__.py index 2a742ed65d..c5d6420273 100644 --- a/src/blenderbim/blenderbim/__init__.py +++ b/src/blenderbim/blenderbim/__init__.py @@ -24,6 +24,7 @@ import traceback import subprocess import webbrowser import addon_utils +from collections import deque bl_info = { "name": "BlenderBIM", @@ -38,6 +39,7 @@ bl_info = { } last_error = None +last_actions: deque = deque(maxlen=10) def get_debug_info(): @@ -60,10 +62,22 @@ def get_debug_info(): "processor": platform.processor(), "blender_version": bpy.app.version_string, "blenderbim_version": version, + "last_actions": last_actions, "last_error": last_error, } +def format_debug_info(info: dict): + last_actions = "" + for action in info["last_actions"]: + last_actions += f"\n# {action['type']}: {action['name']}" + if settings := action.get("settings"): + last_actions += f"\n>>> {settings}" + info["last_actions"] = last_actions + text = "\n".join(f"{k}: {v}" for k, v in info.items()) + return text.strip() + + if sys.modules.get("bpy", None): # Process *.pth in /libs/site/packages to setup globally importable modules # This is 3 levels deep as required by the static RPATH of ../../ from dependencies taken from Anaconda @@ -72,6 +86,18 @@ if sys.modules.get("bpy", None): try: import blenderbim.bim + import ifcopenshell.api + + def log_api(usecase_path, ifc_file, settings): + last_actions.append( + { + "type": "ifcopenshell.api", + "name": usecase_path, + "settings": ifcopenshell.api.serialise_settings(settings), + } + ) + + ifcopenshell.api.add_pre_listener("*", "action_logger", log_api) def register(): blenderbim.bim.register() @@ -83,7 +109,7 @@ if sys.modules.get("bpy", None): last_error = traceback.format_exc() print(last_error) - print(get_debug_info()) + print(format_debug_info(get_debug_info())) print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on") class BIM_PT_fatal_error(bpy.types.Panel): @@ -122,21 +148,14 @@ if sys.modules.get("bpy", None): bl_description = "Copies debugging information to your clipboard for use in bugreports" def execute(self, context): - info = get_debug_info() - # Format it in a readable way - text = "\n".join(f"{k}: {v}" for k, v in info.items()) - print(text) + info = format_debug_info(get_debug_info()) if platform.system() == "Windows": - command = "echo | set /p nul=" + text.strip() + command = "echo | set /p nul=" + info elif platform.system() == "Darwin": # for MacOS - command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy' + command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | pbcopy' else: # Linux - command = ( - 'printf "' - + text.strip().replace("\n", "\\n").replace('"', "") - + '" | xclip -selection clipboard' - ) + command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard' subprocess.run(command, shell=True, check=True) return {"FINISHED"} diff --git a/src/blenderbim/blenderbim/bim/ifc.py b/src/blenderbim/blenderbim/bim/ifc.py index 93014e6cac..60210e9700 100644 --- a/src/blenderbim/blenderbim/bim/ifc.py +++ b/src/blenderbim/blenderbim/bim/ifc.py @@ -22,9 +22,11 @@ import uuid import hashlib import zipfile import tempfile +import traceback import ifcopenshell import ifcopenshell.geom import ifcopenshell.ifcopenshell_wrapper +import blenderbim import blenderbim.bim.handler import blenderbim.tool as tool from pathlib import Path @@ -37,19 +39,19 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object] class IfcStore: path: str = "" - file: ifcopenshell.file = None - schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None - cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None - cache_path: str = None + file: Optional[ifcopenshell.file] = None + schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None + cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None + cache_path: Optional[str] = None id_map: dict[int, IFC_CONNECTED_TYPE] = {} guid_map: dict[str, IFC_CONNECTED_TYPE] = {} edited_objs: Set[bpy.types.Object] = set() pset_template_path: str = "" - pset_template_file: ifcopenshell.file = None + pset_template_file: Optional[ifcopenshell.file] = None classification_path: str = "" - classification_file: ifcopenshell.file = None + classification_file: Optional[ifcopenshell.file] = None library_path: str = "" - library_file: ifcopenshell.file = None + library_file: Optional[ifcopenshell.file] = None current_transaction = "" last_transaction = "" history = [] @@ -329,6 +331,7 @@ class IfcStore: @staticmethod def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False): + blenderbim.last_actions.append({"type": "operator", "name": operator.bl_idname}) bpy.context.scene.BIMProperties.is_dirty = True is_top_level_operator = not bool(IfcStore.current_transaction) @@ -343,10 +346,14 @@ class IfcStore: else: operator.transaction_key = IfcStore.current_transaction - if is_invoke: - result = getattr(operator, "_invoke")(context, None) - else: - result = getattr(operator, "_execute")(context) + try: + if is_invoke: + result = getattr(operator, "_invoke")(context, None) + else: + result = getattr(operator, "_execute")(context) + except: + blenderbim.last_error = traceback.format_exc() + raise if is_top_level_operator: if tool.Ifc.get(): diff --git a/src/blenderbim/blenderbim/bim/module/debug/operator.py b/src/blenderbim/blenderbim/bim/module/debug/operator.py index 7d6b4b8c94..412d08fd43 100644 --- a/src/blenderbim/blenderbim/bim/module/debug/operator.py +++ b/src/blenderbim/blenderbim/bim/module/debug/operator.py @@ -33,7 +33,7 @@ import blenderbim.tool as tool import blenderbim.core.debug as core import blenderbim.bim.handler import blenderbim.bim.import_ifc as import_ifc -from blenderbim import get_debug_info +from blenderbim import get_debug_info, format_debug_info from blenderbim.bim.ifc import IfcStore @@ -54,16 +54,18 @@ class CopyDebugInformation(bpy.types.Operator): } ) - # Format it in a readable way - text = "\n".join(f"{k}: {v}" for k, v in info.items()) + text = format_debug_info(info) + + print("-" * 80) print(text) + print("-" * 80) if platform.system() == "Windows": - command = "echo | set /p nul=" + text.strip() + command = "echo | set /p nul=" + text elif platform.system() == "Darwin": # for MacOS - command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | pbcopy' + command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | pbcopy' else: # Linux - command = 'printf "' + text.strip().replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard' + command = 'printf "' + text.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard' subprocess.run(command, shell=True, check=True) return {"FINISHED"} diff --git a/src/ifcopenshell-python/ifcopenshell/api/__init__.py b/src/ifcopenshell-python/ifcopenshell/api/__init__.py index b7670b2293..9c352f652d 100644 --- a/src/ifcopenshell-python/ifcopenshell/api/__init__.py +++ b/src/ifcopenshell-python/ifcopenshell/api/__init__.py @@ -295,10 +295,11 @@ def serialise_settings(settings): vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()} elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance): vcs_settings[key] = [serialise_entity_instance(i) for i in value] - if "add_representation" in usecase_path: - return "" - elif "owner." in usecase_path: - return "" + else: + try: + vcs_settings[key] = str(value) + except: + vcs_settings[key] = "n/a" try: return json.dumps(vcs_settings) except: diff --git a/src/ifctester/ifctester/facet.py b/src/ifctester/ifctester/facet.py index 867e06b36c..e42cbc21f3 100644 --- a/src/ifctester/ifctester/facet.py +++ b/src/ifctester/ifctester/facet.py @@ -72,6 +72,8 @@ class Facet: def __init__(self, *parameters): self.status = None self.failures: list[FacetFailure] = [] + self.parameters = [] + self.applicability_templates = [] for i, name in enumerate(self.parameters): setattr(self, name.replace("@", ""), parameters[i]) @@ -101,8 +103,10 @@ class Facet: return self def filter( - self, ifc_file: ifcopenshell.file, elements: list[ifcopenshell.entity_instance] + self, ifc_file: ifcopenshell.file, elements: Optional[list[ifcopenshell.entity_instance]] ) -> list[ifcopenshell.entity_instance]: + if not elements: + return [] return [e for e in elements if self(e)] def to_string( @@ -133,6 +137,7 @@ class Facet: total_replacements += 1 if total_replacements == total_variables: return template + return "This facet cannot be interpreted" def to_ids_value(self, parameter: Union[str, Restriction, list]) -> dict[str, Any]: if isinstance(parameter, str): diff --git a/src/ifctester/ifctester/ids.py b/src/ifctester/ifctester/ids.py index ea5c6791f9..5b4e0dc3cd 100644 --- a/src/ifctester/ifctester/ids.py +++ b/src/ifctester/ifctester/ids.py @@ -75,8 +75,8 @@ class Ids: milestone=None, ): # Not part of the IDS spec, but very useful in practice - self.filepath = None - self.filename = None + self.filepath: Optional[str] = None + self.filename: Optional[str] = None self.specifications: List[Specification] = [] self.info = {} @@ -300,6 +300,7 @@ class Specification: return "optional" elif self.maxOccurs == 0: return "prohibited" + return "required" # Fallback def set_usage(self, usage: Cardinality) -> None: if usage == "optional":