From 1ea23e14df871ff97aa4082dddecfd11c4465cbe Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 24 Mar 2019 01:22:30 +0900 Subject: [PATCH] Fix #6165: autodoc: ``tab_width`` setting of docutils has been ignored --- CHANGES | 1 + sphinx/ext/autodoc/__init__.py | 11 ++++++++--- sphinx/ext/autodoc/directive.py | 22 ++++++++++++++++++---- sphinx/ext/autosummary/__init__.py | 4 ++-- sphinx/util/docstrings.py | 6 +++--- tests/test_autodoc.py | 7 ++++++- 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/CHANGES b/CHANGES index 9cc8cab22..a8ea6c387 100644 --- a/CHANGES +++ b/CHANGES @@ -91,6 +91,7 @@ Bugs fixed * #6213: ifconfig: contents after headings are not shown * commented term in glossary directive is wrongly recognized * #6299: rst domain: rst:directive directive generates waste space +* #6165: autodoc: ``tab_width`` setting of docutils has been ignored Testing -------- diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 2a4df2159..412c336e3 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -442,7 +442,8 @@ class Documenter: docstring = getdoc(self.object, self.get_attr, self.env.config.autodoc_inherit_docstrings) if docstring: - return [prepare_docstring(docstring, ignore)] + tab_width = self.directive.state.document.settings.tab_width + return [prepare_docstring(docstring, ignore, tab_width)] return [] def process_doc(self, docstrings): @@ -936,7 +937,9 @@ class DocstringSignatureMixin: if base not in valid_names: continue # 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 # don't look any further break @@ -1179,7 +1182,9 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: docstrings = [initdocstring] else: 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): # type: (Any, bool) -> None diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index 42415433b..6b002b101 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -6,10 +6,14 @@ :license: BSD, see LICENSE for details. """ +import warnings + from docutils import nodes +from docutils.parsers.rst.states import Struct from docutils.statemachine import StringList from docutils.utils import assemble_option_dict +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.ext.autodoc import Options, get_documenters from sphinx.util import logging from sphinx.util.docutils import SphinxDirective, switch_source_input @@ -17,7 +21,7 @@ from sphinx.util.nodes import nested_parse_with_titles if False: # 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.utils import Reporter # NOQA from sphinx.config import Config # NOQA @@ -50,8 +54,8 @@ class DummyOptionSpec(dict): class DocumenterBridge: """A parameters container for Documenters.""" - def __init__(self, env, reporter, options, lineno): - # type: (BuildEnvironment, Reporter, Options, int) -> None + def __init__(self, env, reporter, options, lineno, state=None): + # type: (BuildEnvironment, Reporter, Options, int, Any) -> None self.env = env self.reporter = reporter self.genopt = options @@ -59,6 +63,16 @@ class DocumenterBridge: self.filename_set = set() # type: Set[str] 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): # type: (str) -> None logger.warning(msg, location=(self.env.docname, self.lineno)) @@ -131,7 +145,7 @@ class AutodocDirective(SphinxDirective): return [] # 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.generate(more_content=self.content) if not params.result: diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 952bd9e2a..5840f0ccd 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -175,7 +175,7 @@ _app = None # type: Sphinx class FakeDirective(DocumenterBridge): def __init__(self): # type: () -> None - super().__init__({}, None, Options(), 0) # type: ignore + super().__init__({}, None, Options(), 0, None) # type: ignore def get_documenter(app, obj, parent): @@ -236,7 +236,7 @@ class Autosummary(SphinxDirective): def run(self): # type: () -> List[nodes.Node] 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 if x.strip() and re.search(r'^[~a-zA-Z_]', x.strip()[0])] diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 97dd60294..31943b2cb 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -15,8 +15,8 @@ if False: from typing import List # NOQA -def prepare_docstring(s, ignore=1): - # type: (str, int) -> List[str] +def prepare_docstring(s, ignore=1, tabsize=8): + # type: (str, int, int) -> List[str] """Convert a docstring into lines of parseable reST. Remove common leading indentation, where the indentation of a given number of lines (usually just 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 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. margin = sys.maxsize for line in lines[ignore:]: diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 5f616b791..75d59db14 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -11,6 +11,7 @@ import platform import sys +from unittest.mock import Mock from warnings import catch_warnings import pytest @@ -36,7 +37,9 @@ def do_autodoc(app, objtype, name, options=None): app.env.temp_data.setdefault('docname', 'index') # set dummy docname doccls = app.registry.documenters[objtype] 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.generate() @@ -95,7 +98,9 @@ def setup_test(): genopt = options, result = ViewList(), filename_set = set(), + state = Mock(), ) + directive.state.document.settings.tab_width = 8 processed_docstrings = [] processed_signatures = []