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 (actually implemented with a deque here,
to keep the lines in their physical order).

This tightens the public API and makes it easier to extract napoleon for
vendoring independently of sphinx.
  • Loading branch information
anntzer committed Jun 25, 2022
1 parent 1786e98 commit 2f97349
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Deprecated

* #10467: Deprecated ``sphinx.util.stemmer`` in favour of ``snowballstemmer``.
Patch by Adam Turner.
* #9856: Deprecated ``sphinx.ext.napoleon.iterators``.

Features added
--------------
Expand Down
5 changes: 5 additions & 0 deletions doc/extdev/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ The following is a list of deprecated interfaces.
- (will be) Removed
- Alternatives

* - ``sphinx.ext.napoleon.iterators``
- 5.1
- 7.0
- ``pockets.iterators``

* - ``sphinx.util.stemmer``
- 5.1
- 7.0
Expand Down
74 changes: 43 additions & 31 deletions sphinx/ext/napoleon/docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from sphinx.application import Sphinx
from sphinx.config import Config as SphinxConfig
from sphinx.deprecation import RemovedInSphinx60Warning
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 @@ -46,6 +45,19 @@
_SINGLETONS = ("None", "True", "False", "Ellipsis")


class Deque(collections.deque):
"""A subclass of deque with an additional `.Deque.get` method."""

sentinel = object()

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[n] if n < len(self) else self.sentinel


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 @@ -153,7 +165,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 = Deque(map(str.rstrip, lines))
self._parsed_lines: List[str] = []
self._is_in_section = False
self._section_indent = 0
Expand Down Expand Up @@ -225,32 +237,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.popleft())
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.popleft())
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.popleft())
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.popleft()

before, colon, after = self._partition_field_on_colon(line)
_name, _type, _desc = before, '', after
Expand Down Expand Up @@ -288,7 +300,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.popleft()
_type, colon, _desc = self._partition_field_on_colon(line)
if not colon or not _desc:
_type, _desc = _desc, _type
Expand Down Expand Up @@ -326,23 +338,23 @@ def _consume_usage_section(self) -> List[str]:
return lines

def _consume_section_header(self) -> str:
section = next(self._line_iter)
section = self._lines.popleft()
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.popleft())
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.popleft())
return lines + self._consume_empty()

def _dedent(self, lines: List[str], full: bool = False) -> List[str]:
Expand Down Expand Up @@ -468,12 +480,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 @@ -528,7 +540,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 @@ -542,8 +554,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 @@ -585,7 +597,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 @@ -1158,7 +1170,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.popleft()
if parse_type:
_name, _, _type = self._partition_field_on_colon(line)
else:
Expand Down Expand Up @@ -1189,23 +1201,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.popleft()
if not _directive_regex.match(section):
# Consume the header underline
next(self._line_iter)
self._lines.popleft()
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
@@ -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.
Expand Down
9 changes: 9 additions & 0 deletions tests/test_ext_napoleon_iterators.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
"""Tests for :mod:`sphinx.ext.napoleon.iterators` module."""

import sys
from unittest import TestCase

from sphinx.deprecation import RemovedInSphinx70Warning
from sphinx.ext.napoleon.iterators import modify_iter, peek_iter


class ModuleIsDeprecatedTest(TestCase):
def test_module_is_deprecated(self):
sys.modules.pop("sphinx.ext.napoleon.iterators")
with self.assertWarns(RemovedInSphinx70Warning):
import sphinx.ext.napoleon.iterators # noqa


class BaseIteratorsTest(TestCase):
def assertEqualTwice(self, expected, func, *args):
self.assertEqual(expected, func(*args))
Expand Down

0 comments on commit 2f97349

Please sign in to comment.