Fix #6165: autodoc: `tab_width` setting of docutils has been ignored

This commit is contained in:
Takeshi KOMIYA 2019-03-24 01:22:30 +09:00
parent de0c44196e
commit 1ea23e14df
6 changed files with 38 additions and 13 deletions

View File

@ -91,6 +91,7 @@ Bugs fixed
* #6213: ifconfig: contents after headings are not shown * #6213: ifconfig: contents after headings are not shown
* commented term in glossary directive is wrongly recognized * commented term in glossary directive is wrongly recognized
* #6299: rst domain: rst:directive directive generates waste space * #6299: rst domain: rst:directive directive generates waste space
* #6165: autodoc: ``tab_width`` setting of docutils has been ignored
Testing Testing
-------- --------

View File

@ -442,7 +442,8 @@ class Documenter:
docstring = getdoc(self.object, self.get_attr, docstring = getdoc(self.object, self.get_attr,
self.env.config.autodoc_inherit_docstrings) self.env.config.autodoc_inherit_docstrings)
if docstring: if docstring:
return [prepare_docstring(docstring, ignore)] tab_width = self.directive.state.document.settings.tab_width
return [prepare_docstring(docstring, ignore, tab_width)]
return [] return []
def process_doc(self, docstrings): def process_doc(self, docstrings):
@ -936,7 +937,9 @@ class DocstringSignatureMixin:
if base not in valid_names: if base not in valid_names:
continue continue
# re-prepare docstring to ignore more leading indentation # re-prepare docstring to ignore more leading indentation
self._new_docstrings[i] = prepare_docstring('\n'.join(doclines[1:])) tab_width = self.directive.state.document.settings.tab_width # type: ignore
self._new_docstrings[i] = prepare_docstring('\n'.join(doclines[1:]),
tabsize=tab_width)
result = args, retann result = args, retann
# don't look any further # don't look any further
break break
@ -1179,7 +1182,9 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
docstrings = [initdocstring] docstrings = [initdocstring]
else: else:
docstrings.append(initdocstring) docstrings.append(initdocstring)
return [prepare_docstring(docstring, ignore) for docstring in docstrings]
tab_width = self.directive.state.document.settings.tab_width
return [prepare_docstring(docstring, ignore, tab_width) for docstring in docstrings]
def add_content(self, more_content, no_docstring=False): def add_content(self, more_content, no_docstring=False):
# type: (Any, bool) -> None # type: (Any, bool) -> None

View File

@ -6,10 +6,14 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import warnings
from docutils import nodes from docutils import nodes
from docutils.parsers.rst.states import Struct
from docutils.statemachine import StringList from docutils.statemachine import StringList
from docutils.utils import assemble_option_dict from docutils.utils import assemble_option_dict
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.ext.autodoc import Options, get_documenters from sphinx.ext.autodoc import Options, get_documenters
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.docutils import SphinxDirective, switch_source_input from sphinx.util.docutils import SphinxDirective, switch_source_input
@ -17,7 +21,7 @@ from sphinx.util.nodes import nested_parse_with_titles
if False: if False:
# For type annotation # For type annotation
from typing import Callable, Dict, List, Set, Type # NOQA from typing import Any, Callable, Dict, List, Set, Type # NOQA
from docutils.parsers.rst.state import RSTState # NOQA from docutils.parsers.rst.state import RSTState # NOQA
from docutils.utils import Reporter # NOQA from docutils.utils import Reporter # NOQA
from sphinx.config import Config # NOQA from sphinx.config import Config # NOQA
@ -50,8 +54,8 @@ class DummyOptionSpec(dict):
class DocumenterBridge: class DocumenterBridge:
"""A parameters container for Documenters.""" """A parameters container for Documenters."""
def __init__(self, env, reporter, options, lineno): def __init__(self, env, reporter, options, lineno, state=None):
# type: (BuildEnvironment, Reporter, Options, int) -> None # type: (BuildEnvironment, Reporter, Options, int, Any) -> None
self.env = env self.env = env
self.reporter = reporter self.reporter = reporter
self.genopt = options self.genopt = options
@ -59,6 +63,16 @@ class DocumenterBridge:
self.filename_set = set() # type: Set[str] self.filename_set = set() # type: Set[str]
self.result = StringList() self.result = StringList()
if state:
self.state = state
else:
# create fake object for self.state.document.settings.tab_width
warnings.warn('DocumenterBridge requires a state object on instantiation.',
RemovedInSphinx40Warning)
settings = Struct(tab_width=8)
document = Struct(settings=settings)
self.state = Struct(document=document)
def warn(self, msg): def warn(self, msg):
# type: (str) -> None # type: (str) -> None
logger.warning(msg, location=(self.env.docname, self.lineno)) logger.warning(msg, location=(self.env.docname, self.lineno))
@ -131,7 +145,7 @@ class AutodocDirective(SphinxDirective):
return [] return []
# generate the output # generate the output
params = DocumenterBridge(self.env, reporter, documenter_options, lineno) params = DocumenterBridge(self.env, reporter, documenter_options, lineno, self.state)
documenter = doccls(params, self.arguments[0]) documenter = doccls(params, self.arguments[0])
documenter.generate(more_content=self.content) documenter.generate(more_content=self.content)
if not params.result: if not params.result:

View File

@ -175,7 +175,7 @@ _app = None # type: Sphinx
class FakeDirective(DocumenterBridge): class FakeDirective(DocumenterBridge):
def __init__(self): def __init__(self):
# type: () -> None # type: () -> None
super().__init__({}, None, Options(), 0) # type: ignore super().__init__({}, None, Options(), 0, None) # type: ignore
def get_documenter(app, obj, parent): def get_documenter(app, obj, parent):
@ -236,7 +236,7 @@ class Autosummary(SphinxDirective):
def run(self): def run(self):
# type: () -> List[nodes.Node] # type: () -> List[nodes.Node]
self.bridge = DocumenterBridge(self.env, self.state.document.reporter, self.bridge = DocumenterBridge(self.env, self.state.document.reporter,
Options(), self.lineno) Options(), self.lineno, self.state)
names = [x.strip().split()[0] for x in self.content names = [x.strip().split()[0] for x in self.content
if x.strip() and re.search(r'^[~a-zA-Z_]', x.strip()[0])] if x.strip() and re.search(r'^[~a-zA-Z_]', x.strip()[0])]

View File

@ -15,8 +15,8 @@ if False:
from typing import List # NOQA from typing import List # NOQA
def prepare_docstring(s, ignore=1): def prepare_docstring(s, ignore=1, tabsize=8):
# type: (str, int) -> List[str] # type: (str, int, int) -> List[str]
"""Convert a docstring into lines of parseable reST. Remove common leading """Convert a docstring into lines of parseable reST. Remove common leading
indentation, where the indentation of a given number of lines (usually just indentation, where the indentation of a given number of lines (usually just
one) is ignored. one) is ignored.
@ -25,7 +25,7 @@ def prepare_docstring(s, ignore=1):
ViewList (used as argument of nested_parse().) An empty line is added to ViewList (used as argument of nested_parse().) An empty line is added to
act as a separator between this docstring and following content. act as a separator between this docstring and following content.
""" """
lines = s.expandtabs().splitlines() lines = s.expandtabs(tabsize).splitlines()
# Find minimum indentation of any non-blank lines after ignored lines. # Find minimum indentation of any non-blank lines after ignored lines.
margin = sys.maxsize margin = sys.maxsize
for line in lines[ignore:]: for line in lines[ignore:]:

View File

@ -11,6 +11,7 @@
import platform import platform
import sys import sys
from unittest.mock import Mock
from warnings import catch_warnings from warnings import catch_warnings
import pytest import pytest
@ -36,7 +37,9 @@ def do_autodoc(app, objtype, name, options=None):
app.env.temp_data.setdefault('docname', 'index') # set dummy docname app.env.temp_data.setdefault('docname', 'index') # set dummy docname
doccls = app.registry.documenters[objtype] doccls = app.registry.documenters[objtype]
docoptions = process_documenter_options(doccls, app.config, options) docoptions = process_documenter_options(doccls, app.config, options)
bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1) state = Mock()
state.document.settings.tab_width = 8
bridge = DocumenterBridge(app.env, LoggingReporter(''), docoptions, 1, state)
documenter = doccls(bridge, name) documenter = doccls(bridge, name)
documenter.generate() documenter.generate()
@ -95,7 +98,9 @@ def setup_test():
genopt = options, genopt = options,
result = ViewList(), result = ViewList(),
filename_set = set(), filename_set = set(),
state = Mock(),
) )
directive.state.document.settings.tab_width = 8
processed_docstrings = [] processed_docstrings = []
processed_signatures = [] processed_signatures = []