Skip to content

Commit

Permalink
Replace sphinx.ext.napoleon.iterators by simpler stack implementation.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
anntzer committed Nov 16, 2021
1 parent 6753a0e commit 4d279dc
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 32 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ Incompatible changes
Deprecated
----------

* ``sphinx.ext.napoleon.iterators``

Features added
--------------

Expand Down
89 changes: 57 additions & 32 deletions sphinx/ext/napoleon/docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@
import inspect
import re
from functools import partial
from typing import Any, Callable, Dict, List, Tuple, Type, Union
from typing import Any, Callable, Dict, Iterable, List, Tuple, Type, 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
Expand Down Expand Up @@ -54,6 +53,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:
Expand Down Expand Up @@ -161,7 +186,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
Expand Down Expand Up @@ -233,32 +258,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
Expand Down Expand Up @@ -296,7 +321,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
Expand Down Expand Up @@ -334,23 +359,23 @@ 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
return section

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]:
Expand Down Expand Up @@ -476,12 +501,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:
Expand Down Expand Up @@ -536,7 +561,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)
Expand All @@ -550,8 +575,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
Expand Down Expand Up @@ -593,7 +618,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()
Expand Down Expand Up @@ -1167,7 +1192,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:
Expand Down Expand Up @@ -1198,23 +1223,23 @@ 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
line1 and
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))
Expand Down
6 changes: 6 additions & 0 deletions sphinx/ext/napoleon/iterators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@
"""

import collections
import warnings
from typing import Any, Iterable, Optional

from sphinx.deprecation import RemovedInSphinx60Warning

warnings.warn('sphinx.ext.napoleon.iterators is deprecated.',
RemovedInSphinx60Warning)


class peek_iter:
"""An iterator object that supports peeking ahead.
Expand Down

0 comments on commit 4d279dc

Please sign in to comment.