mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
co-authored by
Copilot
parent
c82c0133fc
commit
578416a379
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user