From 0864041759ee8aa6f3515c7919f80ea609b9392d Mon Sep 17 00:00:00 2001 From: Zac Kienzle Date: Sun, 25 Oct 2026 18:55:44 +1000 Subject: [PATCH] Keep unresolvable deferred annotations in inferred signatures On Python 3.14 inspect.signature evaluates deferred annotations (PEP 649) in the VALUE format, which raises NameError for a type imported only under TYPE_CHECKING, such as IO in the standard library _colorize. infer_signature now asks for the FORWARDREF format there, so the parameters keep their names and the type hint provider treats the annotation as unknown, as it already does for NameError. Fixes #153. --- src/pynguin/analyses/typesystem.py | 13 ++++++++++++- tests/analyses/test_typesystem.py | 11 +++++++++++ tests/fixtures/types/deferred_annotations.py | 14 ++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/types/deferred_annotations.py diff --git a/src/pynguin/analyses/typesystem.py b/src/pynguin/analyses/typesystem.py index 24c715ec1..c0c759edc 100644 --- a/src/pynguin/analyses/typesystem.py +++ b/src/pynguin/analyses/typesystem.py @@ -35,6 +35,9 @@ import networkx as nx from networkx.drawing.nx_pydot import to_pydot +if sys.version_info >= (3, 14): + import annotationlib + import pynguin.configuration as config import pynguin.utils.typetracing as tt from pynguin.analyses.string_subtypes import infer_regex_from_methods @@ -2369,7 +2372,15 @@ def infer_signature( """ method_for_signature = get_method_for_signature(method) try: - method_signature = inspect.signature(method_for_signature) + if sys.version_info >= (3, 14): + # Deferred annotations (PEP 649) may name types imported only for + # type checking, which raises NameError in the default VALUE format. + method_signature = inspect.signature( + method_for_signature, + annotation_format=annotationlib.Format.FORWARDREF, + ) + else: + method_signature = inspect.signature(method_for_signature) except (ValueError, TypeError): method_signature = inspect.Signature( parameters=[ diff --git a/tests/analyses/test_typesystem.py b/tests/analyses/test_typesystem.py index 28505b426..6f8eedf65 100644 --- a/tests/analyses/test_typesystem.py +++ b/tests/analyses/test_typesystem.py @@ -9,6 +9,7 @@ import inspect import operator import re +import sys from typing import Any, ParamSpec, TypeVar, Union from unittest import mock @@ -192,6 +193,16 @@ def test_infer_type_info(func, infer_types, expected_parameters, expected_return assert result.return_type == expected_return +@pytest.mark.skipif(sys.version_info < (3, 14), reason="Annotations are deferred from Python 3.14") +def test_infer_type_info_annotation_imported_for_type_checking(): + from tests.fixtures.types.deferred_annotations import length # noqa: PLC0415 + + type_system = TypeSystem() + result = type_system.infer_type_info(length, type_inference_provider=HintInference()) + assert list(result.signature.parameters) == ["obj"] + assert result.original_parameters == {"obj": AnyType()} + + A = TypeVar("A") diff --git a/tests/fixtures/types/deferred_annotations.py b/tests/fixtures/types/deferred_annotations.py new file mode 100644 index 000000000..3ba9bab9c --- /dev/null +++ b/tests/fixtures/types/deferred_annotations.py @@ -0,0 +1,14 @@ +# This file is part of Pynguin. +# +# SPDX-FileCopyrightText: 2019–2026 Pynguin Contributors +# +# SPDX-License-Identifier: MIT +# +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Sized + + +def length(obj: Sized) -> int: + return len(obj)