Fix #8597: autodoc: metadata only docstring is treated as undocumented

The metadata in docstring is invisible content. Therefore docstring
having only metadata should be treated as undocumented.
This commit is contained in:
Takeshi KOMIYA 2021-05-02 22:44:44 +09:00
parent 30237c004d
commit 469def56b6
7 changed files with 93 additions and 23 deletions

View File

@ -10,6 +10,8 @@ Incompatible changes
Deprecated Deprecated
---------- ----------
* ``sphinx.util.docstrings.extract_metadata()``
Features added Features added
-------------- --------------
@ -22,6 +24,9 @@ Features added
Bugs fixed Bugs fixed
---------- ----------
* #8597: autodoc: a docsting having metadata only should be treated as
undocumented
Testing Testing
-------- --------

View File

@ -22,6 +22,11 @@ The following is a list of deprecated interfaces.
- (will be) Removed - (will be) Removed
- Alternatives - Alternatives
* - ``sphinx.util.docstrings.extract_metadata()``
- 4.1
- 6.0
- ``sphinx.util.docstrings.separate_metadata()``
* - ``favicon`` variable in HTML templates * - ``favicon`` variable in HTML templates
- 4.0 - 4.0
- TBD - TBD

View File

@ -30,7 +30,7 @@ from sphinx.ext.autodoc.mock import ismock, mock, undecorate
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import inspect, logging from sphinx.util import inspect, logging
from sphinx.util.docstrings import extract_metadata, prepare_docstring from sphinx.util.docstrings import prepare_docstring, separate_metadata
from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr, from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr,
stringify_signature) stringify_signature)
from sphinx.util.typing import OptionSpec, get_type_hints, restify from sphinx.util.typing import OptionSpec, get_type_hints, restify
@ -722,9 +722,9 @@ class Documenter:
# hack for ClassDocumenter to inject docstring via ObjectMember # hack for ClassDocumenter to inject docstring via ObjectMember
doc = obj.docstring doc = obj.docstring
doc, metadata = separate_metadata(doc)
has_doc = bool(doc) has_doc = bool(doc)
metadata = extract_metadata(doc)
if 'private' in metadata: if 'private' in metadata:
# consider a member private if docstring has "private" metadata # consider a member private if docstring has "private" metadata
isprivate = True isprivate = True
@ -1918,7 +1918,7 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
return True return True
else: else:
doc = self.get_doc() doc = self.get_doc()
metadata = extract_metadata('\n'.join(sum(doc, []))) docstring, metadata = separate_metadata('\n'.join(sum(doc, [])))
if 'hide-value' in metadata: if 'hide-value' in metadata:
return True return True
@ -2456,7 +2456,7 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
else: else:
doc = self.get_doc() doc = self.get_doc()
if doc: if doc:
metadata = extract_metadata('\n'.join(sum(doc, []))) docstring, metadata = separate_metadata('\n'.join(sum(doc, [])))
if 'hide-value' in metadata: if 'hide-value' in metadata:
return True return True

View File

@ -11,26 +11,28 @@
import re import re
import sys import sys
import warnings import warnings
from typing import Dict, List from typing import Dict, List, Tuple
from docutils.parsers.rst.states import Body from docutils.parsers.rst.states import Body
from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.deprecation import RemovedInSphinx50Warning, RemovedInSphinx60Warning
field_list_item_re = re.compile(Body.patterns['field_marker']) field_list_item_re = re.compile(Body.patterns['field_marker'])
def extract_metadata(s: str) -> Dict[str, str]: def separate_metadata(s: str) -> Tuple[str, Dict[str, str]]:
"""Extract metadata from docstring.""" """Separate docstring into metadata and others."""
in_other_element = False in_other_element = False
metadata: Dict[str, str] = {} metadata: Dict[str, str] = {}
lines = []
if not s: if not s:
return metadata return s, metadata
for line in prepare_docstring(s): for line in prepare_docstring(s):
if line.strip() == '': if line.strip() == '':
in_other_element = False in_other_element = False
lines.append(line)
else: else:
matched = field_list_item_re.match(line) matched = field_list_item_re.match(line)
if matched and not in_other_element: if matched and not in_other_element:
@ -38,9 +40,20 @@ def extract_metadata(s: str) -> Dict[str, str]:
if field_name.startswith('meta '): if field_name.startswith('meta '):
name = field_name[5:].strip() name = field_name[5:].strip()
metadata[name] = line[matched.end():].strip() metadata[name] = line[matched.end():].strip()
else:
lines.append(line)
else: else:
in_other_element = True in_other_element = True
lines.append(line)
return '\n'.join(lines), metadata
def extract_metadata(s: str) -> Dict[str, str]:
warnings.warn("extract_metadata() is deprecated.",
RemovedInSphinx60Warning, stacklevel=2)
docstring, metadata = separate_metadata(s)
return metadata return metadata

View File

@ -0,0 +1,2 @@
def foo():
""":meta metadata-only-docstring:"""

View File

@ -735,6 +735,34 @@ def test_autodoc_undoc_members(app):
] ]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_undoc_members_for_metadata_only(app):
# metadata only member is not displayed
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.metadata', options)
assert list(actual) == [
'',
'.. py:module:: target.metadata',
'',
]
# metadata only member is displayed when undoc-member given
options = {"members": None,
"undoc-members": None}
actual = do_autodoc(app, 'module', 'target.metadata', options)
assert list(actual) == [
'',
'.. py:module:: target.metadata',
'',
'',
'.. py:function:: foo()',
' :module: target.metadata',
'',
' :meta metadata-only-docstring:',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_inherited_members(app): def test_autodoc_inherited_members(app):
options = {"members": None, options = {"members": None,

View File

@ -8,31 +8,48 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
from sphinx.util.docstrings import extract_metadata, prepare_commentdoc, prepare_docstring from sphinx.util.docstrings import prepare_commentdoc, prepare_docstring, separate_metadata
def test_extract_metadata(): def test_separate_metadata():
metadata = extract_metadata(":meta foo: bar\n" # metadata only
text = (":meta foo: bar\n"
":meta baz:\n") ":meta baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == ''
assert metadata == {'foo': 'bar', 'baz': ''} assert metadata == {'foo': 'bar', 'baz': ''}
# non metadata field list item
text = (":meta foo: bar\n"
":param baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == ':param baz:\n'
assert metadata == {'foo': 'bar'}
# field_list like text following just after paragaph is not a field_list # field_list like text following just after paragaph is not a field_list
metadata = extract_metadata("blah blah blah\n" text = ("blah blah blah\n"
":meta foo: bar\n" ":meta foo: bar\n"
":meta baz:\n") ":meta baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == text
assert metadata == {} assert metadata == {}
# field_list like text following after blank line is a field_list # field_list like text following after blank line is a field_list
metadata = extract_metadata("blah blah blah\n" text = ("blah blah blah\n"
"\n" "\n"
":meta foo: bar\n" ":meta foo: bar\n"
":meta baz:\n") ":meta baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == "blah blah blah\n\n"
assert metadata == {'foo': 'bar', 'baz': ''} assert metadata == {'foo': 'bar', 'baz': ''}
# non field_list item breaks field_list # non field_list item breaks field_list
metadata = extract_metadata(":meta foo: bar\n" text = (":meta foo: bar\n"
"blah blah blah\n" "blah blah blah\n"
":meta baz:\n") ":meta baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == ("blah blah blah\n"
":meta baz:\n")
assert metadata == {'foo': 'bar'} assert metadata == {'foo': 'bar'}