Skip to content

Commit 1de63f8

Browse files
author
Lihu Ben-Ezri-Ravin
authored
Updated special dataclass handling (#153)
* Add a protection against dataclass handling errors The existing test did not have any fields and missed an edge case in a bugfix python version update. * Update special dataclass handling The dataclass generated qualnames were updated in a patch version of python, likely the update that either caused or fixed https://bugs.python.org/issue34776
1 parent 2b7d248 commit 1de63f8

File tree

8 files changed

+102
-16
lines changed

8 files changed

+102
-16
lines changed

sphinx_autodoc_typehints.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,20 @@ def process_signature(app, what: str, name: str, obj, options, signature, return
165165
for param in signature.parameters.values()
166166
]
167167

168-
# The generated dataclass __init__() is weird and needs the second condition
169-
if '<locals>' in obj.__qualname__ and not (what == 'method' and name.endswith('.__init__')):
168+
# The generated dataclass __init__() and class are weird and need extra checks
169+
# This helper function operates on the generated class and methods
170+
# of a dataclass, not an instantiated dataclass object. As such,
171+
# it cannot be replaced by a call to `dataclasses.is_dataclass()`.
172+
def _is_dataclass(name: str, what: str, qualname: str) -> bool:
173+
if what == 'method' and name.endswith('.__init__'):
174+
# generated __init__()
175+
return True
176+
if what == 'class' and qualname.endswith('.__init__'):
177+
# generated class
178+
return True
179+
return False
180+
181+
if '<locals>' in obj.__qualname__ and not _is_dataclass(name, what, obj.__qualname__):
170182
logger.warning(
171183
'Cannot treat a function defined as a local function: "%s" (use @functools.wraps)',
172184
name)

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import re
23
import sys
34
import pathlib
45
import shutil
@@ -42,3 +43,12 @@ def remove_sphinx_projects(sphinx_test_tempdir):
4243
@pytest.fixture
4344
def rootdir():
4445
return path(os.path.dirname(__file__) or '.').abspath() / 'roots'
46+
47+
48+
def pytest_ignore_collect(path, config):
49+
version_re = re.compile(r'_py(\d)(\d)\.py$')
50+
match = version_re.search(path.basename)
51+
if match:
52+
version = tuple(int(x) for x in match.groups())
53+
if sys.version_info < version:
54+
return True

tests/roots/test-dataclass/conf.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import pathlib
2+
import sys
3+
4+
5+
# Make dataclass_module.py available for autodoc.
6+
sys.path.insert(0, str(pathlib.Path(__file__).parent))
7+
8+
9+
master_doc = 'index'
10+
11+
extensions = [
12+
'sphinx.ext.autodoc',
13+
'sphinx.ext.napoleon',
14+
'sphinx_autodoc_typehints',
15+
]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from dataclasses import dataclass
2+
3+
4+
@dataclass
5+
class DataClass:
6+
"""Class docstring."""
7+
8+
x: int

tests/roots/test-dataclass/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Dataclass Module
2+
================
3+
4+
.. autoclass:: dataclass_module.DataClass
5+
:undoc-members:
6+
:special-members: __init__

tests/roots/test-dummy/dummy_module.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,6 @@ def undocumented_function(x: int) -> str:
235235
return str(x)
236236

237237

238-
@dataclass
239-
class DataClass:
240-
"""Class docstring."""
241-
242-
243238
class Decorator:
244239
"""
245240
Initializer docstring.

tests/test_dataclass_py36.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import pathlib
2+
import sys
3+
import textwrap
4+
5+
import pytest
6+
7+
8+
@pytest.mark.parametrize('always_document_param_types', [True, False])
9+
@pytest.mark.sphinx('text', testroot='dataclass')
10+
def test_sphinx_output(app, status, warning, always_document_param_types):
11+
test_path = pathlib.Path(__file__).parent
12+
13+
# Add test directory to sys.path to allow imports of dummy module.
14+
if str(test_path) not in sys.path:
15+
sys.path.insert(0, str(test_path))
16+
17+
app.config.always_document_param_types = always_document_param_types
18+
app.build()
19+
20+
assert 'build succeeded' in status.getvalue() # Build succeeded
21+
22+
format_args = {}
23+
if always_document_param_types:
24+
for indentation_level in range(3):
25+
format_args['undoc_params_{}'.format(indentation_level)] = textwrap.indent(
26+
'\n\n Parameters:\n **x** ("int") --', ' ' * indentation_level
27+
)
28+
else:
29+
for indentation_level in range(3):
30+
format_args['undoc_params_{}'.format(indentation_level)] = ''
31+
32+
text_path = pathlib.Path(app.srcdir) / '_build' / 'text' / 'index.txt'
33+
with text_path.open('r') as f:
34+
text_contents = f.read().replace('–', '--')
35+
expected_contents = textwrap.dedent('''\
36+
Dataclass Module
37+
****************
38+
39+
class dataclass_module.DataClass(x)
40+
41+
Class docstring.{undoc_params_0}
42+
43+
__init__(x)
44+
45+
Initialize self. See help(type(self)) for accurate signature.{undoc_params_1}
46+
''')
47+
expected_contents = expected_contents.format(**format_args).replace('–', '--')
48+
assert text_contents == expected_contents

tests/test_sphinx_autodoc_typehints.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def test_sphinx_output(app, status, warning, always_document_param_types):
226226
if always_document_param_types:
227227
format_args['undoc_params'] = '\n\n Parameters:\n **x** ("int") --'
228228
else:
229-
format_args['undoc_params'] = ""
229+
format_args['undoc_params'] = ''
230230

231231
text_path = pathlib.Path(app.srcdir) / '_build' / 'text' / 'index.txt'
232232
with text_path.open('r') as f:
@@ -474,14 +474,6 @@ class dummy_module.ClassWithTypehintsNotInline(x=None)
474474
Return type:
475475
"str"
476476
477-
class dummy_module.DataClass
478-
479-
Class docstring.
480-
481-
__init__()
482-
483-
Initialize self. See help(type(self)) for accurate signature.
484-
485477
@dummy_module.Decorator(func)
486478
487479
Initializer docstring.

0 commit comments

Comments
 (0)