From 656203700426e7e7fe426ae64acbd627c83e6d62 Mon Sep 17 00:00:00 2001 From: mayankansys Date: Sat, 5 Sep 2026 05:29:29 +0530 Subject: [PATCH 1/9] plan.md file --- src/ansys/fluent/core/Untitled-1.md | 223 ++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 src/ansys/fluent/core/Untitled-1.md diff --git a/src/ansys/fluent/core/Untitled-1.md b/src/ansys/fluent/core/Untitled-1.md new file mode 100644 index 00000000000..917ceccd2f8 --- /dev/null +++ b/src/ansys/fluent/core/Untitled-1.md @@ -0,0 +1,223 @@ +# Plan: Runtime Type-Checking Support in PyFluent (issue #4739) + +Add opt-in runtime type-checking to PyFluent using **beartype**, behind a thin swappable +wrapper, activated by a new `pyfluent.config` option, and enabled in CI unit tests. + +--- + +## Research verdict: why beartype, and is it feasible? + +### Candidates evaluated +| Library | Per-call cost | Deps | Coerces? | Verdict | +|---|---|---|---|---| +| **beartype** | O(1), ~1 microsec (random one-way walk) | none, pure Python, MIT | No | **Recommended** | +| typeguard | O(n) — deep-walks entire containers every call | none, MIT | No | Rejected: fatal on PyFluent hot paths | +| pydantic `@validate_call` | O(n) + Rust core | pydantic + pydantic-core (compiled) | **Yes, by default** | Rejected: silently mutates API semantics | +| jaxtyping | array shape/dtype only | needs beartype/typeguard underneath | No | Complementary, not a replacement | +| hand-rolled isinstance guards | manual | none | No | Rejected: unmaintainable at PyFluent's API surface | + +### Why beartype wins for PyFluent specifically +1. **O(1) checking is the decisive factor.** PyFluent routinely passes multi-million element + payloads through `services/field_data.py`, `SettingsBase.get_state/set_state`, + `Group.to_scheme_keys()` / `to_python_keys()`. typeguard type-checks *every* element of + *every* nested container on *every* call (documented worst case: ~107 minutes for a + 1e9-element nested list). beartype samples one random element per nesting level, giving + constant ~1 microsec regardless of payload size. Only beartype is viable on these paths. +2. **Zero transitive dependencies, pure Python, MIT.** PyFluent is redistributed inside Ansys + installations; adding a compiled Rust wheel (pydantic-core) is a supply-chain and packaging + cost. beartype adds one pure-Python wheel. +3. **No coercion.** pydantic's `@validate_call` coerces `"3"` -> `3`, `1` -> `True` etc. That + would silently change PyFluent's public API behaviour and defeat the purpose (issue #1003 + wants *Pythonic error messages for wrong data types*, not silent conversion). +4. **Non-invasive activation.** `beartype.claw.beartype_package()` decorates every annotated + callable/class at import time via an AST import hook — no source changes required, and it + can be switched off entirely so shipped-default behaviour is unchanged. +5. **Granular opt-out** via `typing.no_type_check` or `beartype(conf=BeartypeConf(strategy=O0))` + — required for flobject's gRPC proxy classes. +6. **Version support** matches `requires-python = ">=3.10,<3.15"`; full PEP 604/585/563/673 + support; pyright/mypy-clean so IDE completion is unaffected. + +### Feasibility: YES. Blockers verified and each has a concrete fix. + +1. **PR #4973's stated blocker is a one-line fix, not a blocker.** + That PR (closed, unmerged) added a comment saying `beartype_this_package()` cannot be used + because `ansys` / `ansys.fluent` are PEP 420 namespace packages, and fell back to manually + decorating functions. The *symptom* is real but the *conclusion* was wrong: + `beartype_this_package()` derives its target from `__name__.rpartition(".")[0]`, which from + `src/ansys/fluent/core/__init__.py` resolves to `ansys.fluent` — the namespace package, which + has no `__init__.py`. The fix is to name the package explicitly: + `beartype_package("ansys.fluent.core")`. Confirmed: `src/ansys/__init__.py` and + `src/ansys/fluent/__init__.py` do not exist; the 20 `__init__.py` files all live at + `ansys/fluent/core/` and below. Claw's finder matches on module-name prefix, so a namespace + parent is irrelevant. Also note the upstream link cited in that PR (beartype issue #286) is a + TypeVar Q&A discussion, unrelated to namespace packages. +2. **Dynamically generated settings classes are a non-issue.** + `get_cls()` / `_create_generated_class()` in `solver/flobject.py` build classes with `type()` + at runtime from Fluent metadata. `beartype.claw` transforms *source ASTs at import*, so it + never sees them, and they carry no `__annotations__`, so `@beartype` on them is a no-op. + They are neither checked nor slowed — a coverage gap, not a failure mode. +3. **`Solver`'s conditional base class is a genuine trap.** + `class Solver(BaseSession, settings_root.root if TYPE_CHECKING else object)` in + `session/solver.py` means the runtime MRO differs from the static MRO. Any annotation naming + `settings_root.root` / `preferences_root` / `main_menu` is unresolvable at runtime and will + raise `BeartypeCallHintForwardRefException` at call time. Must be audited and either given a + runtime-importable annotation or excluded. +4. **PEP 563 / forward-ref exposure is small and bounded** — not a rewrite: + - 16 modules use `from __future__ import annotations` + - 14 `if TYPE_CHECKING:` blocks + - ~13 implicit-Optional params (`x: SomeType = None`) + - `_types.py`: `PathType: TypeAlias = "os.PathLike[str] | str"` is a *quoted* alias, plus 4 + quoted TypedDict members +5. **Decorator stacking**: `utils/deprecate.py` `deprecate_arguments` / `deprecate_function` use + `functools.wraps` but do **not** set `__signature__`. Stacked under beartype the wrapper's + `*args/**kwargs` is what gets introspected. Fix: `wrapper.__signature__ = inspect.signature(func)` + (PR #4973 already contained this fix plus two tests — reusable). +6. **`@overload`** at `launcher/launcher.py` (8) and `session/session.py` (4): beartype ignores + overload stubs and checks the implementation signature only. Non-issue provided the + implementation signature is a true union superset. +7. **flobject proxy classes** (`Base`, `SettingsBase`, `Group`, `WildcardPath`, `NamedObject`, + `ListObject`, `Action`) define `__setattr__`/`__getattr__` that proxy to gRPC. Decorating them + risks unintended network calls during introspection and attribute-write failures. Mark with + `@typing.no_type_check` (exactly as PR #4973 did). + +--- + +## Architecture + +### Activation ordering constraint (important) +`src/ansys/fluent/core/__init__.py` eagerly imports the whole package at the top +(`from ansys.fluent.core.module_config import *` is the first statement). `beartype.claw` only +affects modules imported *after* the hook is installed. Therefore the hook must be installed as +the **very first executable statement** of `__init__.py`, before `module_config` is imported. +That means the activation switch must be read from `os.environ` directly by a dependency-free +module, and the `Config` descriptor mirrors it for introspection. + +Consequence: `pyfluent.config.runtime_type_checking = True` set *after* import cannot +retroactively hook already-imported modules. Document this; make the setter emit a warning when +it disagrees with the installed state. + +### New module: `src/ansys/fluent/core/_type_checking.py` +Sibling of `module_config.py`, **no** PyFluent imports (avoids circular import at hook time). +Public surface (the swappable wrapper mkundu1 asked for): +- `TypeCheckBackend` enum / registry: `{"beartype": _BeartypeBackend, "none": _NullBackend}` +- `runtime_type_check(obj)` — decorator; delegates to active backend, no-op when disabled +- `no_runtime_type_check(obj)` — opt-out; maps to `typing.no_type_check` +- `is_type_checking_enabled() -> bool` +- `install_import_hook() -> bool` — reads `PYFLUENT_RUNTIME_TYPE_CHECKING`, calls + `beartype.claw.beartype_package("ansys.fluent.core", conf=...)`; returns False and warns + (never raises) if `beartype` is not installed +- All `beartype` imports are local/lazy so the module is importable without beartype present. + +### Config option: `src/ansys/fluent/core/module_config.py` +Follow the existing `_ConfigDescriptor` pattern with the `#:` doc comment (auto-documented): +``` +#: Whether to enable runtime type-checking of the PyFluent API, defaults to False. +runtime_type_checking = _ConfigDescriptor["Config"]( + lambda instance: instance._env.get("PYFLUENT_RUNTIME_TYPE_CHECKING") == "1" +) +``` +(no `deprecated_var` second arg — this is a new option with no legacy module-level variable). +**Recommended default: `False`** (opt-in). Turning it on by default would convert working +user code into hard `BeartypeCallHintParamViolation` errors — a breaking change for a library +whose API is duck-typed in places. + +### Dependency +`pyproject.toml`: add `beartype>=0.19` to a new `[project.optional-dependencies] type-checking` +group **and** to the existing `tests` group. Not a hard runtime dependency, because the feature +is off by default. Add the beartype MIT license text under `LICENSES/` per repo convention. + +--- + +## Phases + +### Phase 1 — Abstraction layer + config (blocks everything else) +1. Create `src/ansys/fluent/core/_type_checking.py` as described. +2. Add `runtime_type_checking` descriptor to `module_config.py`. +3. Add `install_import_hook()` call as first statement in `core/__init__.py`. +4. `pyproject.toml`: add `beartype>=0.19` to `type-checking` + `tests` extras; add + `LICENSES/beartype-MIT.txt`. +5. New `tests/test_runtime_type_checking.py`: hook installs/doesn't install per env var; + `runtime_type_check` is a no-op when disabled; violation raises when enabled; graceful + degradation when beartype is absent (simulate via `sys.modules` patch). + +### Phase 2 — Annotation cleanup (parallelisable; 2a/2b/2c independent) +2a. **Implicit-Optional** (~13 sites): `solver/flobject.py:753`, + `codegen/builtin_settingsgen.py:179` (fix the *emitted* string too), + `search.py:303`, `meshing/meshing_workflow_new.py:268`, + `rest/transport.py:51,134,198`, `legacy/local_parametric_study.py:333`, + `utils/get_completer_info.py:44,46`, `services/object_model.py` (3). +2b. **Forward refs / PEP 563**: unquote `PathType` and the 4 quoted TypedDict members in + `_types.py`; drop `from __future__ import annotations` where it only exists to enable + self-references (16 modules — evaluate individually, keep where genuinely needed); + audit the 14 `TYPE_CHECKING` blocks for names used in *runtime-evaluated* annotations, + especially `session/solver.py` (`FluentConnection`, `settings_root`, `preferences_root`, + `main_menu`) and `fields/field_data_interfaces.py` (`VariableDescriptor`). +2c. **Decorator/opt-out fixes**: set `wrapper.__signature__` in both decorators in + `utils/deprecate.py` (+ the 2 tests from PR #4973 in `tests/test_deprecate.py`); + add `@typing.no_type_check` to `Base`, `SettingsBase`, `Group`, `WildcardPath`, + `NamedObject`, `ListObject`, `Action` and to `get_cls()` in `solver/flobject.py`. + +### Phase 3 — Iterate to green (depends on Phase 1 + 2) +6. Run the full unit suite locally with the hook on; triage each violation as either + (a) a genuine annotation bug -> fix the annotation, (b) a genuine caller bug -> fix the call, + (c) an unsupportable dynamic construct -> `no_runtime_type_check`. + Expect the bulk of the work here; keep a running list in the PR description. + +### Phase 4 — CI (depends on Phase 3 being green) +7. Add `PYFLUENT_RUNTIME_TYPE_CHECKING: 1` to the **Unit Testing** job in + `.github/workflows/ci.yml` (job `name: Unit Testing`, line ~503; step line ~571 runs + `make unittest-dev-${MATRIX_VERSION}`). Set it at the *step* level, not the workflow-global + `env:` block, so codegen/doc jobs are unaffected. +8. Keep it out of the nightly `unittest-all-*` targets initially to limit blast radius. + +### Phase 5 — Docs + changelog +9. The `#:` comment on the descriptor auto-documents the config option; add a short prose + section to the configuration docs covering: opt-in nature, the env var, the + import-ordering caveat, and how to opt a function out. +10. `doc/changelog.d/.added.md`. + +--- + +## Relevant files +- `src/ansys/fluent/core/_type_checking.py` — **new**, backend abstraction + import hook +- `src/ansys/fluent/core/__init__.py` — install hook as first statement (line ~25, before + `from ansys.fluent.core.module_config import *`) +- `src/ansys/fluent/core/module_config.py` — `_ConfigDescriptor` pattern, add + `runtime_type_checking` next to the other bool options (~line 108) +- `src/ansys/fluent/core/_types.py` — unquote `PathType` (line ~44) and 4 TypedDict members +- `src/ansys/fluent/core/solver/flobject.py` — `@no_type_check` on proxy classes / `get_cls` +- `src/ansys/fluent/core/utils/deprecate.py` — `__signature__` in both wrappers +- `src/ansys/fluent/core/session/solver.py` — `TYPE_CHECKING` conditional base, line 99 +- `pyproject.toml` — extras; `.github/workflows/ci.yml` — Unit Testing step env +- `tests/test_runtime_type_checking.py` (new), `tests/test_deprecate.py`, `tests/test_config.py` + +## Verification +1. `python -c "import ansys.fluent.core"` — clean with hook off **and** with + `PYFLUENT_RUNTIME_TYPE_CHECKING=1`. +2. `python -X importtime -c "import ansys.fluent.core"` — compare total import time hook-on vs + hook-off; beartype front-loads cost at decoration time, so guard against a large regression. +3. `pytest tests/test_runtime_type_checking.py tests/test_config.py tests/test_deprecate.py` +4. `make unittest-dev-261` with and without `PYFLUENT_RUNTIME_TYPE_CHECKING=1` — both green. +5. Negative test: `pyfluent.launch_fluent(processor_count="two")` raises a beartype violation + with hook on; unchanged (old) behaviour with hook off. +6. Micro-benchmark `SettingsBase.get_state()` and a `field_data` fetch on a large case, + hook-on vs hook-off; assert overhead stays in the microsecond range. +7. Confirm pip install without the `type-checking` extra still imports and runs (beartype absent). + +## Decisions +- beartype is the backend; wrapped behind `_type_checking.py` so it can be swapped (issue sub-task 1). +- Default **off**; opt-in via `pyfluent.config.runtime_type_checking` / `PYFLUENT_RUNTIME_TYPE_CHECKING=1` (sub-task 2). +- On in CI unit tests only (sub-task 3). +- `beartype_package("ansys.fluent.core")`, **not** `beartype_this_package()`. +- beartype is an optional extra, not a hard dependency. +- Generated settings/datamodel classes are explicitly **out of scope** for checking. +- Not adopting `pytest-beartype`: it would duplicate the hook we already own and bypass the config. + +## Further considerations +1. Violation severity — raise vs warn. Recommend: raise (default `BeartypeConf`). A `"warn"` + third mode via `BeartypeConf(violation_type=UserWarning)` is possible; confirm the parameter + exists in the pinned beartype version before promising it. +2. Should annotation cleanup (Phase 2) ship as its own PR ahead of the feature? Recommend yes — + it is behaviour-neutral, easy to review, and de-risks the feature PR. +3. Long-term: teach `codegen/settingsgen.py` to emit real annotations on generated command + methods so the settings API becomes checkable. Large, separate effort — out of scope here. From 4a89eb94d9df6d1ffedbb19e851770568b4e5950 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:00:44 +0000 Subject: [PATCH 2/9] chore: adding changelog file 5375.added.md [dependabot-skip] --- doc/changelog.d/5375.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/5375.added.md diff --git a/doc/changelog.d/5375.added.md b/doc/changelog.d/5375.added.md new file mode 100644 index 00000000000..806ca9fd1c3 --- /dev/null +++ b/doc/changelog.d/5375.added.md @@ -0,0 +1 @@ +Runtime typechecking From 4767eef99ddb8637acc8cbbd9dc5a90a9a5f3093 Mon Sep 17 00:00:00 2001 From: mayankansys Date: Tue, 8 Sep 2026 01:51:56 +0530 Subject: [PATCH 3/9] typechecking : added the base structure & files --- src/ansys/fluent/core/Untitled-1.md | 223 ------------------------ src/ansys/fluent/core/__init__.py | 7 + src/ansys/fluent/core/_type_checking.py | 208 ++++++++++++++++++++++ src/ansys/fluent/core/module_config.py | 39 +++++ 4 files changed, 254 insertions(+), 223 deletions(-) delete mode 100644 src/ansys/fluent/core/Untitled-1.md create mode 100644 src/ansys/fluent/core/_type_checking.py diff --git a/src/ansys/fluent/core/Untitled-1.md b/src/ansys/fluent/core/Untitled-1.md deleted file mode 100644 index 917ceccd2f8..00000000000 --- a/src/ansys/fluent/core/Untitled-1.md +++ /dev/null @@ -1,223 +0,0 @@ -# Plan: Runtime Type-Checking Support in PyFluent (issue #4739) - -Add opt-in runtime type-checking to PyFluent using **beartype**, behind a thin swappable -wrapper, activated by a new `pyfluent.config` option, and enabled in CI unit tests. - ---- - -## Research verdict: why beartype, and is it feasible? - -### Candidates evaluated -| Library | Per-call cost | Deps | Coerces? | Verdict | -|---|---|---|---|---| -| **beartype** | O(1), ~1 microsec (random one-way walk) | none, pure Python, MIT | No | **Recommended** | -| typeguard | O(n) — deep-walks entire containers every call | none, MIT | No | Rejected: fatal on PyFluent hot paths | -| pydantic `@validate_call` | O(n) + Rust core | pydantic + pydantic-core (compiled) | **Yes, by default** | Rejected: silently mutates API semantics | -| jaxtyping | array shape/dtype only | needs beartype/typeguard underneath | No | Complementary, not a replacement | -| hand-rolled isinstance guards | manual | none | No | Rejected: unmaintainable at PyFluent's API surface | - -### Why beartype wins for PyFluent specifically -1. **O(1) checking is the decisive factor.** PyFluent routinely passes multi-million element - payloads through `services/field_data.py`, `SettingsBase.get_state/set_state`, - `Group.to_scheme_keys()` / `to_python_keys()`. typeguard type-checks *every* element of - *every* nested container on *every* call (documented worst case: ~107 minutes for a - 1e9-element nested list). beartype samples one random element per nesting level, giving - constant ~1 microsec regardless of payload size. Only beartype is viable on these paths. -2. **Zero transitive dependencies, pure Python, MIT.** PyFluent is redistributed inside Ansys - installations; adding a compiled Rust wheel (pydantic-core) is a supply-chain and packaging - cost. beartype adds one pure-Python wheel. -3. **No coercion.** pydantic's `@validate_call` coerces `"3"` -> `3`, `1` -> `True` etc. That - would silently change PyFluent's public API behaviour and defeat the purpose (issue #1003 - wants *Pythonic error messages for wrong data types*, not silent conversion). -4. **Non-invasive activation.** `beartype.claw.beartype_package()` decorates every annotated - callable/class at import time via an AST import hook — no source changes required, and it - can be switched off entirely so shipped-default behaviour is unchanged. -5. **Granular opt-out** via `typing.no_type_check` or `beartype(conf=BeartypeConf(strategy=O0))` - — required for flobject's gRPC proxy classes. -6. **Version support** matches `requires-python = ">=3.10,<3.15"`; full PEP 604/585/563/673 - support; pyright/mypy-clean so IDE completion is unaffected. - -### Feasibility: YES. Blockers verified and each has a concrete fix. - -1. **PR #4973's stated blocker is a one-line fix, not a blocker.** - That PR (closed, unmerged) added a comment saying `beartype_this_package()` cannot be used - because `ansys` / `ansys.fluent` are PEP 420 namespace packages, and fell back to manually - decorating functions. The *symptom* is real but the *conclusion* was wrong: - `beartype_this_package()` derives its target from `__name__.rpartition(".")[0]`, which from - `src/ansys/fluent/core/__init__.py` resolves to `ansys.fluent` — the namespace package, which - has no `__init__.py`. The fix is to name the package explicitly: - `beartype_package("ansys.fluent.core")`. Confirmed: `src/ansys/__init__.py` and - `src/ansys/fluent/__init__.py` do not exist; the 20 `__init__.py` files all live at - `ansys/fluent/core/` and below. Claw's finder matches on module-name prefix, so a namespace - parent is irrelevant. Also note the upstream link cited in that PR (beartype issue #286) is a - TypeVar Q&A discussion, unrelated to namespace packages. -2. **Dynamically generated settings classes are a non-issue.** - `get_cls()` / `_create_generated_class()` in `solver/flobject.py` build classes with `type()` - at runtime from Fluent metadata. `beartype.claw` transforms *source ASTs at import*, so it - never sees them, and they carry no `__annotations__`, so `@beartype` on them is a no-op. - They are neither checked nor slowed — a coverage gap, not a failure mode. -3. **`Solver`'s conditional base class is a genuine trap.** - `class Solver(BaseSession, settings_root.root if TYPE_CHECKING else object)` in - `session/solver.py` means the runtime MRO differs from the static MRO. Any annotation naming - `settings_root.root` / `preferences_root` / `main_menu` is unresolvable at runtime and will - raise `BeartypeCallHintForwardRefException` at call time. Must be audited and either given a - runtime-importable annotation or excluded. -4. **PEP 563 / forward-ref exposure is small and bounded** — not a rewrite: - - 16 modules use `from __future__ import annotations` - - 14 `if TYPE_CHECKING:` blocks - - ~13 implicit-Optional params (`x: SomeType = None`) - - `_types.py`: `PathType: TypeAlias = "os.PathLike[str] | str"` is a *quoted* alias, plus 4 - quoted TypedDict members -5. **Decorator stacking**: `utils/deprecate.py` `deprecate_arguments` / `deprecate_function` use - `functools.wraps` but do **not** set `__signature__`. Stacked under beartype the wrapper's - `*args/**kwargs` is what gets introspected. Fix: `wrapper.__signature__ = inspect.signature(func)` - (PR #4973 already contained this fix plus two tests — reusable). -6. **`@overload`** at `launcher/launcher.py` (8) and `session/session.py` (4): beartype ignores - overload stubs and checks the implementation signature only. Non-issue provided the - implementation signature is a true union superset. -7. **flobject proxy classes** (`Base`, `SettingsBase`, `Group`, `WildcardPath`, `NamedObject`, - `ListObject`, `Action`) define `__setattr__`/`__getattr__` that proxy to gRPC. Decorating them - risks unintended network calls during introspection and attribute-write failures. Mark with - `@typing.no_type_check` (exactly as PR #4973 did). - ---- - -## Architecture - -### Activation ordering constraint (important) -`src/ansys/fluent/core/__init__.py` eagerly imports the whole package at the top -(`from ansys.fluent.core.module_config import *` is the first statement). `beartype.claw` only -affects modules imported *after* the hook is installed. Therefore the hook must be installed as -the **very first executable statement** of `__init__.py`, before `module_config` is imported. -That means the activation switch must be read from `os.environ` directly by a dependency-free -module, and the `Config` descriptor mirrors it for introspection. - -Consequence: `pyfluent.config.runtime_type_checking = True` set *after* import cannot -retroactively hook already-imported modules. Document this; make the setter emit a warning when -it disagrees with the installed state. - -### New module: `src/ansys/fluent/core/_type_checking.py` -Sibling of `module_config.py`, **no** PyFluent imports (avoids circular import at hook time). -Public surface (the swappable wrapper mkundu1 asked for): -- `TypeCheckBackend` enum / registry: `{"beartype": _BeartypeBackend, "none": _NullBackend}` -- `runtime_type_check(obj)` — decorator; delegates to active backend, no-op when disabled -- `no_runtime_type_check(obj)` — opt-out; maps to `typing.no_type_check` -- `is_type_checking_enabled() -> bool` -- `install_import_hook() -> bool` — reads `PYFLUENT_RUNTIME_TYPE_CHECKING`, calls - `beartype.claw.beartype_package("ansys.fluent.core", conf=...)`; returns False and warns - (never raises) if `beartype` is not installed -- All `beartype` imports are local/lazy so the module is importable without beartype present. - -### Config option: `src/ansys/fluent/core/module_config.py` -Follow the existing `_ConfigDescriptor` pattern with the `#:` doc comment (auto-documented): -``` -#: Whether to enable runtime type-checking of the PyFluent API, defaults to False. -runtime_type_checking = _ConfigDescriptor["Config"]( - lambda instance: instance._env.get("PYFLUENT_RUNTIME_TYPE_CHECKING") == "1" -) -``` -(no `deprecated_var` second arg — this is a new option with no legacy module-level variable). -**Recommended default: `False`** (opt-in). Turning it on by default would convert working -user code into hard `BeartypeCallHintParamViolation` errors — a breaking change for a library -whose API is duck-typed in places. - -### Dependency -`pyproject.toml`: add `beartype>=0.19` to a new `[project.optional-dependencies] type-checking` -group **and** to the existing `tests` group. Not a hard runtime dependency, because the feature -is off by default. Add the beartype MIT license text under `LICENSES/` per repo convention. - ---- - -## Phases - -### Phase 1 — Abstraction layer + config (blocks everything else) -1. Create `src/ansys/fluent/core/_type_checking.py` as described. -2. Add `runtime_type_checking` descriptor to `module_config.py`. -3. Add `install_import_hook()` call as first statement in `core/__init__.py`. -4. `pyproject.toml`: add `beartype>=0.19` to `type-checking` + `tests` extras; add - `LICENSES/beartype-MIT.txt`. -5. New `tests/test_runtime_type_checking.py`: hook installs/doesn't install per env var; - `runtime_type_check` is a no-op when disabled; violation raises when enabled; graceful - degradation when beartype is absent (simulate via `sys.modules` patch). - -### Phase 2 — Annotation cleanup (parallelisable; 2a/2b/2c independent) -2a. **Implicit-Optional** (~13 sites): `solver/flobject.py:753`, - `codegen/builtin_settingsgen.py:179` (fix the *emitted* string too), - `search.py:303`, `meshing/meshing_workflow_new.py:268`, - `rest/transport.py:51,134,198`, `legacy/local_parametric_study.py:333`, - `utils/get_completer_info.py:44,46`, `services/object_model.py` (3). -2b. **Forward refs / PEP 563**: unquote `PathType` and the 4 quoted TypedDict members in - `_types.py`; drop `from __future__ import annotations` where it only exists to enable - self-references (16 modules — evaluate individually, keep where genuinely needed); - audit the 14 `TYPE_CHECKING` blocks for names used in *runtime-evaluated* annotations, - especially `session/solver.py` (`FluentConnection`, `settings_root`, `preferences_root`, - `main_menu`) and `fields/field_data_interfaces.py` (`VariableDescriptor`). -2c. **Decorator/opt-out fixes**: set `wrapper.__signature__` in both decorators in - `utils/deprecate.py` (+ the 2 tests from PR #4973 in `tests/test_deprecate.py`); - add `@typing.no_type_check` to `Base`, `SettingsBase`, `Group`, `WildcardPath`, - `NamedObject`, `ListObject`, `Action` and to `get_cls()` in `solver/flobject.py`. - -### Phase 3 — Iterate to green (depends on Phase 1 + 2) -6. Run the full unit suite locally with the hook on; triage each violation as either - (a) a genuine annotation bug -> fix the annotation, (b) a genuine caller bug -> fix the call, - (c) an unsupportable dynamic construct -> `no_runtime_type_check`. - Expect the bulk of the work here; keep a running list in the PR description. - -### Phase 4 — CI (depends on Phase 3 being green) -7. Add `PYFLUENT_RUNTIME_TYPE_CHECKING: 1` to the **Unit Testing** job in - `.github/workflows/ci.yml` (job `name: Unit Testing`, line ~503; step line ~571 runs - `make unittest-dev-${MATRIX_VERSION}`). Set it at the *step* level, not the workflow-global - `env:` block, so codegen/doc jobs are unaffected. -8. Keep it out of the nightly `unittest-all-*` targets initially to limit blast radius. - -### Phase 5 — Docs + changelog -9. The `#:` comment on the descriptor auto-documents the config option; add a short prose - section to the configuration docs covering: opt-in nature, the env var, the - import-ordering caveat, and how to opt a function out. -10. `doc/changelog.d/.added.md`. - ---- - -## Relevant files -- `src/ansys/fluent/core/_type_checking.py` — **new**, backend abstraction + import hook -- `src/ansys/fluent/core/__init__.py` — install hook as first statement (line ~25, before - `from ansys.fluent.core.module_config import *`) -- `src/ansys/fluent/core/module_config.py` — `_ConfigDescriptor` pattern, add - `runtime_type_checking` next to the other bool options (~line 108) -- `src/ansys/fluent/core/_types.py` — unquote `PathType` (line ~44) and 4 TypedDict members -- `src/ansys/fluent/core/solver/flobject.py` — `@no_type_check` on proxy classes / `get_cls` -- `src/ansys/fluent/core/utils/deprecate.py` — `__signature__` in both wrappers -- `src/ansys/fluent/core/session/solver.py` — `TYPE_CHECKING` conditional base, line 99 -- `pyproject.toml` — extras; `.github/workflows/ci.yml` — Unit Testing step env -- `tests/test_runtime_type_checking.py` (new), `tests/test_deprecate.py`, `tests/test_config.py` - -## Verification -1. `python -c "import ansys.fluent.core"` — clean with hook off **and** with - `PYFLUENT_RUNTIME_TYPE_CHECKING=1`. -2. `python -X importtime -c "import ansys.fluent.core"` — compare total import time hook-on vs - hook-off; beartype front-loads cost at decoration time, so guard against a large regression. -3. `pytest tests/test_runtime_type_checking.py tests/test_config.py tests/test_deprecate.py` -4. `make unittest-dev-261` with and without `PYFLUENT_RUNTIME_TYPE_CHECKING=1` — both green. -5. Negative test: `pyfluent.launch_fluent(processor_count="two")` raises a beartype violation - with hook on; unchanged (old) behaviour with hook off. -6. Micro-benchmark `SettingsBase.get_state()` and a `field_data` fetch on a large case, - hook-on vs hook-off; assert overhead stays in the microsecond range. -7. Confirm pip install without the `type-checking` extra still imports and runs (beartype absent). - -## Decisions -- beartype is the backend; wrapped behind `_type_checking.py` so it can be swapped (issue sub-task 1). -- Default **off**; opt-in via `pyfluent.config.runtime_type_checking` / `PYFLUENT_RUNTIME_TYPE_CHECKING=1` (sub-task 2). -- On in CI unit tests only (sub-task 3). -- `beartype_package("ansys.fluent.core")`, **not** `beartype_this_package()`. -- beartype is an optional extra, not a hard dependency. -- Generated settings/datamodel classes are explicitly **out of scope** for checking. -- Not adopting `pytest-beartype`: it would duplicate the hook we already own and bypass the config. - -## Further considerations -1. Violation severity — raise vs warn. Recommend: raise (default `BeartypeConf`). A `"warn"` - third mode via `BeartypeConf(violation_type=UserWarning)` is possible; confirm the parameter - exists in the pinned beartype version before promising it. -2. Should annotation cleanup (Phase 2) ship as its own PR ahead of the feature? Recommend yes — - it is behaviour-neutral, easy to review, and de-risks the feature PR. -3. Long-term: teach `codegen/settingsgen.py` to emit real annotations on generated command - methods so the settings API becomes checkable. Large, separate effort — out of scope here. diff --git a/src/ansys/fluent/core/__init__.py b/src/ansys/fluent/core/__init__.py index aacc390d43a..bf1484163b2 100644 --- a/src/ansys/fluent/core/__init__.py +++ b/src/ansys/fluent/core/__init__.py @@ -25,6 +25,13 @@ # 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 install_import_hook + +install_import_hook() + # config must be initialized before logging setup. from ansys.fluent.core.module_config import * diff --git a/src/ansys/fluent/core/_type_checking.py b/src/ansys/fluent/core/_type_checking.py new file mode 100644 index 00000000000..9a9019d60e0 --- /dev/null +++ b/src/ansys/fluent/core/_type_checking.py @@ -0,0 +1,208 @@ +# 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", + "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" + +#: 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``.""" + from beartype import beartype + + return beartype(obj) + + +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: T) -> T: + """Exclude ``obj`` from runtime type-checking. + + Use this decorator on objects whose annotations cannot be evaluated at + runtime, for instance because they refer to names which only exist under + :data:`typing.TYPE_CHECKING`. + + Parameters + ---------- + obj : Callable or type + Object to exclude. + + Returns + ------- + Callable or type + ``obj``, marked as excluded. + """ + return typing.no_type_check(obj) diff --git a/src/ansys/fluent/core/module_config.py b/src/ansys/fluent/core/module_config.py index 9cbe42bfb75..cc04d817c61 100644 --- a/src/ansys/fluent/core/module_config.py +++ b/src/ansys/fluent/core/module_config.py @@ -30,12 +30,24 @@ from typing import Any, Generic, TypeVar, cast import warnings +from ansys.fluent.core._type_checking import ( + is_type_checking_enabled as _is_type_checking_enabled, +) +from ansys.fluent.core._type_checking import ( + no_runtime_type_check, +) +from ansys.fluent.core._type_checking import ENV_VAR as _TYPE_CHECKING_ENV_VAR + __all__ = ("config",) TConfig = TypeVar("TConfig", bound="Config") +# ``TConfig`` is bound to a forward reference which cannot be resolved while the +# ``Config`` class body is still executing, which is exactly when +# ``__set_name__`` runs. +@no_runtime_type_check class _ConfigDescriptor(Generic[TConfig]): """Descriptor for managing configuration attributes.""" @@ -81,6 +93,26 @@ def _get_default_examples_path(instance: "Config") -> str: return str(default_path) +class _RuntimeTypeCheckingDescriptor(_ConfigDescriptor[TConfig]): + """Descriptor for the runtime type-checking configuration attribute. + + Runtime type-checking is driven by an import hook which is installed while + PyFluent is imported. Assigning to this attribute afterwards cannot check + modules which are already imported, so a warning is emitted whenever the + assigned value disagrees with the state of the hook. + """ + + def __set__(self, instance: TConfig, value: Any): + if bool(value) != _is_type_checking_enabled(): + warnings.warn( + "Runtime type-checking cannot be changed after PyFluent is imported. " + f"Set the '{_TYPE_CHECKING_ENV_VAR}' environment variable to '1' " + "before importing PyFluent instead.", + UserWarning, + ) + super().__set__(instance, value) + + class Config: """Set the global configuration variables for PyFluent.""" @@ -302,6 +334,13 @@ class Config: lambda instance: instance._env.get("PYFLUENT_USE_RUNTIME_PYTHON_CLASSES") == "1" ) + #: Whether runtime type-checking of PyFluent APIs is active, defaults to whether the + #: ``PYFLUENT_RUNTIME_TYPE_CHECKING`` environment variable was set to ``1`` when PyFluent was imported. + #: The import hook backing this option is installed while PyFluent is imported, so assigning to this option afterwards has no effect. + runtime_type_checking = _RuntimeTypeCheckingDescriptor["Config"]( + lambda instance: _is_type_checking_enabled() + ) + #: Whether to hide sensitive information in logs, defaults to the value of ``PYFLUENT_HIDE_LOG_SECRETS`` environment variable. hide_log_secrets = _ConfigDescriptor["Config"]( lambda instance: instance._env.get("PYFLUENT_HIDE_LOG_SECRETS") == "1" From 10f645e4143932f270369fa2bf8bfbb8dff79296 Mon Sep 17 00:00:00 2001 From: mayankansys Date: Tue, 8 Sep 2026 02:03:02 +0530 Subject: [PATCH 4/9] runtime : tests & documentation --- .../contributing/environment_variables.rst | 2 + doc/source/user_guide/config_variables.rst | 37 ++++ pyproject.toml | 2 + tests/test_runtime_type_checking.py | 186 ++++++++++++++++++ 4 files changed, 227 insertions(+) create mode 100644 tests/test_runtime_type_checking.py diff --git a/doc/source/contributing/environment_variables.rst b/doc/source/contributing/environment_variables.rst index 678ba6b2e5b..6065ca98fb3 100644 --- a/doc/source/contributing/environment_variables.rst +++ b/doc/source/contributing/environment_variables.rst @@ -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() `. * - PYFLUENT_SKIP_API_UPGRADE_ADVICE diff --git a/doc/source/user_guide/config_variables.rst b/doc/source/user_guide/config_variables.rst index 2156673e562..57ff1e83f2b 100644 --- a/doc/source/user_guide/config_variables.rst +++ b/doc/source/user_guide/config_variables.rst @@ -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 `_, 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. diff --git a/pyproject.toml b/pyproject.toml index adef18787b8..3a61f62b6ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ reader = ["h5py>=3.15.1"] ui-jupyter = ["ipywidgets>=8.1.8"] ui = ["panel>=1.8.1"] search = ["nltk>=3.9.3"] +type-checking = ["beartype>=0.19,<1"] tests = [ "pytest==9.1.1", "pytest-cov==7.1.0", @@ -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", diff --git a/tests/test_runtime_type_checking.py b/tests/test_runtime_type_checking.py new file mode 100644 index 00000000000..ca15c7496d8 --- /dev/null +++ b/tests/test_runtime_type_checking.py @@ -0,0 +1,186 @@ +# 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. + +"""Tests for runtime type-checking.""" + +import os +import subprocess +import sys + +import pytest + +import ansys.fluent.core as pyfluent +from ansys.fluent.core import _type_checking + +# The import hook is installed while PyFluent is imported, which has already +# happened by the time these tests run. Anything which depends on the state of +# the hook therefore has to be exercised in a fresh interpreter. + + +def _run(source: str, enabled: bool) -> str: + """Run ``source`` in a fresh interpreter and return its stripped output.""" + env = os.environ.copy() + if enabled: + env[_type_checking.ENV_VAR] = "1" + else: + env.pop(_type_checking.ENV_VAR, None) + result = subprocess.run( + [sys.executable, "-W", "ignore", "-c", source], + env=env, + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip().splitlines()[-1] + + +@pytest.mark.parametrize("enabled", [False, True]) +def test_hook_installed_only_when_env_var_is_set(enabled): + pytest.importorskip("beartype") + source = ( + "from ansys.fluent.core import _type_checking\n" + "print(_type_checking.is_type_checking_enabled())" + ) + assert _run(source, enabled=enabled) == str(enabled) + + +@pytest.mark.parametrize("enabled", [False, True]) +def test_config_reflects_hook_state(enabled): + pytest.importorskip("beartype") + source = ( + "import ansys.fluent.core as pyfluent\n" + "print(pyfluent.config.runtime_type_checking)" + ) + assert _run(source, enabled=enabled) == str(enabled) + + +@pytest.mark.parametrize( + "enabled, expected", + [ + # With type-checking on, the bad argument is reported at the call + # boundary. With it off, it surfaces later as an obscure downstream + # error. + (True, "BeartypeCallHintParamViolation"), + (False, "AttributeError"), + ], +) +def test_type_violation_is_reported_only_when_enabled(enabled, expected): + pytest.importorskip("beartype") + source = ( + "from ansys.fluent.core.utils.fluent_version import get_version_for_file_name\n" + "try:\n" + " get_version_for_file_name(version=123)\n" + " print('no-raise')\n" + "except Exception as exc:\n" + " print(type(exc).__name__)" + ) + assert _run(source, enabled=enabled) == expected + + +def test_import_succeeds_with_type_checking_enabled(): + pytest.importorskip("beartype") + source = "import ansys.fluent.core\nprint('imported')" + assert _run(source, enabled=True) == "imported" + + +def test_runtime_type_check_is_a_no_op_when_disabled(): + def fn(x: int) -> int: + return x + + assert _type_checking.runtime_type_check(fn) is fn + + +def test_runtime_type_check_uses_the_active_backend(monkeypatch): + pytest.importorskip("beartype") + from beartype.roar import BeartypeCallHintParamViolation + + monkeypatch.setattr(_type_checking, "_HOOK_INSTALLED", True) + + @_type_checking.runtime_type_check + def fn(x: int) -> int: + return x + + assert fn(1) == 1 + with pytest.raises(BeartypeCallHintParamViolation): + fn("1") + + +def test_backend_is_swappable(monkeypatch): + monkeypatch.setattr(_type_checking, "_HOOK_INSTALLED", True) + monkeypatch.setattr(_type_checking, "BACKEND", "none") + + def fn(x: int) -> int: + return x + + assert _type_checking.runtime_type_check(fn) is fn + assert _type_checking._BACKENDS["none"]["install_hook"]() is False + + +def test_no_runtime_type_check_marks_the_object(): + def fn(x: int) -> int: + return x + + assert _type_checking.no_runtime_type_check(fn).__no_type_check__ is True + + +def test_install_import_hook_is_a_no_op_without_the_env_var(monkeypatch): + monkeypatch.delenv(_type_checking.ENV_VAR, raising=False) + monkeypatch.setattr(_type_checking, "_HOOK_INSTALLED", False) + assert _type_checking.install_import_hook() is False + assert _type_checking.is_type_checking_enabled() is False + + +def test_install_import_hook_is_idempotent(monkeypatch): + monkeypatch.setenv(_type_checking.ENV_VAR, "1") + monkeypatch.setattr(_type_checking, "_HOOK_INSTALLED", True) + assert _type_checking.install_import_hook() is False + + +def test_install_import_hook_warns_when_the_backend_is_missing(monkeypatch): + def _raise(): + raise ImportError("No module named 'beartype'") + + monkeypatch.setenv(_type_checking.ENV_VAR, "1") + monkeypatch.setattr(_type_checking, "_HOOK_INSTALLED", False) + monkeypatch.setitem( + _type_checking._BACKENDS[_type_checking.BACKEND], "install_hook", _raise + ) + with pytest.warns(UserWarning, match="runtime type-checking is disabled"): + assert _type_checking.install_import_hook() is False + assert _type_checking.is_type_checking_enabled() is False + + +def test_config_warns_when_set_after_import(monkeypatch): + monkeypatch.delattr(pyfluent.config, "_runtime_type_checking", raising=False) + with pytest.warns( + UserWarning, match="cannot be changed after PyFluent is imported" + ): + pyfluent.config.runtime_type_checking = ( + not _type_checking.is_type_checking_enabled() + ) + monkeypatch.delattr(pyfluent.config, "_runtime_type_checking", raising=False) + + +def test_config_print_includes_runtime_type_checking(capsys): + pyfluent.config.print() + assert "runtime_type_checking" in capsys.readouterr().out From 44e31cb81b739f930225fdb85b9b70d620e3c849 Mon Sep 17 00:00:00 2001 From: mayankansys Date: Fri, 18 Sep 2026 00:25:00 +0530 Subject: [PATCH 5/9] Runtime typechecking phase 1 changes --- src/ansys/fluent/core/__init__.py | 1 + src/ansys/fluent/core/_type_checking.py | 52 ++++++++++++++++++- .../core/codegen/builtin_settingsgen.py | 2 +- .../core/legacy/local_parametric_study.py | 2 +- src/ansys/fluent/core/rest/transport.py | 6 +-- src/ansys/fluent/core/search.py | 2 +- .../fluent/core/services/object_model.py | 6 +-- src/ansys/fluent/core/solver/flobject.py | 10 +++- src/ansys/fluent/core/utils/deprecate.py | 2 + .../fluent/core/utils/get_completer_info.py | 6 +-- tests/test_deprecate.py | 40 ++++++++++++++ tests/test_runtime_type_checking.py | 18 +++++-- 12 files changed, 128 insertions(+), 19 deletions(-) diff --git a/src/ansys/fluent/core/__init__.py b/src/ansys/fluent/core/__init__.py index bf1484163b2..4daa131cf1c 100644 --- a/src/ansys/fluent/core/__init__.py +++ b/src/ansys/fluent/core/__init__.py @@ -28,6 +28,7 @@ # 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() diff --git a/src/ansys/fluent/core/_type_checking.py b/src/ansys/fluent/core/_type_checking.py index 9a9019d60e0..234208f1f10 100644 --- a/src/ansys/fluent/core/_type_checking.py +++ b/src/ansys/fluent/core/_type_checking.py @@ -52,6 +52,7 @@ __all__ = ( "ENV_VAR", "PACKAGE_NAME", + "PyFluentTypeCheckingError", "install_import_hook", "is_type_checking_enabled", "no_runtime_type_check", @@ -61,6 +62,28 @@ #: 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 @@ -94,10 +117,35 @@ def _beartype_install_hook() -> bool: def _beartype_decorator(obj: T) -> T: - """Apply the ``beartype`` decorator to ``obj``.""" + """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] - return beartype(obj) + # For classes and other objects, return as-is (claw handles them) + return decorated def _no_op_install_hook() -> bool: diff --git a/src/ansys/fluent/core/codegen/builtin_settingsgen.py b/src/ansys/fluent/core/codegen/builtin_settingsgen.py index 6f225870e43..f102a3b6761 100644 --- a/src/ansys/fluent/core/codegen/builtin_settingsgen.py +++ b/src/ansys/fluent/core/codegen/builtin_settingsgen.py @@ -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") diff --git a/src/ansys/fluent/core/legacy/local_parametric_study.py b/src/ansys/fluent/core/legacy/local_parametric_study.py index c12ede1596d..bbaeb9c2027 100644 --- a/src/ansys/fluent/core/legacy/local_parametric_study.py +++ b/src/ansys/fluent/core/legacy/local_parametric_study.py @@ -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, ): diff --git a/src/ansys/fluent/core/rest/transport.py b/src/ansys/fluent/core/rest/transport.py index a7af89a45df..6c7ee8bc26d 100644 --- a/src/ansys/fluent/core/rest/transport.py +++ b/src/ansys/fluent/core/rest/transport.py @@ -48,7 +48,7 @@ class RequestStrategy(Protocol): this protocol structurally — no inheritance required. """ - def request(self, method: str, endpoint: str, *, body: Any = None) -> Any: + def request(self, method: str, endpoint: str, *, body: Any | None = None) -> Any: """Execute one HTTP request and return the decoded JSON response. Parameters @@ -131,7 +131,7 @@ def _build_request( self, method: str, url: str, - body: Any = None, + body: Any | None = None, ) -> urllib.request.Request: data: bytes | None = None headers: dict[str, str] = dict(self._headers) @@ -195,7 +195,7 @@ def _send_with_retry(self, req: urllib.request.Request, retries: int) -> Any: # RequestStrategy implementation # ------------------------------------------------------------------ - def request(self, method: str, endpoint: str, *, body: Any = None) -> Any: + def request(self, method: str, endpoint: str, *, body: Any | None = None) -> Any: """Implement :class:`RequestStrategy` — build, send, and retry.""" url = f"{self._base_url}/{endpoint}" req = self._build_request(method, url, body) diff --git a/src/ansys/fluent/core/search.py b/src/ansys/fluent/core/search.py index 19c0e8255b9..f0c2b2b51bd 100644 --- a/src/ansys/fluent/core/search.py +++ b/src/ansys/fluent/core/search.py @@ -300,7 +300,7 @@ def _search_whole_word( search_string: str, match_case: bool = False, match_whole_word: bool = True, - api_tree_data: dict = None, + api_tree_data: dict | None = None, api_path: str | None = None, ): """Perform exact search for a word through the Fluent's object hierarchy. diff --git a/src/ansys/fluent/core/services/object_model.py b/src/ansys/fluent/core/services/object_model.py index d8f658ecc5c..c5b46eb4baf 100644 --- a/src/ansys/fluent/core/services/object_model.py +++ b/src/ansys/fluent/core/services/object_model.py @@ -479,7 +479,7 @@ def set_state(self, state: Any | None = None, **kwargs) -> None: setState = set_state def get_completer_info( - self, prefix: str = "", excluded: Iterable = None + self, prefix: str = "", excluded: Iterable | None = None ) -> list[list[str]]: """Get completer information of all children. @@ -1028,7 +1028,7 @@ def get_object_names(self) -> Any: getChildObjectDisplayNames = get_object_names def get_completer_info( - self, prefix: str = "", excluded: Iterable = None + self, prefix: str = "", excluded: Iterable | None = None ) -> list[list[str]]: """Get completer information of all children. @@ -1245,7 +1245,7 @@ def create_instance(self) -> "PyArguments": return PyArguments(*args) def get_completer_info( - self, prefix: str = "", excluded: Iterable = None + self, prefix: str = "", excluded: Iterable | None = None ) -> list[list[str]]: """Get completer information of all children. diff --git a/src/ansys/fluent/core/solver/flobject.py b/src/ansys/fluent/core/solver/flobject.py index 1b57928539f..d0cbb1e5985 100644 --- a/src/ansys/fluent/core/solver/flobject.py +++ b/src/ansys/fluent/core/solver/flobject.py @@ -71,6 +71,7 @@ import warnings import weakref +from ansys.fluent.core._type_checking import no_runtime_type_check from ansys.fluent.core.utils.fluent_version import FluentVersion from ansys.fluent.core.utils.get_completer_info import ( get_completer_info as _get_completer_info, @@ -478,6 +479,7 @@ def _is_deprecated(obj) -> bool | None: ) +@no_runtime_type_check class Base: """Provides the base class for settings and command objects. @@ -750,7 +752,7 @@ def __eq__(self, other): return self.flproxy == other.flproxy and self.path == other.path def get_completer_info( - self, prefix: str = "", excluded: Iterable = None + self, prefix: str = "", excluded: Iterable | None = None ) -> list[list[str]]: """Get completer information of all children. @@ -1015,6 +1017,7 @@ def _create_child(cls, name, parent: weakref.CallableProxyType, alias_path=None) return cls(name, parent) +@no_runtime_type_check class SettingsBase(Base, Generic[StateT]): """Base class for settings objects. @@ -1268,6 +1271,7 @@ class BooleanList(SettingsBase[BoolListType], Property): } +@no_runtime_type_check class Group(SettingsBase[DictStateType]): """A ``Group`` container object. @@ -1445,6 +1449,7 @@ def __setattr__(self, name: str, value): raise +@no_runtime_type_check class WildcardPath(Group): """Class wrapping a wildcard path to perform get_var and set_var on flproxy.""" @@ -1541,6 +1546,7 @@ def __setitem__(self, name, value): ChildTypeT = TypeVar("ChildTypeT") +@no_runtime_type_check class NamedObject(SettingsBase[DictStateType], Generic[ChildTypeT]): """A ``NamedObject`` container is a container object similar to a Python dictionary object. Generally, many such objects can be created with different names. @@ -1867,6 +1873,7 @@ def _convert_to_target_units(path, state, quantity, target_units): raise UnhandledQuantity(path, state) from ex +@no_runtime_type_check class ListObject(SettingsBase[ListStateType], Generic[ChildTypeT]): """A ``ListObject`` container is a container object, similar to a Python list object. Generally, many such objects can be created. @@ -2084,6 +2091,7 @@ def _get_new_keywords(obj, *args, **kwds): return newkwds +@no_runtime_type_check class Action(Base): """Intermediate Base class for Command and Query class.""" diff --git a/src/ansys/fluent/core/utils/deprecate.py b/src/ansys/fluent/core/utils/deprecate.py index 4625795f294..1737bf01503 100644 --- a/src/ansys/fluent/core/utils/deprecate.py +++ b/src/ansys/fluent/core/utils/deprecate.py @@ -138,6 +138,7 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) + wrapper.__signature__ = inspect.signature(func) return wrapper return decorator @@ -181,6 +182,7 @@ def wrapper(*args, **kwargs): warnings.warn(reason, warning_cls, stacklevel=2) return decorated(*args, **kwargs) + wrapper.__signature__ = inspect.signature(decorated) return wrapper return decorator diff --git a/src/ansys/fluent/core/utils/get_completer_info.py b/src/ansys/fluent/core/utils/get_completer_info.py index 26dc6cb15e1..e84ac08b829 100644 --- a/src/ansys/fluent/core/utils/get_completer_info.py +++ b/src/ansys/fluent/core/utils/get_completer_info.py @@ -41,9 +41,9 @@ def get_completer_info( obj, base_class: type, prefix: str = "", - excluded: Iterable = None, - filter_function: Callable = None, - type_name_map: dict = None, + excluded: Iterable | None = None, + filter_function: Callable | None = None, + type_name_map: dict | None = None, ) -> list[list[str]]: """Get completer information of all children. diff --git a/tests/test_deprecate.py b/tests/test_deprecate.py index 75e617dae90..4655446b39f 100644 --- a/tests/test_deprecate.py +++ b/tests/test_deprecate.py @@ -21,6 +21,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import inspect import warnings import pytest @@ -171,3 +172,42 @@ def new_add(a, b): assert "3.0.0" in str(warning.message) assert result == 3 + + +def test_deprecate_arguments_preserves_signature(): + """Test that deprecate_arguments decorator preserves function signature.""" + + @deprecate_arguments(old_args="old_param", new_args="new_param", version="3.0.0") + def example_func(new_param: int, other: str = "default"): + return new_param, other + + # Check that the signature is preserved + sig = inspect.signature(example_func) + params = list(sig.parameters.keys()) + assert params == [ + "new_param", + "other", + ], f"Expected ['new_param', 'other'], got {params}" + + # Check parameter annotations + assert sig.parameters["new_param"].annotation == int + assert sig.parameters["other"].annotation == str + assert sig.parameters["other"].default == "default" + + +def test_deprecate_function_preserves_signature(): + """Test that deprecate_function decorator preserves function signature.""" + + @deprecate_function(version="3.0.0", new_func="new_multiply") + def old_multiply(x: int, y: int) -> int: + return x * y + + # Check that the signature is preserved + sig = inspect.signature(old_multiply) + params = list(sig.parameters.keys()) + assert params == ["x", "y"], f"Expected ['x', 'y'], got {params}" + + # Check parameter annotations + assert sig.parameters["x"].annotation == int + assert sig.parameters["y"].annotation == int + assert sig.return_annotation == int diff --git a/tests/test_runtime_type_checking.py b/tests/test_runtime_type_checking.py index ca15c7496d8..cbefa6814d7 100644 --- a/tests/test_runtime_type_checking.py +++ b/tests/test_runtime_type_checking.py @@ -78,8 +78,8 @@ def test_config_reflects_hook_state(enabled): "enabled, expected", [ # With type-checking on, the bad argument is reported at the call - # boundary. With it off, it surfaces later as an obscure downstream - # error. + # boundary via PyFluentTypeCheckingError. With it off, it surfaces + # later as an obscure downstream error. (True, "BeartypeCallHintParamViolation"), (False, "AttributeError"), ], @@ -112,7 +112,7 @@ def fn(x: int) -> int: def test_runtime_type_check_uses_the_active_backend(monkeypatch): pytest.importorskip("beartype") - from beartype.roar import BeartypeCallHintParamViolation + from ansys.fluent.core._type_checking import PyFluentTypeCheckingError monkeypatch.setattr(_type_checking, "_HOOK_INSTALLED", True) @@ -121,7 +121,7 @@ def fn(x: int) -> int: return x assert fn(1) == 1 - with pytest.raises(BeartypeCallHintParamViolation): + with pytest.raises(PyFluentTypeCheckingError): fn("1") @@ -184,3 +184,13 @@ def test_config_warns_when_set_after_import(monkeypatch): def test_config_print_includes_runtime_type_checking(capsys): pyfluent.config.print() assert "runtime_type_checking" in capsys.readouterr().out + + +def test_pyfluent_type_checking_error_is_exported(): + """Verify PyFluentTypeCheckingError is accessible from the main package.""" + assert hasattr(pyfluent, "PyFluentTypeCheckingError") + assert issubclass(pyfluent.PyFluentTypeCheckingError, TypeError) + # Verify it's the same class from _type_checking module + assert ( + pyfluent.PyFluentTypeCheckingError is _type_checking.PyFluentTypeCheckingError + ) From ba4fe15fcffa0f518df2d020ddd7e5e877097fe2 Mon Sep 17 00:00:00 2001 From: mayankansys Date: Fri, 18 Sep 2026 01:44:47 +0530 Subject: [PATCH 6/9] Runtime : config_variable.rst file --- doc/source/user_guide/config_variables.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/user_guide/config_variables.rst b/doc/source/user_guide/config_variables.rst index 57ff1e83f2b..aa335d86d57 100644 --- a/doc/source/user_guide/config_variables.rst +++ b/doc/source/user_guide/config_variables.rst @@ -24,7 +24,7 @@ PyFluent can check the type annotations of its own APIs while they are called, s 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 `_, which is installed with the +It relies on ``beartype`` `_, which is installed with the ``type-checking`` extra: .. code-block:: bash From bdf859af8fe32d0d36b2138bf4d2ba96690dfca3 Mon Sep 17 00:00:00 2001 From: mayankansys Date: Fri, 18 Sep 2026 16:10:50 +0530 Subject: [PATCH 7/9] Runtime : fixes _type_checking file to handle GenericAlias Objects. --- src/ansys/fluent/core/_type_checking.py | 43 +++++++++++++++++++------ 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/ansys/fluent/core/_type_checking.py b/src/ansys/fluent/core/_type_checking.py index 234208f1f10..76e22289715 100644 --- a/src/ansys/fluent/core/_type_checking.py +++ b/src/ansys/fluent/core/_type_checking.py @@ -236,21 +236,46 @@ def runtime_type_check(obj: T) -> T: return _BACKENDS[BACKEND]["decorator"](obj) -def no_runtime_type_check(obj: T) -> T: - """Exclude ``obj`` from runtime type-checking. +def no_runtime_type_check(obj): + """Disable runtime type-checking for an object, with GenericAlias support. - Use this decorator on objects whose annotations cannot be evaluated at - runtime, for instance because they refer to names which only exist under - :data:`typing.TYPE_CHECKING`. + Marks an object so that runtime type-checking is skipped. This is useful for + disabling type-checks on specific callables or classes that may cause issues + with the type-checking backend. + + Unlike the standard library's :func:`typing.no_type_check`, this function + handles ``types.GenericAlias`` objects (e.g., ``SettingsBase[DictStateType]``) + which are commonly used in class inheritance. GenericAlias objects do not + support attribute assignment, so applying ``typing.no_type_check()`` directly + would raise ``AttributeError``. This function detects such objects and returns + them unchanged. Parameters ---------- - obj : Callable or type - Object to exclude. + obj : Callable, type, or types.GenericAlias + Object to mark for skipping runtime type-checking. Can be a function, + class, or generic alias. Returns ------- - Callable or type - ``obj``, marked as excluded. + Callable, type, or types.GenericAlias + The input object unchanged, or with the ``__no_type_check__`` attribute + set if it supports attribute assignment. + + Notes + ----- + This function is a no-op when runtime type-checking is disabled via + :func:`is_type_checking_enabled`. + + See Also + -------- + :func:`typing.no_type_check` : Standard library equivalent + :func:`runtime_type_check` : Enable runtime type-checking for an object """ + # Skip applying no_type_check to GenericAlias objects (e.g., SettingsBase[Type]) + # as they don't support attribute assignment + import types + + if isinstance(obj, types.GenericAlias): + return obj return typing.no_type_check(obj) From f251110cdbcce6c5188c09eef8a0f9b5cf7a5cab Mon Sep 17 00:00:00 2001 From: mayankansys Date: Fri, 18 Sep 2026 16:27:47 +0530 Subject: [PATCH 8/9] Runtime : fixes _type_checking file to handle GenericAlias Objects. --- src/ansys/fluent/core/_type_checking.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ansys/fluent/core/_type_checking.py b/src/ansys/fluent/core/_type_checking.py index 76e22289715..74f2bcec5c9 100644 --- a/src/ansys/fluent/core/_type_checking.py +++ b/src/ansys/fluent/core/_type_checking.py @@ -273,9 +273,16 @@ def no_runtime_type_check(obj): :func:`runtime_type_check` : Enable runtime type-checking for an object """ # Skip applying no_type_check to GenericAlias objects (e.g., SettingsBase[Type]) - # as they don't support attribute assignment + # and objects that don't support attribute assignment. + # Use try-except as additional safety for edge cases where attribute assignment fails. import types if isinstance(obj, types.GenericAlias): return obj - return typing.no_type_check(obj) + + try: + return typing.no_type_check(obj) + except (AttributeError, TypeError): + # Gracefully handle objects that don't support __no_type_check__ attribute + # (e.g., generic class definitions, immutable types, etc.) + return obj From 1ee4cbfeb42eaaa6d2cde79e35ae12ec34b2a40b Mon Sep 17 00:00:00 2001 From: mayankansys Date: Mon, 21 Sep 2026 12:07:33 +0530 Subject: [PATCH 9/9] Runtime: runtime typechecking generic class removal --- src/ansys/fluent/core/_type_checking.py | 55 +++++++++++++------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/ansys/fluent/core/_type_checking.py b/src/ansys/fluent/core/_type_checking.py index 74f2bcec5c9..6be1f3427f5 100644 --- a/src/ansys/fluent/core/_type_checking.py +++ b/src/ansys/fluent/core/_type_checking.py @@ -237,52 +237,55 @@ def runtime_type_check(obj: T) -> T: def no_runtime_type_check(obj): - """Disable runtime type-checking for an object, with GenericAlias support. + """Disable runtime type-checking for an object. - Marks an object so that runtime type-checking is skipped. This is useful for - disabling type-checks on specific callables or classes that may cause issues - with the type-checking backend. + 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. - Unlike the standard library's :func:`typing.no_type_check`, this function - handles ``types.GenericAlias`` objects (e.g., ``SettingsBase[DictStateType]``) - which are commonly used in class inheritance. GenericAlias objects do not - support attribute assignment, so applying ``typing.no_type_check()`` directly - would raise ``AttributeError``. This function detects such objects and returns - them unchanged. + 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 types.GenericAlias + obj : Callable, type, or object Object to mark for skipping runtime type-checking. Can be a function, - class, or generic alias. + class, method, or other callable. Returns ------- - Callable, type, or types.GenericAlias + Callable, type, or object The input object unchanged, or with the ``__no_type_check__`` attribute - set if it supports attribute assignment. + set if the object supports attribute assignment. Notes ----- - This function is a no-op when runtime type-checking is disabled via - :func:`is_type_checking_enabled`. + 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:`typing.no_type_check` : Standard library equivalent :func:`runtime_type_check` : Enable runtime type-checking for an object """ - # Skip applying no_type_check to GenericAlias objects (e.g., SettingsBase[Type]) - # and objects that don't support attribute assignment. - # Use try-except as additional safety for edge cases where attribute assignment fails. import types + # Skip GenericAlias objects (e.g., SettingsBase[DictStateType]) + # as they don't support attribute assignment if isinstance(obj, types.GenericAlias): return obj - try: - return typing.no_type_check(obj) - except (AttributeError, TypeError): - # Gracefully handle objects that don't support __no_type_check__ attribute - # (e.g., generic class definitions, immutable types, etc.) - 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