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

Commit 2bd973e

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

File tree

7 files changed

+134
-16
lines changed
Filter options

7 files changed

+134
-16
lines changed

‎CHANGES.rst

Copy file name to clipboardExpand all lines: CHANGES.rst
+4Lines changed: 4 additions & 0 deletions
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
=====================================

‎sphinx/ext/autodoc/__init__.py

Copy file name to clipboardExpand all lines: sphinx/ext/autodoc/__init__.py
+8-4Lines changed: 8 additions & 4 deletions
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]),

‎sphinx/util/inspect.py

Copy file name to clipboardExpand all lines: sphinx/util/inspect.py
+1-1Lines changed: 1 addition & 1 deletion
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]

‎sphinx/util/typing.py

Copy file name to clipboardExpand all lines: sphinx/util/typing.py
+36-3Lines changed: 36 additions & 3 deletions
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}]'
+35-1Lines changed: 35 additions & 1 deletion
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

‎tests/test_extensions/test_ext_autodoc.py

Copy file name to clipboardExpand all lines: tests/test_extensions/test_ext_autodoc.py
+46-2Lines changed: 46 additions & 2 deletions
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

‎tests/test_util/test_util_typing.py

Copy file name to clipboardExpand all lines: tests/test_util/test_util_typing.py
+4-5Lines changed: 4 additions & 5 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.