Python: fix(core): point @experimental warnings at user code, not stdlib internals (#5996)

* fix(core): point @experimental warnings at user code, not stdlib internals

Previously the wrappers installed by @experimental called warnings.warn
with a fixed stacklevel=3. ABCMeta inserts an extra abc.__new__ frame
when an experimental ABC is subclassed, so the warning landed inside
abc.py (or <frozen abc>:106 on modern CPython) instead of the user's
class Sub(...) line.

Resolve the user frame by walking inspect.currentframe(), skipping
frames whose module name is abc/functools/typing/contextlib (or
submodules), then emit via warnings.warn_explicit so the recorded
filename/lineno point at user code. Falls back to warnings.warn with
stacklevel=2 if no user frame is found. Module-name matching is used
because frozen stdlib modules report '<frozen abc>' as their filename.

Also install a one-line warnings.formatwarning specifically for
FeatureStageWarning so 'file:line: ExperimentalWarning: [ID] Name ...'
prints without the secondary source-snippet line. Other categories
delegate to the stdlib default formatter unchanged.

Added a regression test that subclasses an @experimental ABC inside
warnings.catch_warnings and asserts the recorded filename equals the
test file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(core): address review feedback on @experimental warning fix

- Make _install_feature_stage_formatter idempotent: tag the installed
  formatter with a marker attribute and short-circuit re-installation,
  so re-imports/reloads don't wrap the formatter on top of itself.
  Also expose the previous formatter via __wrapped__ for restoration.
- Avoid leaking frame references in _resolve_user_frame: capture data
  into plain locals inside try and del frame/candidate in finally,
  per CPython's guidance on inspect.currentframe usage.
- Drop redundant _WARNED_FEATURES.clear() in the new ABC subclass test
  (the autouse fixture already handles it).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* changed query for foundry web search test

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-05-22 12:07:10 +00:00
committed by GitHub
co-authored by Copilot
parent c82c0133fc
commit 578416a379
3 changed files with 158 additions and 14 deletions
@@ -142,6 +142,39 @@ def test_experimental_class_warns_on_instantiation_and_not_on_definition() -> No
assert ExperimentalClass.__feature_id__ == AlternateExperimentalFeature.EXPERIMENTAL_FEATURE.value
def test_experimental_abc_subclass_warning_points_at_user_file() -> None:
"""Subclassing an experimental ABC must report the warning at the user's
``class Sub(...):`` line, not at internal abc.py / <frozen abc> frames.
Regression: previously the fixed ``stacklevel=3`` landed inside abc.py for
ABC-driven class creation, surfacing ``<frozen abc>:106`` to users.
"""
from abc import ABC, abstractmethod
@experimental(feature_id=AlternateExperimentalFeature.EXPERIMENTAL_FEATURE) # type: ignore[arg-type]
class ExperimentalABC(ABC):
@abstractmethod
def do(self) -> int: ...
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
subclass_line = inspect.currentframe().f_lineno + 1
class Concrete(ExperimentalABC):
def do(self) -> int:
return 1
assert len(caught) == 1
assert caught[0].filename == __file__
# __init_subclass__ fires at the end of the class body, so the lineno
# points somewhere inside the Concrete class definition rather than at
# the ``class Concrete`` header itself. The key behaviour we want to
# guarantee is that it is in the *user* file at all (not abc.py).
assert subclass_line <= caught[0].lineno <= subclass_line + 5
assert issubclass(caught[0].category, ExperimentalWarning)
assert Concrete().do() == 1
def test_experimental_runtime_checkable_protocol_keeps_protocol_runtime_checks() -> None:
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")