Skip to content

Commit d187948

Browse files
FIX: Fix issue 6719 get_string_version (#6722)
Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent a44c5ff commit d187948

File tree

6 files changed

+193
-66
lines changed

6 files changed

+193
-66
lines changed

doc/changelog.d/6722.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix issue 6719 get_string_version

src/ansys/aedt/core/desktop.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@
5656
from ansys.aedt.core.generic.file_utils import available_license_feature
5757
from ansys.aedt.core.generic.file_utils import generate_unique_name
5858
from ansys.aedt.core.generic.file_utils import open_file
59+
from ansys.aedt.core.generic.general_methods import _is_version_format_valid
60+
from ansys.aedt.core.generic.general_methods import _normalize_version_to_string
5961
from ansys.aedt.core.generic.general_methods import active_sessions
6062
from ansys.aedt.core.generic.general_methods import com_active_sessions
6163
from ansys.aedt.core.generic.general_methods import deprecate_argument
62-
from ansys.aedt.core.generic.general_methods import get_string_version
6364
from ansys.aedt.core.generic.general_methods import grpc_active_sessions
6465
from ansys.aedt.core.generic.general_methods import inside_desktop_ironpython_console
6566
from ansys.aedt.core.generic.general_methods import is_linux
@@ -395,7 +396,7 @@ def __new__(cls, *args, **kwargs):
395396

396397
# student_version = kwargs.get("student_version") or False if (not args or len(args)<5) else args[4]
397398
# machine = kwargs.get("machine") or "" if (not args or len(args)<6) else args[5]
398-
specified_version = get_string_version(specified_version)
399+
specified_version = _normalize_version_to_string(specified_version)
399400
port = kwargs.get("port") or 0 if (not args or len(args) < 7) else args[6]
400401
aedt_process_id = kwargs.get("aedt_process_id") or None if (not args or len(args) < 8) else args[7]
401402
if not settings.remote_api:
@@ -2123,6 +2124,7 @@ def __init_desktop(self):
21232124
def __check_version(self, specified_version, student_version):
21242125
if self.current_version == "" and aedt_versions.latest_version == "":
21252126
raise AEDTRuntimeError("AEDT is not installed on your system. Install AEDT version 2022 R2 or higher.")
2127+
specified_version = _normalize_version_to_string(specified_version)
21262128
if not specified_version:
21272129
if student_version and self.current_student_version:
21282130
specified_version = self.current_student_version
@@ -2140,7 +2142,9 @@ def __check_version(self, specified_version, student_version):
21402142
self.logger.warning("Only AEDT Student Version found on the system. Using Student Version.")
21412143
elif student_version:
21422144
specified_version += "SV"
2143-
specified_version = get_string_version(specified_version)
2145+
2146+
if not _is_version_format_valid(specified_version):
2147+
raise AEDTRuntimeError(f"Internal version format is not correct: specified_version: {specified_version}")
21442148

21452149
if float(specified_version[0:6]) < 2019:
21462150
raise ValueError("PyAEDT supports AEDT version 2021 R1 and later. Recommended version is 2022 R2 or later.")

src/ansys/aedt/core/generic/general_methods.py

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,9 @@ def _log_method(func, new_args, new_kwargs):
386386

387387
@pyaedt_function_handler()
388388
def get_version_and_release(input_version):
389+
"""Convert the standard five-digit AEDT version format to a tuple of version and release.
390+
Used for environment variable management.
391+
"""
389392
version = int(input_version[2:4])
390393
release = int(input_version[5])
391394
if version < 20:
@@ -397,21 +400,53 @@ def get_version_and_release(input_version):
397400

398401

399402
@pyaedt_function_handler()
400-
def get_string_version(input_version):
401-
output_version = input_version
402-
if isinstance(input_version, float):
403-
output_version = str(input_version)
404-
if len(output_version) == 4:
405-
output_version = "20" + output_version
406-
elif isinstance(input_version, int):
407-
output_version = str(input_version)
408-
output_version = f"20{output_version[:2]}.{output_version[-1]}"
409-
elif isinstance(input_version, str):
410-
if len(input_version) == 3:
411-
output_version = f"20{input_version[:2]}.{input_version[-1]}"
412-
elif len(input_version) == 4:
413-
output_version = "20" + input_version
414-
return output_version
403+
def _normalize_version_to_string(input_version):
404+
"""Convert various AEDT version formats to a standard five-digit string format.
405+
Used to check and convert the version user input to a standard format.
406+
If the input is ``None``, return ``None``.
407+
"""
408+
error_msg = (
409+
"Version argument is not valid.\n"
410+
"Accepted formats are:\n"
411+
" - 3-digit format (e.g., '232')\n"
412+
" - 5-digit format (e.g., '2023.2')\n"
413+
" - Float format (e.g., 2023.2 or 23.2)\n"
414+
" - Integer format (e.g., 232)\n"
415+
" - Release format with 'R' (e.g., '2023R2' or '23R2')"
416+
)
417+
if input_version is None:
418+
return None
419+
if not isinstance(input_version, (str, int, float)):
420+
raise ValueError(error_msg)
421+
input_version_str = str(input_version)
422+
# Matches 2000.0 – 2099.9 style floats and strings
423+
if re.match(r"^20\d{2}\.\d$", input_version_str):
424+
return input_version_str
425+
# Matches 00.0 – 99.9 style floats and strings
426+
elif re.match(r"^\d{2}\.\d$", input_version_str):
427+
return "20" + input_version_str
428+
# Matches 000 – 999 style ints and strings
429+
elif re.match(r"^\d{3}$", input_version_str):
430+
return f"20{input_version_str[:2]}.{input_version_str[-1]}"
431+
# Matches "2025R2" or "2025 R2" string
432+
elif re.match(r"^20\d{2}\s?R\d$", input_version_str):
433+
return input_version_str.replace("R", ".").replace(" ", "")
434+
# Matches "25R2" or "25 R2" string
435+
elif re.match(r"^\d{2}\s?R\d$", input_version_str):
436+
return "20" + input_version_str.replace("R", ".").replace(" ", "")
437+
else:
438+
raise ValueError(error_msg)
439+
440+
441+
@pyaedt_function_handler()
442+
def _is_version_format_valid(version):
443+
"""Check if the internal version format is valid.
444+
Version must be a string in the five-digit format (e.g., '2023.2').
445+
It can optionally end with 'SV' for student versions.
446+
"""
447+
if not isinstance(version, str):
448+
return False
449+
return bool(re.match(r"^\d{4}\.[1-9]\d*(SV)?$", version))
415450

416451

417452
@pyaedt_function_handler()

tests/system/general/test_01_general_methods.py

Lines changed: 0 additions & 47 deletions
This file was deleted.

tests/unit/test_desktop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def test_desktop_check_version_failure(mock_aedt_versions, mock_desktop):
163163
def test_desktop_check_version_failure_with_old_specified_version(mock_aedt_versions, mock_desktop):
164164
mock_student_version = MagicMock()
165165
desktop = Desktop()
166-
specified_version = "1989.6"
166+
specified_version = "2001.6"
167167

168168
with pytest.raises(
169169
ValueError, match="PyAEDT supports AEDT version 2021 R1 and later. Recommended version is 2022 R2 or later."

tests/unit/test_general_methods.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
4+
# SPDX-License-Identifier: MIT
5+
#
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining a copy
8+
# of this software and associated documentation files (the "Software"), to deal
9+
# in the Software without restriction, including without limitation the rights
10+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
# copies of the Software, and to permit persons to whom the Software is
12+
# furnished to do so, subject to the following conditions:
13+
#
14+
# The above copyright notice and this permission notice shall be included in all
15+
# copies or substantial portions of the Software.
16+
#
17+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23+
# SOFTWARE.
24+
25+
import pytest
26+
27+
from ansys.aedt.core.generic.general_methods import _is_version_format_valid
28+
from ansys.aedt.core.generic.general_methods import _normalize_version_to_string
29+
from ansys.aedt.core.generic.general_methods import number_aware_string_key
30+
31+
32+
@pytest.fixture(scope="module", autouse=True)
33+
def desktop():
34+
"""Override the desktop fixture to DO NOT open the Desktop when running this test class"""
35+
return
36+
37+
38+
class TestGeneralMethods:
39+
def test_00_number_aware_string_key(self):
40+
assert number_aware_string_key("C1") == ("C", 1)
41+
assert number_aware_string_key("1234asdf") == (1234, "asdf")
42+
assert number_aware_string_key("U100") == ("U", 100)
43+
assert number_aware_string_key("U100X0") == ("U", 100, "X", 0)
44+
45+
def test_01_number_aware_string_key(self):
46+
component_names = ["U10", "U2", "C1", "Y1000", "Y200"]
47+
expected_sort_order = ["C1", "U2", "U10", "Y200", "Y1000"]
48+
assert sorted(component_names, key=number_aware_string_key) == expected_sort_order
49+
assert sorted(component_names + [""], key=number_aware_string_key) == [""] + expected_sort_order
50+
51+
def test_valid_full_year_float(self):
52+
assert _normalize_version_to_string(2023.2) == "2023.2"
53+
assert _normalize_version_to_string("2024.5") == "2024.5"
54+
55+
def test_valid_short_year_float(self):
56+
assert _normalize_version_to_string(23.4) == "2023.4"
57+
assert _normalize_version_to_string("25.9") == "2025.9"
58+
59+
def test_valid_int_formats(self):
60+
assert _normalize_version_to_string(232) == "2023.2"
61+
assert _normalize_version_to_string("245") == "2024.5"
62+
63+
def test_valid_string_R_formats(self):
64+
assert _normalize_version_to_string("2025R2") == "2025.2"
65+
assert _normalize_version_to_string("2025 R2") == "2025.2"
66+
assert _normalize_version_to_string("25R2") == "2025.2"
67+
assert _normalize_version_to_string("25 R2") == "2025.2"
68+
69+
def test_none_value_as_version(self):
70+
assert _normalize_version_to_string(None) is None
71+
72+
def test_invalid_types(self):
73+
with pytest.raises(ValueError):
74+
_normalize_version_to_string([2023.2]) # list not allowed
75+
with pytest.raises(ValueError):
76+
_normalize_version_to_string({"year": 2023.2}) # dict not allowed
77+
78+
def test_invalid_formats(self):
79+
with pytest.raises(ValueError):
80+
_normalize_version_to_string("20232") # too many digits
81+
with pytest.raises(ValueError):
82+
_normalize_version_to_string("2023.23") # too many decimals
83+
with pytest.raises(ValueError):
84+
_normalize_version_to_string("23") # too short
85+
with pytest.raises(ValueError):
86+
_normalize_version_to_string("1999.2") # not 20XX
87+
with pytest.raises(ValueError):
88+
_normalize_version_to_string("20A3.2") # invalid chars
89+
with pytest.raises(ValueError):
90+
_normalize_version_to_string("2025 R") # missing digit after R
91+
92+
def test_edge_cases(self):
93+
assert _normalize_version_to_string("2000.0") == "2000.0" # earliest year
94+
assert _normalize_version_to_string("2099.9") == "2099.9" # latest year
95+
assert _normalize_version_to_string("00.1") == "2000.1" # short float lowest
96+
assert _normalize_version_to_string("99.9") == "2099.9" # short float highest
97+
assert _normalize_version_to_string("000") == "2000.0" # int lowest
98+
assert _normalize_version_to_string("999") == "2099.9" # int highest
99+
assert _normalize_version_to_string("00R1") == "2000.1" # short R format
100+
assert _normalize_version_to_string("99R9") == "2099.9" # short R format max
101+
assert _normalize_version_to_string("2000R1") == "2000.1" # long R format
102+
assert _normalize_version_to_string("2099R9") == "2099.9" # long R format max
103+
104+
@pytest.mark.parametrize(
105+
"version",
106+
[
107+
"2023.2",
108+
"2023.12",
109+
"1999.1",
110+
"2023.2SV",
111+
"2023.12SV",
112+
],
113+
)
114+
def test_valid_versions(self, version):
115+
assert _is_version_format_valid(version)
116+
117+
@pytest.mark.parametrize(
118+
"version",
119+
[
120+
"23.2", # too short year
121+
"202.2", # too short year
122+
"20234.2", # too long year
123+
"2023.02", # leading zero
124+
"2023", # missing minor version
125+
"2023.", # dangling dot
126+
"2023.2Extra", # extra text
127+
"2023.2 SV", # space before SV
128+
"", # empty
129+
None, # None value
130+
2023.2, # float type
131+
],
132+
)
133+
def test_invalid_versions(self, version):
134+
assert not _is_version_format_valid(version)

0 commit comments

Comments
 (0)