Track last actions and friendlier error reporting when something breaks

This commit is contained in:
Dion Moult
2024-05-09 13:37:57 +10:00
parent f075a6178b
commit a1e0b8858e
6 changed files with 71 additions and 36 deletions
+31 -12
View File
@@ -24,6 +24,7 @@ import traceback
import subprocess import subprocess
import webbrowser import webbrowser
import addon_utils import addon_utils
from collections import deque
bl_info = { bl_info = {
"name": "BlenderBIM", "name": "BlenderBIM",
@@ -38,6 +39,7 @@ bl_info = {
} }
last_error = None last_error = None
last_actions: deque = deque(maxlen=10)
def get_debug_info(): def get_debug_info():
@@ -60,10 +62,22 @@ def get_debug_info():
"processor": platform.processor(), "processor": platform.processor(),
"blender_version": bpy.app.version_string, "blender_version": bpy.app.version_string,
"blenderbim_version": version, "blenderbim_version": version,
"last_actions": last_actions,
"last_error": last_error, "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): if sys.modules.get("bpy", None):
# Process *.pth in /libs/site/packages to setup globally importable modules # 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 # 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: try:
import blenderbim.bim 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(): def register():
blenderbim.bim.register() blenderbim.bim.register()
@@ -83,7 +109,7 @@ if sys.modules.get("bpy", None):
last_error = traceback.format_exc() last_error = traceback.format_exc()
print(last_error) print(last_error)
print(get_debug_info()) print(format_debug_info(get_debug_info()))
print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on") print("\nFATAL ERROR: Unable to load the BlenderBIM Add-on")
class BIM_PT_fatal_error(bpy.types.Panel): 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" bl_description = "Copies debugging information to your clipboard for use in bugreports"
def execute(self, context): def execute(self, context):
info = get_debug_info() info = format_debug_info(get_debug_info())
# Format it in a readable way
text = "\n".join(f"{k}: {v}" for k, v in info.items())
print(text)
if platform.system() == "Windows": if platform.system() == "Windows":
command = "echo | set /p nul=" + text.strip() command = "echo | set /p nul=" + info
elif platform.system() == "Darwin": # for MacOS 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 else: # Linux
command = ( command = 'printf "' + info.replace("\n", "\\n").replace('"', "") + '" | xclip -selection clipboard'
'printf "'
+ text.strip().replace("\n", "\\n").replace('"', "")
+ '" | xclip -selection clipboard'
)
subprocess.run(command, shell=True, check=True) subprocess.run(command, shell=True, check=True)
return {"FINISHED"} return {"FINISHED"}
+18 -11
View File
@@ -22,9 +22,11 @@ import uuid
import hashlib import hashlib
import zipfile import zipfile
import tempfile import tempfile
import traceback
import ifcopenshell import ifcopenshell
import ifcopenshell.geom import ifcopenshell.geom
import ifcopenshell.ifcopenshell_wrapper import ifcopenshell.ifcopenshell_wrapper
import blenderbim
import blenderbim.bim.handler import blenderbim.bim.handler
import blenderbim.tool as tool import blenderbim.tool as tool
from pathlib import Path from pathlib import Path
@@ -37,19 +39,19 @@ IFC_CONNECTED_TYPE = Union[bpy.types.Material, bpy.types.Object]
class IfcStore: class IfcStore:
path: str = "" path: str = ""
file: ifcopenshell.file = None file: Optional[ifcopenshell.file] = None
schema: ifcopenshell.ifcopenshell_wrapper.schema_definition = None schema: Optional[ifcopenshell.ifcopenshell_wrapper.schema_definition] = None
cache: ifcopenshell.ifcopenshell_wrapper.HdfSerializer = None cache: Optional[ifcopenshell.ifcopenshell_wrapper.HdfSerializer] = None
cache_path: str = None cache_path: Optional[str] = None
id_map: dict[int, IFC_CONNECTED_TYPE] = {} id_map: dict[int, IFC_CONNECTED_TYPE] = {}
guid_map: dict[str, IFC_CONNECTED_TYPE] = {} guid_map: dict[str, IFC_CONNECTED_TYPE] = {}
edited_objs: Set[bpy.types.Object] = set() edited_objs: Set[bpy.types.Object] = set()
pset_template_path: str = "" pset_template_path: str = ""
pset_template_file: ifcopenshell.file = None pset_template_file: Optional[ifcopenshell.file] = None
classification_path: str = "" classification_path: str = ""
classification_file: ifcopenshell.file = None classification_file: Optional[ifcopenshell.file] = None
library_path: str = "" library_path: str = ""
library_file: ifcopenshell.file = None library_file: Optional[ifcopenshell.file] = None
current_transaction = "" current_transaction = ""
last_transaction = "" last_transaction = ""
history = [] history = []
@@ -329,6 +331,7 @@ class IfcStore:
@staticmethod @staticmethod
def execute_ifc_operator(operator: bpy.types.Operator, context: bpy.types.Context, is_invoke=False): 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 bpy.context.scene.BIMProperties.is_dirty = True
is_top_level_operator = not bool(IfcStore.current_transaction) is_top_level_operator = not bool(IfcStore.current_transaction)
@@ -343,10 +346,14 @@ class IfcStore:
else: else:
operator.transaction_key = IfcStore.current_transaction operator.transaction_key = IfcStore.current_transaction
if is_invoke: try:
result = getattr(operator, "_invoke")(context, None) if is_invoke:
else: result = getattr(operator, "_invoke")(context, None)
result = getattr(operator, "_execute")(context) else:
result = getattr(operator, "_execute")(context)
except:
blenderbim.last_error = traceback.format_exc()
raise
if is_top_level_operator: if is_top_level_operator:
if tool.Ifc.get(): if tool.Ifc.get():
@@ -33,7 +33,7 @@ import blenderbim.tool as tool
import blenderbim.core.debug as core import blenderbim.core.debug as core
import blenderbim.bim.handler import blenderbim.bim.handler
import blenderbim.bim.import_ifc as import_ifc 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 from blenderbim.bim.ifc import IfcStore
@@ -54,16 +54,18 @@ class CopyDebugInformation(bpy.types.Operator):
} }
) )
# Format it in a readable way text = format_debug_info(info)
text = "\n".join(f"{k}: {v}" for k, v in info.items())
print("-" * 80)
print(text) print(text)
print("-" * 80)
if platform.system() == "Windows": if platform.system() == "Windows":
command = "echo | set /p nul=" + text.strip() command = "echo | set /p nul=" + text
elif platform.system() == "Darwin": # for MacOS 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 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) subprocess.run(command, shell=True, check=True)
return {"FINISHED"} return {"FINISHED"}
@@ -295,10 +295,11 @@ def serialise_settings(settings):
vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()} vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance): elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
vcs_settings[key] = [serialise_entity_instance(i) for i in value] vcs_settings[key] = [serialise_entity_instance(i) for i in value]
if "add_representation" in usecase_path: else:
return "" try:
elif "owner." in usecase_path: vcs_settings[key] = str(value)
return "" except:
vcs_settings[key] = "n/a"
try: try:
return json.dumps(vcs_settings) return json.dumps(vcs_settings)
except: except:
+6 -1
View File
@@ -72,6 +72,8 @@ class Facet:
def __init__(self, *parameters): def __init__(self, *parameters):
self.status = None self.status = None
self.failures: list[FacetFailure] = [] self.failures: list[FacetFailure] = []
self.parameters = []
self.applicability_templates = []
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])
@@ -101,8 +103,10 @@ class Facet:
return self return self
def filter( 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]: ) -> list[ifcopenshell.entity_instance]:
if not elements:
return []
return [e for e in elements if self(e)] return [e for e in elements if self(e)]
def to_string( def to_string(
@@ -133,6 +137,7 @@ class Facet:
total_replacements += 1 total_replacements += 1
if total_replacements == total_variables: if total_replacements == total_variables:
return template return template
return "This facet cannot be interpreted"
def to_ids_value(self, parameter: Union[str, Restriction, list]) -> dict[str, Any]: def to_ids_value(self, parameter: Union[str, Restriction, list]) -> dict[str, Any]:
if isinstance(parameter, str): if isinstance(parameter, str):
+3 -2
View File
@@ -75,8 +75,8 @@ class Ids:
milestone=None, milestone=None,
): ):
# Not part of the IDS spec, but very useful in practice # Not part of the IDS spec, but very useful in practice
self.filepath = None self.filepath: Optional[str] = None
self.filename = None self.filename: Optional[str] = None
self.specifications: List[Specification] = [] self.specifications: List[Specification] = []
self.info = {} self.info = {}
@@ -300,6 +300,7 @@ class Specification:
return "optional" return "optional"
elif self.maxOccurs == 0: elif self.maxOccurs == 0:
return "prohibited" return "prohibited"
return "required" # Fallback
def set_usage(self, usage: Cardinality) -> None: def set_usage(self, usage: Cardinality) -> None:
if usage == "optional": if usage == "optional":