Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

annotationlib.get_annotations() infinite loop on __wrapped__ cycle (eval_str=True) #146556

Copy link
Copy link
@raminfp

Description

@raminfp
Issue body actions

Bug report

Bug description:

annotationlib.get_annotations() hangs indefinitely when called with eval_str=True on a callable that has a circular __wrapped__ reference chain.

Reproducer

import annotationlib

def f(x: 'int') -> 'str': pass
f.__wrapped__ = f  # self-referential cycle

# Hangs forever, never returns
annotationlib.get_annotations(f, eval_str=True)

Two-node cycle also triggers it:

def g(): pass
f.__wrapped__ = g
g.__wrapped__ = f
annotationlib.get_annotations(f, eval_str=True)  # hangs

Root Cause

In Lib/annotationlib.py, get_annotations() has an early-return guard at line 1010:

if not eval_str:
    return dict(ann)   # fast path, skips unwrap entirely for default eval_str=False

When eval_str=True the code falls through to a while True: loop (lines 1039–1048)
that unwraps __wrapped__ chains with no cycle detection:

if unwrap is not None:
    while True:
        if hasattr(unwrap, "__wrapped__"):
            unwrap = unwrap.__wrapped__   #  no cycle detection
            continue
        if functools := sys.modules.get("functools"):
            if isinstance(unwrap, functools.partial):
                unwrap = unwrap.func      #  also no cycle detection
                continue
        break

Fix

Apply the same cycle-detection pattern used by inspect.unwrap() (visited id-set):

if unwrap is not None:
    seen = {id(unwrap)}
    while True:
        if hasattr(unwrap, "__wrapped__"):
            candidate = unwrap.__wrapped__
            if id(candidate) in seen:
                break
            seen.add(id(candidate))
            unwrap = candidate
            continue
        if functools := sys.modules.get("functools"):
            if isinstance(unwrap, functools.partial):
                candidate = unwrap.func
                if id(candidate) in seen:
                    break
                seen.add(id(candidate))
                unwrap = candidate
                continue
        break
    if hasattr(unwrap, "__globals__"):
        obj_globals = unwrap.__globals__

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    stdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Morty Proxy This is a proxified and sanitized view of the page, visit original site.