From 1dfc18e68378efe5a3bfc8a7d96bf452a34bdd82 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 16 Nov 2021 02:19:29 +0100 Subject: [PATCH] Replace sphinx.ext.napoleon.iterators by simpler stack implementation. The "peekable iterator" API is not actually needed by napoleon, as all elements are known from the beginning, so `_line_iter` can be readily replaced by a stack-like object. This tightens the public API and makes it easier to extract napoleon for vendoring independently of sphinx. --- CHANGES | 1 + sphinx/ext/napoleon/docstring.py | 89 ++++++++++++++++++++------------ sphinx/ext/napoleon/iterators.py | 6 +++ 3 files changed, 64 insertions(+), 32 deletions(-) diff --git a/CHANGES b/CHANGES index a727f483f89..a58b8fae288 100644 --- a/CHANGES +++ b/CHANGES @@ -35,6 +35,7 @@ Deprecated * #10467: Deprecated ``sphinx.util.stemmer`` in favour of ``snowballstemmer``. Patch by Adam Turner. +* #9856: Deprecated ``sphinx.ext.napoleon.iterators``. Features added -------------- diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 30043f2d1e3..2fbfdc05cc7 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -4,11 +4,10 @@ import inspect import re from functools import partial -from typing import Any, Callable, Dict, List, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Tuple, Union from sphinx.application import Sphinx from sphinx.config import Config as SphinxConfig -from sphinx.ext.napoleon.iterators import modify_iter from sphinx.locale import _, __ from sphinx.util import logging from sphinx.util.inspect import stringify_annotation @@ -44,6 +43,32 @@ _SINGLETONS = ("None", "True", "False", "Ellipsis") +class _Stack: + """A stack of strs, represented intenally as a python list.""" + + sentinel = object() + + def __init__(self, items: Iterable[str]): + """Initialize the stack from an Iterable of items.""" + self._items = list(items)[::-1] + + def __bool__(self) -> bool: + """Return whether the stack is empty.""" + return bool(self._items) + + def get(self, n: int) -> Any: + """ + Return the nth element of the stack, or ``self.sentinel`` if n is + greater than the stack size. + """ + return (self._items[len(self._items) - 1 - n] + if n < len(self._items) else self.sentinel) + + def pop(self) -> str: + """Pop and return the topmost stack element.""" + return self._items.pop() + + def _convert_type_spec(_type: str, translations: Dict[str, str] = {}) -> str: """Convert type specification to reference in reST.""" if _type in translations: @@ -151,7 +176,7 @@ def __init__(self, docstring: Union[str, List[str]], config: SphinxConfig = None lines = docstring.splitlines() else: lines = docstring - self._line_iter = modify_iter(lines, modifier=lambda s: s.rstrip()) + self._lines = _Stack(map(str.rstrip, lines)) self._parsed_lines: List[str] = [] self._is_in_section = False self._section_indent = 0 @@ -223,32 +248,32 @@ def lines(self) -> List[str]: def _consume_indented_block(self, indent: int = 1) -> List[str]: lines = [] - line = self._line_iter.peek() + line = self._lines.get(0) while(not self._is_section_break() and (not line or self._is_indented(line, indent))): - lines.append(next(self._line_iter)) - line = self._line_iter.peek() + lines.append(self._lines.pop()) + line = self._lines.get(0) return lines def _consume_contiguous(self) -> List[str]: lines = [] - while (self._line_iter.has_next() and - self._line_iter.peek() and + while (self._lines and + self._lines.get(0) and not self._is_section_header()): - lines.append(next(self._line_iter)) + lines.append(self._lines.pop()) return lines def _consume_empty(self) -> List[str]: lines = [] - line = self._line_iter.peek() - while self._line_iter.has_next() and not line: - lines.append(next(self._line_iter)) - line = self._line_iter.peek() + line = self._lines.get(0) + while self._lines and not line: + lines.append(self._lines.pop()) + line = self._lines.get(0) return lines def _consume_field(self, parse_type: bool = True, prefer_type: bool = False ) -> Tuple[str, str, List[str]]: - line = next(self._line_iter) + line = self._lines.pop() before, colon, after = self._partition_field_on_colon(line) _name, _type, _desc = before, '', after @@ -286,7 +311,7 @@ def _consume_fields(self, parse_type: bool = True, prefer_type: bool = False, return fields def _consume_inline_attribute(self) -> Tuple[str, List[str]]: - line = next(self._line_iter) + line = self._lines.pop() _type, colon, _desc = self._partition_field_on_colon(line) if not colon or not _desc: _type, _desc = _desc, _type @@ -324,7 +349,7 @@ def _consume_usage_section(self) -> List[str]: return lines def _consume_section_header(self) -> str: - section = next(self._line_iter) + section = self._lines.pop() stripped_section = section.strip(':') if stripped_section.lower() in self._sections: section = stripped_section @@ -332,15 +357,15 @@ def _consume_section_header(self) -> str: def _consume_to_end(self) -> List[str]: lines = [] - while self._line_iter.has_next(): - lines.append(next(self._line_iter)) + while self._lines: + lines.append(self._lines.pop()) return lines def _consume_to_next_section(self) -> List[str]: self._consume_empty() lines = [] while not self._is_section_break(): - lines.append(next(self._line_iter)) + lines.append(self._lines.pop()) return lines + self._consume_empty() def _dedent(self, lines: List[str], full: bool = False) -> List[str]: @@ -466,12 +491,12 @@ def _format_fields(self, field_type: str, fields: List[Tuple[str, str, List[str] return lines def _get_current_indent(self, peek_ahead: int = 0) -> int: - line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] - while line != self._line_iter.sentinel: + line = self._lines.get(peek_ahead) + while line is not self._lines.sentinel: if line: return self._get_indent(line) peek_ahead += 1 - line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] + line = self._lines.get(peek_ahead) return 0 def _get_indent(self, line: str) -> int: @@ -526,7 +551,7 @@ def _is_list(self, lines: List[str]) -> bool: return next_indent > indent def _is_section_header(self) -> bool: - section = self._line_iter.peek().lower() + section = self._lines.get(0).lower() match = _google_section_regex.match(section) if match and section.strip(':') in self._sections: header_indent = self._get_indent(section) @@ -540,8 +565,8 @@ def _is_section_header(self) -> bool: return False def _is_section_break(self) -> bool: - line = self._line_iter.peek() - return (not self._line_iter.has_next() or + line = self._lines.get(0) + return (not self._lines or self._is_section_header() or (self._is_in_section and line and @@ -583,7 +608,7 @@ def _parse(self) -> None: self._parsed_lines.extend(res) return - while self._line_iter.has_next(): + while self._lines: if self._is_section_header(): try: section = self._consume_section_header() @@ -1143,7 +1168,7 @@ def _escape_args_and_kwargs(self, name: str) -> str: def _consume_field(self, parse_type: bool = True, prefer_type: bool = False ) -> Tuple[str, str, List[str]]: - line = next(self._line_iter) + line = self._lines.pop() if parse_type: _name, _, _type = self._partition_field_on_colon(line) else: @@ -1174,15 +1199,15 @@ def _consume_returns_section(self, preprocess_types: bool = False return self._consume_fields(prefer_type=True) def _consume_section_header(self) -> str: - section = next(self._line_iter) + section = self._lines.pop() if not _directive_regex.match(section): # Consume the header underline - next(self._line_iter) + self._lines.pop() return section def _is_section_break(self) -> bool: - line1, line2 = self._line_iter.peek(2) - return (not self._line_iter.has_next() or + line1, line2 = self._lines.get(0), self._lines.get(1) + return (not self._lines or self._is_section_header() or ['', ''] == [line1, line2] or (self._is_in_section and @@ -1190,7 +1215,7 @@ def _is_section_break(self) -> bool: not self._is_indented(line1, self._section_indent))) def _is_section_header(self) -> bool: - section, underline = self._line_iter.peek(2) + section, underline = self._lines.get(0), self._lines.get(1) section = section.lower() if section in self._sections and isinstance(underline, str): return bool(_numpy_section_regex.match(underline)) diff --git a/sphinx/ext/napoleon/iterators.py b/sphinx/ext/napoleon/iterators.py index 9459ad4a6f7..8c9de73e6b4 100644 --- a/sphinx/ext/napoleon/iterators.py +++ b/sphinx/ext/napoleon/iterators.py @@ -1,8 +1,14 @@ """A collection of helpful iterators.""" import collections +import warnings from typing import Any, Iterable, Optional +from sphinx.deprecation import RemovedInSphinx70Warning + +warnings.warn('sphinx.ext.napoleon.iterators is deprecated.', + RemovedInSphinx70Warning) + class peek_iter: """An iterator object that supports peeking ahead.