ansys-fluent-core 0.35.dev1__py3-none-any.whl → 0.36.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ansys-fluent-core might be problematic. Click here for more details.

Files changed (67) hide show
  1. ansys/fluent/core/__init__.py +2 -2
  2. ansys/fluent/core/codegen/__init__.py +1 -0
  3. ansys/fluent/core/codegen/datamodelgen.py +13 -2
  4. ansys/fluent/core/codegen/settingsgen.py +4 -0
  5. ansys/fluent/core/docker/docker_compose.py +30 -1
  6. ansys/fluent/core/examples/downloads.py +3 -4
  7. ansys/fluent/core/fluent_connection.py +2 -3
  8. ansys/fluent/core/generated/api_tree/api_objects.json +1 -1
  9. ansys/fluent/core/generated/datamodel_231/flicing.py +25 -25
  10. ansys/fluent/core/generated/datamodel_231/meshing.py +202 -202
  11. ansys/fluent/core/generated/datamodel_232/flicing.py +25 -25
  12. ansys/fluent/core/generated/datamodel_232/meshing.py +161 -161
  13. ansys/fluent/core/generated/datamodel_241/flicing.py +30 -30
  14. ansys/fluent/core/generated/datamodel_241/meshing.py +241 -241
  15. ansys/fluent/core/generated/datamodel_242/flicing.py +45 -45
  16. ansys/fluent/core/generated/datamodel_242/meshing.py +318 -318
  17. ansys/fluent/core/generated/datamodel_242/part_management.py +9 -9
  18. ansys/fluent/core/generated/datamodel_251/flicing.py +25 -25
  19. ansys/fluent/core/generated/datamodel_251/meshing.py +395 -395
  20. ansys/fluent/core/generated/datamodel_251/part_management.py +6 -6
  21. ansys/fluent/core/generated/datamodel_252/flicing.py +45 -45
  22. ansys/fluent/core/generated/datamodel_252/meshing.py +394 -394
  23. ansys/fluent/core/generated/datamodel_252/part_management.py +5 -5
  24. ansys/fluent/core/generated/datamodel_261/flicing.py +20 -20
  25. ansys/fluent/core/generated/datamodel_261/meshing.py +500 -493
  26. ansys/fluent/core/generated/datamodel_261/meshing_workflow.py +61694 -0
  27. ansys/fluent/core/generated/datamodel_261/part_management.py +5 -5
  28. ansys/fluent/core/generated/datamodel_261/preferences.py +28 -0
  29. ansys/fluent/core/generated/datamodel_261/solver_workflow.py +14 -0
  30. ansys/fluent/core/generated/fluent_version_261.py +3 -3
  31. ansys/fluent/core/generated/meshing/tui_261.py +388 -10
  32. ansys/fluent/core/generated/solver/settings_261.py +8273 -3465
  33. ansys/fluent/core/generated/solver/settings_261.pyi +5805 -2181
  34. ansys/fluent/core/generated/solver/tui_261.py +1049 -335
  35. ansys/fluent/core/launcher/container_launcher.py +12 -3
  36. ansys/fluent/core/launcher/fluent_container.py +5 -3
  37. ansys/fluent/core/launcher/launch_options.py +2 -2
  38. ansys/fluent/core/launcher/launcher.py +2 -6
  39. ansys/fluent/core/launcher/pim_launcher.py +76 -3
  40. ansys/fluent/core/launcher/process_launch_string.py +1 -2
  41. ansys/fluent/core/launcher/slurm_launcher.py +4 -3
  42. ansys/fluent/core/launcher/standalone_launcher.py +3 -2
  43. ansys/fluent/core/module_config.py +5 -10
  44. ansys/fluent/core/report.py +1 -1
  45. ansys/fluent/core/services/__init__.py +2 -0
  46. ansys/fluent/core/services/datamodel_se.py +4 -1
  47. ansys/fluent/core/services/field_data.py +24 -0
  48. ansys/fluent/core/services/reduction.py +2 -0
  49. ansys/fluent/core/services/settings.py +1 -1
  50. ansys/fluent/core/services/solution_variables.py +92 -0
  51. ansys/fluent/core/session.py +1 -2
  52. ansys/fluent/core/session_base_meshing.py +8 -0
  53. ansys/fluent/core/session_meshing.py +5 -0
  54. ansys/fluent/core/session_pure_meshing.py +6 -0
  55. ansys/fluent/core/session_pure_meshing.pyi +5 -0
  56. ansys/fluent/core/session_utilities.py +8 -5
  57. ansys/fluent/core/solver/flobject.py +19 -0
  58. ansys/fluent/core/solver/function/reduction.py +2 -0
  59. ansys/fluent/core/ui/__init__.py +64 -0
  60. ansys/fluent/core/ui/jupyter_ui.py +203 -0
  61. ansys/fluent/core/ui/standalone_web_ui.py +296 -0
  62. ansys/fluent/core/ui/utils.py +173 -0
  63. ansys/fluent/core/utils/deprecate.py +1 -0
  64. {ansys_fluent_core-0.35.dev1.dist-info → ansys_fluent_core-0.36.dev0.dist-info}/METADATA +25 -19
  65. {ansys_fluent_core-0.35.dev1.dist-info → ansys_fluent_core-0.36.dev0.dist-info}/RECORD +67 -62
  66. {ansys_fluent_core-0.35.dev1.dist-info → ansys_fluent_core-0.36.dev0.dist-info}/WHEEL +0 -0
  67. {ansys_fluent_core-0.35.dev1.dist-info → ansys_fluent_core-0.36.dev0.dist-info}/licenses/LICENSE +0 -0
@@ -34,6 +34,9 @@ from ansys.fluent.core.generated.datamodel_252.preferences import (
34
34
  Root as preferences_root,
35
35
  )
36
36
  from ansys.fluent.core.generated.datamodel_252.workflow import Root as workflow_root
37
+ from ansys.fluent.core.generated.datamodel_261.meshing_workflow import (
38
+ Root as meshing_workflow_root,
39
+ )
37
40
  from ansys.fluent.core.generated.meshing.tui_252 import main_menu
38
41
 
39
42
  class PureMeshing:
@@ -45,6 +48,8 @@ class PureMeshing:
45
48
  def meshing_utilities(self) -> meshing_utilities_root: ...
46
49
  @property
47
50
  def workflow(self) -> workflow_root: ...
51
+ @property
52
+ def meshing_workflow(self) -> meshing_workflow_root: ...
48
53
  def watertight(self): ...
49
54
  def fault_tolerant(self): ...
50
55
  def two_dimensional_meshing(self): ...
@@ -90,8 +90,9 @@ class SessionBase:
90
90
 
91
91
  Parameters
92
92
  ----------
93
- ui_mode : UIMode
94
- Defines the user interface mode for Fluent. Options correspond to values in the ``UIMode`` enum.
93
+ ui_mode : UIMode or str, optional
94
+ Defines the user interface mode for Fluent. Accepts either a ``UIMode`` value
95
+ or a corresponding string such as ``"no_gui"``, ``"hidden_gui"``, or ``"gui"``.
95
96
  graphics_driver : FluentWindowsGraphicsDriver or FluentLinuxGraphicsDriver
96
97
  Specifies the graphics driver for Fluent. Options are from the ``FluentWindowsGraphicsDriver`` enum
97
98
  (for Windows) or the ``FluentLinuxGraphicsDriver`` enum (for Linux).
@@ -194,8 +195,9 @@ class SessionBase:
194
195
 
195
196
  Parameters
196
197
  ----------
197
- ui_mode : UIMode
198
- Defines the user interface mode for Fluent. Options correspond to values in the ``UIMode`` enum.
198
+ ui_mode : UIMode or str, optional
199
+ Defines the user interface mode for Fluent. Accepts either a ``UIMode`` value
200
+ or a corresponding string such as ``"no_gui"``, ``"hidden_gui"``, or ``"gui"``.
199
201
  graphics_driver : FluentWindowsGraphicsDriver or FluentLinuxGraphicsDriver
200
202
  Specifies the graphics driver for Fluent. Options are from the ``FluentWindowsGraphicsDriver`` enum
201
203
  (for Windows) or the ``FluentLinuxGraphicsDriver`` enum (for Linux).
@@ -287,7 +289,8 @@ class SessionBase:
287
289
  Parameters
288
290
  ----------
289
291
  ui_mode : UIMode or str, optional
290
- Defines the user interface mode for Fluent. Options correspond to values in the ``UIMode`` enum.
292
+ Defines the user interface mode for Fluent. Accepts either a ``UIMode`` value
293
+ or a corresponding string such as ``"no_gui"``, ``"hidden_gui"``, or ``"gui"``.
291
294
  graphics_driver : FluentWindowsGraphicsDriver or FluentLinuxGraphicsDriver
292
295
  Specifies the graphics driver for Fluent. Options are from the ``FluentWindowsGraphicsDriver`` enum
293
296
  (for Windows) or the ``FluentLinuxGraphicsDriver`` enum (for Linux).
@@ -80,6 +80,7 @@ from ansys.fluent.core.variable_strategies import (
80
80
  FluentFieldDataNamingStrategy as naming_strategy,
81
81
  )
82
82
  import ansys.units
83
+ from ansys.units import VariableDescriptor
83
84
 
84
85
  from . import _docstrings
85
86
  from ..pyfluent_warnings import warning_for_fluent_dev_version
@@ -676,7 +677,21 @@ class Textual(Property):
676
677
  Either str or VariableDescriptor.
677
678
  kwargs : Any
678
679
  Keyword arguments.
680
+
681
+ Raises
682
+ ------
683
+ TypeError
684
+ If state is not a string.
679
685
  """
686
+ allowed_types = (str, VariableDescriptor)
687
+
688
+ if not isinstance(state, allowed_types):
689
+ if self._has_migration_adapter:
690
+ return self.base_set_state(state=state, **kwargs)
691
+ raise TypeError(
692
+ f"Expected state to be {' or '.join(t.__name__ for t in allowed_types)}, "
693
+ f"got {type(state).__name__}."
694
+ )
680
695
  return self.base_set_state(state=_to_field_name_str(state), **kwargs)
681
696
 
682
697
 
@@ -2282,6 +2297,10 @@ def get_cls(name, info, parent=None, version=None, parent_taboo=None):
2282
2297
  )
2283
2298
  cls._allowed_values = allowed_values
2284
2299
 
2300
+ has_migration_adapter = info.get("has-migration-adapter?", False)
2301
+ if has_migration_adapter:
2302
+ cls._has_migration_adapter = True
2303
+
2285
2304
  except Exception:
2286
2305
  print(
2287
2306
  f"Unable to construct class for '{name}' of "
@@ -211,6 +211,8 @@ def _eval_reduction(
211
211
  weight = "Weight=" + str(weight)
212
212
  locations = str(locations) + ", " + weight
213
213
 
214
+ if hasattr(expr, "definition"):
215
+ expr = expr.definition()
214
216
  expr_str = _expr_to_expr_str(naming_strategy().to_string(expr))
215
217
  if condition:
216
218
  expr_str = expr_str + ", " + condition
@@ -0,0 +1,64 @@
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
+ """Public exposure of ui UI for PyFluent."""
24
+
25
+
26
+ def in_jupyter():
27
+ """Checks if the library is being used in a Jupyter environment."""
28
+ try:
29
+ from IPython import get_ipython
30
+
31
+ return "IPKernelApp" in get_ipython().config
32
+ except (ImportError, AttributeError):
33
+ return False
34
+
35
+
36
+ if in_jupyter():
37
+ from ansys.fluent.core.ui.jupyter_ui import (
38
+ set_auto_refresh,
39
+ settings_ui,
40
+ )
41
+ else:
42
+ from ansys.fluent.core.ui.standalone_web_ui import ( # noqa: F401
43
+ build_settings_view,
44
+ set_auto_refresh,
45
+ )
46
+
47
+
48
+ def ui(settings_obj):
49
+ """PyFluent ui UI wrapper."""
50
+ if in_jupyter():
51
+ import IPython
52
+ from IPython.display import display
53
+
54
+ if hasattr(IPython, "get_ipython") and "ZMQInteractiveShell" in str(
55
+ type(IPython.get_ipython())
56
+ ):
57
+ display(settings_ui(settings_obj))
58
+ else:
59
+ import panel as pn
60
+
61
+ pn.extension()
62
+ view = build_settings_view(settings_obj)
63
+ view.servable()
64
+ pn.serve(view)
@@ -0,0 +1,203 @@
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
+ """Render ui UI in Jupyter notebook."""
24
+
25
+ from ansys.fluent.core.ui.utils import (
26
+ _parse_path,
27
+ _render_widget_from_props_generic,
28
+ _safe_get_properties,
29
+ )
30
+
31
+ try:
32
+ import ipywidgets as widgets
33
+ except ModuleNotFoundError as exc:
34
+ raise ModuleNotFoundError(
35
+ "Missing dependencies, use 'pip install ansys-fluent-core[ui-jupyter]' to install them."
36
+ ) from exc
37
+
38
+ from ansys.fluent.core.solver.flobject import (
39
+ BaseCommand,
40
+ Group,
41
+ NamedObject,
42
+ )
43
+
44
+
45
+ def set_auto_refresh():
46
+ """Refreshes the UI w.r.t. server state for each command execution or parameter invocation."""
47
+ raise NotImplementedError("This is yet to be implemented in jupyter environment.")
48
+
49
+
50
+ def _render_widgets_from_props(settings_obj, label, props):
51
+ """Render widget using pre-fetched props instead of repeated calls."""
52
+ return _render_widget_from_props_generic(settings_obj, label, props, widgets)
53
+
54
+
55
+ def _param_ui(settings_obj, props):
56
+ label = props["python_name"].replace("_", " ").capitalize()
57
+
58
+ def get_fn():
59
+ try:
60
+ return getattr(props["parent"], props["python_name"])
61
+ except AttributeError:
62
+ return props["parent"][props["obj_name"]]
63
+
64
+ def set_fn(v):
65
+ return setattr(settings_obj.parent, props["python_name"], v)
66
+
67
+ widget = _render_widgets_from_props(get_fn(), label, props)
68
+ output = widgets.Output()
69
+ with output:
70
+ output.clear_output()
71
+ print(_parse_path(settings_obj))
72
+ if hasattr(widget, "_is_list_text"):
73
+ typ, parse_csv = widget._is_list_text
74
+
75
+ def commit_text_list(change):
76
+ if change["name"] == "value":
77
+ raw = change["new"]
78
+ vals = (
79
+ [typ(v.strip()) for v in raw.split(",") if v.strip()]
80
+ if parse_csv
81
+ else list(change["new"])
82
+ )
83
+ with output:
84
+ output.clear_output()
85
+ set_fn(vals)
86
+ print(f"{_parse_path(settings_obj)} = {vals}")
87
+
88
+ widget.observe(commit_text_list)
89
+ else:
90
+
91
+ def on_change(change):
92
+ if change["name"] == "value":
93
+ with output:
94
+ output.clear_output()
95
+ try:
96
+ set_fn(change["new"])
97
+ print(f"{_parse_path(settings_obj)} = {change['new']}")
98
+ except Exception as e:
99
+ print(f"Error setting {label}: {e}")
100
+
101
+ widget.observe(on_change)
102
+ return widgets.VBox([widget, output])
103
+
104
+
105
+ def _command_ui(func, props):
106
+ """
107
+ Renders input widgets for function arguments based on .argument_names()
108
+ and executes func(**kwargs) on button click.
109
+ """
110
+
111
+ # Get argument names from the function object
112
+ if not hasattr(func, "argument_names"):
113
+ return widgets.HTML("Command has no 'argument_names()'.")
114
+
115
+ arg_names = func.argument_names
116
+ arg_widgets = {}
117
+ controls = []
118
+ for name in arg_names:
119
+ child_obj = getattr(func, name)
120
+ child_props = _safe_get_properties(child_obj)
121
+ widget = _render_widgets_from_props(child_obj, name, child_props)
122
+ arg_widgets[name] = widget
123
+ controls.append(widget)
124
+
125
+ # Run button
126
+ button = widgets.Button(
127
+ description=f"Run {props['python_name']}", button_style="success"
128
+ )
129
+ output = widgets.Output()
130
+ with output:
131
+ output.clear_output()
132
+ print(_parse_path(func))
133
+
134
+ def on_click(_):
135
+ kwargs = {name: w.value for name, w in arg_widgets.items()}
136
+ with output:
137
+ output.clear_output()
138
+ try:
139
+ func(**kwargs)
140
+ kwargs_str = "("
141
+ for k, v in kwargs.items():
142
+ if type(v) is str:
143
+ if v != "":
144
+ kwargs_str += f"{k}='{v}', "
145
+ else:
146
+ kwargs_str += f"{k}={v}, "
147
+ print(f"{_parse_path(func)}" + kwargs_str.strip()[:-1] + ")")
148
+ except Exception as e:
149
+ print("Error:", e)
150
+
151
+ button.on_click(on_click)
152
+ return widgets.VBox(controls + [button, output])
153
+
154
+
155
+ def settings_ui(obj, indent=0):
156
+ """Render settings objects into ui graphics."""
157
+ props = _safe_get_properties(obj)
158
+ if isinstance(obj, (Group, NamedObject)):
159
+ if isinstance(obj, Group):
160
+ command_names = obj.get_active_command_names()
161
+ child_names = obj.get_active_child_names() + command_names
162
+ else:
163
+ command_names = obj.command_names
164
+ child_names = list(obj) + command_names
165
+ accordions = []
166
+ for child_name in child_names:
167
+
168
+ def lazy_loader(name=child_name, parent=obj, lvl=indent + 1):
169
+ try:
170
+ child_obj = getattr(parent, name)
171
+ except AttributeError:
172
+ child_obj = parent[name]
173
+ return settings_ui(child_obj, lvl)
174
+
175
+ acc = widgets.Accordion(children=[widgets.HTML("Loading...")])
176
+ if child_name in command_names:
177
+ acc.set_title(0, f"⚡ {child_name}")
178
+ else:
179
+ acc.set_title(0, child_name)
180
+
181
+ def on_selected(change, loader=lazy_loader, accordion=acc):
182
+ if change["name"] == "selected_index" and change["new"] == 0:
183
+ if isinstance(accordion.children[0], widgets.HTML):
184
+ accordion.children = [loader()]
185
+
186
+ acc.observe(on_selected, names="selected_index")
187
+ accordions.append(acc)
188
+
189
+ return widgets.VBox(accordions)
190
+
191
+ else:
192
+ if isinstance(obj, BaseCommand):
193
+ return (
194
+ widgets.VBox([_command_ui(obj, props)])
195
+ if props["is_active"]
196
+ else widgets.HTML("")
197
+ )
198
+ else:
199
+ return (
200
+ widgets.VBox([_param_ui(obj, props)])
201
+ if props["is_active"]
202
+ else widgets.HTML("")
203
+ )
@@ -0,0 +1,296 @@
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
+ """Web UI for Fluent settings using Panel with lazy loading and batched property access."""
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Callable, Dict, List
28
+
29
+ try:
30
+ import panel as pn
31
+ import param
32
+ except ModuleNotFoundError as exc:
33
+ raise ModuleNotFoundError(
34
+ "Missing dependencies, use 'pip install ansys-fluent-core[ui]' to install them."
35
+ ) from exc
36
+
37
+ from ansys.fluent.core.solver.flobject import (
38
+ BaseCommand,
39
+ Group,
40
+ NamedObject,
41
+ )
42
+ from ansys.fluent.core.ui.utils import (
43
+ _parse_path,
44
+ _render_widget_from_props_generic,
45
+ _safe_get_properties,
46
+ )
47
+
48
+ _path_backup_dict = {}
49
+
50
+ AUTO_REFRESH = False
51
+
52
+
53
+ def set_auto_refresh():
54
+ """Refreshes the UI w.r.t. server state for each command execution or parameter invocation."""
55
+ global AUTO_REFRESH
56
+ AUTO_REFRESH = True
57
+
58
+
59
+ pn.extension()
60
+
61
+ # global trigger for refresh (Panel Param depends on it)
62
+ _refresh = pn.state.cache.get("fluent_refresh", None)
63
+ if _refresh is None:
64
+
65
+ class Refresh(param.Parameterized):
66
+ """Refresh."""
67
+
68
+ bump = param.Event()
69
+
70
+ _refresh = Refresh()
71
+ pn.state.cache["fluent_refresh"] = _refresh
72
+
73
+
74
+ def _render_widget_from_props(
75
+ settings_obj, label: str, props: Dict[str, Any]
76
+ ) -> pn.viewable.Viewable:
77
+ """Produce a Panel widget from type+props. No backend mutation here."""
78
+ return _render_widget_from_props_generic(settings_obj, label, props, pn.widgets)
79
+
80
+
81
+ def _param_view(settings_obj, props: Dict[str, Any]) -> pn.viewable.Viewable:
82
+ label = props["python_name"].replace("_", " ").capitalize()
83
+
84
+ def get_fn():
85
+ try:
86
+ return getattr(props["parent"], props["python_name"])
87
+ except AttributeError:
88
+ return props["parent"][props["obj_name"]]
89
+
90
+ def set_fn(v):
91
+ return setattr(settings_obj.parent, props["python_name"], v)
92
+
93
+ w = _render_widget_from_props(get_fn(), label, props)
94
+ obj_apth = _parse_path(settings_obj)
95
+ if obj_apth in _path_backup_dict:
96
+ console = pn.pane.Markdown(
97
+ f"```\n{obj_apth} = {_path_backup_dict[obj_apth]}\n```",
98
+ sizing_mode="stretch_width",
99
+ )
100
+ else:
101
+ console = pn.pane.Markdown(f"```\n{obj_apth}\n```", sizing_mode="stretch_width")
102
+
103
+ # Change handlers
104
+ if hasattr(w, "_is_list_text"):
105
+ typ, parse_csv = w._is_list_text
106
+
107
+ def _commit_list(event):
108
+ if event is None:
109
+ return
110
+ newv = event.new
111
+ if parse_csv:
112
+ raw = newv or ""
113
+ vals = [typ(v.strip()) for v in raw.split(",") if v.strip()]
114
+ else:
115
+ vals = list(newv or [])
116
+ try:
117
+ set_fn(vals)
118
+ console.object = f"```\n{_parse_path(settings_obj)} = {vals}\n```"
119
+ _path_backup_dict[_parse_path(settings_obj)] = vals
120
+ if AUTO_REFRESH:
121
+ _refresh.bump = True
122
+ except Exception as e:
123
+ console.object = f"```\nError setting {label}: {e}\n```"
124
+
125
+ w.param.watch(_commit_list, "value", onlychanged=True)
126
+ else:
127
+
128
+ def _commit(event):
129
+ if event is None:
130
+ return
131
+ try:
132
+ set_fn(event.new)
133
+ console.object = f"```\n{_parse_path(settings_obj)} = {event.new}\n```"
134
+ _path_backup_dict[_parse_path(settings_obj)] = event.new
135
+ if AUTO_REFRESH:
136
+ _refresh.bump = True
137
+ except Exception as e:
138
+ console.object = f"```\nError setting {label}: {e}\n```"
139
+
140
+ w.param.watch(_commit, "value", onlychanged=True)
141
+
142
+ return pn.Column(
143
+ w, console, sizing_mode="stretch_width", margin=(10, 20), align="start"
144
+ )
145
+
146
+
147
+ def _command_view(func, props: Dict[str, Any]) -> pn.viewable.Viewable:
148
+ """Render command arguments (on demand) and execute only on click."""
149
+ # Safely fetch argument names (does NOT execute the command)
150
+ if not hasattr(func, "argument_names"):
151
+ return pn.pane.HTML("<i>Command has no 'argument_names()'.</i>")
152
+ arg_names = func.argument_names
153
+ arg_widgets: Dict[str, Any] = {}
154
+ controls: List[pn.viewable.Viewable] = []
155
+
156
+ # Build argument widgets immediately when this view is created
157
+ for name in arg_names:
158
+ param_obj = getattr(func, name) # safe: this just references the arg handle
159
+ pprops = _safe_get_properties(param_obj)
160
+ widget = _render_widget_from_props(param_obj, name, pprops)
161
+ arg_widgets[name] = widget
162
+ controls.append(widget)
163
+
164
+ btn = pn.widgets.Button(name=f"Run {props['python_name']}", button_type="success")
165
+ obj_path = _parse_path(func)
166
+ if obj_path in _path_backup_dict:
167
+ console = pn.pane.Markdown(
168
+ f"```\n{_path_backup_dict[obj_path]}\n```", sizing_mode="stretch_width"
169
+ )
170
+ else:
171
+ console = pn.pane.Markdown(f"```\n{obj_path}\n```", sizing_mode="stretch_width")
172
+
173
+ def _run(_):
174
+ kwargs = {n: w.value for n, w in arg_widgets.items()}
175
+ try:
176
+ func(**kwargs) # Executes ONLY here
177
+ # Render kwargs similarly to your ipywidgets formatter
178
+ parts = []
179
+ for k, v in kwargs.items():
180
+ if isinstance(v, str):
181
+ if v != "":
182
+ parts.append(f"{k}='{v}'")
183
+ else:
184
+ parts.append(f"{k}={v}")
185
+ call = f"{_parse_path(func)}({', '.join(parts)})"
186
+ console.object = f"```\n{call}\n```"
187
+ _path_backup_dict[_parse_path(func)] = call
188
+ if AUTO_REFRESH:
189
+ _refresh.bump = True
190
+ except Exception as e:
191
+ console.object = f"```\nError: {e}\n```"
192
+
193
+ btn.on_click(_run)
194
+ return pn.Column(
195
+ *controls,
196
+ btn,
197
+ console,
198
+ sizing_mode="stretch_width",
199
+ margin=(10, 20),
200
+ align="start",
201
+ )
202
+
203
+
204
+ # ---------------------------
205
+ # Lazy accordion (recursive)
206
+ # ---------------------------
207
+
208
+
209
+ def _lazy_section(
210
+ title: str, loader: Callable[[], pn.viewable.Viewable]
211
+ ) -> pn.Accordion:
212
+ """
213
+ A single-node Accordion whose body is constructed ON FIRST EXPAND.
214
+ Subsequent expands reuse the existing content.
215
+ """
216
+ placeholder = pn.pane.Markdown("*(loading…)*")
217
+ acc = pn.Accordion((title, placeholder), sizing_mode="stretch_width")
218
+
219
+ def _load_content():
220
+ try:
221
+ return loader()
222
+ except Exception as e:
223
+ return pn.pane.Markdown(f"**Error loading section**: {e}")
224
+
225
+ def _maybe_load(event=None):
226
+ if acc.active and 0 in acc.active:
227
+ content = _load_content()
228
+ acc[0] = (title, content)
229
+
230
+ # Trigger when first opened
231
+ acc.param.watch(_maybe_load, "active", onlychanged=True)
232
+
233
+ def _on_refresh(event):
234
+ if acc.active and 0 in acc.active:
235
+ _maybe_load()
236
+
237
+ _refresh.param.watch(_on_refresh, "bump")
238
+
239
+ return acc
240
+
241
+
242
+ # ---------------------------
243
+ # Main entry (recursive renderer)
244
+ # ---------------------------
245
+
246
+
247
+ def _settings_view(obj, indent: int = 0) -> pn.viewable.Viewable:
248
+ """Recursively build the view for a settings object (lazy children)."""
249
+ props = _safe_get_properties(obj)
250
+
251
+ if isinstance(obj, (Group, NamedObject)):
252
+ if isinstance(obj, Group):
253
+ command_names = obj.get_active_command_names()
254
+ child_names = obj.get_active_child_names() + command_names
255
+ else:
256
+ command_names = obj.command_names
257
+ child_names = list(obj) + command_names
258
+ else:
259
+ if isinstance(obj, BaseCommand):
260
+ return _command_view(obj, props) if props["is_active"] else pn.pane.HTML("")
261
+ else:
262
+ return _param_view(obj, props) if props["is_active"] else pn.pane.HTML("")
263
+
264
+ sections: List[pn.viewable.Viewable] = []
265
+
266
+ for child_name in child_names:
267
+ # Build a lazy loader that only resolves the child on expand
268
+ def _loader(name=child_name, parent=obj, lvl=indent + 1):
269
+ try:
270
+ child_obj = getattr(parent, name)
271
+ except AttributeError:
272
+ child_obj = parent[name]
273
+ return _settings_view(child_obj, lvl)
274
+
275
+ # Each child gets its own one-item accordion (mirrors your ipywidgets UX)
276
+ if child_name in command_names:
277
+ display_name = f"⚡ {child_name}"
278
+ else:
279
+ display_name = child_name
280
+ sections.append(_lazy_section(display_name, _loader))
281
+
282
+ return pn.Column(
283
+ *(pn.Column(sec, margin=(5, 0)) for sec in sections),
284
+ sizing_mode="fixed",
285
+ margin=(10, 20),
286
+ align="start",
287
+ css_classes=["rounded-box"],
288
+ )
289
+
290
+
291
+ def build_settings_view(settings_obj) -> pn.viewable.Viewable:
292
+ """
293
+ Public API: pass any Fluent settings object.
294
+ Internally uses _root to render absolute paths and builds a lazy, web UI.
295
+ """
296
+ return _settings_view(settings_obj)