mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 02:23:34 +00:00
outliner.delete - make errors from attempt to delete locked object less scary
Traceback errors give users an impression that some broke and maybe project now is in some invalid state and they should ctrl-z immediately, error reports are much more friendly. Before - https://i.imgur.com/N4fUU1M.png After - https://i.imgur.com/6On1Be0.png
This commit is contained in:
@@ -996,8 +996,14 @@ class OverrideOutlinerDelete(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
def _execute(self, context):
|
||||
selected_ids_data = self.get_selected_ids_data(context)
|
||||
with context.temp_override(selected_objects=list(selected_ids_data.objects)):
|
||||
bpy.ops.bim.override_object_delete(is_batch=self.is_batch)
|
||||
try:
|
||||
with context.temp_override(selected_objects=list(selected_ids_data.objects)):
|
||||
bpy.ops.bim.override_object_delete(is_batch=self.is_batch)
|
||||
except RuntimeError as e:
|
||||
error_reports = tool.Blender.extract_error_reports(e)
|
||||
if not error_reports:
|
||||
raise
|
||||
tool.Blender.report_operator_errors(self, error_reports)
|
||||
|
||||
for collection in selected_ids_data.collections:
|
||||
# Removing an aggregate object would also remove it's collection
|
||||
|
||||
@@ -1716,3 +1716,40 @@ class Blender(bonsai.core.tool.Blender):
|
||||
if len(pos) == 0 or (indices is not None and len(indices) == 0):
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def extract_error_reports(cls, exception: RuntimeError) -> list[str]:
|
||||
"""Extracts error report lines from a runtime exception during operator execution.
|
||||
|
||||
If operator had any `ERROR` reports, it will always raise a `RuntimeError`,
|
||||
no matter what status is returned.
|
||||
And sometimes it's useful to pass those reports to another operator
|
||||
that called it. That way user won't get to see a scary traceback.
|
||||
|
||||
If empty list is returned, then exception should be reraised,
|
||||
as it is an actual unhandled runtime error.
|
||||
"""
|
||||
error_message = str(exception)
|
||||
extracted: list[str] = []
|
||||
|
||||
# If operator was cancelled and had error message,
|
||||
# it will always start with this (warnings and info msgs are ignored).
|
||||
if not error_message.startswith("Error: "):
|
||||
return extracted
|
||||
|
||||
# Ignore actual runtime errors, as they has to be handled separately
|
||||
# and not just rereported.
|
||||
if error_message.startswith("Error: Python: Traceback (most recent call last):"):
|
||||
return extracted
|
||||
|
||||
for report in error_message.strip().split("Error: "):
|
||||
report = report.strip()
|
||||
if not report:
|
||||
continue
|
||||
extracted.append(report)
|
||||
return extracted
|
||||
|
||||
@classmethod
|
||||
def report_operator_errors(cls, operator: bpy.types.Operator, error_reports: list[str]) -> None:
|
||||
for report in error_reports:
|
||||
operator.report({"ERROR"}, report)
|
||||
|
||||
@@ -59,3 +59,47 @@ class TestSortPanelsForRegister(NewFile):
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
subject.sort_panels_for_register(items, {"J": "A"})
|
||||
|
||||
|
||||
class TestBlenderErrorMessageExtraction(NewFile):
|
||||
def test_extract_operator_reports(self) -> None:
|
||||
|
||||
ERROR_REPORTS = ["ERROR!!!\nERROR", "ERROR"]
|
||||
|
||||
class OBJECT_OT_test_fail_operator(bpy.types.Operator):
|
||||
bl_idname = "object.test_fail_operator"
|
||||
bl_label = "Test Fail Operator"
|
||||
|
||||
def execute(self, context):
|
||||
self.report({"INFO"}, "Info message.")
|
||||
subject.report_operator_errors(self, ERROR_REPORTS)
|
||||
return {"FINISHED"}
|
||||
|
||||
bpy.utils.register_class(OBJECT_OT_test_fail_operator)
|
||||
|
||||
try:
|
||||
bpy.ops.object.test_fail_operator()
|
||||
except RuntimeError as e:
|
||||
error_reports = subject.extract_error_reports(e)
|
||||
assert error_reports == ERROR_REPORTS
|
||||
|
||||
bpy.utils.unregister_class(OBJECT_OT_test_fail_operator)
|
||||
|
||||
def test_ignore_actual_runtime_errors_from_operators(self) -> None:
|
||||
|
||||
class OBJECT_OT_test_fail_operator(bpy.types.Operator):
|
||||
bl_idname = "object.test_fail_operator"
|
||||
bl_label = "Test Fail Operator"
|
||||
|
||||
def execute(self, context):
|
||||
raise RuntimeError("Intentional runtime error.")
|
||||
|
||||
bpy.utils.register_class(OBJECT_OT_test_fail_operator)
|
||||
|
||||
try:
|
||||
bpy.ops.object.test_fail_operator()
|
||||
except RuntimeError as e:
|
||||
error_reports = subject.extract_error_reports(e)
|
||||
assert error_reports == []
|
||||
|
||||
bpy.utils.unregister_class(OBJECT_OT_test_fail_operator)
|
||||
|
||||
Reference in New Issue
Block a user