Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace napoleon.iterators by simpler stack implementation #9856

Merged
merged 1 commit into from Jun 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES
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
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``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is pockets.iterators? I think the chance of someone actually being impacted by this deprecation is vanishingly small, but wouldn't we recommend using the simpler method from this PR instead?

A

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the original upstream version (https://github.com/RobRuana/pockets/blob/master/pockets/iterators.py) that got vendored into sphinx. It should be a drop-in replacement, so it seems to be easier to point any (likely rare) third party users to that module.


* - ``sphinx.util.stemmer``
- 5.1
- 7.0
Expand Down
74 changes: 43 additions & 31 deletions sphinx/ext/napoleon/docstring.py
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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agreed that no reason to evaluate the modifier lazily. So +1 for using peek_iter instead of modify_iter.

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
@@ -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
@@ -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