Skip to content

Commit 2bd973e

Browse files
authored
autodoc: Fix warnings with dataclasses in Annotated metadata (#12622)
1 parent dd77f85 commit 2bd973e

File tree

7 files changed

+134
-16
lines changed

7 files changed

+134
-16
lines changed

Diff for: CHANGES.rst

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Bugs fixed
1111
* #12601, #12625: Support callable objects in :py:class:`~typing.Annotated` type
1212
metadata in the Python domain.
1313
Patch by Adam Turner.
14+
* #12601, #12622: Resolve :py:class:`~typing.Annotated` warnings with
15+
``sphinx.ext.autodoc``,
16+
especially when using :mod:`dataclasses` as type metadata.
17+
Patch by Adam Turner.
1418

1519
Release 7.4.6 (released Jul 18, 2024)
1620
=====================================

Diff for: sphinx/ext/autodoc/__init__.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -2008,7 +2008,8 @@ def import_object(self, raiseerror: bool = False) -> bool:
20082008
with mock(self.config.autodoc_mock_imports):
20092009
parent = import_module(self.modname, self.config.autodoc_warningiserror)
20102010
annotations = get_type_hints(parent, None,
2011-
self.config.autodoc_type_aliases)
2011+
self.config.autodoc_type_aliases,
2012+
include_extras=True)
20122013
if self.objpath[-1] in annotations:
20132014
self.object = UNINITIALIZED_ATTR
20142015
self.parent = parent
@@ -2097,7 +2098,8 @@ def add_directive_header(self, sig: str) -> None:
20972098
if self.config.autodoc_typehints != 'none':
20982099
# obtain annotation for this data
20992100
annotations = get_type_hints(self.parent, None,
2100-
self.config.autodoc_type_aliases)
2101+
self.config.autodoc_type_aliases,
2102+
include_extras=True)
21012103
if self.objpath[-1] in annotations:
21022104
if self.config.autodoc_typehints_format == "short":
21032105
objrepr = stringify_annotation(annotations.get(self.objpath[-1]),
@@ -2541,7 +2543,8 @@ class Foo:
25412543

25422544
def is_uninitialized_instance_attribute(self, parent: Any) -> bool:
25432545
"""Check the subject is an annotation only attribute."""
2544-
annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases)
2546+
annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases,
2547+
include_extras=True)
25452548
return self.objpath[-1] in annotations
25462549

25472550
def import_object(self, raiseerror: bool = False) -> bool:
@@ -2673,7 +2676,8 @@ def add_directive_header(self, sig: str) -> None:
26732676
if self.config.autodoc_typehints != 'none':
26742677
# obtain type annotation for this attribute
26752678
annotations = get_type_hints(self.parent, None,
2676-
self.config.autodoc_type_aliases)
2679+
self.config.autodoc_type_aliases,
2680+
include_extras=True)
26772681
if self.objpath[-1] in annotations:
26782682
if self.config.autodoc_typehints_format == "short":
26792683
objrepr = stringify_annotation(annotations.get(self.objpath[-1]),

Diff for: sphinx/util/inspect.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ def signature(
652652
try:
653653
# Resolve annotations using ``get_type_hints()`` and type_aliases.
654654
localns = TypeAliasNamespace(type_aliases)
655-
annotations = typing.get_type_hints(subject, None, localns)
655+
annotations = typing.get_type_hints(subject, None, localns, include_extras=True)
656656
for i, param in enumerate(parameters):
657657
if param.name in annotations:
658658
annotation = annotations[param.name]

Diff for: sphinx/util/typing.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import dataclasses
56
import sys
67
import types
78
import typing
@@ -157,6 +158,7 @@ def get_type_hints(
157158
obj: Any,
158159
globalns: dict[str, Any] | None = None,
159160
localns: dict[str, Any] | None = None,
161+
include_extras: bool = False,
160162
) -> dict[str, Any]:
161163
"""Return a dictionary containing type hints for a function, method, module or class
162164
object.
@@ -167,7 +169,7 @@ def get_type_hints(
167169
from sphinx.util.inspect import safe_getattr # lazy loading
168170

169171
try:
170-
return typing.get_type_hints(obj, globalns, localns)
172+
return typing.get_type_hints(obj, globalns, localns, include_extras=include_extras)
171173
except NameError:
172174
# Failed to evaluate ForwardRef (maybe TYPE_CHECKING)
173175
return safe_getattr(obj, '__annotations__', {})
@@ -267,7 +269,20 @@ def restify(cls: Any, mode: _RestifyMode = 'fully-qualified-except-typing') -> s
267269
return f':py:class:`{module_prefix}{_INVALID_BUILTIN_CLASSES[cls]}`'
268270
elif _is_annotated_form(cls):
269271
args = restify(cls.__args__[0], mode)
270-
meta = ', '.join(map(repr, cls.__metadata__))
272+
meta_args = []
273+
for m in cls.__metadata__:
274+
if isinstance(m, type):
275+
meta_args.append(restify(m, mode))
276+
elif dataclasses.is_dataclass(m):
277+
# use restify for the repr of field values rather than repr
278+
d_fields = ', '.join([
279+
fr"{f.name}=\ {restify(getattr(m, f.name), mode)}"
280+
for f in dataclasses.fields(m) if f.repr
281+
])
282+
meta_args.append(fr'{restify(type(m), mode)}\ ({d_fields})')
283+
else:
284+
meta_args.append(repr(m))
285+
meta = ', '.join(meta_args)
271286
if sys.version_info[:2] <= (3, 11):
272287
# Hardcoded to fix errors on Python 3.11 and earlier.
273288
return fr':py:class:`~typing.Annotated`\ [{args}, {meta}]'
@@ -510,7 +525,25 @@ def stringify_annotation(
510525
return f'{module_prefix}Literal[{args}]'
511526
elif _is_annotated_form(annotation): # for py39+
512527
args = stringify_annotation(annotation_args[0], mode)
513-
meta = ', '.join(map(repr, annotation.__metadata__))
528+
meta_args = []
529+
for m in annotation.__metadata__:
530+
if isinstance(m, type):
531+
meta_args.append(stringify_annotation(m, mode))
532+
elif dataclasses.is_dataclass(m):
533+
# use stringify_annotation for the repr of field values rather than repr
534+
d_fields = ', '.join([
535+
f"{f.name}={stringify_annotation(getattr(m, f.name), mode)}"
536+
for f in dataclasses.fields(m) if f.repr
537+
])
538+
meta_args.append(f'{stringify_annotation(type(m), mode)}({d_fields})')
539+
else:
540+
meta_args.append(repr(m))
541+
meta = ', '.join(meta_args)
542+
if sys.version_info[:2] <= (3, 9):
543+
if mode == 'smart':
544+
return f'~typing.Annotated[{args}, {meta}]'
545+
if mode == 'fully-qualified':
546+
return f'typing.Annotated[{args}, {meta}]'
514547
if sys.version_info[:2] <= (3, 11):
515548
if mode == 'fully-qualified-except-typing':
516549
return f'Annotated[{args}, {meta}]'

Diff for: tests/roots/test-ext-autodoc/target/annotated.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,42 @@
1-
from __future__ import annotations
1+
# from __future__ import annotations
22

3+
import dataclasses
4+
import types
35
from typing import Annotated
46

57

8+
@dataclasses.dataclass(frozen=True)
9+
class FuncValidator:
10+
func: types.FunctionType
11+
12+
13+
@dataclasses.dataclass(frozen=True)
14+
class MaxLen:
15+
max_length: int
16+
whitelisted_words: list[str]
17+
18+
19+
def validate(value: str) -> str:
20+
return value
21+
22+
23+
#: Type alias for a validated string.
24+
ValidatedString = Annotated[str, FuncValidator(validate)]
25+
26+
627
def hello(name: Annotated[str, "attribute"]) -> None:
728
"""docstring"""
829
pass
30+
31+
32+
class AnnotatedAttributes:
33+
"""docstring"""
34+
35+
#: Docstring about the ``name`` attribute.
36+
name: Annotated[str, "attribute"]
37+
38+
#: Docstring about the ``max_len`` attribute.
39+
max_len: list[Annotated[str, MaxLen(10, ['word_one', 'word_two'])]]
40+
41+
#: Docstring about the ``validated`` attribute.
42+
validated: ValidatedString

Diff for: tests/test_extensions/test_ext_autodoc.py

+46-2
Original file line numberDiff line numberDiff line change
@@ -2321,18 +2321,62 @@ def test_autodoc_TypeVar(app):
23212321

23222322
@pytest.mark.sphinx('html', testroot='ext-autodoc')
23232323
def test_autodoc_Annotated(app):
2324-
options = {"members": None}
2324+
options = {'members': None, 'member-order': 'bysource'}
23252325
actual = do_autodoc(app, 'module', 'target.annotated', options)
23262326
assert list(actual) == [
23272327
'',
23282328
'.. py:module:: target.annotated',
23292329
'',
23302330
'',
2331-
'.. py:function:: hello(name: str) -> None',
2331+
'.. py:class:: FuncValidator(func: function)',
2332+
' :module: target.annotated',
2333+
'',
2334+
'',
2335+
'.. py:class:: MaxLen(max_length: int, whitelisted_words: list[str])',
2336+
' :module: target.annotated',
2337+
'',
2338+
'',
2339+
'.. py:data:: ValidatedString',
2340+
' :module: target.annotated',
2341+
'',
2342+
' Type alias for a validated string.',
2343+
'',
2344+
' alias of :py:class:`~typing.Annotated`\\ [:py:class:`str`, '
2345+
':py:class:`~target.annotated.FuncValidator`\\ (func=\\ :py:class:`~target.annotated.validate`)]',
2346+
'',
2347+
'',
2348+
".. py:function:: hello(name: ~typing.Annotated[str, 'attribute']) -> None",
2349+
' :module: target.annotated',
2350+
'',
2351+
' docstring',
2352+
'',
2353+
'',
2354+
'.. py:class:: AnnotatedAttributes()',
23322355
' :module: target.annotated',
23332356
'',
23342357
' docstring',
23352358
'',
2359+
'',
2360+
' .. py:attribute:: AnnotatedAttributes.name',
2361+
' :module: target.annotated',
2362+
" :type: ~typing.Annotated[str, 'attribute']",
2363+
'',
2364+
' Docstring about the ``name`` attribute.',
2365+
'',
2366+
'',
2367+
' .. py:attribute:: AnnotatedAttributes.max_len',
2368+
' :module: target.annotated',
2369+
" :type: list[~typing.Annotated[str, ~target.annotated.MaxLen(max_length=10, whitelisted_words=['word_one', 'word_two'])]]",
2370+
'',
2371+
' Docstring about the ``max_len`` attribute.',
2372+
'',
2373+
'',
2374+
' .. py:attribute:: AnnotatedAttributes.validated',
2375+
' :module: target.annotated',
2376+
' :type: ~typing.Annotated[str, ~target.annotated.FuncValidator(func=~target.annotated.validate)]',
2377+
'',
2378+
' Docstring about the ``validated`` attribute.',
2379+
'',
23362380
]
23372381

23382382

Diff for: tests/test_util/test_util_typing.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ def test_restify_type_hints_containers():
196196
def test_restify_Annotated():
197197
assert restify(Annotated[str, "foo", "bar"]) == ":py:class:`~typing.Annotated`\\ [:py:class:`str`, 'foo', 'bar']"
198198
assert restify(Annotated[str, "foo", "bar"], 'smart') == ":py:class:`~typing.Annotated`\\ [:py:class:`str`, 'foo', 'bar']"
199-
assert restify(Annotated[float, Gt(-10.0)]) == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, Gt(gt=-10.0)]'
200-
assert restify(Annotated[float, Gt(-10.0)], 'smart') == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, Gt(gt=-10.0)]'
199+
assert restify(Annotated[float, Gt(-10.0)]) == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, :py:class:`tests.test_util.test_util_typing.Gt`\\ (gt=\\ -10.0)]'
200+
assert restify(Annotated[float, Gt(-10.0)], 'smart') == ':py:class:`~typing.Annotated`\\ [:py:class:`float`, :py:class:`~tests.test_util.test_util_typing.Gt`\\ (gt=\\ -10.0)]'
201201

202202

203203
def test_restify_type_hints_Callable():
@@ -521,12 +521,11 @@ def test_stringify_type_hints_pep_585():
521521
assert stringify_annotation(tuple[List[dict[int, str]], str, ...], "smart") == "tuple[~typing.List[dict[int, str]], str, ...]"
522522

523523

524-
@pytest.mark.xfail(sys.version_info[:2] <= (3, 9), reason='Needs fixing.')
525524
def test_stringify_Annotated():
526525
assert stringify_annotation(Annotated[str, "foo", "bar"], 'fully-qualified-except-typing') == "Annotated[str, 'foo', 'bar']"
527526
assert stringify_annotation(Annotated[str, "foo", "bar"], 'smart') == "~typing.Annotated[str, 'foo', 'bar']"
528-
assert stringify_annotation(Annotated[float, Gt(-10.0)], 'fully-qualified-except-typing') == "Annotated[float, Gt(gt=-10.0)]"
529-
assert stringify_annotation(Annotated[float, Gt(-10.0)], 'smart') == "~typing.Annotated[float, Gt(gt=-10.0)]"
527+
assert stringify_annotation(Annotated[float, Gt(-10.0)], 'fully-qualified-except-typing') == "Annotated[float, tests.test_util.test_util_typing.Gt(gt=-10.0)]"
528+
assert stringify_annotation(Annotated[float, Gt(-10.0)], 'smart') == "~typing.Annotated[float, ~tests.test_util.test_util_typing.Gt(gt=-10.0)]"
530529

531530

532531
def test_stringify_Unpack():

0 commit comments

Comments
 (0)