From 686fff7f47f74ddb1a860582bb8ef2fb5ccfefdb Mon Sep 17 00:00:00 2001 From: Mainak Kundu Date: Wed, 18 Jun 2025 10:51:50 -0400 Subject: [PATCH 1/6] feat: Set string settings with allowed values via string constants --- src/ansys/fluent/core/codegen/settingsgen.py | 12 ++++++- src/ansys/fluent/core/solver/flobject.py | 37 +++++++++++++++++++- tests/test_settings_api.py | 23 ++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/ansys/fluent/core/codegen/settingsgen.py b/src/ansys/fluent/core/codegen/settingsgen.py index a5519b79d603..1798798ac01b 100644 --- a/src/ansys/fluent/core/codegen/settingsgen.py +++ b/src/ansys/fluent/core/codegen/settingsgen.py @@ -38,6 +38,7 @@ ListObject, NamedObject, get_cls, + to_constant_name, to_python_name, ) from ansys.fluent.core.utils.fix_doc import fix_settings_doc @@ -151,6 +152,7 @@ def _populate_data(cls, api_tree: dict, version: str) -> dict: data["child_object_type"]["doc"] = f"'child_object_type' of {cls.__name__}." else: data["child_object_type"] = None + data["allowed_values"] = getattr(cls, "_allowed_values", []) return data @@ -307,6 +309,13 @@ def _write_data(cls_name: str, python_name: str, data: dict, f: IO, f_stub: IO | if return_type: s.write(f" return_type = {return_type!r}\n") s_stub.write(" return_type: str\n") + for allowed_value in data["allowed_values"]: + s.write( + f" {to_constant_name(allowed_value)} = _FlConstant({allowed_value!r})\n" + ) + s_stub.write( + f" {to_constant_name(allowed_value)}: Final[str] = {allowed_value!r}\n" + ) s.write("\n") for name, (python_name, data, hash_, should_write_stub) in classes_to_write.items(): if name not in _CLASS_WRITTEN: @@ -371,10 +380,11 @@ def generate(version: str, static_infos: dict, verbose: bool = False) -> None: header.write(" _InputFile,\n") header.write(" _OutputFile,\n") header.write(" _InOutFile,\n") + header.write(" _FlConstant,\n") header.write(")\n\n") f.write(header.getvalue()) f_stub.write(header.getvalue()) - f_stub.write("from typing import Any\n\n") + f_stub.write("from typing import Any, Final\n\n") f.write(f'SHASH = "{shash}"\n\n') name = data["name"] _NAME_BY_HASH[_gethash(data)] = name diff --git a/src/ansys/fluent/core/solver/flobject.py b/src/ansys/fluent/core/solver/flobject.py index 1865f5f6e6f1..bccbefb0eeea 100644 --- a/src/ansys/fluent/core/solver/flobject.py +++ b/src/ansys/fluent/core/solver/flobject.py @@ -187,7 +187,7 @@ def assert_type(val, tp): def to_python_name(fluent_name: str) -> str: """Convert a scheme string to a Python variable name. - This function replaces symbols with _. Any ``?`` symbols are + This function replaces symbols with _. ``'`` and ``?`` symbols are ignored. """ if not fluent_name: @@ -198,6 +198,20 @@ def to_python_name(fluent_name: str) -> str: return name +def to_constant_name(fluent_name: str) -> str: + """Convert a scheme string to a Python constant name. + + This function replaces symbols and spaces with _ and converts the name to uppercase. + ``'`` and ``?`` symbols are ignored. + """ + fluent_name = fluent_name.replace(" ", "_") + name = fluent_name.translate(_ttable).upper() + if name[0].isdigit(): + # If the first character is a digit, prepend "CASE_" + name = "CASE_" + name + return name + + _to_field_name_str = naming_strategy().to_string @@ -2029,6 +2043,19 @@ def allowed_values(self): return [] +class _FlConstant: + """A descriptor class to hold a constant value.""" + + def __init__(self, value): + self._value = value + + def __get__(self, instance, owner): + return self._value + + def __set__(self, instance, value): + raise AttributeError("Cannot set a constant value.") + + _bases_by_class = {} @@ -2217,6 +2244,14 @@ def _process_cls_names(info_dict, names, write_doc=False): k, ) + allowed_values = info.get("allowed-values") or info.get("allowed_values", []) + if allowed_values: + for allowed_value in allowed_values: + setattr( + cls, to_constant_name(allowed_value), _FlConstant(allowed_value) + ) + cls._allowed_values = allowed_values + except Exception: print( f"Unable to construct class for '{name}' of " diff --git a/tests/test_settings_api.py b/tests/test_settings_api.py index 21421fb3720a..c52f1fa6d040 100644 --- a/tests/test_settings_api.py +++ b/tests/test_settings_api.py @@ -27,6 +27,7 @@ from ansys.fluent.core.examples import download_file from ansys.fluent.core.pyfluent_warnings import PyFluentUserWarning +from ansys.fluent.core.solver import Viscous from ansys.fluent.core.solver.flobject import ( DeprecatedSettingWarning, _Alias, @@ -762,3 +763,25 @@ def test_runtime_python_classes( ].general.material() == "water-liquid" ) + + +@pytest.mark.fluent_version(">=26.1") +def test_setting_string_constants(mixing_elbow_settings_session): + solver = mixing_elbow_settings_session + viscous = Viscous(solver) + + # viscous.model.INVISCID is a string constant + assert viscous.model.INVISCID == "inviscid" + with pytest.raises(AttributeError): + viscous.model.INVISCID = "invalid" + + # Setting using string constants + viscous.model = viscous.model.INVISCID + assert viscous.model() == "inviscid" + viscous.model = viscous.model.K_EPSILON + assert viscous.model() == "k-epsilon" + viscous.k_epsilon_model = viscous.k_epsilon_model.RNG + assert viscous.k_epsilon_model() == "rng" + + with pytest.raises(ValueError): + viscous.k_epsilon_model = viscous.k_epsilon_model.EASM From 38e0004022c985d480bd4d3caa4aad5ba466f810 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Wed, 18 Jun 2025 20:08:51 +0000 Subject: [PATCH 2/6] chore: adding changelog file 4190.added.md [dependabot-skip] --- doc/changelog.d/4190.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/4190.added.md diff --git a/doc/changelog.d/4190.added.md b/doc/changelog.d/4190.added.md new file mode 100644 index 000000000000..4026522e3bb3 --- /dev/null +++ b/doc/changelog.d/4190.added.md @@ -0,0 +1 @@ +Set string settings with allowed values via string constants \ No newline at end of file From 489d27519c61e93f563b8cdc5b4065c994aa12fe Mon Sep 17 00:00:00 2001 From: Mainak Kundu Date: Wed, 18 Jun 2025 18:30:24 -0400 Subject: [PATCH 3/6] fix: codegen --- src/ansys/fluent/core/codegen/settingsgen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ansys/fluent/core/codegen/settingsgen.py b/src/ansys/fluent/core/codegen/settingsgen.py index 1798798ac01b..cd1fa827ab29 100644 --- a/src/ansys/fluent/core/codegen/settingsgen.py +++ b/src/ansys/fluent/core/codegen/settingsgen.py @@ -317,6 +317,7 @@ def _write_data(cls_name: str, python_name: str, data: dict, f: IO, f_stub: IO | f" {to_constant_name(allowed_value)}: Final[str] = {allowed_value!r}\n" ) s.write("\n") + s_stub.write("\n") for name, (python_name, data, hash_, should_write_stub) in classes_to_write.items(): if name not in _CLASS_WRITTEN: _write_data( From c3238e66f72c8063d982a8ca8ff7ff55c903c9c2 Mon Sep 17 00:00:00 2001 From: Mainak Kundu Date: Thu, 19 Jun 2025 14:51:34 -0400 Subject: [PATCH 4/6] feat: request allowed-values via optional_attrs --- src/ansys/fluent/core/services/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ansys/fluent/core/services/settings.py b/src/ansys/fluent/core/services/settings.py index 720763094674..ba0c5eac6ebe 100644 --- a/src/ansys/fluent/core/services/settings.py +++ b/src/ansys/fluent/core/services/settings.py @@ -335,6 +335,7 @@ def get_static_info(self) -> dict[str, Any]: """ request = SettingsModule.GetStaticInfoRequest() request.root = "fluent" + request.optional_attrs.append("allowed-values") response = self._service_impl.get_static_info(request) # The RPC calls no longer raise an exception. Force an exception if # type is empty From 18ca6e05af4cfdf6b53f17a5b53c4989f21077cf Mon Sep 17 00:00:00 2001 From: Mainak Kundu Date: Wed, 2 Jul 2025 15:06:34 -0400 Subject: [PATCH 5/6] test: fix --- tests/test_codegen.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_codegen.py b/tests/test_codegen.py index 7618a9ae81fd..37707643d20d 100644 --- a/tests/test_codegen.py +++ b/tests/test_codegen.py @@ -504,6 +504,7 @@ def _get_query_settings_static_info(name, args): _InputFile, _OutputFile, _InOutFile, + _FlConstant, ) SHASH = "3e6d76a4601701388ea8258912d145b7b7c436699a50b6c7fe9a29f41eeff194" From 225c0b008773eb0df66405474fbbf8e821e9f52f Mon Sep 17 00:00:00 2001 From: Mainak Kundu Date: Wed, 2 Jul 2025 16:15:48 -0400 Subject: [PATCH 6/6] feat: Add is_active() method to string constant --- src/ansys/fluent/core/codegen/settingsgen.py | 4 ++-- src/ansys/fluent/core/solver/flobject.py | 25 ++++++++++++++++---- tests/test_codegen.py | 2 +- tests/test_settings_api.py | 3 +++ 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/ansys/fluent/core/codegen/settingsgen.py b/src/ansys/fluent/core/codegen/settingsgen.py index cd1fa827ab29..52bb96d39712 100644 --- a/src/ansys/fluent/core/codegen/settingsgen.py +++ b/src/ansys/fluent/core/codegen/settingsgen.py @@ -311,7 +311,7 @@ def _write_data(cls_name: str, python_name: str, data: dict, f: IO, f_stub: IO | s_stub.write(" return_type: str\n") for allowed_value in data["allowed_values"]: s.write( - f" {to_constant_name(allowed_value)} = _FlConstant({allowed_value!r})\n" + f" {to_constant_name(allowed_value)} = _FlStringConstant({allowed_value!r})\n" ) s_stub.write( f" {to_constant_name(allowed_value)}: Final[str] = {allowed_value!r}\n" @@ -381,7 +381,7 @@ def generate(version: str, static_infos: dict, verbose: bool = False) -> None: header.write(" _InputFile,\n") header.write(" _OutputFile,\n") header.write(" _InOutFile,\n") - header.write(" _FlConstant,\n") + header.write(" _FlStringConstant,\n") header.write(")\n\n") f.write(header.getvalue()) f_stub.write(header.getvalue()) diff --git a/src/ansys/fluent/core/solver/flobject.py b/src/ansys/fluent/core/solver/flobject.py index bccbefb0eeea..fbb292af492c 100644 --- a/src/ansys/fluent/core/solver/flobject.py +++ b/src/ansys/fluent/core/solver/flobject.py @@ -55,6 +55,7 @@ import types from typing import ( Any, + Callable, Dict, ForwardRef, Generic, @@ -2043,14 +2044,28 @@ def allowed_values(self): return [] -class _FlConstant: - """A descriptor class to hold a constant value.""" +class _MaybeActiveString(str): + """A string class with an is_active() method.""" + + def __new__(cls, value, is_active: Callable[[], bool]): + return super().__new__(cls, value) + + def __init__(self, value, is_active: Callable[[], bool]): + super().__init__() + self.is_active = is_active + + +class _FlStringConstant: + """A descriptor class to hold a constant string value.""" def __init__(self, value): self._value = value def __get__(self, instance, owner): - return self._value + def is_active(): + return self._value in instance.allowed_values() + + return _MaybeActiveString(self._value, is_active=is_active) def __set__(self, instance, value): raise AttributeError("Cannot set a constant value.") @@ -2248,7 +2263,9 @@ def _process_cls_names(info_dict, names, write_doc=False): if allowed_values: for allowed_value in allowed_values: setattr( - cls, to_constant_name(allowed_value), _FlConstant(allowed_value) + cls, + to_constant_name(allowed_value), + _FlStringConstant(allowed_value), ) cls._allowed_values = allowed_values diff --git a/tests/test_codegen.py b/tests/test_codegen.py index 37707643d20d..fa15efb35b25 100644 --- a/tests/test_codegen.py +++ b/tests/test_codegen.py @@ -504,7 +504,7 @@ def _get_query_settings_static_info(name, args): _InputFile, _OutputFile, _InOutFile, - _FlConstant, + _FlStringConstant, ) SHASH = "3e6d76a4601701388ea8258912d145b7b7c436699a50b6c7fe9a29f41eeff194" diff --git a/tests/test_settings_api.py b/tests/test_settings_api.py index c52f1fa6d040..1fd670ae8e65 100644 --- a/tests/test_settings_api.py +++ b/tests/test_settings_api.py @@ -772,6 +772,7 @@ def test_setting_string_constants(mixing_elbow_settings_session): # viscous.model.INVISCID is a string constant assert viscous.model.INVISCID == "inviscid" + assert isinstance(viscous.model.INVISCID, str) with pytest.raises(AttributeError): viscous.model.INVISCID = "invalid" @@ -781,7 +782,9 @@ def test_setting_string_constants(mixing_elbow_settings_session): viscous.model = viscous.model.K_EPSILON assert viscous.model() == "k-epsilon" viscous.k_epsilon_model = viscous.k_epsilon_model.RNG + assert viscous.k_epsilon_model.RNG.is_active() is True assert viscous.k_epsilon_model() == "rng" + assert viscous.k_epsilon_model.EASM.is_active() is False with pytest.raises(ValueError): viscous.k_epsilon_model = viscous.k_epsilon_model.EASM