Skip to content

Commit 30efa3d

Browse files
authored
Merge branch '4.x' into 8597_metadata_only_docstring
2 parents 469def5 + cb7b41f commit 30efa3d

File tree

10 files changed

+118
-31
lines changed

10 files changed

+118
-31
lines changed

CHANGES

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Deprecated
1515
Features added
1616
--------------
1717

18+
* #8107: autodoc: Add ``class-doc-from`` option to :rst:dir:`autoclass`
19+
directive to control the content of the specific class like
20+
:confval:`autoclass_content`
1821
* #9129: html search: Show search summaries when html_copy_source = False
1922
* #9120: html theme: Eliminate prompt characters of code-block from copyable
2023
text
@@ -24,6 +27,7 @@ Features added
2427
Bugs fixed
2528
----------
2629

30+
* #8872: autodoc: stacked singledispatches are wrongly rendered
2731
* #8597: autodoc: a docsting having metadata only should be treated as
2832
undocumented
2933

doc/usage/extensions/autodoc.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,10 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
343343

344344
.. autoclass:: module.name::Noodle
345345

346+
* :rst:dir:`autoclass` also recognizes the ``class-doc-from`` option that
347+
can be used to override the global value of :confval:`autoclass_content`.
348+
349+
.. versionadded:: 4.1
346350

347351
.. rst:directive:: autofunction
348352
autodecorator
@@ -507,7 +511,7 @@ There are also config values that you can set:
507511
The supported options are ``'members'``, ``'member-order'``,
508512
``'undoc-members'``, ``'private-members'``, ``'special-members'``,
509513
``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'``,
510-
``'imported-members'`` and ``'exclude-members'``.
514+
``'imported-members'``, ``'exclude-members'`` and ``'class-doc-from'``.
511515

512516
.. versionadded:: 1.8
513517

@@ -517,6 +521,9 @@ There are also config values that you can set:
517521
.. versionchanged:: 2.1
518522
Added ``'imported-members'``.
519523

524+
.. versionchanged:: 4.1
525+
Added ``'class-doc-from'``.
526+
520527
.. confval:: autodoc_docstring_signature
521528

522529
Functions imported from C modules cannot be introspected, and therefore the

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
'sphinxcontrib-htmlhelp',
2222
'sphinxcontrib-serializinghtml',
2323
'sphinxcontrib-qthelp',
24-
'Jinja2>=2.3',
24+
'Jinja2>=2.3,<3.0',
25+
'MarkupSafe<2.0',
2526
'Pygments>=2.0',
2627
'docutils>=0.14,<0.18',
2728
'snowballstemmer>=1.1',

sphinx/ext/autodoc/__init__.py

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ def member_order_option(arg: Any) -> Optional[str]:
129129
raise ValueError(__('invalid value for member-order option: %s') % arg)
130130

131131

132+
def class_doc_from_option(arg: Any) -> Optional[str]:
133+
"""Used to convert the :class-doc-from: option to autoclass directives."""
134+
if arg in ('both', 'class', 'init'):
135+
return arg
136+
else:
137+
raise ValueError(__('invalid value for class-doc-from option: %s') % arg)
138+
139+
132140
SUPPRESS = object()
133141

134142

@@ -1320,12 +1328,12 @@ def format_signature(self, **kwargs: Any) -> str:
13201328
if typ is object:
13211329
pass # default implementation. skipped.
13221330
else:
1323-
self.annotate_to_first_argument(func, typ)
1324-
1325-
documenter = FunctionDocumenter(self.directive, '')
1326-
documenter.object = func
1327-
documenter.objpath = [None]
1328-
sigs.append(documenter.format_signature())
1331+
dispatchfunc = self.annotate_to_first_argument(func, typ)
1332+
if dispatchfunc:
1333+
documenter = FunctionDocumenter(self.directive, '')
1334+
documenter.object = dispatchfunc
1335+
documenter.objpath = [None]
1336+
sigs.append(documenter.format_signature())
13291337
if overloaded:
13301338
actual = inspect.signature(self.object,
13311339
type_aliases=self.config.autodoc_type_aliases)
@@ -1350,28 +1358,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu
13501358

13511359
return overload.replace(parameters=parameters)
13521360

1353-
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
1361+
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
13541362
"""Annotate type hint to the first argument of function if needed."""
13551363
try:
13561364
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
13571365
except TypeError as exc:
13581366
logger.warning(__("Failed to get a function signature for %s: %s"),
13591367
self.fullname, exc)
1360-
return
1368+
return None
13611369
except ValueError:
1362-
return
1370+
return None
13631371

13641372
if len(sig.parameters) == 0:
1365-
return
1373+
return None
1374+
1375+
def dummy():
1376+
pass
13661377

13671378
params = list(sig.parameters.values())
13681379
if params[0].annotation is Parameter.empty:
13691380
params[0] = params[0].replace(annotation=typ)
13701381
try:
1371-
func.__signature__ = sig.replace(parameters=params) # type: ignore
1382+
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
1383+
return dummy
13721384
except (AttributeError, TypeError):
13731385
# failed to update signature (ex. built-in or extension types)
1374-
return
1386+
return None
1387+
else:
1388+
return None
13751389

13761390

13771391
class DecoratorDocumenter(FunctionDocumenter):
@@ -1417,6 +1431,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
14171431
'show-inheritance': bool_option, 'member-order': member_order_option,
14181432
'exclude-members': exclude_members_option,
14191433
'private-members': members_option, 'special-members': members_option,
1434+
'class-doc-from': class_doc_from_option,
14201435
}
14211436

14221437
_signature_class: Any = None
@@ -1651,7 +1666,7 @@ def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
16511666
if lines is not None:
16521667
return lines
16531668

1654-
content = self.config.autoclass_content
1669+
classdoc_from = self.options.get('class-doc-from', self.config.autoclass_content)
16551670

16561671
docstrings = []
16571672
attrdocstring = self.get_attr(self.object, '__doc__', None)
@@ -1660,7 +1675,7 @@ def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
16601675

16611676
# for classes, what the "docstring" is can be controlled via a
16621677
# config value; the default is only the class docstring
1663-
if content in ('both', 'init'):
1678+
if classdoc_from in ('both', 'init'):
16641679
__init__ = self.get_attr(self.object, '__init__', None)
16651680
initdocstring = getdoc(__init__, self.get_attr,
16661681
self.config.autodoc_inherit_docstrings,
@@ -1682,7 +1697,7 @@ def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
16821697
initdocstring.strip() == object.__new__.__doc__)): # for !pypy
16831698
initdocstring = None
16841699
if initdocstring:
1685-
if content == 'init':
1700+
if classdoc_from == 'init':
16861701
docstrings = [initdocstring]
16871702
else:
16881703
docstrings.append(initdocstring)
@@ -2109,13 +2124,13 @@ def format_signature(self, **kwargs: Any) -> str:
21092124
if typ is object:
21102125
pass # default implementation. skipped.
21112126
else:
2112-
self.annotate_to_first_argument(func, typ)
2113-
2114-
documenter = MethodDocumenter(self.directive, '')
2115-
documenter.parent = self.parent
2116-
documenter.object = func
2117-
documenter.objpath = [None]
2118-
sigs.append(documenter.format_signature())
2127+
dispatchmeth = self.annotate_to_first_argument(func, typ)
2128+
if dispatchmeth:
2129+
documenter = MethodDocumenter(self.directive, '')
2130+
documenter.parent = self.parent
2131+
documenter.object = dispatchmeth
2132+
documenter.objpath = [None]
2133+
sigs.append(documenter.format_signature())
21192134
if overloaded:
21202135
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
21212136
actual = inspect.signature(self.object, bound_method=False,
@@ -2149,27 +2164,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu
21492164

21502165
return overload.replace(parameters=parameters)
21512166

2152-
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
2167+
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
21532168
"""Annotate type hint to the first argument of function if needed."""
21542169
try:
21552170
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
21562171
except TypeError as exc:
21572172
logger.warning(__("Failed to get a method signature for %s: %s"),
21582173
self.fullname, exc)
2159-
return
2174+
return None
21602175
except ValueError:
2161-
return
2176+
return None
2177+
21622178
if len(sig.parameters) == 1:
2163-
return
2179+
return None
2180+
2181+
def dummy():
2182+
pass
21642183

21652184
params = list(sig.parameters.values())
21662185
if params[1].annotation is Parameter.empty:
21672186
params[1] = params[1].replace(annotation=typ)
21682187
try:
2169-
func.__signature__ = sig.replace(parameters=params) # type: ignore
2188+
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
2189+
return dummy
21702190
except (AttributeError, TypeError):
21712191
# failed to update signature (ex. built-in or extension types)
2172-
return
2192+
return None
2193+
else:
2194+
return None
21732195

21742196

21752197
class NonDataDescriptorMixin(DataDocumenterMixinBase):

sphinx/ext/autodoc/directive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members',
3131
'show-inheritance', 'private-members', 'special-members',
3232
'ignore-module-all', 'exclude-members', 'member-order',
33-
'imported-members']
33+
'imported-members', 'class-doc-from']
3434

3535
AUTODOC_EXTENDABLE_OPTIONS = ['members', 'private-members', 'special-members',
3636
'exclude-members']

tests/roots/test-ext-autodoc/target/singledispatch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def func(arg, kwarg=None):
1515

1616

1717
@func.register(int)
18+
@func.register(float)
1819
def _func_int(arg, kwarg=None):
1920
"""A function for int."""
2021
pass

tests/roots/test-ext-autodoc/target/singledispatchmethod.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def meth(self, arg, kwarg=None):
1010
pass
1111

1212
@meth.register(int)
13+
@meth.register(float)
1314
def _meth_int(self, arg, kwarg=None):
1415
"""A method for int."""
1516
pass

tests/test_ext_autodoc.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2108,6 +2108,7 @@ def test_singledispatch(app):
21082108
'',
21092109
'',
21102110
'.. py:function:: func(arg, kwarg=None)',
2111+
' func(arg: float, kwarg=None)',
21112112
' func(arg: int, kwarg=None)',
21122113
' func(arg: str, kwarg=None)',
21132114
' :module: target.singledispatch',
@@ -2135,6 +2136,7 @@ def test_singledispatchmethod(app):
21352136
'',
21362137
'',
21372138
' .. py:method:: Foo.meth(arg, kwarg=None)',
2139+
' Foo.meth(arg: float, kwarg=None)',
21382140
' Foo.meth(arg: int, kwarg=None)',
21392141
' Foo.meth(arg: str, kwarg=None)',
21402142
' :module: target.singledispatchmethod',
@@ -2153,6 +2155,7 @@ def test_singledispatchmethod_automethod(app):
21532155
assert list(actual) == [
21542156
'',
21552157
'.. py:method:: Foo.meth(arg, kwarg=None)',
2158+
' Foo.meth(arg: float, kwarg=None)',
21562159
' Foo.meth(arg: int, kwarg=None)',
21572160
' Foo.meth(arg: str, kwarg=None)',
21582161
' :module: target.singledispatchmethod',

tests/test_ext_autodoc_autoclass.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,53 @@ def test_show_inheritance_for_subclass_of_generic_type(app):
264264
]
265265

266266

267+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
268+
def test_class_doc_from_class(app):
269+
options = {"members": None,
270+
"class-doc-from": "class"}
271+
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
272+
assert list(actual) == [
273+
'',
274+
'.. py:class:: C()',
275+
' :module: target.autoclass_content',
276+
'',
277+
' A class having __init__, no __new__',
278+
'',
279+
]
280+
281+
282+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
283+
def test_class_doc_from_init(app):
284+
options = {"members": None,
285+
"class-doc-from": "init"}
286+
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
287+
assert list(actual) == [
288+
'',
289+
'.. py:class:: C()',
290+
' :module: target.autoclass_content',
291+
'',
292+
' __init__ docstring',
293+
'',
294+
]
295+
296+
297+
@pytest.mark.sphinx('html', testroot='ext-autodoc')
298+
def test_class_doc_from_both(app):
299+
options = {"members": None,
300+
"class-doc-from": "both"}
301+
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
302+
assert list(actual) == [
303+
'',
304+
'.. py:class:: C()',
305+
' :module: target.autoclass_content',
306+
'',
307+
' A class having __init__, no __new__',
308+
'',
309+
' __init__ docstring',
310+
'',
311+
]
312+
313+
267314
def test_class_alias(app):
268315
def autodoc_process_docstring(*args):
269316
"""A handler always raises an error.

tests/test_ext_autodoc_autofunction.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def test_singledispatch(app):
119119
assert list(actual) == [
120120
'',
121121
'.. py:function:: func(arg, kwarg=None)',
122+
' func(arg: float, kwarg=None)',
122123
' func(arg: int, kwarg=None)',
123124
' func(arg: str, kwarg=None)',
124125
' :module: target.singledispatch',

0 commit comments

Comments
 (0)