From 5f55a51c589b64c8fcb0e577111e8db1bf8c410a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 4 Aug 2026 14:09:19 +0000 Subject: [PATCH] Fix stubgen --inspect-mode crash on PEP 604 unions types.UnionType annotations (X | Y) have neither __qualname__ nor __name__. getattr(..., typ.__name__) also evaluates the default eagerly, so inspect-mode stubgen crashed with AttributeError. Format UnionType args as X | Y and map NoneType to None. Fixes #21689 Co-authored-by: Ayush Anand Srivastava --- mypy/stubgenc.py | 20 ++++++++++++++++++-- mypy/test/teststubgen.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/mypy/stubgenc.py b/mypy/stubgenc.py index d86818adf2a43..57353d9a9e638 100755 --- a/mypy/stubgenc.py +++ b/mypy/stubgenc.py @@ -12,6 +12,7 @@ import inspect import keyword import os.path +import types from collections.abc import Callable, Mapping from types import FunctionType, ModuleType from typing import Any @@ -768,11 +769,26 @@ def generate_property_stub( rw_properties.append(f"{self._indent}{name}: {inferred_type}") - def get_type_fullname(self, typ: type) -> str: + def get_type_fullname(self, typ: object) -> str: """Given a type, return a string representation""" if typ is Any: return "Any" - typename = getattr(typ, "__qualname__", typ.__name__) + if typ is type(None): + return "None" + # PEP 604 unions (X | Y) are instances of types.UnionType at runtime. + # They have neither __qualname__ nor __name__, so format them explicitly. + if isinstance(typ, types.UnionType): + return " | ".join(self.get_type_fullname(arg) for arg in typ.__args__) + # Avoid evaluating typ.__name__ as a getattr default: that is evaluated + # eagerly and crashes for types.UnionType and similar constructs. + typename = getattr(typ, "__qualname__", None) + if typename is None: + typename = getattr(typ, "__name__", None) + if typename is None: + # This should not normally happen, but some types may resist our + # introspection attempts too hard. See + # https://github.com/python/mypy/issues/19031 + return "_typeshed.Incomplete" module_name = self.get_obj_module(typ) if module_name is None: # This should not normally happen, but some types may resist our diff --git a/mypy/test/teststubgen.py b/mypy/test/teststubgen.py index 605409f995232..a91fe6248b9ce 100644 --- a/mypy/test/teststubgen.py +++ b/mypy/test/teststubgen.py @@ -1556,6 +1556,28 @@ def __init__(self, arg0: str) -> None: ) assert_equal(gen.get_imports().splitlines(), ["from typing import overload"]) + def test_get_type_fullname_pep604_union(self) -> None: + # Regression test for https://github.com/python/mypy/issues/21689: + # types.UnionType (X | Y) must not crash under --inspect-mode. + mod = ModuleType("module", "") + gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod) + assert_equal(gen.get_type_fullname(int | str), "int | str") + assert_equal(gen.get_type_fullname(float | None), "float | None") + assert_equal(gen.get_type_fullname(int | str | bytes), "int | str | bytes") + + def test_generate_function_stub_pep604_union_annotations(self) -> None: + def process(value: int | str) -> float | None: + if isinstance(value, int): + return float(value) + return None + + output: list[str] = [] + mod = ModuleType(process.__module__, "") + gen = InspectionStubGenerator(mod.__name__, known_modules=[mod.__name__], module=mod) + gen.is_c_module = False + gen.generate_function_stub("process", process, output=output) + assert_equal(output, ["def process(value: int | str) -> float | None: ..."]) + class ArgSigSuite(unittest.TestCase): def test_repr(self) -> None: