From 4becb216d8d75d8d1ae8d6deadcf3c952c3ff366 Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Tue, 7 Jan 2025 09:16:22 +0100 Subject: [PATCH 01/19] FEAT: min AEDT version decorator This decorator should be compatible in many classes of PyAEDT. However, there is a limitation which requires the decorated object to be able to access the AEDT desktop. Current ways to achieve such check is to leverage any existing attribute among odesktop, _odesktop and _desktop. --- src/ansys/aedt/core/generic/checks.py | 71 +++++++++++++++++++++++++++ src/ansys/aedt/core/generic/errors.py | 43 ++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/ansys/aedt/core/generic/checks.py create mode 100644 src/ansys/aedt/core/generic/errors.py diff --git a/src/ansys/aedt/core/generic/checks.py b/src/ansys/aedt/core/generic/checks.py new file mode 100644 index 00000000000..e9fc1d35b6a --- /dev/null +++ b/src/ansys/aedt/core/generic/checks.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 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. + +"""Provides functions for performing common checks.""" + +from ansys.aedt.core.generic.errors import AEDTRuntimeError + + +def min_aedt_version(min_version: str): + """Compare a minimum required version to the current AEDT version. + + This decorator should only be used on methods where the associated object can reach the desktop instance. + Otherwise, there is no way to check version compatibility and an error is raised. + + Parameters + ---------- + min_version: str + Minimum AEDT version required by the method. + The value should follow the format YEAR.RELEASE, for example '2024.2'. + + Raises + ------ + + AEDTRuntimeError + If the method version is higher than the AEDT version. + """ + + def aedt_version_decorator(method): + def wrapper(self, *args, **kwargs): + attributes_to_check = ["odesktop", "_odesktop", "_desktop"] + # Browse attributes and retrieve the first non-null value + available_attributes = [attr for attr in attributes_to_check if hasattr(self, attr)] + if not available_attributes: + raise AEDTRuntimeError("The desktop is not available.") + odesktop = next( + (getattr(self, attr) for attr in available_attributes if getattr(self, attr) is not None), None + ) + if odesktop is not None: + desktop_version = odesktop.GetVersion() + if desktop_version < min_version: + raise AEDTRuntimeError( + f"The method '{method.__name__}' requires a minimum Ansys release version of " + + f"{min_version}, but the current version used is {desktop_version}." + ) + else: + return method(self, *args, **kwargs) + + return wrapper + + return aedt_version_decorator diff --git a/src/ansys/aedt/core/generic/errors.py b/src/ansys/aedt/core/generic/errors.py new file mode 100644 index 00000000000..3dbcaa2e3b6 --- /dev/null +++ b/src/ansys/aedt/core/generic/errors.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 - 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. + +"""Provide PyAEDT errors.""" + + +class GrpcApiError(RuntimeError): + """Exception raised for errors encountered while interacting with the gRPC API.""" + + pass + + +class MethodNotSupportedError(RuntimeError): + """Exception raised when attempting to call a method that is not supported.""" + + pass + + +class AEDTRuntimeError(RuntimeError): + """Exception raised for errors occurring during the runtime execution of AEDT scripts.""" + + pass From 355613d3a6ef813846d877248a7a444f8c4288c6 Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Tue, 7 Jan 2025 09:18:10 +0100 Subject: [PATCH 02/19] TESTS: min AEDT version decorator --- tests/unit/test_utils.py | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 7e29982b83f..9318471dd3b 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -27,10 +27,13 @@ import logging import os +import time from unittest.mock import MagicMock from unittest.mock import PropertyMock from unittest.mock import patch +from ansys.aedt.core.generic.checks import AEDTRuntimeError +from ansys.aedt.core.generic.checks import min_aedt_version from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.settings import ALLOWED_AEDT_ENV_VAR_SETTINGS from ansys.aedt.core.generic.settings import ALLOWED_GENERAL_SETTINGS @@ -266,3 +269,58 @@ def test_write_toml(tmp_path): _create_toml_file(TOML_DATA, file_path) assert file_path.exists() + + +CURRENT_YEAR = current_year = time.localtime().tm_year +CURRENT_YEAR_VERSION = f"{CURRENT_YEAR}.2" +NEXT_YEAR_VERSION = f"{CURRENT_YEAR + 1}.2" +PREVIOUS_YEAR_VERSION = f"{CURRENT_YEAR - 1}.2" + + +def test_min_aedt_version_success(): + class Dummy: + """Dummy class to test min version.""" + + odesktop = MagicMock() + odesktop.GetVersion.return_value = CURRENT_YEAR_VERSION + + @min_aedt_version(PREVIOUS_YEAR_VERSION) + def old_method(self): + pass + + dummy = Dummy() + + dummy.old_method() + + +def test_min_aedt_version_raise_error_on_future_version(): + class Dummy: + """Dummy class to test min version.""" + + odesktop = MagicMock() + odesktop.GetVersion.return_value = CURRENT_YEAR_VERSION + + @min_aedt_version(NEXT_YEAR_VERSION) + def future_method(self): + pass + + dummy = Dummy() + pattern = ( + f"The method 'future_method' requires a minimum Ansys release version of {NEXT_YEAR_VERSION}, " + "but the current version used is .+" + ) + + with pytest.raises(AEDTRuntimeError, match=pattern): + dummy.future_method() + + +def test_min_aedt_version_raise_error_on_non_decorable_object(): + class Dummy: + @min_aedt_version(PREVIOUS_YEAR_VERSION) + def dummy_method(self): + pass + + dummy = Dummy() + + with pytest.raises(AEDTRuntimeError, match="The desktop is not available."): + dummy.dummy_method() From d1ecd6bb42b5a9a72606f25998531587d3359f7b Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Tue, 7 Jan 2025 09:20:05 +0100 Subject: [PATCH 03/19] REFACTOR: Use errors.py exceptions --- src/ansys/aedt/core/application/design.py | 2 +- src/ansys/aedt/core/application/variables.py | 2 +- src/ansys/aedt/core/generic/configurations.py | 2 +- src/ansys/aedt/core/generic/general_methods.py | 14 ++------------ .../aedt/core/generic/grpc_plugin_dll_class.py | 2 +- src/ansys/aedt/core/icepak.py | 2 +- src/ansys/aedt/core/modeler/modeler_3d.py | 2 +- .../aedt/core/modules/boundary/layout_boundary.py | 2 +- src/ansys/aedt/core/modules/mesh.py | 2 +- src/ansys/aedt/core/modules/mesh_icepak.py | 2 +- .../aedt/core/visualization/post/field_data.py | 2 +- 11 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/ansys/aedt/core/application/design.py b/src/ansys/aedt/core/application/design.py index 4f5ba3ea57d..9c3542056cc 100644 --- a/src/ansys/aedt/core/application/design.py +++ b/src/ansys/aedt/core/application/design.py @@ -68,7 +68,7 @@ from ansys.aedt.core.generic.constants import AEDT_UNITS from ansys.aedt.core.generic.constants import unit_system from ansys.aedt.core.generic.data_handlers import variation_string_to_dict -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import check_and_download_file from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import inner_project_settings diff --git a/src/ansys/aedt/core/application/variables.py b/src/ansys/aedt/core/application/variables.py index 84a545533fb..b5f698e2d6b 100644 --- a/src/ansys/aedt/core/application/variables.py +++ b/src/ansys/aedt/core/application/variables.py @@ -50,7 +50,7 @@ from ansys.aedt.core.generic.constants import SI_UNITS from ansys.aedt.core.generic.constants import _resolve_unit_system from ansys.aedt.core.generic.constants import unit_system -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import check_numeric_equivalence from ansys.aedt.core.generic.general_methods import is_array from ansys.aedt.core.generic.general_methods import is_number diff --git a/src/ansys/aedt/core/generic/configurations.py b/src/ansys/aedt/core/generic/configurations.py index 4dc4af88361..1c74729882b 100644 --- a/src/ansys/aedt/core/generic/configurations.py +++ b/src/ansys/aedt/core/generic/configurations.py @@ -32,7 +32,7 @@ from ansys.aedt.core import __version__ from ansys.aedt.core.application.variables import decompose_variable_value from ansys.aedt.core.generic.data_handlers import _arg2dict -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import generate_unique_folder_name from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import open_file diff --git a/src/ansys/aedt/core/generic/general_methods.py b/src/ansys/aedt/core/generic/general_methods.py index 76f6c91a94e..af149b8bb32 100644 --- a/src/ansys/aedt/core/generic/general_methods.py +++ b/src/ansys/aedt/core/generic/general_methods.py @@ -47,6 +47,8 @@ from ansys.aedt.core.aedt_logger import pyaedt_logger from ansys.aedt.core.generic.aedt_versions import aedt_versions from ansys.aedt.core.generic.constants import CSS4_COLORS +from ansys.aedt.core.generic.errors import GrpcApiError +from ansys.aedt.core.generic.errors import MethodNotSupportedError from ansys.aedt.core.generic.settings import inner_project_settings # noqa: F401 from ansys.aedt.core.generic.settings import settings import psutil @@ -67,18 +69,6 @@ ] -class GrpcApiError(Exception): - """ """ - - pass - - -class MethodNotSupportedError(Exception): - """ """ - - pass - - def _write_mes(mes_text): mes_text = str(mes_text) parts = [mes_text[i : i + 250] for i in range(0, len(mes_text), 250)] diff --git a/src/ansys/aedt/core/generic/grpc_plugin_dll_class.py b/src/ansys/aedt/core/generic/grpc_plugin_dll_class.py index de715b3d428..701c0e59e32 100644 --- a/src/ansys/aedt/core/generic/grpc_plugin_dll_class.py +++ b/src/ansys/aedt/core/generic/grpc_plugin_dll_class.py @@ -34,7 +34,7 @@ import time import types -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import _retry_ntimes from ansys.aedt.core.generic.general_methods import inclusion_list from ansys.aedt.core.generic.general_methods import settings diff --git a/src/ansys/aedt/core/icepak.py b/src/ansys/aedt/core/icepak.py index e449ea60c2b..9dc1fa4e2b8 100644 --- a/src/ansys/aedt/core/icepak.py +++ b/src/ansys/aedt/core/icepak.py @@ -36,7 +36,7 @@ from ansys.aedt.core.generic.data_handlers import _arg2dict from ansys.aedt.core.generic.data_handlers import _dict2arg from ansys.aedt.core.generic.data_handlers import random_string -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import pyaedt_function_handler diff --git a/src/ansys/aedt/core/modeler/modeler_3d.py b/src/ansys/aedt/core/modeler/modeler_3d.py index 4945c344536..56c6d779c65 100644 --- a/src/ansys/aedt/core/modeler/modeler_3d.py +++ b/src/ansys/aedt/core/modeler/modeler_3d.py @@ -31,7 +31,7 @@ import warnings from ansys.aedt.core.application.variables import generate_validation_errors -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import pyaedt_function_handler diff --git a/src/ansys/aedt/core/modules/boundary/layout_boundary.py b/src/ansys/aedt/core/modules/boundary/layout_boundary.py index e539655934d..e21c3108874 100644 --- a/src/ansys/aedt/core/modules/boundary/layout_boundary.py +++ b/src/ansys/aedt/core/modules/boundary/layout_boundary.py @@ -24,7 +24,7 @@ from ansys.aedt.core.generic.data_handlers import _dict2arg from ansys.aedt.core.generic.data_handlers import random_string -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.modeler.cad.elements_3d import BinaryTreeNode from ansys.aedt.core.modules.boundary.common import BoundaryCommon diff --git a/src/ansys/aedt/core/modules/mesh.py b/src/ansys/aedt/core/modules/mesh.py index 0053300274b..e9bf55741ec 100644 --- a/src/ansys/aedt/core/modules/mesh.py +++ b/src/ansys/aedt/core/modules/mesh.py @@ -31,7 +31,7 @@ from ansys.aedt.core.application.design_solutions import model_names from ansys.aedt.core.generic.data_handlers import _dict2arg -from ansys.aedt.core.generic.general_methods import MethodNotSupportedError +from ansys.aedt.core.generic.errors import MethodNotSupportedError from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import pyaedt_function_handler from ansys.aedt.core.generic.general_methods import settings diff --git a/src/ansys/aedt/core/modules/mesh_icepak.py b/src/ansys/aedt/core/modules/mesh_icepak.py index 252cadd6b20..d6a271abe6e 100644 --- a/src/ansys/aedt/core/modules/mesh_icepak.py +++ b/src/ansys/aedt/core/modules/mesh_icepak.py @@ -26,7 +26,7 @@ import warnings from ansys.aedt.core.generic.data_handlers import _dict2arg -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import _dim_arg from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import pyaedt_function_handler diff --git a/src/ansys/aedt/core/visualization/post/field_data.py b/src/ansys/aedt/core/visualization/post/field_data.py index 2882fae70c6..efc01da5ea6 100644 --- a/src/ansys/aedt/core/visualization/post/field_data.py +++ b/src/ansys/aedt/core/visualization/post/field_data.py @@ -34,7 +34,7 @@ from ansys.aedt.core.generic.constants import AllowedMarkers from ansys.aedt.core.generic.constants import EnumUnits from ansys.aedt.core.generic.data_handlers import _dict2arg -from ansys.aedt.core.generic.general_methods import GrpcApiError +from ansys.aedt.core.generic.errors import GrpcApiError from ansys.aedt.core.generic.general_methods import check_and_download_file from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import pyaedt_function_handler From 697848defad6cce8cacb51591df4fa13adf02b0b Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Tue, 7 Jan 2025 10:45:28 +0100 Subject: [PATCH 04/19] REFACTOR: Remove Coday issue --- src/ansys/aedt/core/generic/errors.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ansys/aedt/core/generic/errors.py b/src/ansys/aedt/core/generic/errors.py index 3dbcaa2e3b6..d12450d2d4f 100644 --- a/src/ansys/aedt/core/generic/errors.py +++ b/src/ansys/aedt/core/generic/errors.py @@ -28,16 +28,10 @@ class GrpcApiError(RuntimeError): """Exception raised for errors encountered while interacting with the gRPC API.""" - pass - class MethodNotSupportedError(RuntimeError): """Exception raised when attempting to call a method that is not supported.""" - pass - class AEDTRuntimeError(RuntimeError): """Exception raised for errors occurring during the runtime execution of AEDT scripts.""" - - pass From 6588e5b09c5e09857f14b7750e974fd68e485e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Morais?= <146729917+SMoraisAnsys@users.noreply.github.com> Date: Wed, 8 Jan 2025 10:01:24 +0100 Subject: [PATCH 05/19] Update tests/unit/test_utils.py Co-authored-by: Maxime Rey <87315832+MaxJPRey@users.noreply.github.com> --- tests/unit/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 9318471dd3b..74225e2ca1c 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -289,7 +289,6 @@ def old_method(self): pass dummy = Dummy() - dummy.old_method() From d3b1bef872b936a8902916363abb20987cf0b4b4 Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Wed, 8 Jan 2025 10:03:26 +0100 Subject: [PATCH 06/19] TESTS: Move constants at file begining --- tests/unit/test_utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 74225e2ca1c..c15d113fe3a 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -47,6 +47,10 @@ SETTINGS_ENABLE_ERROR_HANDLER = settings.enable_error_handler ERROR_MESSAGE = "Dummy message." TOML_DATA = {"key_0": "dummy", "key_1": 12, "key_2": [1, 2], "key_3": {"key_4": 42}} +CURRENT_YEAR = current_year = time.localtime().tm_year +CURRENT_YEAR_VERSION = f"{CURRENT_YEAR}.2" +NEXT_YEAR_VERSION = f"{CURRENT_YEAR + 1}.2" +PREVIOUS_YEAR_VERSION = f"{CURRENT_YEAR - 1}.2" @pytest.fixture(scope="module", autouse=True) @@ -271,12 +275,6 @@ def test_write_toml(tmp_path): assert file_path.exists() -CURRENT_YEAR = current_year = time.localtime().tm_year -CURRENT_YEAR_VERSION = f"{CURRENT_YEAR}.2" -NEXT_YEAR_VERSION = f"{CURRENT_YEAR + 1}.2" -PREVIOUS_YEAR_VERSION = f"{CURRENT_YEAR - 1}.2" - - def test_min_aedt_version_success(): class Dummy: """Dummy class to test min version.""" From 65dd2a13362fb1cb14575a6fdafcc36282d21b17 Mon Sep 17 00:00:00 2001 From: Giulia Malinverno Date: Mon, 13 Jan 2025 11:25:23 +0100 Subject: [PATCH 07/19] add decorator in couplings.py --- src/ansys/aedt/core/emit_core/couplings.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ansys/aedt/core/emit_core/couplings.py b/src/ansys/aedt/core/emit_core/couplings.py index a7166894664..ef7e67cc5cd 100644 --- a/src/ansys/aedt/core/emit_core/couplings.py +++ b/src/ansys/aedt/core/emit_core/couplings.py @@ -27,7 +27,7 @@ This module provides for interacting with EMIT Analysis and Results windows. """ -import warnings +from ansys.aedt.core.generic.checks import min_aedt_version class CouplingsEmit(object): @@ -156,9 +156,12 @@ def update_link(self, coupling_name): self._odesign.UpdateLink(coupling_name) @property + @min_aedt_version("2022.2") def linkable_design_names(self): """List the available link names. + This property is only available in AEDT version 2022.2 or higher. + Parameters ---------- None @@ -174,12 +177,7 @@ def linkable_design_names(self): >>> app = Emit("Apache with multiple links") >>> links = app.couplings.linkable_design_names """ - desktop_version = self._desktop.GetVersion()[0:6] - if desktop_version >= "2022.2": - return self._odesign.GetAvailableLinkNames() - else: - warnings.warn("The function linkable_design_names() requires AEDT 2022 R2 or later.") - return [] + return self._odesign.GetAvailableLinkNames() @property def cad_nodes(self): From 4d724fc3939ae2ea4089901754ff011124a2e38c Mon Sep 17 00:00:00 2001 From: Giulia Malinverno Date: Mon, 13 Jan 2025 11:35:17 +0100 Subject: [PATCH 08/19] add decorator in object_3d.py --- src/ansys/aedt/core/modeler/cad/object_3d.py | 41 ++++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/ansys/aedt/core/modeler/cad/object_3d.py b/src/ansys/aedt/core/modeler/cad/object_3d.py index 157acc22ae7..757b75dc402 100644 --- a/src/ansys/aedt/core/modeler/cad/object_3d.py +++ b/src/ansys/aedt/core/modeler/cad/object_3d.py @@ -37,6 +37,7 @@ import os import re +from ansys.aedt.core.generic.checks import min_aedt_version from ansys.aedt.core.generic.constants import AEDT_UNITS from ansys.aedt.core.generic.general_methods import _to_boolean from ansys.aedt.core.generic.general_methods import _uname @@ -253,6 +254,7 @@ def _odesign(self): return self._primitives._modeler._app._odesign @pyaedt_function_handler() + @min_aedt_version("2021.2") def plot(self, show=True): """Plot model with PyVista. @@ -270,19 +272,18 @@ def plot(self, show=True): ----- Works from AEDT 2021.2 in CPython only. PyVista has to be installed. """ - if self._primitives._app._aedt_version >= "2021.2": - return self._primitives._app.post.plot_model_obj( - objects=[self.name], - plot_as_separate_objects=True, - plot_air_objects=True, - show=show, - ) + return self._primitives._app.post.plot_model_obj( + objects=[self.name], + plot_as_separate_objects=True, + plot_air_objects=True, + show=show, + ) @pyaedt_function_handler(file_path="output_file") + @min_aedt_version("2021.2") def export_image(self, output_file=None): """Export the current object to a specified file path. - .. note:: Works from AEDT 2021.2 in CPython only. PyVista has to be installed. @@ -298,19 +299,17 @@ def export_image(self, output_file=None): str File path. """ - if self._primitives._app._aedt_version >= "2021.2": - if not output_file: - output_file = os.path.join(self._primitives._app.working_directory, self.name + ".png") - model_obj = self._primitives._app.post.plot_model_obj( - objects=[self.name], - show=False, - export_path=output_file, - plot_as_separate_objects=True, - clean_files=True, - ) - if model_obj: - return model_obj.image_file - return False + if not output_file: + output_file = os.path.join(self._primitives._app.working_directory, self.name + ".png") + model_obj = self._primitives._app.post.plot_model_obj( + objects=[self.name], + show=False, + export_path=output_file, + plot_as_separate_objects=True, + clean_files=True, + ) + if model_obj: + return model_obj.image_file @pyaedt_function_handler() def touching_conductors(self): From 27ff34d694a163370323114c4876bf8bf974d1d4 Mon Sep 17 00:00:00 2001 From: Giulia Malinverno Date: Mon, 13 Jan 2025 11:41:09 +0100 Subject: [PATCH 09/19] add decorator in analysis_3d.py.py --- .../aedt/core/application/analysis_3d.py | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/src/ansys/aedt/core/application/analysis_3d.py b/src/ansys/aedt/core/application/analysis_3d.py index 56c862032e4..9feda48dc05 100644 --- a/src/ansys/aedt/core/application/analysis_3d.py +++ b/src/ansys/aedt/core/application/analysis_3d.py @@ -27,6 +27,7 @@ import os from ansys.aedt.core.application.analysis import Analysis +from ansys.aedt.core.generic.checks import min_aedt_version from ansys.aedt.core.generic.configurations import Configurations from ansys.aedt.core.generic.constants import unit_converter from ansys.aedt.core.generic.general_methods import generate_unique_name @@ -224,6 +225,7 @@ def components3d(self): return components_dict @pyaedt_function_handler(objects="assignment", export_path="output_file") + @min_aedt_version("2021.2") def plot( self, assignment=None, @@ -280,23 +282,20 @@ def plot( :class:`ansys.aedt.core.generic.plot.ModelPlotter` Model Object. """ - if self._aedt_version < "2021.2": - self.logger.warning("Plot is supported from AEDT 2021 R2.") - else: - return self.post.plot_model_obj( - objects=assignment, - show=show, - export_path=output_file, - plot_as_separate_objects=plot_as_separate_objects, - plot_air_objects=plot_air_objects, - force_opacity_value=force_opacity_value, - clean_files=clean_files, - view=view, - show_legend=show_legend, - dark_mode=dark_mode, - show_bounding=show_bounding, - show_grid=show_grid, - ) + return self.post.plot_model_obj( + objects=assignment, + show=show, + export_path=output_file, + plot_as_separate_objects=plot_as_separate_objects, + plot_air_objects=plot_air_objects, + force_opacity_value=force_opacity_value, + clean_files=clean_files, + view=view, + show_legend=show_legend, + dark_mode=dark_mode, + show_bounding=show_bounding, + show_grid=show_grid, + ) @pyaedt_function_handler(setup_name="setup", variation_string="variations") def export_mesh_stats(self, setup, variations="", mesh_path=None): @@ -1161,9 +1160,6 @@ def identify_touching_conductors(self, assignment=None): dict """ - if settings.aedt_version < "2023.2": # pragma: no cover - self.logger.error("This method requires CPython and PyVista.") - return {} if settings.aedt_version >= "2023.2" and self.design_type == "HFSS": # pragma: no cover nets_aedt = self.oboundary.IdentifyNets(True) nets = {} From 9f045fc137990c473d67a4034c8237abd7425dd1 Mon Sep 17 00:00:00 2001 From: Giulia Malinverno Date: Mon, 13 Jan 2025 11:42:41 +0100 Subject: [PATCH 10/19] add decorator in hfss3dlayout.py.py.py --- src/ansys/aedt/core/hfss3dlayout.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ansys/aedt/core/hfss3dlayout.py b/src/ansys/aedt/core/hfss3dlayout.py index 2a2e4a5d73b..5481ddede9d 100644 --- a/src/ansys/aedt/core/hfss3dlayout.py +++ b/src/ansys/aedt/core/hfss3dlayout.py @@ -34,6 +34,7 @@ from ansys.aedt.core.application.analysis_3d_layout import FieldAnalysis3DLayout from ansys.aedt.core.application.analysis_hf import ScatteringMethods +from ansys.aedt.core.generic.checks import min_aedt_version from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import parse_excitation_file @@ -916,6 +917,7 @@ def export_touchstone_on_completion(self, export=True, output_dir=""): return True @pyaedt_function_handler() + @min_aedt_version("2025.1") def set_export_touchstone( self, file_format="TouchStone1.0", @@ -996,9 +998,6 @@ def set_export_touchstone( """ - if settings.aedt_version < "2025.1": - self.logger.warning("Touchstone export setup aborted. This method is available from AEDT 2025.1.") - return False preferences = "Planar EM\\" design_name = self.design_name From 30da09c81096008a7b501438aaf4851c504aebaf Mon Sep 17 00:00:00 2001 From: Giulia Malinverno Date: Mon, 13 Jan 2025 11:45:24 +0100 Subject: [PATCH 11/19] add decorator in component_array.py --- src/ansys/aedt/core/modeler/cad/component_array.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ansys/aedt/core/modeler/cad/component_array.py b/src/ansys/aedt/core/modeler/cad/component_array.py index bd54b5caec7..d5e5d84373e 100644 --- a/src/ansys/aedt/core/modeler/cad/component_array.py +++ b/src/ansys/aedt/core/modeler/cad/component_array.py @@ -27,6 +27,7 @@ import os import re +from ansys.aedt.core.generic.checks import min_aedt_version from ansys.aedt.core.generic.constants import AEDT_UNITS from ansys.aedt.core.generic.general_methods import _uname from ansys.aedt.core.generic.general_methods import pyaedt_function_handler @@ -368,6 +369,7 @@ def delete(self): self.__app.component_array_names = list(self.__app.get_oo_name(self.__app.odesign, "Model")) @pyaedt_function_handler(array_path="output_file") + @min_aedt_version("2024.1") def export_array_info(self, output_file=None): # pragma: no cover """Export array information to a CSV file. @@ -379,12 +381,7 @@ def export_array_info(self, output_file=None): # pragma: no cover References ---------- >>> oModule.ExportArray - """ - if self.__app.settings.aedt_version < "2024.1": # pragma: no cover - self.logger.warning(f"This feature is not available in {str(self.__app.settings.aedt_version)}.") - return False - if not output_file: # pragma: no cover output_file = os.path.join(self.__app.toolkit_directory, "array_info.csv") self.__app.omodelsetup.ExportArray(self.name, output_file) From 202657091bc8513fc3f7f081ba9a52d0db05a9ee Mon Sep 17 00:00:00 2001 From: Giulia Malinverno Date: Mon, 13 Jan 2025 11:48:22 +0100 Subject: [PATCH 12/19] add decorator in post_icepak.py --- src/ansys/aedt/core/visualization/post/post_icepak.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ansys/aedt/core/visualization/post/post_icepak.py b/src/ansys/aedt/core/visualization/post/post_icepak.py index d093a675751..0c8d8810538 100644 --- a/src/ansys/aedt/core/visualization/post/post_icepak.py +++ b/src/ansys/aedt/core/visualization/post/post_icepak.py @@ -34,10 +34,10 @@ import os import re +from ansys.aedt.core.generic.checks import min_aedt_version from ansys.aedt.core.generic.general_methods import generate_unique_name from ansys.aedt.core.generic.general_methods import open_file from ansys.aedt.core.generic.general_methods import pyaedt_function_handler -from ansys.aedt.core.generic.settings import settings from ansys.aedt.core.visualization.post.field_summary import FieldSummary from ansys.aedt.core.visualization.post.field_summary import TOTAL_QUANTITIES from ansys.aedt.core.visualization.post.post_common_3d import PostProcessor3D @@ -283,6 +283,7 @@ def evaluate_boundary_quantity( return self._parse_field_summary_content(fs, setup_name, variations, quantity) @pyaedt_function_handler(monitor_name="monitor", quantity_name="quantity", design_variation="variations") + @min_aedt_version("2024.1") def evaluate_monitor_quantity( self, monitor, quantity, side="Default", setup_name=None, variations=None, ref_temperature="", time="0s" ): @@ -322,8 +323,6 @@ def evaluate_monitor_quantity( """ if variations is None: variations = {} - if settings.aedt_version < "2024.1": - raise NotImplementedError("Monitors are not supported in field summary in versions earlier than 2024 R1.") else: # pragma: no cover if self._app.monitor.face_monitors.get(monitor, None): field_type = "Surface" From 01b060875db426a11e4ff3eaba66f0e569b55f61 Mon Sep 17 00:00:00 2001 From: Giulia Malinverno Date: Mon, 13 Jan 2025 11:54:16 +0100 Subject: [PATCH 13/19] add decorator in analysis_3d.py --- src/ansys/aedt/core/application/analysis_3d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ansys/aedt/core/application/analysis_3d.py b/src/ansys/aedt/core/application/analysis_3d.py index 9feda48dc05..3b4ca2fc9ad 100644 --- a/src/ansys/aedt/core/application/analysis_3d.py +++ b/src/ansys/aedt/core/application/analysis_3d.py @@ -1144,6 +1144,7 @@ def flatten_3d_components(self, components=None, purge_history=True, password=No return True @pyaedt_function_handler(object_name="assignment") + @min_aedt_version("2023.2") def identify_touching_conductors(self, assignment=None): # type: (str) -> dict """Identify all touching components and group in a dictionary. @@ -1160,7 +1161,7 @@ def identify_touching_conductors(self, assignment=None): dict """ - if settings.aedt_version >= "2023.2" and self.design_type == "HFSS": # pragma: no cover + if self.design_type == "HFSS": # pragma: no cover nets_aedt = self.oboundary.IdentifyNets(True) nets = {} for net in nets_aedt[1:]: From 96966a565c9689e27e1c3a02dc3ac9189434086a Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Mon, 13 Jan 2025 16:16:20 +0100 Subject: [PATCH 14/19] FIX: Extend decorator use cases --- src/ansys/aedt/core/generic/checks.py | 52 ++++++++++++++++++--------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/ansys/aedt/core/generic/checks.py b/src/ansys/aedt/core/generic/checks.py index e9fc1d35b6a..5fc3fb5be7d 100644 --- a/src/ansys/aedt/core/generic/checks.py +++ b/src/ansys/aedt/core/generic/checks.py @@ -46,25 +46,43 @@ def min_aedt_version(min_version: str): If the method version is higher than the AEDT version. """ + def fetch_odesktop_from_common_attributes_names(item): + attributes_to_check = ["odesktop", "_odesktop", "_desktop"] + for attribute in attributes_to_check: + odesktop = getattr(item, attribute, None) + if odesktop is not None: + return odesktop + + def fetch_odesktop_from_private_app_attribute(item): + app = getattr(item, f"_{item.__class__.__name__}__app", None) + if app is not None: + return app.odesktop + + def fetch_odesktop_from_desktop_class(item): + attributes_to_check = ["desktop_class", "_desktop_class"] + for attribute in attributes_to_check: + desktop_class = getattr(item, attribute, None) + if desktop_class is not None: + return desktop_class.odesktop + def aedt_version_decorator(method): def wrapper(self, *args, **kwargs): - attributes_to_check = ["odesktop", "_odesktop", "_desktop"] - # Browse attributes and retrieve the first non-null value - available_attributes = [attr for attr in attributes_to_check if hasattr(self, attr)] - if not available_attributes: - raise AEDTRuntimeError("The desktop is not available.") - odesktop = next( - (getattr(self, attr) for attr in available_attributes if getattr(self, attr) is not None), None - ) - if odesktop is not None: - desktop_version = odesktop.GetVersion() - if desktop_version < min_version: - raise AEDTRuntimeError( - f"The method '{method.__name__}' requires a minimum Ansys release version of " - + f"{min_version}, but the current version used is {desktop_version}." - ) - else: - return method(self, *args, **kwargs) + odesktop = fetch_odesktop_from_common_attributes_names(self) + if odesktop is None: + odesktop = fetch_odesktop_from_private_app_attribute(self) + if odesktop is None: + odesktop = fetch_odesktop_from_desktop_class(self) + if odesktop is None: + raise AEDTRuntimeError("The AEDT desktop object is not available.") + + desktop_version = odesktop.GetVersion() + if desktop_version < min_version: + raise AEDTRuntimeError( + f"The method '{method.__name__}' requires a minimum Ansys release version of " + + f"{min_version}, but the current version used is {desktop_version}." + ) + else: + return method(self, *args, **kwargs) return wrapper From 5e4e6ee27620f2b4df519235b19dce8f3973b289 Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Mon, 13 Jan 2025 16:17:08 +0100 Subject: [PATCH 15/19] TESTS: Test extra use cases --- tests/unit/test_utils.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index c15d113fe3a..070fc9aaf1c 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -275,12 +275,45 @@ def test_write_toml(tmp_path): assert file_path.exists() -def test_min_aedt_version_success(): +def test_min_aedt_version_success_with_common_attributes_names(): class Dummy: - """Dummy class to test min version.""" + """Dummy class to test min version with common attribute.""" + + odesktop = MagicMock() + odesktop.GetVersion.return_value = CURRENT_YEAR_VERSION + + @min_aedt_version(PREVIOUS_YEAR_VERSION) + def old_method(self): + pass + + dummy = Dummy() + dummy.old_method() + + +def test_min_aedt_version_success_with_app_private_attribute(): + class Dummy: + """Dummy class to test min version with __app attribute.""" + + odesktop = MagicMock() + odesktop.GetVersion.return_value = CURRENT_YEAR_VERSION + __app = odesktop + + @min_aedt_version(PREVIOUS_YEAR_VERSION) + def old_method(self): + pass + + dummy = Dummy() + dummy.old_method() + + +def test_min_aedt_version_success_with_desktop_class(): + class Dummy: + """Dummy class to test min version with __app attribute.""" odesktop = MagicMock() odesktop.GetVersion.return_value = CURRENT_YEAR_VERSION + desktop_class = MagicMock() + desktop_class.odesktop = odesktop @min_aedt_version(PREVIOUS_YEAR_VERSION) def old_method(self): From c3aa333463a2e10e98840fa3cc834edc37ecc6d5 Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Mon, 13 Jan 2025 17:38:36 +0100 Subject: [PATCH 16/19] TEST: Update exception msg --- tests/unit/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 070fc9aaf1c..1db36e5d092 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -352,5 +352,5 @@ def dummy_method(self): dummy = Dummy() - with pytest.raises(AEDTRuntimeError, match="The desktop is not available."): + with pytest.raises(AEDTRuntimeError, match="The AEDT desktop object is not available."): dummy.dummy_method() From 33d43f495d556a46c44ecd587ff2aa7e7e50b42e Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Tue, 14 Jan 2025 13:41:41 +0100 Subject: [PATCH 17/19] TESTS: Fix expected test behavior --- tests/system/solvers/test_00_analyze.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/system/solvers/test_00_analyze.py b/tests/system/solvers/test_00_analyze.py index bfdd63ac9ee..350b206724c 100644 --- a/tests/system/solvers/test_00_analyze.py +++ b/tests/system/solvers/test_00_analyze.py @@ -33,6 +33,7 @@ from ansys.aedt.core import Icepak from ansys.aedt.core import Maxwell3d from ansys.aedt.core import Rmxprt +from ansys.aedt.core.generic.errors import AEDTRuntimeError from ansys.aedt.core.generic.settings import is_linux from ansys.aedt.core.visualization.post.spisim import SpiSim import pytest @@ -316,7 +317,7 @@ def test_03a_icepak_analyze_and_export_summary(self): out = self.icepak_app.post.evaluate_boundary_quantity(opening.name, "Ux") assert out["Mean"] if self.icepak_app.settings.aedt_version < "2024.1": - with pytest.raises(NotImplementedError): + with pytest.raises(AEDTRuntimeError): self.icepak_app.post.evaluate_monitor_quantity("test_monitor2", "Temperature") else: out = self.icepak_app.post.evaluate_monitor_quantity("test_monitor2", "Temperature") @@ -409,7 +410,8 @@ def test_04b_3dl_analyze_setup(self): if desktop_version > "2024.2": assert self.hfss3dl_solve.set_export_touchstone() else: - assert not self.hfss3dl_solve.set_export_touchstone() + with pytest.raises(AEDTRuntimeError): + self.hfss3dl_solve.set_export_touchstone() assert self.hfss3dl_solve.analyze_setup("Setup1", cores=4, blocking=False) assert self.hfss3dl_solve.are_there_simulations_running assert self.hfss3dl_solve.stop_simulations() From 47c0a8bc00fec019b48cfa8d59355880629855e2 Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Tue, 14 Jan 2025 13:48:45 +0100 Subject: [PATCH 18/19] FIX: Wrong behavior in evaluate_monitor_quantity --- .../core/visualization/post/post_icepak.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/ansys/aedt/core/visualization/post/post_icepak.py b/src/ansys/aedt/core/visualization/post/post_icepak.py index 0c8d8810538..bc9d29fee1e 100644 --- a/src/ansys/aedt/core/visualization/post/post_icepak.py +++ b/src/ansys/aedt/core/visualization/post/post_icepak.py @@ -323,18 +323,17 @@ def evaluate_monitor_quantity( """ if variations is None: variations = {} - else: # pragma: no cover - if self._app.monitor.face_monitors.get(monitor, None): - field_type = "Surface" - elif self._app.monitor.point_monitors.get(monitor, None): - field_type = "Volume" - else: - raise AttributeError(f"Monitor {monitor} is not found in the design.") - fs = self.create_field_summary() - fs.add_calculation( - "Monitor", field_type, monitor, quantity, side=side, ref_temperature=ref_temperature, time=time - ) - return self._parse_field_summary_content(fs, setup_name, variations, quantity) + if self._app.monitor.face_monitors.get(monitor, None): + field_type = "Surface" + elif self._app.monitor.point_monitors.get(monitor, None): + field_type = "Volume" + else: + raise AttributeError(f"Monitor {monitor} is not found in the design.") + fs = self.create_field_summary() + fs.add_calculation( + "Monitor", field_type, monitor, quantity, side=side, ref_temperature=ref_temperature, time=time + ) + return self._parse_field_summary_content(fs, setup_name, variations, quantity) @pyaedt_function_handler(design_variation="variations") def evaluate_object_quantity( From f1c7db9401aa614b84254badd9259521b0a92877 Mon Sep 17 00:00:00 2001 From: Sebastien Morais Date: Tue, 14 Jan 2025 17:25:02 +0100 Subject: [PATCH 19/19] REFACTOR: Apply review changes --- src/ansys/aedt/core/generic/checks.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ansys/aedt/core/generic/checks.py b/src/ansys/aedt/core/generic/checks.py index 5fc3fb5be7d..50c0b3d356f 100644 --- a/src/ansys/aedt/core/generic/checks.py +++ b/src/ansys/aedt/core/generic/checks.py @@ -67,11 +67,11 @@ def fetch_odesktop_from_desktop_class(item): def aedt_version_decorator(method): def wrapper(self, *args, **kwargs): - odesktop = fetch_odesktop_from_common_attributes_names(self) - if odesktop is None: - odesktop = fetch_odesktop_from_private_app_attribute(self) - if odesktop is None: - odesktop = fetch_odesktop_from_desktop_class(self) + odesktop = ( + fetch_odesktop_from_common_attributes_names(self) + or fetch_odesktop_from_private_app_attribute(self) + or fetch_odesktop_from_desktop_class(self) + ) if odesktop is None: raise AEDTRuntimeError("The AEDT desktop object is not available.")