From 1d9d99497220327b23225c0ab5a8d3f8edfa3d34 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 23 Dec 2024 17:07:59 +0300 Subject: [PATCH 1/4] gh-128184: Fix docstring generation in dataclasses with forward refs --- Lib/dataclasses.py | 7 +++++++ Lib/test/test_dataclasses/__init__.py | 19 +++++++++++++++++++ ...-12-23-17-00-35.gh-issue-128184.cRQvgM.rst | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 7a24f8a9e5ccee..0fc230959a3f73 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1164,6 +1164,13 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # In some cases fetching a signature is not possible. # But, we surely should not fail in this case. text_sig = str(inspect.signature(cls)).replace(' -> None', '') + except NameError: + # This means that some types where not defined, maybe due to + # forward references, etc. In this case, try different format. + text_sig = str(inspect.signature( + cls, + annotation_format=annotationlib.Format.STRING, + )).replace(" -> 'None'", '') except (TypeError, ValueError): text_sig = '' cls.__doc__ = (cls.__name__ + text_sig) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 2e6c49e29ce828..0050fea2e31e2a 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -12,6 +12,7 @@ import types import weakref import traceback +import textwrap import unittest from unittest.mock import Mock from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol, DefaultDict @@ -2343,6 +2344,24 @@ class C: self.assertDocStrEqual(C.__doc__, "C(x:collections.deque=)") + def test_docstring_with_unsolvable_forward_ref_in_init(self): + # See: https://github.com/python/cpython/issues/128184 + ns = {} + exec( + textwrap.dedent( + """ + from dataclasses import dataclass + + @dataclass + class C: + def __init__(self, x: X) -> None: ... + """, + ), + ns, + ) + + self.assertDocStrEqual(ns['C'].__doc__, "C(x:'X')") + def test_docstring_with_no_signature(self): # See https://github.com/python/cpython/issues/103449 class Meta(type): diff --git a/Misc/NEWS.d/next/Library/2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst b/Misc/NEWS.d/next/Library/2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst new file mode 100644 index 00000000000000..3d194b12c16ae4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst @@ -0,0 +1,2 @@ +Fixes :exc:`NameError` when using :func:`dataclasses.dataclass` on classes +with unresolvable forward references. From bdfea9fc94464c82f133c4de448ba9ab1da28390 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 25 Dec 2024 10:54:52 +0300 Subject: [PATCH 2/4] Also check existing type --- Lib/test/test_dataclasses/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 0050fea2e31e2a..04c894f8b493e1 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -2354,13 +2354,13 @@ def test_docstring_with_unsolvable_forward_ref_in_init(self): @dataclass class C: - def __init__(self, x: X) -> None: ... + def __init__(self, x: X, num: int) -> None: ... """, ), ns, ) - self.assertDocStrEqual(ns['C'].__doc__, "C(x:'X')") + self.assertDocStrEqual(ns['C'].__doc__, "C(x:'X',num:'int')") def test_docstring_with_no_signature(self): # See https://github.com/python/cpython/issues/103449 From 1f3e6cf53d11c9595f9e6b8175f0aeabebac6762 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 3 Mar 2025 15:54:40 -0800 Subject: [PATCH 3/4] Fix it in inspect --- Lib/dataclasses.py | 8 ++------ Lib/inspect.py | 4 +++- Lib/test/test_inspect/test_inspect.py | 10 ++++++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 0fc230959a3f73..0f7dc9ae6b82f5 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1163,14 +1163,10 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, try: # In some cases fetching a signature is not possible. # But, we surely should not fail in this case. - text_sig = str(inspect.signature(cls)).replace(' -> None', '') - except NameError: - # This means that some types where not defined, maybe due to - # forward references, etc. In this case, try different format. text_sig = str(inspect.signature( cls, - annotation_format=annotationlib.Format.STRING, - )).replace(" -> 'None'", '') + annotation_format=annotationlib.Format.FORWARDREF, + )).replace(' -> None', '') except (TypeError, ValueError): text_sig = '' cls.__doc__ = (cls.__name__ + text_sig) diff --git a/Lib/inspect.py b/Lib/inspect.py index 124293727ca84a..5660e501a5e566 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -143,7 +143,7 @@ import abc -from annotationlib import Format +from annotationlib import Format, ForwardRef from annotationlib import get_annotations # re-exported import ast import dis @@ -1341,6 +1341,8 @@ def repl(match): if annotation.__module__ in ('builtins', base_module): return annotation.__qualname__ return annotation.__module__+'.'+annotation.__qualname__ + if isinstance(annotation, ForwardRef): + return annotation.__forward_arg__ return repr(annotation) def formatannotationrelativeto(object): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 06785e275f6b11..641caeb18fcb26 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1,3 +1,4 @@ +from Lib import annotationlib from annotationlib import Format, ForwardRef import asyncio import builtins @@ -1732,6 +1733,10 @@ def test_typing_replacement(self): self.assertEqual(inspect.formatannotation(ann), 'Union[List[str], int]') self.assertEqual(inspect.formatannotation(ann1), 'Union[List[testModule.typing.A], int]') + def test_forwardref(self): + fwdref = ForwardRef('fwdref') + self.assertEqual(inspect.formatannotation(fwdref), 'fwdref') + class TestIsMethodDescriptor(unittest.TestCase): @@ -4566,6 +4571,11 @@ def foo(a: list[str]) -> Tuple[str, float]: self.assertEqual(str(inspect.signature(foo)), inspect.signature(foo).format()) + def foo(x: undef): + pass + sig = inspect.signature(foo, annotation_format=annotationlib.Format.FORWARDREF) + self.assertEqual(str(sig), '(x: undef)') + def test_signature_str_positional_only(self): P = inspect.Parameter S = inspect.Signature From e36c0e068999ec7b92067982f1db7e0ba33bb550 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 3 Mar 2025 15:59:02 -0800 Subject: [PATCH 4/4] Fixes and additions --- Lib/test/test_dataclasses/__init__.py | 9 ++++++++- Lib/test/test_inspect/test_inspect.py | 3 +-- .../2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst | 6 ++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 04c894f8b493e1..8209374c36bca2 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -2344,6 +2344,13 @@ class C: self.assertDocStrEqual(C.__doc__, "C(x:collections.deque=)") + def test_docstring_undefined_name(self): + @dataclass + class C: + x: undef + + self.assertDocStrEqual(C.__doc__, "C(x:undef)") + def test_docstring_with_unsolvable_forward_ref_in_init(self): # See: https://github.com/python/cpython/issues/128184 ns = {} @@ -2360,7 +2367,7 @@ def __init__(self, x: X, num: int) -> None: ... ns, ) - self.assertDocStrEqual(ns['C'].__doc__, "C(x:'X',num:'int')") + self.assertDocStrEqual(ns['C'].__doc__, "C(x:X,num:int)") def test_docstring_with_no_signature(self): # See https://github.com/python/cpython/issues/103449 diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 2328d745752b18..03f2bacb3a4e88 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -1,4 +1,3 @@ -from Lib import annotationlib from annotationlib import Format, ForwardRef import asyncio import builtins @@ -4594,7 +4593,7 @@ def foo(a: list[str]) -> Tuple[str, float]: def foo(x: undef): pass - sig = inspect.signature(foo, annotation_format=annotationlib.Format.FORWARDREF) + sig = inspect.signature(foo, annotation_format=Format.FORWARDREF) self.assertEqual(str(sig), '(x: undef)') def test_signature_str_positional_only(self): diff --git a/Misc/NEWS.d/next/Library/2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst b/Misc/NEWS.d/next/Library/2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst index 3d194b12c16ae4..448dcfe5a7ccdb 100644 --- a/Misc/NEWS.d/next/Library/2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst +++ b/Misc/NEWS.d/next/Library/2024-12-23-17-00-35.gh-issue-128184.cRQvgM.rst @@ -1,2 +1,4 @@ -Fixes :exc:`NameError` when using :func:`dataclasses.dataclass` on classes -with unresolvable forward references. +Improve display of :class:`annotationlib.ForwardRef` object +within :class:`inspect.Signature` representations. +This also fixes a :exc:`NameError` that was raised when using +:func:`dataclasses.dataclass` on classes with unresolvable forward references.