Skip to content

Commit

Permalink
Merge branch '4.x' into 8597_metadata_only_docstring
Browse files Browse the repository at this point in the history
  • Loading branch information
tk0miya committed May 3, 2021
2 parents 469def5 + cb7b41f commit 30efa3d
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGES
Expand Up @@ -15,6 +15,9 @@ Deprecated
Features added
--------------

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

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

Expand Down
9 changes: 8 additions & 1 deletion doc/usage/extensions/autodoc.rst
Expand Up @@ -343,6 +343,10 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,

.. autoclass:: module.name::Noodle

* :rst:dir:`autoclass` also recognizes the ``class-doc-from`` option that
can be used to override the global value of :confval:`autoclass_content`.

.. versionadded:: 4.1

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

.. versionadded:: 1.8

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

.. versionchanged:: 4.1
Added ``'class-doc-from'``.

.. confval:: autodoc_docstring_signature

Functions imported from C modules cannot be introspected, and therefore the
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Expand Up @@ -21,7 +21,8 @@
'sphinxcontrib-htmlhelp',
'sphinxcontrib-serializinghtml',
'sphinxcontrib-qthelp',
'Jinja2>=2.3',
'Jinja2>=2.3,<3.0',
'MarkupSafe<2.0',
'Pygments>=2.0',
'docutils>=0.14,<0.18',
'snowballstemmer>=1.1',
Expand Down
78 changes: 50 additions & 28 deletions sphinx/ext/autodoc/__init__.py
Expand Up @@ -129,6 +129,14 @@ def member_order_option(arg: Any) -> Optional[str]:
raise ValueError(__('invalid value for member-order option: %s') % arg)


def class_doc_from_option(arg: Any) -> Optional[str]:
"""Used to convert the :class-doc-from: option to autoclass directives."""
if arg in ('both', 'class', 'init'):
return arg
else:
raise ValueError(__('invalid value for class-doc-from option: %s') % arg)


SUPPRESS = object()


Expand Down Expand Up @@ -1320,12 +1328,12 @@ def format_signature(self, **kwargs: Any) -> str:
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)

documenter = FunctionDocumenter(self.directive, '')
documenter.object = func
documenter.objpath = [None]
sigs.append(documenter.format_signature())
dispatchfunc = self.annotate_to_first_argument(func, typ)
if dispatchfunc:
documenter = FunctionDocumenter(self.directive, '')
documenter.object = dispatchfunc
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
actual = inspect.signature(self.object,
type_aliases=self.config.autodoc_type_aliases)
Expand All @@ -1350,28 +1358,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu

return overload.replace(parameters=parameters)

def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
"""Annotate type hint to the first argument of function if needed."""
try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
return
return None
except ValueError:
return
return None

if len(sig.parameters) == 0:
return
return None

def dummy():
pass

params = list(sig.parameters.values())
if params[0].annotation is Parameter.empty:
params[0] = params[0].replace(annotation=typ)
try:
func.__signature__ = sig.replace(parameters=params) # type: ignore
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types)
return
return None
else:
return None


class DecoratorDocumenter(FunctionDocumenter):
Expand Down Expand Up @@ -1417,6 +1431,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
'show-inheritance': bool_option, 'member-order': member_order_option,
'exclude-members': exclude_members_option,
'private-members': members_option, 'special-members': members_option,
'class-doc-from': class_doc_from_option,
}

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

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

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

# for classes, what the "docstring" is can be controlled via a
# config value; the default is only the class docstring
if content in ('both', 'init'):
if classdoc_from in ('both', 'init'):
__init__ = self.get_attr(self.object, '__init__', None)
initdocstring = getdoc(__init__, self.get_attr,
self.config.autodoc_inherit_docstrings,
Expand All @@ -1682,7 +1697,7 @@ def get_doc(self, ignore: int = None) -> Optional[List[List[str]]]:
initdocstring.strip() == object.__new__.__doc__)): # for !pypy
initdocstring = None
if initdocstring:
if content == 'init':
if classdoc_from == 'init':
docstrings = [initdocstring]
else:
docstrings.append(initdocstring)
Expand Down Expand Up @@ -2109,13 +2124,13 @@ def format_signature(self, **kwargs: Any) -> str:
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)

documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = func
documenter.objpath = [None]
sigs.append(documenter.format_signature())
dispatchmeth = self.annotate_to_first_argument(func, typ)
if dispatchmeth:
documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = dispatchmeth
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
actual = inspect.signature(self.object, bound_method=False,
Expand Down Expand Up @@ -2149,27 +2164,34 @@ def merge_default_value(self, actual: Signature, overload: Signature) -> Signatu

return overload.replace(parameters=parameters)

def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
"""Annotate type hint to the first argument of function if needed."""
try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc)
return
return None
except ValueError:
return
return None

if len(sig.parameters) == 1:
return
return None

def dummy():
pass

params = list(sig.parameters.values())
if params[1].annotation is Parameter.empty:
params[1] = params[1].replace(annotation=typ)
try:
func.__signature__ = sig.replace(parameters=params) # type: ignore
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types)
return
return None
else:
return None


class NonDataDescriptorMixin(DataDocumenterMixinBase):
Expand Down
2 changes: 1 addition & 1 deletion sphinx/ext/autodoc/directive.py
Expand Up @@ -30,7 +30,7 @@
AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members',
'show-inheritance', 'private-members', 'special-members',
'ignore-module-all', 'exclude-members', 'member-order',
'imported-members']
'imported-members', 'class-doc-from']

AUTODOC_EXTENDABLE_OPTIONS = ['members', 'private-members', 'special-members',
'exclude-members']
Expand Down
1 change: 1 addition & 0 deletions tests/roots/test-ext-autodoc/target/singledispatch.py
Expand Up @@ -15,6 +15,7 @@ def func(arg, kwarg=None):


@func.register(int)
@func.register(float)
def _func_int(arg, kwarg=None):
"""A function for int."""
pass
Expand Down
Expand Up @@ -10,6 +10,7 @@ def meth(self, arg, kwarg=None):
pass

@meth.register(int)
@meth.register(float)
def _meth_int(self, arg, kwarg=None):
"""A method for int."""
pass
Expand Down
3 changes: 3 additions & 0 deletions tests/test_ext_autodoc.py
Expand Up @@ -2108,6 +2108,7 @@ def test_singledispatch(app):
'',
'',
'.. py:function:: func(arg, kwarg=None)',
' func(arg: float, kwarg=None)',
' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)',
' :module: target.singledispatch',
Expand Down Expand Up @@ -2135,6 +2136,7 @@ def test_singledispatchmethod(app):
'',
'',
' .. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: float, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod',
Expand All @@ -2153,6 +2155,7 @@ def test_singledispatchmethod_automethod(app):
assert list(actual) == [
'',
'.. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: float, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod',
Expand Down
47 changes: 47 additions & 0 deletions tests/test_ext_autodoc_autoclass.py
Expand Up @@ -264,6 +264,53 @@ def test_show_inheritance_for_subclass_of_generic_type(app):
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_class_doc_from_class(app):
options = {"members": None,
"class-doc-from": "class"}
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
assert list(actual) == [
'',
'.. py:class:: C()',
' :module: target.autoclass_content',
'',
' A class having __init__, no __new__',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_class_doc_from_init(app):
options = {"members": None,
"class-doc-from": "init"}
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
assert list(actual) == [
'',
'.. py:class:: C()',
' :module: target.autoclass_content',
'',
' __init__ docstring',
'',
]


@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_class_doc_from_both(app):
options = {"members": None,
"class-doc-from": "both"}
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
assert list(actual) == [
'',
'.. py:class:: C()',
' :module: target.autoclass_content',
'',
' A class having __init__, no __new__',
'',
' __init__ docstring',
'',
]


def test_class_alias(app):
def autodoc_process_docstring(*args):
"""A handler always raises an error.
Expand Down
1 change: 1 addition & 0 deletions tests/test_ext_autodoc_autofunction.py
Expand Up @@ -119,6 +119,7 @@ def test_singledispatch(app):
assert list(actual) == [
'',
'.. py:function:: func(arg, kwarg=None)',
' func(arg: float, kwarg=None)',
' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)',
' :module: target.singledispatch',
Expand Down

0 comments on commit 30efa3d

Please sign in to comment.