Skip to content

Commit bb450fc

Browse files
Copilotrchiodo
andauthored
Fix: Treat __annotate__ functions as library code in Python 3.14+ (#1988)
* Initial plan * Add special handling for __annotate__ functions in Python 3.14+ Co-authored-by: rchiodo <[email protected]> * Add Python version check for __annotate__ special handling Co-authored-by: rchiodo <[email protected]> * Fix remaining trailing whitespace Co-authored-by: rchiodo <[email protected]> * Add clarifying comment about __annotate__ name reservation Co-authored-by: rchiodo <[email protected]> * Fix ruff error: remove unused 'stop' variable in test Co-authored-by: rchiodo <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: rchiodo <[email protected]>
1 parent 1bbecdf commit bb450fc

File tree

2 files changed

+63
-1
lines changed

2 files changed

+63
-1
lines changed

src/debugpy/_vendored/pydevd/pydevd.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
PYDEVD_IPYTHON_COMPATIBLE_DEBUGGING,
9494
PYDEVD_IPYTHON_CONTEXT,
9595
PYDEVD_USE_SYS_MONITORING,
96+
IS_PY314_OR_GREATER,
9697
)
9798
from _pydevd_bundle.pydevd_defaults import PydevdCustomization # Note: import alias used on pydev_monkey.
9899
from _pydevd_bundle.pydevd_custom_frames import CustomFramesContainer, custom_frames_container_init
@@ -1292,6 +1293,16 @@ def in_project_scope(self, frame, absolute_filename=None):
12921293
if file_type == self.PYDEV_FILE:
12931294
cache[cache_key] = False
12941295

1296+
elif IS_PY314_OR_GREATER and frame.f_code.co_name == "__annotate__":
1297+
# Special handling for __annotate__ functions (PEP 649 in Python 3.14+).
1298+
# These are compiler-generated functions that can raise NotImplementedError
1299+
# when called with unsupported format arguments by inspect.call_annotate_function.
1300+
# They should be treated as library code to avoid false positives in exception handling.
1301+
# Note: PEP 649 reserves the __annotate__ name for compiler-generated functions,
1302+
# so user-defined functions with this name are discouraged and will also be treated
1303+
# as library code to maintain consistency with the language design.
1304+
cache[cache_key] = False
1305+
12951306
elif absolute_filename == "<string>":
12961307
# Special handling for '<string>'
12971308
if file_type == self.LIB_FILE:

tests/debugpy/test_exception.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
import sys
77

8-
from _pydevd_bundle.pydevd_constants import IS_PY312_OR_GREATER
8+
from _pydevd_bundle.pydevd_constants import IS_PY312_OR_GREATER, IS_PY314_OR_GREATER
99
from tests import debug
1010
from tests.debug import runners, targets
1111
from tests.patterns import some
@@ -390,3 +390,54 @@ def do_something2(n):
390390
assert min_expected_lines <= stack_line_count <= max_expected_lines
391391

392392
session.request_continue()
393+
394+
395+
@pytest.mark.skipif(not IS_PY314_OR_GREATER, reason="Test requires Python 3.14+")
396+
def test_annotate_function_not_treated_as_user_exception(pyfile, target, run):
397+
"""
398+
Test that __annotate__ functions (PEP 649) are treated as library code.
399+
In Python 3.14+, compiler-generated __annotate__ functions can raise
400+
NotImplementedError when called by inspect.call_annotate_function with
401+
unsupported format arguments. These should not be reported as user exceptions.
402+
"""
403+
@pyfile
404+
def code_to_debug():
405+
import debuggee
406+
from typing import get_type_hints
407+
408+
debuggee.setup()
409+
410+
# Define a class with annotations that will trigger __annotate__ function generation
411+
class AnnotatedClass:
412+
value: int = 42
413+
name: str = "test"
414+
415+
# This will trigger the __annotate__ function to be called by the runtime
416+
# which may raise NotImplementedError internally (expected behavior)
417+
try:
418+
hints = get_type_hints(AnnotatedClass)
419+
print(f"Type hints: {hints}") # @bp
420+
except Exception as e:
421+
print(f"Exception: {e}")
422+
423+
with debug.Session() as session:
424+
session.config["justMyCode"] = True
425+
426+
with run(session, target(code_to_debug)):
427+
# Set exception breakpoints for user uncaught exceptions
428+
session.request(
429+
"setExceptionBreakpoints",
430+
{"filters": ["userUnhandled"]}
431+
)
432+
session.set_breakpoints(code_to_debug, all)
433+
434+
# Wait for the breakpoint
435+
session.wait_for_stop(
436+
"breakpoint",
437+
expected_frames=[some.dap.frame(code_to_debug, "bp")]
438+
)
439+
440+
# The test passes if we reach here without stopping on a NotImplementedError
441+
# from __annotate__ function
442+
session.request_continue()
443+

0 commit comments

Comments
 (0)