Skip to content

feat: First tentative for Plugin Mapdl Mechanism python API #3627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7a8a716
First tentative for plugin mapdl mechanism
FredAns Dec 19, 2024
538936d
ci: auto fixes from pre-commit.com hooks.
pre-commit-ci[bot] Dec 19, 2024
35bcf88
chore: adding changelog file 3627.miscellaneous.md [dependabot-skip]
pyansys-ci-bot Dec 19, 2024
2dea3bc
ci: auto fixes from pre-commit.com hooks.
pre-commit-ci[bot] Dec 19, 2024
a1d7ee2
ci: auto fixes from pre-commit.com hooks.
pre-commit-ci[bot] Jan 7, 2025
57e2de6
Merge branch 'main' into feat/plugins
germa89 Feb 17, 2025
cd63cc8
Merge branch 'main' into feat/plugins
germa89 Mar 7, 2025
1d2d4d5
chore: merge remote-tracking branch 'origin/main' into feat/plugins
germa89 Apr 23, 2025
4afa907
feat: add plugin error handling classes and improve plugin loading/un…
germa89 Apr 23, 2025
16c744d
test: adding plugin tests
germa89 Apr 23, 2025
6eaf391
Merge branch 'main' into feat/plugins
germa89 Jul 16, 2025
83997de
ci: auto fixes from pre-commit.com hooks.
pre-commit-ci[bot] Jul 16, 2025
ff30eae
chore: adding changelog file 3627.miscellaneous.md [dependabot-skip]
pyansys-ci-bot Jul 16, 2025
fec2b24
feat: rename plugin to plugins. Enhance plugin functionality with com…
germa89 Jul 16, 2025
f4e9ef5
feat: add plugin property to _MapdlCore and remove from MapdlGrpc
germa89 Jul 16, 2025
302ecc1
fix: streamline command replacement in ansPlugin class
germa89 Jul 16, 2025
6dc488f
Update tests/test_plugin.py
germa89 Jul 16, 2025
d1e8471
fix: update plugin response handling to return an empty list and add …
germa89 Jul 16, 2025
3c25fe9
Merge branch 'feat/plugins' of https://github.com/ansys/pymapdl into …
germa89 Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/3627.miscellaneous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Feat: first tentative for plugin mapdl mechanism python api
21 changes: 21 additions & 0 deletions src/ansys/mapdl/core/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,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."""
Expand Down
23 changes: 23 additions & 0 deletions src/ansys/mapdl/core/mapdl_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
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

Expand Down Expand Up @@ -350,6 +351,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)
Expand Down Expand Up @@ -1081,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):
Expand Down
247 changes: 247 additions & 0 deletions src/ansys/mapdl/core/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# 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 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:
"""
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: 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) -> Mapdl:
"""Return the weakly referenced instance of mapdl."""
return self._mapdl_weakref()

@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)
Comment on lines +108 to +109
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The dynamically created 'passer' function may not bind correctly to the MAPDL instance.

Assigning 'passer' directly may cause binding issues with 'self'. Use functools.partial or types.MethodType to properly bind the method to the MAPDL instance.


# 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.

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."
)
self._load_commands(response, plugin_name=plugin_name)
return response

def unload(self, plugin_name: str) -> str:
"""
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 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.

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) 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)
plugins = [line.strip() for line in response.splitlines() if line.strip()]
return plugins
Loading
Loading