Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/5375.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Runtime typechecking
2 changes: 2 additions & 0 deletions doc/source/contributing/environment_variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ control the behavior of PyFluent within the same Python process. Please see the
- Enabled PyFluent logging and specifies the logging level. Possible values are ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, and ``CRITICAL``.
* - PYFLUENT_NO_FIX_PARAMETER_LIST_RETURN
- Disables the return value fix for the parameter list command in settings API.
* - PYFLUENT_RUNTIME_TYPE_CHECKING
- Set to ``1`` to check the type annotations of PyFluent APIs at runtime. Requires the ``type-checking`` extra.
* - PYFLUENT_SHOW_SERVER_GUI
- Shows the Fluent GUI while launching Fluent in :func:`launch_fluent() <ansys.fluent.core.launcher.launcher.launch_fluent>`.
* - PYFLUENT_SKIP_API_UPGRADE_ADVICE
Expand Down
37 changes: 37 additions & 0 deletions doc/source/user_guide/config_variables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,40 @@ The following code demonstrates how to access and modify the path within the Flu
>>> config.container_mount_target = '/home/my_user/workdir' # set a new value
>>> config.container_mount_target # new value
'/home/my_user/workdir'

Runtime type-checking
---------------------

PyFluent can check the type annotations of its own APIs while they are called, so that an
argument of the wrong type is reported at the call itself instead of surfacing later as an
obscure failure. This is intended for development and testing, and is disabled by default.

It relies on ``beartype`` <https://beartype.readthedocs.io>`_, which is installed with the
``type-checking`` extra:

.. code-block:: bash

pip install ansys-fluent-core[type-checking]

Type-checking is applied by an import hook, so it has to be requested through the
``PYFLUENT_RUNTIME_TYPE_CHECKING`` environment variable **before** PyFluent is imported.
Setting ``config.runtime_type_checking`` afterwards has no effect and issues a warning, because
modules which are already imported cannot be checked retrospectively.

.. code-block:: bash

export PYFLUENT_RUNTIME_TYPE_CHECKING=1

The ``config.runtime_type_checking`` variable reports whether type-checking is active in the
current process:

.. code-block:: python

>>> from ansys.fluent.core import config
>>> config.runtime_type_checking
True

Passing an argument of the wrong type then raises a ``beartype.roar.BeartypeCallHintParamViolation``.

If the ``type-checking`` extra is not installed, PyFluent warns and continues with
type-checking disabled.
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ include = ["src/ansys/fluent/core/generated/"]
reader = ["h5py>=3.15.1"]
ui-jupyter = ["ipywidgets>=8.1.8"]
ui = ["panel>=1.8.1"]
type-checking = ["beartype>=0.19,<1"]
search = ["nltk>=3.10.3"]
tests = [
"pytest==9.1.1",
Expand All @@ -54,6 +55,7 @@ tests = [
"pyfakefs==6.2.0",
"ansys-platform-instancemanagement==1.1.2",
"ansys-tools-common==0.5.2",
"beartype==0.22.9",
"docker==7.1.0",
"grpcio==1.81.1",
"grpcio-health-checking==1.81.1",
Expand Down
8 changes: 8 additions & 0 deletions src/ansys/fluent/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@

# isort: off

# Runtime type-checking works by transforming module source at import time, so
# the hook has to be installed before any other PyFluent module is imported.
# This module only depends on the standard library.
from ansys.fluent.core._type_checking import PyFluentTypeCheckingError
from ansys.fluent.core._type_checking import install_import_hook

install_import_hook()

# config must be initialized before logging setup.
from ansys.fluent.core.module_config import *

Expand Down
291 changes: 291 additions & 0 deletions src/ansys/fluent/core/_type_checking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
# Copyright (C) 2021 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
# 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.

"""Runtime type-checking support for PyFluent.

This module is the single integration point for the third-party runtime
type-checking library, so that the library can be swapped without touching any
call site. ``beartype`` is the default backend.

Runtime type-checking is **disabled by default**. It is activated by setting the
``PYFLUENT_RUNTIME_TYPE_CHECKING`` environment variable to ``"1"`` before
importing PyFluent, and is reflected by
:attr:`ansys.fluent.core.config.runtime_type_checking`.

Notes
-----
The hook has to be installed before any PyFluent submodule is imported, because
it works by transforming module source at import time. Modules which are
already imported are never checked. This is why the switch is read from the
environment here instead of from the configuration object, which itself lives in
a PyFluent submodule.

This module must only depend on the standard library so that it can be imported
as the very first statement of ``ansys.fluent.core``.
"""

from collections.abc import Callable
import os
import typing
import warnings

__all__ = (
"ENV_VAR",
"PACKAGE_NAME",
"PyFluentTypeCheckingError",
"install_import_hook",
"is_type_checking_enabled",
"no_runtime_type_check",
"runtime_type_check",
)

#: Environment variable which activates runtime type-checking.
ENV_VAR = "PYFLUENT_RUNTIME_TYPE_CHECKING"


class PyFluentTypeCheckingError(TypeError):
"""Runtime type-checking violation in PyFluent API.

Raised when a PyFluent function or method is called with arguments that do
not match its declared type annotations, and runtime type-checking is
enabled via the ``PYFLUENT_RUNTIME_TYPE_CHECKING`` environment variable.

This exception wraps the underlying type-checking backend's exception,
providing a stable PyFluent API that is independent of the backend
implementation (e.g., ``beartype`` vs ``typeguard``).

Users should not need to know or import the specific type-checking library.

See Also
--------
:attr:`ansys.fluent.core.config.runtime_type_checking` : Configuration option
"""

pass


#: Package whose submodules are type-checked at import time.
#:
#: ``ansys`` and ``ansys.fluent`` are :pep:`420` namespace packages, so the
#: package has to be named explicitly. ``beartype_this_package()`` must not be
#: used here: it derives its target from the parent of ``__name__``, which
#: resolves to the ``ansys.fluent`` namespace package and fails.
PACKAGE_NAME = "ansys.fluent.core"

#: Name of the active backend.
BACKEND = "beartype"

_HOOK_INSTALLED = False

T = typing.TypeVar("T")


def _beartype_install_hook() -> bool:
"""Install the ``beartype`` import hook on the PyFluent package."""
from beartype import BeartypeConf
from beartype.claw import beartype_package

beartype_package(
PACKAGE_NAME,
conf=BeartypeConf(
# Do not check :pep:`526` annotated variable assignments. Only
# callable parameters and return values are checked.
claw_is_pep526=False,
),
)
return True


def _beartype_decorator(obj: T) -> T:
"""Apply the ``beartype`` decorator to ``obj``, wrapping exceptions.

Catches backend-specific exceptions and re-raises as PyFluentTypeCheckingError
to maintain encapsulation and API stability.
"""
import functools

from beartype import beartype
from beartype.roar import BeartypeException

# Apply beartype decorator
decorated = beartype(obj)

# For callables (not classes), wrap to catch backend exceptions
if callable(decorated) and not isinstance(decorated, type):

@functools.wraps(decorated)
def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
try:
return decorated(*args, **kwargs)
except BeartypeException as exc:
raise PyFluentTypeCheckingError(
f"Type-checking violation: {exc}"
) from exc

return wrapper # type: ignore[return-value]

# For classes and other objects, return as-is (claw handles them)
return decorated


def _no_op_install_hook() -> bool:
"""Do not install any import hook."""
return False


def _no_op_decorator(obj: T) -> T:
"""Return ``obj`` unchanged."""
return obj


#: Supported backends. Each entry maps a backend name to its import-hook
#: installer and its decorator, which is the only pair of operations the rest of
#: PyFluent relies on.
_BACKENDS: dict[str, dict[str, Callable]] = {
"beartype": {
"install_hook": _beartype_install_hook,
"decorator": _beartype_decorator,
},
"none": {
"install_hook": _no_op_install_hook,
"decorator": _no_op_decorator,
},
}


def is_type_checking_enabled() -> bool:
"""Whether runtime type-checking is active in the current process.

Returns
-------
bool
``True`` if the import hook has been installed.
"""
return _HOOK_INSTALLED


def install_import_hook() -> bool:
"""Install the runtime type-checking import hook.

This is a no-op unless the ``PYFLUENT_RUNTIME_TYPE_CHECKING`` environment
variable is set to ``"1"``. It never raises: if the backend is not
installed, a warning is emitted and type-checking stays disabled.

Returns
-------
bool
``True`` if the hook was installed by this call.
"""
global _HOOK_INSTALLED
if _HOOK_INSTALLED:
return False
if os.environ.get(ENV_VAR) != "1":
return False
try:
_HOOK_INSTALLED = _BACKENDS[BACKEND]["install_hook"]()
except ImportError:
warnings.warn(
f"{ENV_VAR} is set but the '{BACKEND}' package is not installed, so "
"runtime type-checking is disabled. Install it with "
"'pip install ansys-fluent-core[type-checking]'.",
UserWarning,
)
_HOOK_INSTALLED = False
return _HOOK_INSTALLED


def runtime_type_check(obj: T) -> T:
"""Type-check the annotations of ``obj`` at runtime.

Use this decorator for objects which the import hook cannot reach, such as
classes created dynamically. It is a no-op when runtime type-checking is
disabled.

Parameters
----------
obj : Callable or type
Object to type-check.

Returns
-------
Callable or type
The type-checked object, or ``obj`` unchanged when disabled.
"""
if not _HOOK_INSTALLED:
return obj
return _BACKENDS[BACKEND]["decorator"](obj)


def no_runtime_type_check(obj):
"""Disable runtime type-checking for an object.

Marks an object so that beartype (or other type-checking backends) skip
validation. This is useful for disabling type-checks on specific callables
or classes that may cause issues with type-checking or have incompatible
type annotations.

This function directly sets the ``__no_type_check__`` attribute that beartype
and other type-checking libraries recognize, without relying on the standard
library's :func:`typing.no_type_check` which has issues with generic class
definitions.

Parameters
----------
obj : Callable, type, or object
Object to mark for skipping runtime type-checking. Can be a function,
class, method, or other callable.

Returns
-------
Callable, type, or object
The input object unchanged, or with the ``__no_type_check__`` attribute
set if the object supports attribute assignment.

Notes
-----
Objects that cannot have attributes assigned (e.g., types.GenericAlias,
built-in types) are returned unchanged. This is acceptable because these
objects are typically not directly callable or type-checkable anyway.

See Also
--------
:func:`runtime_type_check` : Enable runtime type-checking for an object
"""
import types

# Skip GenericAlias objects (e.g., SettingsBase[DictStateType])
# as they don't support attribute assignment
if isinstance(obj, types.GenericAlias):
return obj

# Try to set __no_type_check__ directly on objects that support it
# Skip objects that don't support attribute assignment
if hasattr(obj, "__dict__") or isinstance(obj, type):
try:
obj.__no_type_check__ = True # type: ignore[attr-defined]
except (AttributeError, TypeError):
# Object doesn't support attribute assignment, return unchanged
# This is acceptable - such objects typically aren't type-checkable anyway
pass

return obj
2 changes: 1 addition & 1 deletion src/ansys/fluent/core/codegen/builtin_settingsgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def _write_init_signature(f, kind: str, named_objects: list) -> None:
f.write(f", {named_object}: str")
f.write(", settings_source: SettingsBase | Solver | None = None")
if kind == "NonCreatableNamedObject":
f.write(", name: str = None")
f.write(", name: str | None = None")
elif kind == "CreatableNamedObject":
f.write(", name: str | None = None, new_instance_name: str | None = None")
f.write("):\n")
Expand Down
2 changes: 1 addition & 1 deletion src/ansys/fluent/core/local_parametric_study.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ def design_point(self, idx_or_name) -> LocalDesignPoint:
def run_in_fluent(
self,
num_servers: int,
launcher: Any = None,
launcher: Any | None = None,
start_transcript: bool = False,
capture_report_data: bool = False,
):
Expand Down
Loading
Loading