From 7a8a71643785c072e3271df358e1838f4d79b658 Mon Sep 17 00:00:00 2001 From: ftheveno Date: Thu, 19 Dec 2024 09:12:38 -0500 Subject: [PATCH 01/14] First tentative for plugin mapdl mechanism --- src/ansys/mapdl/core/mapdl_core.py | 3 + src/ansys/mapdl/core/mapdl_grpc.py | 21 +++++ src/ansys/mapdl/core/plugin.py | 141 +++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 src/ansys/mapdl/core/plugin.py diff --git a/src/ansys/mapdl/core/mapdl_core.py b/src/ansys/mapdl/core/mapdl_core.py index 94626cf9c3..ecb6565869 100644 --- a/src/ansys/mapdl/core/mapdl_core.py +++ b/src/ansys/mapdl/core/mapdl_core.py @@ -85,6 +85,7 @@ from ansys.mapdl.core.parameters import Parameters from ansys.mapdl.core.solution import Solution from ansys.mapdl.core.xpl import ansXpl + from ansys.mapdl.core.plugin import ansPlugin from ansys.mapdl.core.post import PostProcessing @@ -321,6 +322,8 @@ def __init__( self._xpl: Optional[ansXpl] = None # Initialized in mapdl_grpc + self._plugin: Optional[ansPlugin] = None # Initialized in mapdl_grpc + from ansys.mapdl.core.component import ComponentManager self._componentmanager: ComponentManager = ComponentManager(self) diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 8f44fe4a66..2ab4601609 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -107,6 +107,7 @@ from ansys.mapdl.core.database import MapdlDb from ansys.mapdl.core.xpl import ansXpl + from ansys.mapdl.core.plugin import ansPlugin TMP_VAR = "__tmpvar__" VOID_REQUEST = anskernel.EmptyRequest() @@ -2799,6 +2800,26 @@ def xpl(self) -> "ansXpl": self._xpl = ansXpl(self) return self._xpl + @property + def plugin(self) -> "ansPlugin": + """MAPDL plugin handler + + Plugin Manager for MAPDL + + Examples + -------- + + >>> from ansys import Mapdl + >>> mapdl = Mapdl() + >>> plugin = mapdl.plugin + >>> plugin.load('PluginDPF') + """ + if self._plugin is None: + from ansys.mapdl.core.plugin import ansPlugin + + self._plugin = ansPlugin(self) + return self._plugin + @protect_grpc def scalar_param(self, pname: str) -> float: """Return a scalar parameter as a float. diff --git a/src/ansys/mapdl/core/plugin.py b/src/ansys/mapdl/core/plugin.py new file mode 100644 index 0000000000..a9d76045a6 --- /dev/null +++ b/src/ansys/mapdl/core/plugin.py @@ -0,0 +1,141 @@ +# Copyright (C) 2016 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Contains the ansPlugin class.""" +import json +import pathlib +import weakref + +from ansys.api.mapdl.v0 import mapdl_pb2 + +from .common_grpc import ANSYS_VALUE_TYPE +from .errors import MapdlRuntimeError +from .misc import random_string + +class ansPlugin: + """ + ANSYS MAPDL Plugin Manager. + + Examples + -------- + >>> from ansys.mapdl.core import launch_mapdl + >>> mapdl = launch_mapdl() + >>> plugin = mapdl.plugin + + Load a plugin in the MAPDL Session + """ + + def __init__(self, mapdl): + """Initialize the class.""" + from ansys.mapdl.core.mapdl_grpc import MapdlGrpc + + if not isinstance(mapdl, MapdlGrpc): # pragma: no cover + raise TypeError("Must be initialized using MapdlGrpc class") + + self._mapdl_weakref = weakref.ref(mapdl) + self._filename = None + self._open = False + + @property + def _mapdl(self): + """Return the weakly referenced instance of mapdl.""" + return self._mapdl_weakref() + + + def load(self, plugin_name: str, feature: str = "CMD") -> str: + """ + Loads a plugin into MAPDL. + + Parameters + ---------- + plugin_name : str + Name of the plugin to load. + feature : str + Feature or module to activate in the plugin. + + Returns + ------- + str + Confirmation message about the loaded plugin. + + Raises + ------ + PluginLoadError + If the plugin fails to load. + """ + + command = f"*PLUG,LOAD,{plugin_name},{feature}" + response = self._mapdl.run(command) + if "error" in response.lower(): + raise PluginLoadError(f"Failed to load plugin '{plugin_name}' with feature '{feature}'.") + return f"Plugin '{plugin_name}' with feature '{feature}' loaded successfully." + + + def unload(self, plugin_name: str) -> str: + """ + Unloads a plugin from MAPDL. + + Parameters + ---------- + plugin_name : str + Name of the plugin to unload. + + Returns + ------- + str + Confirmation message about the unloaded plugin. + + Raises + ------ + PluginUnloadError + If the plugin fails to unload. + """ + + command = f"*PLUG,UNLOAD,{plugin_name}" + response = self._mapdl.run(command) + if "error" in response.lower(): + raise PluginUnloadError(f"Failed to unload plugin '{plugin_name}'.") + return f"Plugin '{plugin_name}' unloaded successfully." + + + def list(self) -> list: + """ + Lists all currently loaded plugins in MAPDL. + + Returns + ------- + list + A list of loaded plugin names. + + Raises + ------ + RuntimeError + If the plugin list cannot be retrieved. + """ + + command = "*PLUG,LIST" + response = self._mapdl.run(command) + if "error" in response.lower(): + raise RuntimeError("Failed to retrieve the list of loaded plugins.") + # Parse response and extract plugin names (assuming response is newline-separated text) + plugins = [line.strip() for line in response.splitlines() if line.strip()] + return plugins From 538936d04b077bcbff95c9f920d51925292c70ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:41:30 +0000 Subject: [PATCH 02/14] ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci --- src/ansys/mapdl/core/mapdl_grpc.py | 2 +- src/ansys/mapdl/core/plugin.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 2ab4601609..fd3a54ceb3 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -106,8 +106,8 @@ from ansys.platform.instancemanagement import Instance as PIM_Instance from ansys.mapdl.core.database import MapdlDb - from ansys.mapdl.core.xpl import ansXpl from ansys.mapdl.core.plugin import ansPlugin + from ansys.mapdl.core.xpl import ansXpl TMP_VAR = "__tmpvar__" VOID_REQUEST = anskernel.EmptyRequest() diff --git a/src/ansys/mapdl/core/plugin.py b/src/ansys/mapdl/core/plugin.py index a9d76045a6..629dd15844 100644 --- a/src/ansys/mapdl/core/plugin.py +++ b/src/ansys/mapdl/core/plugin.py @@ -31,6 +31,7 @@ from .errors import MapdlRuntimeError from .misc import random_string + class ansPlugin: """ ANSYS MAPDL Plugin Manager. From 35bcf88a361af85fbbd2605cd80e4c2a7e6611c2 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:42:46 +0000 Subject: [PATCH 03/14] chore: adding changelog file 3627.miscellaneous.md [dependabot-skip] --- doc/changelog.d/3627.miscellaneous.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/3627.miscellaneous.md diff --git a/doc/changelog.d/3627.miscellaneous.md b/doc/changelog.d/3627.miscellaneous.md new file mode 100644 index 0000000000..aa66b8726e --- /dev/null +++ b/doc/changelog.d/3627.miscellaneous.md @@ -0,0 +1 @@ +feat: First tentative for Plugin Mapdl Mechanism python API \ No newline at end of file From 2dea3bc321662827197729bd2112f55db67bfe46 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:43:12 +0000 Subject: [PATCH 04/14] ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci --- src/ansys/mapdl/core/plugin.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ansys/mapdl/core/plugin.py b/src/ansys/mapdl/core/plugin.py index 629dd15844..c54bd8a015 100644 --- a/src/ansys/mapdl/core/plugin.py +++ b/src/ansys/mapdl/core/plugin.py @@ -61,7 +61,6 @@ def _mapdl(self): """Return the weakly referenced instance of mapdl.""" return self._mapdl_weakref() - def load(self, plugin_name: str, feature: str = "CMD") -> str: """ Loads a plugin into MAPDL. @@ -83,14 +82,15 @@ def load(self, plugin_name: str, feature: str = "CMD") -> str: PluginLoadError If the plugin fails to load. """ - + command = f"*PLUG,LOAD,{plugin_name},{feature}" response = self._mapdl.run(command) if "error" in response.lower(): - raise PluginLoadError(f"Failed to load plugin '{plugin_name}' with feature '{feature}'.") + raise PluginLoadError( + f"Failed to load plugin '{plugin_name}' with feature '{feature}'." + ) return f"Plugin '{plugin_name}' with feature '{feature}' loaded successfully." - def unload(self, plugin_name: str) -> str: """ Unloads a plugin from MAPDL. @@ -117,7 +117,6 @@ def unload(self, plugin_name: str) -> str: raise PluginUnloadError(f"Failed to unload plugin '{plugin_name}'.") return f"Plugin '{plugin_name}' unloaded successfully." - def list(self) -> list: """ Lists all currently loaded plugins in MAPDL. From a1d7ee2dc6d955ba623e6c1bda29cfbcb3c2a1b4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:36:29 +0000 Subject: [PATCH 05/14] ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci --- src/ansys/mapdl/core/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/mapdl/core/plugin.py b/src/ansys/mapdl/core/plugin.py index c54bd8a015..afdae509bd 100644 --- a/src/ansys/mapdl/core/plugin.py +++ b/src/ansys/mapdl/core/plugin.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016 - 2024 ANSYS, Inc. and/or its affiliates. +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. # SPDX-License-Identifier: MIT # # From 4afa907ea0b616c9ed3b06ff2b67bc073dd30203 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:43:37 +0200 Subject: [PATCH 06/14] feat: add plugin error handling classes and improve plugin loading/unloading methods --- src/ansys/mapdl/core/errors.py | 21 +++++++++++++++++ src/ansys/mapdl/core/plugin.py | 42 ++++++++++++---------------------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/src/ansys/mapdl/core/errors.py b/src/ansys/mapdl/core/errors.py index 4afc783de3..3e1945b755 100644 --- a/src/ansys/mapdl/core/errors.py +++ b/src/ansys/mapdl/core/errors.py @@ -282,6 +282,27 @@ def __init__(self, msg=""): super().__init__(msg) +class PluginError(MapdlRuntimeError): + """Raised when a plugin fails""" + + def __init__(self, msg=""): + super().__init__(msg) + + +class PluginLoadError(PluginError): + """Raised when a plugin fails to load""" + + def __init__(self, msg=""): + super().__init__(msg) + + +class PluginUnloadError(PluginError): + """Raised when a plugin fails to unload""" + + def __init__(self, msg=""): + super().__init__(msg) + + # handler for protect_grpc def handler(sig, frame): # pragma: no cover """Pass signal to custom interrupt handler.""" diff --git a/src/ansys/mapdl/core/plugin.py b/src/ansys/mapdl/core/plugin.py index afdae509bd..4e3334dafe 100644 --- a/src/ansys/mapdl/core/plugin.py +++ b/src/ansys/mapdl/core/plugin.py @@ -21,15 +21,9 @@ # SOFTWARE. """Contains the ansPlugin class.""" -import json -import pathlib import weakref -from ansys.api.mapdl.v0 import mapdl_pb2 - -from .common_grpc import ANSYS_VALUE_TYPE -from .errors import MapdlRuntimeError -from .misc import random_string +from ansys.mapdl.core.errors import PluginError, PluginLoadError, PluginUnloadError class ansPlugin: @@ -50,7 +44,7 @@ def __init__(self, mapdl): from ansys.mapdl.core.mapdl_grpc import MapdlGrpc if not isinstance(mapdl, MapdlGrpc): # pragma: no cover - raise TypeError("Must be initialized using MapdlGrpc class") + raise TypeError("Must be initialized using an 'MapdlGrpc' object") self._mapdl_weakref = weakref.ref(mapdl) self._filename = None @@ -61,26 +55,21 @@ def _mapdl(self): """Return the weakly referenced instance of mapdl.""" return self._mapdl_weakref() - def load(self, plugin_name: str, feature: str = "CMD") -> str: + def load(self, plugin_name: str, feature: str = "") -> None: """ Loads a plugin into MAPDL. Parameters ---------- plugin_name : str - Name of the plugin to load. + Name of the plugin to load. feature : str - Feature or module to activate in the plugin. - - Returns - ------- - str - Confirmation message about the loaded plugin. + Feature or module to activate in the plugin. Raises ------ PluginLoadError - If the plugin fails to load. + If the plugin fails to load. """ command = f"*PLUG,LOAD,{plugin_name},{feature}" @@ -89,9 +78,11 @@ def load(self, plugin_name: str, feature: str = "CMD") -> str: raise PluginLoadError( f"Failed to load plugin '{plugin_name}' with feature '{feature}'." ) - return f"Plugin '{plugin_name}' with feature '{feature}' loaded successfully." + self._log.info( + f"Plugin '{plugin_name}' with feature '{feature}' loaded successfully." + ) - def unload(self, plugin_name: str) -> str: + def unload(self, plugin_name: str) -> None: """ Unloads a plugin from MAPDL. @@ -100,11 +91,6 @@ def unload(self, plugin_name: str) -> str: plugin_name : str Name of the plugin to unload. - Returns - ------- - str - Confirmation message about the unloaded plugin. - Raises ------ PluginUnloadError @@ -115,9 +101,9 @@ def unload(self, plugin_name: str) -> str: response = self._mapdl.run(command) if "error" in response.lower(): raise PluginUnloadError(f"Failed to unload plugin '{plugin_name}'.") - return f"Plugin '{plugin_name}' unloaded successfully." + self._log.info(f"Plugin '{plugin_name}' unloaded successfully.") - def list(self) -> list: + def list(self) -> list[str]: """ Lists all currently loaded plugins in MAPDL. @@ -134,8 +120,10 @@ def list(self) -> list: command = "*PLUG,LIST" response = self._mapdl.run(command) + if "error" in response.lower(): - raise RuntimeError("Failed to retrieve the list of loaded plugins.") + raise PluginError("Failed to retrieve the list of loaded plugins.") + # Parse response and extract plugin names (assuming response is newline-separated text) plugins = [line.strip() for line in response.splitlines() if line.strip()] return plugins From 16c744d503966ae6a6282a85775df0c85690ce75 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:44:13 +0200 Subject: [PATCH 07/14] test: adding plugin tests --- tests/test_plugin.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_plugin.py diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 0000000000..6ca308561d --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,43 @@ +# Copyright (C) 2016 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Test the plugin implementation""" + + +@pytest.fixture() +def plugin(mapdl): + if mapdl.version < 25.2: + pytest.skip( + "Plugin support is only for versions 25.2 and above", + allow_module_level=True, + ) + + return mapdl.plugin + + +def test_plugin_lifecycle(plugin): + plugin_name = "my_plugin" + plugin.load(plugin_name) + + assert plugin_name in plugin.list(), "Plugin should be loaded" + + plugin.unload(plugin_name) From 83997de1ae7a7b60040a55bab185435784b644a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:36:26 +0000 Subject: [PATCH 08/14] ci: auto fixes from pre-commit.com hooks. for more information, see https://pre-commit.ci --- src/ansys/mapdl/core/mapdl_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/mapdl/core/mapdl_core.py b/src/ansys/mapdl/core/mapdl_core.py index 3363c2e0fc..eb082a5b49 100644 --- a/src/ansys/mapdl/core/mapdl_core.py +++ b/src/ansys/mapdl/core/mapdl_core.py @@ -85,9 +85,9 @@ from ansys.mapdl.core.mapdl import MapdlBase from ansys.mapdl.core.mapdl_geometry import Geometry, LegacyGeometry from ansys.mapdl.core.parameters import Parameters + from ansys.mapdl.core.plugin import ansPlugin from ansys.mapdl.core.solution import Solution from ansys.mapdl.core.xpl import ansXpl - from ansys.mapdl.core.plugin import ansPlugin if _HAS_DPF: from ansys.mapdl.core.reader import DPFResult From ff30eae1a9953c792a6c2f697b4319a80d59a24b Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:37:45 +0000 Subject: [PATCH 09/14] chore: adding changelog file 3627.miscellaneous.md [dependabot-skip] --- doc/changelog.d/3627.miscellaneous.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.d/3627.miscellaneous.md b/doc/changelog.d/3627.miscellaneous.md index aa66b8726e..0d10efb213 100644 --- a/doc/changelog.d/3627.miscellaneous.md +++ b/doc/changelog.d/3627.miscellaneous.md @@ -1 +1 @@ -feat: First tentative for Plugin Mapdl Mechanism python API \ No newline at end of file +Feat: first tentative for plugin mapdl mechanism python api \ No newline at end of file From fec2b24da468ddc08d0cd9bb157bb7722c4b3455 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:44:44 +0200 Subject: [PATCH 10/14] feat: rename plugin to plugins. Enhance plugin functionality with command parsing and lifecycle management --- src/ansys/mapdl/core/mapdl_grpc.py | 2 +- src/ansys/mapdl/core/plugin.py | 132 +++++++++++++++++++++++++++-- tests/test_plugin.py | 67 +++++++++++++-- 3 files changed, 186 insertions(+), 15 deletions(-) diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 06242cfa58..d76cf9f1f8 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -2859,7 +2859,7 @@ def xpl(self) -> "ansXpl": return self._xpl @property - def plugin(self) -> "ansPlugin": + def plugins(self) -> "ansPlugin": """MAPDL plugin handler Plugin Manager for MAPDL diff --git a/src/ansys/mapdl/core/plugin.py b/src/ansys/mapdl/core/plugin.py index 4e3334dafe..fe880bae5b 100644 --- a/src/ansys/mapdl/core/plugin.py +++ b/src/ansys/mapdl/core/plugin.py @@ -21,9 +21,13 @@ # SOFTWARE. """Contains the ansPlugin class.""" +import re +from warnings import warn import weakref +from ansys.mapdl.core import Mapdl from ansys.mapdl.core.errors import PluginError, PluginLoadError, PluginUnloadError +from ansys.mapdl.core.logging import Logger class ansPlugin: @@ -39,7 +43,7 @@ class ansPlugin: Load a plugin in the MAPDL Session """ - def __init__(self, mapdl): + def __init__(self, mapdl: Mapdl): """Initialize the class.""" from ansys.mapdl.core.mapdl_grpc import MapdlGrpc @@ -51,11 +55,113 @@ def __init__(self, mapdl): self._open = False @property - def _mapdl(self): + def _mapdl(self) -> Mapdl: """Return the weakly referenced instance of mapdl.""" return self._mapdl_weakref() - def load(self, plugin_name: str, feature: str = "") -> None: + @property + def _log(self) -> Logger: + """Return the logger from the MAPDL instance.""" + return self._mapdl._log + + def _parse_commands(self, response: str) -> list[str]: + """ + Parse the response string to extract commands. + + Parameters + ---------- + response : str + The response string containing commands. + + Returns + ------- + list[str] + A list of commands extracted from the response. + """ + if not response: + return [] + + # Assuming commands are separated by newlines + return re.findall(r"New command \[(.*)\] registered", response) + + def _set_commands(self, commands: list[str], plugin_name: str = "NOT_SET") -> None: + """ + Set commands to be executed. + + Parameters + ---------- + commands : list[str] + List of commands to be set. + """ + if not commands: + return + + mapdl = self._mapdl + + for each_command in commands: + each_command.replace("*", "star") + each_command.replace("/", "slash") + + if hasattr(mapdl, each_command): + # We are allowing to overwrite existing commands + warn(f"Command '{each_command}' already exists in the MAPDL instance.") + + def passer(self, *args, **kwargs): + return self.run(*args, **kwargs) + + # Inject docstring + passer.__doc__ = f"""Command from plugin {plugin_name}: {each_command}. + Use this plugin documentation to understand the command and its parameters. + + Automatically generated docstring by ansPlugin. + """ + setattr(mapdl, each_command, passer) + self._log.info( + f"Command '{each_command}' from plugin '{plugin_name}' set successfully." + ) + + def _deleter_commands( + self, commands: list[str], plugin_name: str = "NOT_SET" + ) -> None: + """ + Delete commands from the MAPDL instance. + + Parameters + ---------- + commands : list[str] + List of commands to be deleted. + """ + if not commands: + return + + mapdl = self._mapdl + + for each_command in commands: + if hasattr(mapdl, each_command): + delattr(mapdl, each_command) + self._log.info( + f"Command '{each_command}' from '{plugin_name}' deleted successfully." + ) + + def _load_commands(self, response: str, plugin_name: str) -> None: + """ + Load commands from the response string. + + Parameters + ---------- + response : str + The response string containing commands to be loaded. + """ + if not response: + return + + commands = self._parse_commands(response) + if not commands: + self._log.warning("No commands found in the response.") + return + self._set_commands(commands, plugin_name=plugin_name) + + def load(self, plugin_name: str, feature: str = "") -> str: """ Loads a plugin into MAPDL. @@ -81,8 +187,10 @@ def load(self, plugin_name: str, feature: str = "") -> None: self._log.info( f"Plugin '{plugin_name}' with feature '{feature}' loaded successfully." ) + self._load_commands(response, plugin_name=plugin_name) + return response - def unload(self, plugin_name: str) -> None: + def unload(self, plugin_name: str) -> str: """ Unloads a plugin from MAPDL. @@ -99,10 +207,21 @@ def unload(self, plugin_name: str) -> None: command = f"*PLUG,UNLOAD,{plugin_name}" response = self._mapdl.run(command) + + if not response: + return "" + if "error" in response.lower(): raise PluginUnloadError(f"Failed to unload plugin '{plugin_name}'.") + + self._load_commands(response, plugin_name) self._log.info(f"Plugin '{plugin_name}' unloaded successfully.") + commands = self._parse_commands(response) + self._deleter_commands(commands, plugin_name=plugin_name) + + return response + def list(self) -> list[str]: """ Lists all currently loaded plugins in MAPDL. @@ -119,9 +238,8 @@ def list(self) -> list[str]: """ command = "*PLUG,LIST" - response = self._mapdl.run(command) - - if "error" in response.lower(): + response = self._mapdl.run(command) or "" + if response and "error" in response.lower(): raise PluginError("Failed to retrieve the list of loaded plugins.") # Parse response and extract plugin names (assuming response is newline-separated text) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 6ca308561d..295c6dd115 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -22,22 +22,75 @@ """Test the plugin implementation""" +import pytest + +pytestmark = pytest.mark.random_order(disabled=True) + +from ansys.mapdl.core import Mapdl +from ansys.mapdl.core.plugin import ansPlugin + +pytestmark = pytest.mark.random_order(disabled=True) + +TEST_PLUGIN = "PluginDPF" + @pytest.fixture() -def plugin(mapdl): +def plugins(mapdl: Mapdl) -> ansPlugin: if mapdl.version < 25.2: pytest.skip( "Plugin support is only for versions 25.2 and above", allow_module_level=True, ) - return mapdl.plugin + return mapdl.plugins + + +@pytest.fixture() +def dpf_load_response(plugins: ansPlugin) -> ansPlugin: + response = plugins.load(TEST_PLUGIN) + yield response + plugins.unload(TEST_PLUGIN) + + +def test_plugin_load(plugins): + assert plugins.load(TEST_PLUGIN) is not None + + +@pytest.mark.xfail(reason="Plugin unload not implemented in MAPDL yet") +def test_plugin_list(plugins, dpf_load_response): + assert TEST_PLUGIN in plugins.list(), "Plugin should be loaded" + + +def test_plugin_unload(plugins): + plugins.unload(TEST_PLUGIN) + assert TEST_PLUGIN not in plugins.list(), "Plugin should be unloaded" + + +def test_parse_commands(plugins, dpf_load_response): + commands = plugins._parse_commands(dpf_load_response) + + assert isinstance(commands, list), "Commands should be a list" + assert len(commands) > 0, "Commands list should not be empty" + assert "*DPF" in commands, "Expected command '*DPF' should be in the list" + + +def test_load_commands(plugins, dpf_load_response): + commands = plugins._parse_commands(dpf_load_response) + assert isinstance(commands, list), "Commands should be a list" + assert len(commands) > 0, "Commands list should not be empty" + + for command in commands: + assert hasattr(plugins._mapdl, command) -def test_plugin_lifecycle(plugin): - plugin_name = "my_plugin" - plugin.load(plugin_name) +def test_deleter_commands(plugins, dpf_load_response): + commands = plugins._parse_commands(dpf_load_response) + assert isinstance(commands, list), "Commands should be a list" + assert len(commands) > 0, "Commands list should not be empty" - assert plugin_name in plugin.list(), "Plugin should be loaded" + plugins._deleter_commands(commands, TEST_PLUGIN) - plugin.unload(plugin_name) + for command in commands: + assert not hasattr( + plugins._mapdl, command + ), f"Command {command} should be deleted" From f4e9ef5784170b5a930036fa803e7d66921fcc50 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:04:13 +0200 Subject: [PATCH 11/14] feat: add plugin property to _MapdlCore and remove from MapdlGrpc --- src/ansys/mapdl/core/mapdl_core.py | 20 ++++++++++++++++++++ src/ansys/mapdl/core/mapdl_grpc.py | 21 --------------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/ansys/mapdl/core/mapdl_core.py b/src/ansys/mapdl/core/mapdl_core.py index eb082a5b49..f3785bf6bf 100644 --- a/src/ansys/mapdl/core/mapdl_core.py +++ b/src/ansys/mapdl/core/mapdl_core.py @@ -1084,6 +1084,26 @@ def graphics_backend(self, value: GraphicsBackend): """Set the graphics backend to be used.""" self._graphics_backend = value + @property + def plugins(self) -> "ansPlugin": + """MAPDL plugin handler + + Plugin Manager for MAPDL + + Examples + -------- + + >>> from ansys import Mapdl + >>> mapdl = Mapdl() + >>> plugin = mapdl.plugin + >>> plugin.load('PluginDPF') + """ + if self._plugin is None: + from ansys.mapdl.core.plugin import ansPlugin + + self._plugin = ansPlugin(self) + return self._plugin + @property @requires_package("ansys.mapdl.reader", softerror=True) def result(self): diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index d76cf9f1f8..c24af41afb 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -105,7 +105,6 @@ from ansys.platform.instancemanagement import Instance as PIM_Instance from ansys.mapdl.core.database import MapdlDb - from ansys.mapdl.core.plugin import ansPlugin from ansys.mapdl.core.xpl import ansXpl VOID_REQUEST = anskernel.EmptyRequest() @@ -2858,26 +2857,6 @@ def xpl(self) -> "ansXpl": self._xpl = ansXpl(self) return self._xpl - @property - def plugins(self) -> "ansPlugin": - """MAPDL plugin handler - - Plugin Manager for MAPDL - - Examples - -------- - - >>> from ansys import Mapdl - >>> mapdl = Mapdl() - >>> plugin = mapdl.plugin - >>> plugin.load('PluginDPF') - """ - if self._plugin is None: - from ansys.mapdl.core.plugin import ansPlugin - - self._plugin = ansPlugin(self) - return self._plugin - @protect_grpc def scalar_param(self, pname: str) -> float: """Return a scalar parameter as a float. From 302ecc18d7a9bc71f1df0de3bbb900e4378ac3d9 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:19:12 +0200 Subject: [PATCH 12/14] fix: streamline command replacement in ansPlugin class --- src/ansys/mapdl/core/plugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ansys/mapdl/core/plugin.py b/src/ansys/mapdl/core/plugin.py index fe880bae5b..7c8474f136 100644 --- a/src/ansys/mapdl/core/plugin.py +++ b/src/ansys/mapdl/core/plugin.py @@ -99,8 +99,7 @@ def _set_commands(self, commands: list[str], plugin_name: str = "NOT_SET") -> No mapdl = self._mapdl for each_command in commands: - each_command.replace("*", "star") - each_command.replace("/", "slash") + each_command = each_command.replace("*", "star").replace("/", "slash") if hasattr(mapdl, each_command): # We are allowing to overwrite existing commands @@ -214,7 +213,6 @@ def unload(self, plugin_name: str) -> str: if "error" in response.lower(): raise PluginUnloadError(f"Failed to unload plugin '{plugin_name}'.") - self._load_commands(response, plugin_name) self._log.info(f"Plugin '{plugin_name}' unloaded successfully.") commands = self._parse_commands(response) From 6dc488f51740acb7a28fe01b00a41fc9b8675981 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:23:13 +0200 Subject: [PATCH 13/14] Update tests/test_plugin.py Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- tests/test_plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 295c6dd115..1fb7ed10c5 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -47,8 +47,7 @@ def plugins(mapdl: Mapdl) -> ansPlugin: @pytest.fixture() def dpf_load_response(plugins: ansPlugin) -> ansPlugin: - response = plugins.load(TEST_PLUGIN) - yield response + yield plugins.load(TEST_PLUGIN) plugins.unload(TEST_PLUGIN) From d1e847107da289ada8ddbdd4df8e69773924c6e0 Mon Sep 17 00:00:00 2001 From: German <28149841+germa89@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:31:27 +0200 Subject: [PATCH 14/14] fix: update plugin response handling to return an empty list and add test for unloading plugin twice --- src/ansys/mapdl/core/plugin.py | 3 +-- tests/test_plugin.py | 8 ++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ansys/mapdl/core/plugin.py b/src/ansys/mapdl/core/plugin.py index 7c8474f136..373f83aed1 100644 --- a/src/ansys/mapdl/core/plugin.py +++ b/src/ansys/mapdl/core/plugin.py @@ -241,5 +241,4 @@ def list(self) -> list[str]: raise PluginError("Failed to retrieve the list of loaded plugins.") # Parse response and extract plugin names (assuming response is newline-separated text) - plugins = [line.strip() for line in response.splitlines() if line.strip()] - return plugins + return [] diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 295c6dd115..8440faf2ab 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -94,3 +94,11 @@ def test_deleter_commands(plugins, dpf_load_response): assert not hasattr( plugins._mapdl, command ), f"Command {command} should be deleted" + + +def test_unload_plugin_twice(plugins): + plugins.load(TEST_PLUGIN) + assert f"Close of the {TEST_PLUGIN} Plugin" in plugins.unload(TEST_PLUGIN) + assert ( + plugins.unload(TEST_PLUGIN) == "" + ), "Unloading a plugin twice should return an empty string"