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 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/mapdl_core.py b/src/ansys/mapdl/core/mapdl_core.py index 8ed8444d63..266f48a716 100644 --- a/src/ansys/mapdl/core/mapdl_core.py +++ b/src/ansys/mapdl/core/mapdl_core.py @@ -87,6 +87,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 @@ -341,6 +342,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 3a5d075a56..57318412c9 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -106,6 +106,7 @@ 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() @@ -2829,6 +2830,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..4e3334dafe --- /dev/null +++ b/src/ansys/mapdl/core/plugin.py @@ -0,0 +1,129 @@ +# 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. + +"""Contains the ansPlugin class.""" +import weakref + +from ansys.mapdl.core.errors import PluginError, PluginLoadError, PluginUnloadError + + +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 an 'MapdlGrpc' object") + + 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 = "") -> None: + """ + 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. + + 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}'." + ) + self._log.info( + f"Plugin '{plugin_name}' with feature '{feature}' loaded successfully." + ) + + def unload(self, plugin_name: str) -> None: + """ + Unloads a plugin from MAPDL. + + Parameters + ---------- + plugin_name : str + Name of the plugin to unload. + + 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}'.") + self._log.info(f"Plugin '{plugin_name}' unloaded successfully.") + + def list(self) -> list[str]: + """ + 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 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 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)