Skip to content

Commit 18302f3

Browse files
feat: Context managers (#4073)
closes #4053 ``` from ansys.fluent.core import launch_fluent from ansys.fluent.core import examples from ansys.fluent.core.solver import * import_filename = examples.download_file( "elbow.cas.h5", "pyfluent/examples/DOE-ML-Mixing-Elbow", ) solver = launch_fluent() with using(solver): ReadCase(import_filename) assert Setup() == solver.setup assert Viscous().model() == "laminar" ``` --------- Co-authored-by: pyansys-ci-bot <[email protected]>
1 parent d116f1c commit 18302f3

File tree

5 files changed

+142
-11
lines changed

5 files changed

+142
-11
lines changed

doc/changelog.d/4073.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Context managers

src/ansys/fluent/core/solver/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@
2626
from ansys.fluent.core.generated.solver.settings_builtin import * # noqa: F401, F403
2727
except (ImportError, AttributeError, SyntaxError):
2828
pass
29+
30+
from ansys.fluent.core.utils.context_managers import ReadCase, using # noqa: F401

src/ansys/fluent/core/solver/settings_builtin_bases.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
from ansys.fluent.core.solver.flobject import NamedObject, SettingsBase
2828
from ansys.fluent.core.solver.settings_builtin_data import DATA
29+
from ansys.fluent.core.utils.context_managers import _get_active_session
2930
from ansys.fluent.core.utils.fluent_version import FluentVersion
3031

3132

@@ -70,12 +71,19 @@ def _get_settings_obj(settings_root, builtin_settings_obj):
7071
return obj
7172

7273

74+
def _initialize_settings(instance, defaults: dict, settings_source=None, **kwargs):
75+
active_session = _get_active_session()
76+
instance.__dict__.update(defaults | kwargs)
77+
if settings_source is not None:
78+
instance.settings_source = settings_source
79+
elif active_session:
80+
instance.settings_source = active_session
81+
82+
7383
class _SingletonSetting:
7484
# Covers both groups and named-object containers
7585
def __init__(self, settings_source: SettingsBase | Solver | None = None, **kwargs):
76-
self.__dict__.update(dict(settings_source=None) | kwargs)
77-
if settings_source is not None:
78-
self.settings_source = settings_source
86+
_initialize_settings(self, {"settings_source": None}, settings_source, **kwargs)
7987

8088
def __setattr__(self, name, value):
8189
if name == "settings_source":
@@ -92,9 +100,9 @@ class _NonCreatableNamedObjectSetting:
92100
def __init__(
93101
self, name: str, settings_source: SettingsBase | Solver | None = None, **kwargs
94102
):
95-
self.__dict__.update(dict(settings_source=None, name=name) | kwargs)
96-
if settings_source is not None:
97-
self.settings_source = settings_source
103+
_initialize_settings(
104+
self, {"settings_source": None, "name": name}, settings_source, **kwargs
105+
)
98106

99107
def __setattr__(self, name, value):
100108
if name == "settings_source":
@@ -118,12 +126,16 @@ def __init__(
118126
):
119127
if name and new_instance_name:
120128
raise ValueError("Cannot specify both name and new_instance_name.")
121-
self.__dict__.update(
122-
dict(settings_source=None, name=name, new_instance_name=new_instance_name)
123-
| kwargs
129+
_initialize_settings(
130+
self,
131+
{
132+
"settings_source": None,
133+
"name": name,
134+
"new_instance_name": new_instance_name,
135+
},
136+
settings_source,
137+
**kwargs,
124138
)
125-
if settings_source is not None:
126-
self.settings_source = settings_source
127139

128140
def __setattr__(self, name, value):
129141
if name == "settings_source":
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Copyright (C) 2021 - 2025 ANSYS, Inc. and/or its affiliates.
2+
# SPDX-License-Identifier: MIT
3+
#
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
#
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
#
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
"""Module for context managers used in PyFluent."""
24+
25+
from contextlib import contextmanager
26+
from threading import local
27+
28+
_thread_local = local()
29+
30+
31+
def _get_stack():
32+
if not hasattr(_thread_local, "stack"):
33+
_thread_local.stack = []
34+
return _thread_local.stack
35+
36+
37+
@contextmanager
38+
def using(session):
39+
"""Context manager to use a Fluent session."""
40+
stack = _get_stack()
41+
stack.append(session)
42+
try:
43+
yield
44+
finally:
45+
stack.pop()
46+
47+
48+
def _get_active_session():
49+
stack = _get_stack()
50+
if stack:
51+
return stack[-1]
52+
53+
54+
class ReadCase:
55+
"""Context manager to read a Fluent case file."""
56+
57+
def __init__(self, session=None, case_file: str | None = None):
58+
self.session = session or _get_active_session()
59+
self.case_file = case_file
60+
61+
def __enter__(self):
62+
if not self.case_file:
63+
raise ValueError("A case file path must be provided to read.")
64+
self.session.settings.file.read_case(file_name=self.case_file)
65+
return self.session
66+
67+
def __exit__(self, exc_type, exc_value, traceback):
68+
pass

tests/test_builtin_settings.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143
pass # for no-codegen testing workflow
144144
import ansys.fluent.core as pyfluent
145145
from ansys.fluent.core.examples import download_file
146+
from ansys.fluent.core.solver import * # noqa: F401, F403
146147
from ansys.fluent.core.utils.fluent_version import FluentVersion
147148

148149

@@ -778,3 +779,50 @@ def test_builtin_creatable_named_object_setting_assign_session(
778779
report_file.settings_source = solver
779780
assert report_file == solver.solution.monitor.report_files["report-file-2"]
780781
assert report_file.settings_source == solver.settings
782+
783+
784+
@pytest.mark.codegen_required
785+
def test_context_manager_1(mixing_elbow_case_data_session):
786+
import threading
787+
788+
solver = mixing_elbow_case_data_session
789+
790+
# Test that the context manager works with a solver session
791+
with using(solver): # noqa: F405
792+
assert Setup() == solver.setup
793+
assert General() == solver.setup.general
794+
assert Models() == solver.setup.models
795+
assert Multiphase() == solver.setup.models.multiphase
796+
assert Energy() == solver.setup.models.energy
797+
assert Viscous() == solver.setup.models.viscous
798+
799+
# Test that the context manager works with multiple threads
800+
def run_solver_context():
801+
with using(solver): # noqa: F405
802+
setup = Setup()
803+
print(
804+
f"Setup in thread {threading.current_thread().name}: {setup == solver.setup}"
805+
)
806+
807+
threads = [
808+
threading.Thread(target=run_solver_context, name=f"Thread-{i}")
809+
for i in range(3)
810+
]
811+
for t in threads:
812+
t.start()
813+
for t in threads:
814+
t.join()
815+
816+
817+
@pytest.mark.codegen_required
818+
def test_context_manager_2(new_solver_session):
819+
solver = new_solver_session
820+
821+
import_filename = download_file(
822+
"elbow.cas.h5",
823+
"pyfluent/examples/DOE-ML-Mixing-Elbow",
824+
)
825+
826+
with using(solver): # noqa: F405
827+
ReadCase(import_filename) # noqa: F405
828+
assert Viscous().model() == "laminar"

0 commit comments

Comments
 (0)