From f545d8a010eb7f5d9d66512247113e73e3f4eec1 Mon Sep 17 00:00:00 2001 From: Ivo Steinbrecher Date: Wed, 17 Jun 2026 09:50:39 +0200 Subject: [PATCH] Add message and error handlers --- src/cubitpy/conf.py | 4 + .../cubit_wrapper/cubit_wrapper_client.py | 96 +++++++++++++++---- .../cubit_wrapper/cubit_wrapper_host.py | 36 +++++-- src/cubitpy/cubitpy.py | 6 +- tests/test_cubitpy.py | 34 +++++-- 5 files changed, 142 insertions(+), 34 deletions(-) diff --git a/src/cubitpy/conf.py b/src/cubitpy/conf.py index 8caf6a9..1e42953 100644 --- a/src/cubitpy/conf.py +++ b/src/cubitpy/conf.py @@ -41,6 +41,10 @@ ) +class CubitPyWarning(UserWarning): + """Warning emitted by CubitPy.""" + + def get_path(environment_variable, test_function, *, throw_error=True): """Check if he environment variable is set and the path exits.""" if environment_variable in os.environ.keys(): diff --git a/src/cubitpy/cubit_wrapper/cubit_wrapper_client.py b/src/cubitpy/cubit_wrapper/cubit_wrapper_client.py index 92a8271..608feb1 100644 --- a/src/cubitpy/cubit_wrapper/cubit_wrapper_client.py +++ b/src/cubitpy/cubit_wrapper/cubit_wrapper_client.py @@ -83,6 +83,34 @@ def is_cubit_type(obj): return False +class DefaultMessageHandler: + """This class is a dummy class that can be overwritten later on to + intercept messages and errors from Cubit.""" + + def pop(self): + """Return two empty lists for messages and errors.""" + return [], [] + + +message_handler = DefaultMessageHandler() + + +def channel_send(argument): + """Wrapper for all send calls to the host interpreter. + + This wrapper appends information about messages and errors from + Cubit. + """ + messages, errors = message_handler.pop() + channel.send( + { + "return_value": argument, + "messages": messages, + "errors": errors, + } + ) + + # All cubit items that are created are stored in this dictionary. The keys are # the unique object ids. The items are deleted once they run out of scope in # the host interpreter. @@ -91,7 +119,7 @@ def is_cubit_type(obj): # The first call are parameters needed in this script parameters = channel.receive() -channel.send(None) +channel_send(None) if not isinstance(parameters, dict): raise TypeError( "The first item should be a dictionary. Got {}!\nparameters={}".format( @@ -115,7 +143,7 @@ def is_cubit_type(obj): raise ValueError("Two arguments must be given to init!") cubit.init(init[1]) cubit_objects[id(cubit)] = cubit -channel.send(object_to_id(cubit)) +channel_send(object_to_id(cubit)) if parameters["is_remote"]: @@ -128,6 +156,42 @@ def is_cubit_type(obj): temp_dir = tempfile.TemporaryDirectory(prefix="cubitpy_temp_dir") +# Try to add a custom message handler to Cubit +try: + + class MessageHandler(cubit.CubitMessageHandler): + """This class intercepts messages and errors from Cubit.""" + + def setup(self): + """Initialize the variables that track the messages and errors.""" + self.messages = [] + self.errors = [] + + def pop(self): + """Return the stored messages and errors and reset the + variables.""" + return_value = [self.messages, self.errors] + self.setup() + return return_value + + def print_message(self, message): + """Append the message to the list of messages.""" + self.messages.append(message) + + def print_error(self, message): + """Append the error to the list of errors.""" + self.errors.append(message) + + message_handler_cubit = MessageHandler() + message_handler_cubit.setup() + cubit.set_cubit_message_handler(message_handler_cubit) + + # Everything worked, so overwrite the message handler with the one linked to Cubit. + message_handler = message_handler_cubit +except Exception: + pass # nosec B110 + + # Now start an endless loop (until None is sent) and perform the cubit functions while 1: # Get input from the python host. @@ -181,7 +245,7 @@ def deserialize_item(item): # Check what to return if is_base_type(cubit_return): # The return item is a string, integer or float - channel.send(cubit_return) + channel_send(cubit_return) elif isinstance(cubit_return, tuple): # A tuple was returned, loop over each entry and check its type @@ -198,12 +262,12 @@ def deserialize_item(item): item ) ) - channel.send(return_list) + channel_send(return_list) elif is_cubit_type(cubit_return): # Store the object locally and return the id cubit_objects[id(cubit_return)] = cubit_return - channel.send(object_to_id(cubit_return)) + channel_send(object_to_id(cubit_return)) else: raise TypeError( @@ -214,28 +278,28 @@ def deserialize_item(item): elif receive[0] == "iscallable": cubit_object = cubit_objects[cubit_item_to_id(receive[1])] - channel.send(callable(getattr(cubit_object, receive[2]))) + channel_send(callable(getattr(cubit_object, receive[2]))) elif receive[0] == "get_object_type": # Get the type of the cubit object compare_object = cubit_objects[cubit_item_to_id(receive[1])] if isinstance(compare_object, cubit.Vertex): - channel.send(cubit_vertex) + channel_send(cubit_vertex) elif isinstance(compare_object, cubit.Curve): - channel.send(cubit_curve) + channel_send(cubit_curve) elif isinstance(compare_object, cubit.Surface): - channel.send(cubit_surface) + channel_send(cubit_surface) elif isinstance(compare_object, cubit.Volume): - channel.send(cubit_volume) + channel_send(cubit_volume) elif isinstance(compare_object, cubit.Body): - channel.send(cubit_body) + channel_send(cubit_body) else: - channel.send(None) + channel_send(None) elif receive[0] == "get_self_dir": # Return a list with all callable methods of this object cubit_object = cubit_objects[cubit_item_to_id(receive[1])] - channel.send( + channel_send( [ [method_name, callable(getattr(cubit_object, method_name))] for method_name in dir(cubit_object) @@ -257,10 +321,10 @@ def deserialize_item(item): ) # Return to python host - channel.send(None) + channel_send(None) elif receive[0] == "get_temp_dir": - channel.send(temp_dir.name) + channel_send(temp_dir.name) elif receive[0] == "display_in_cubit": # receive = ["display_in_cubit", parameters] @@ -332,7 +396,7 @@ def deserialize_item(item): break time.sleep(2) - channel.send(None) + channel_send(None) else: raise ValueError('The case of "{}" is not implemented!'.format(receive[0])) diff --git a/src/cubitpy/cubit_wrapper/cubit_wrapper_host.py b/src/cubitpy/cubit_wrapper/cubit_wrapper_host.py index 53401b7..297f062 100644 --- a/src/cubitpy/cubit_wrapper/cubit_wrapper_host.py +++ b/src/cubitpy/cubit_wrapper/cubit_wrapper_host.py @@ -24,12 +24,13 @@ import atexit import os +import warnings from pathlib import Path import execnet import numpy as np -from cubitpy.conf import GeometryType, cupy +from cubitpy.conf import CubitPyWarning, GeometryType, cupy from cubitpy.cubit_wrapper.cubit_wrapper_utility import cubit_item_to_id, is_base_type @@ -151,16 +152,35 @@ def send_and_return(self, argument_list): arguments stored in the second entry in argument_list. """ - # If the channel is already finalized we get a runtime error here. This happens in cases - # where we delete items after the connection has been closed. We catch this error here. + def get_log_string(log_lines: list[str], name: str) -> str: + """Get a string from the log lines.""" + text = f"The command\n {argument_list}\nraised the following {name}:" + return "\n".join([text] + log_lines) + try: self.channel.send(argument_list) - return self.channel.receive() - except execnet.gateway_base.RemoteError: - # We still raise errors reported from the client. + return_value = self.channel.receive() + if isinstance(return_value, dict): + if len(return_value["messages"]) > 0: + warnings.warn( + get_log_string(return_value["messages"], "message(s)"), + category=CubitPyWarning, + stacklevel=3, + ) + if len(return_value["errors"]) > 0: + raise RuntimeError( + get_log_string(return_value["errors"], "error(s)") + ) + return return_value["return_value"] + else: + return return_value + except OSError as e: + if "cannot send" in str(e): + # If the channel is already finalized we get this error here. This + # happens in cases where we delete items after the connection has been + # closed. + return None raise - except Exception: - return None def get_attribute(self, cubit_object, name): """Return the attribute 'name' of cubit_object. If the attribute is diff --git a/src/cubitpy/cubitpy.py b/src/cubitpy/cubitpy.py index 036b2f1..b13b171 100644 --- a/src/cubitpy/cubitpy.py +++ b/src/cubitpy/cubitpy.py @@ -30,7 +30,7 @@ from fourcipp.fourc_input import FourCInput -from cubitpy.conf import GeometryType, cupy +from cubitpy.conf import CubitPyWarning, GeometryType, cupy from cubitpy.cubit_group import CubitGroup from cubitpy.cubit_to_fourc_input import ( add_exodus_geometry_section, @@ -146,7 +146,9 @@ def _name_created_set(self, set_type, set_id, name, item): warnings.warn( 'A {} is added for the group "{}" and an explicit name of "{}" is given. This might be unintended, as usually if a group is given, we expect to use the name of the group. In the current case we will use the given name.'.format( set_type, item.name, name - ) + ), + category=CubitPyWarning, + stacklevel=3, ) rename_name = name elif group_name is not None: diff --git a/tests/test_cubitpy.py b/tests/test_cubitpy.py index 6b00441..59cb555 100644 --- a/tests/test_cubitpy.py +++ b/tests/test_cubitpy.py @@ -33,14 +33,7 @@ from fourcipp.fourc_input import FourCInput from fourcipp.utils.dict_utils import compare_nested_dicts_or_lists -# Define the testing paths. -testing_path = os.path.abspath(os.path.dirname(__file__)) -testing_input = os.path.join(testing_path, "input-files-ref") -testing_temp = os.path.join(testing_path, "testing-tmp") -testing_external_geometry = os.path.join(testing_path, "external-geometry") - -# CubitPy imports. -from cubitpy.conf import cupy +from cubitpy.conf import CubitPyWarning, cupy from cubitpy.cubit_utility import ( formatter, get_surface_center, @@ -56,6 +49,13 @@ ) from cubitpy.mesh_creation_functions import create_brick, extrude_mesh_normal_to_surface +# Define the testing paths. +testing_path = os.path.abspath(os.path.dirname(__file__)) +testing_input = os.path.join(testing_path, "input-files-ref") +testing_temp = os.path.join(testing_path, "testing-tmp") +testing_external_geometry = os.path.join(testing_path, "external-geometry") + + # Global variable if this test is run by GitLab. if "TESTING_GITHUB" in os.environ.keys() and os.environ["TESTING_GITHUB"] == "1": TESTING_GITHUB = True @@ -2485,3 +2485,21 @@ def test_node_set_info_to_string_errors(): match="Expected string to start with", ): string_to_node_set_info("abc") + + +def test_cubit_warnings_and_errors(): + """Test that cubit warnings and errors are handled correctly.""" + + cubit = CubitPy() + + # Warning + cubit.cmd("brick x 10") + with pytest.warns(CubitPyWarning, match="is a CUBIT identifier. Please use the"): + cubit.cmd('volume 1 rename "b"') + + # Error + with pytest.raises( + RuntimeError, + match="ERROR: All dimensions must be nonzero and positive. Entered values are:", + ): + cubit.cmd("brick x -10")