From a924f1b17bf964a2c24bd09e380e2497d53a5e98 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 24 May 2021 13:33:37 +0200 Subject: [PATCH 01/15] check that a section without a preceding blank line works --- numpydoc/tests/test_docscrape.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 0706b6d0..baf19937 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -934,6 +934,18 @@ def test_no_summary(): ----------""")) +def test_missing_blank_line_after_summary(): + doc = NumpyDocString(""" + Parameters without separating blank line: + Parameters + ---------- + data + Some parameter. + """) + assert len(doc["Parameters"]) == 1 + assert doc["Parameters"][0].name == "data" + + def test_unicode(): doc = SphinxDocString(""" öäöäöäöäöåååå From 11a67c8ef6fd5f51b05547ccc5c3490bde6fc4a6 Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 24 May 2021 19:19:07 +0200 Subject: [PATCH 02/15] also check that removing the blank line between sections works --- numpydoc/tests/test_docscrape.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index baf19937..831cdd91 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -946,6 +946,22 @@ def test_missing_blank_line_after_summary(): assert doc["Parameters"][0].name == "data" +def test_missing_blank_line_between_sections(): + doc = NumpyDocString(""" + Parameters + ---------- + data + Some parameter. + Returns + ------- + int + """) + assert len(doc["Parameters"]) == 1 + assert doc["Parameters"][0].name == "data" + + assert len(doc["Returns"]) == 1 + + def test_unicode(): doc = SphinxDocString(""" öäöäöäöäöåååå From 95ca79dd008a941b0cafb2bb076f2dc2cbfb716a Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 24 May 2021 19:21:14 +0200 Subject: [PATCH 03/15] allow passing a different reader to _is_at_section --- numpydoc/docscrape.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 383d1924..1129acea 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -164,20 +164,21 @@ def __iter__(self): def __len__(self): return len(self._parsed_data) - def _is_at_section(self): - self._doc.seek_next_non_empty_line() + def _is_at_section(self, doc=None): + if doc is None: + doc = self._doc - if self._doc.eof(): + if doc.eof(): return False - l1 = self._doc.peek().strip() # e.g. Parameters + l1 = doc.peek().strip() # e.g. Parameters if l1.startswith('.. index::'): return True - l2 = self._doc.peek(1).strip() # ---------- or ========== + l2 = doc.peek(1).strip() # ---------- or ========== if len(l2) >= 3 and (set(l2) in ({'-'}, {'='}) ) and len(l2) != len(l1): - snip = '\n'.join(self._doc._str[:2])+'...' + snip = '\n'.join(doc._str[:2])+'...' self._error_location("potentially wrong underline length... \n%s \n%s in \n%s"\ % (l1, l2, snip), error=False) return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1)) From c7d223fca7eff2d64b240ca5a6e857f369297bca Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 24 May 2021 19:23:56 +0200 Subject: [PATCH 04/15] rewrite read_to_next_section in terms of _is_at_section --- numpydoc/docscrape.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 1129acea..c0b12610 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -196,15 +196,15 @@ def _strip(self, doc): return doc[i:len(doc)-j] - def _read_to_next_section(self): - section = self._doc.read_to_next_empty_line() - - while not self._is_at_section() and not self._doc.eof(): - if not self._doc.peek(-1).strip(): # previous line was empty - section += [''] - - section += self._doc.read_to_next_empty_line() + def _read_to_next_section(self, doc=None): + if doc is None: + doc = self._doc + section = [] + # make sure we actually read to the next section + if self._is_at_section(doc=doc): + section.append(doc.read()) + section += doc.read_to_condition(lambda l: self._is_at_section(doc=doc)) return section def _read_sections(self): From db4d8c82a53b9b8ab8d88e89e1ce34578699a00d Mon Sep 17 00:00:00 2001 From: Keewis Date: Mon, 24 May 2021 19:24:52 +0200 Subject: [PATCH 05/15] rewrite _parse_summary to only work on the summary --- numpydoc/docscrape.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index c0b12610..1091aa9d 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -357,22 +357,25 @@ def _parse_summary(self): if self._is_at_section(): return + entire_summary = Reader(self._read_to_next_section()) + # If several signatures present, take the last one while True: - summary = self._doc.read_to_next_empty_line() + summary = entire_summary.read_to_next_empty_line() summary_str = " ".join([s.strip() for s in summary]).strip() compiled = re.compile(r'^([\w., ]+=)?\s*[\w\.]+\(.*\)$') if compiled.match(summary_str): self['Signature'] = summary_str - if not self._is_at_section(): + if not self._is_at_section(doc=entire_summary): continue break if summary is not None: self['Summary'] = summary - if not self._is_at_section(): - self['Extended Summary'] = self._read_to_next_section() + if not self._is_at_section(doc=entire_summary): + entire_summary.seek_next_non_empty_line() + self['Extended Summary'] = self._read_to_next_section(doc=entire_summary) def _parse(self): self._doc.reset() From 4b438703cbf6601072e7340c3f7cb6aa9f7f98e1 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 25 May 2021 01:16:17 +0200 Subject: [PATCH 06/15] make sure tracebacks are not detected as sections --- numpydoc/docscrape.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 1091aa9d..148bbec0 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -181,6 +181,14 @@ def _is_at_section(self, doc=None): snip = '\n'.join(doc._str[:2])+'...' self._error_location("potentially wrong underline length... \n%s \n%s in \n%s"\ % (l1, l2, snip), error=False) + + l3 = doc.peek(1).strip() + if "Traceback (most recent call last)" in l3: + return False + + if not l1.strip() or not l2.strip(): + return False + return l2.startswith('-'*len(l1)) or l2.startswith('='*len(l1)) def _strip(self, doc): From 7d768bada2b42c70544685efa5dbb23a4c9c42ce Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 12:59:13 +0100 Subject: [PATCH 07/15] expect warnings instead --- numpydoc/tests/conftest.py | 50 ++++++++++++++++++++++++++ numpydoc/tests/test_docscrape.py | 62 ++++++++++++++++++-------------- 2 files changed, 86 insertions(+), 26 deletions(-) create mode 100644 numpydoc/tests/conftest.py diff --git a/numpydoc/tests/conftest.py b/numpydoc/tests/conftest.py new file mode 100644 index 00000000..9240ac88 --- /dev/null +++ b/numpydoc/tests/conftest.py @@ -0,0 +1,50 @@ +from copy import deepcopy +from io import StringIO + +import pytest +from numpydoc.xref import DEFAULT_LINKS +from sphinx.util import logging + + +class MockConfig(): + numpydoc_use_plots = False + numpydoc_use_blockquotes = True + numpydoc_show_class_members = True + numpydoc_show_inherited_class_members = True + numpydoc_class_members_toctree = True + numpydoc_xref_param_type = False + numpydoc_xref_aliases = {} + numpydoc_xref_aliases_complete = deepcopy(DEFAULT_LINKS) + numpydoc_xref_ignore = set() + templates_path = [] + numpydoc_edit_link = False + numpydoc_citation_re = '[a-z0-9_.-]+' + numpydoc_attributes_as_param_list = True + numpydoc_validation_checks = set() + numpydoc_validation_exclude = set() + + +class MockBuilder(): + config = MockConfig() + + +class MockApp(): + config = MockConfig() + builder = MockBuilder() + translator = None + + def __init__(self): + self.builder.app = self + # Attrs required for logging + self.verbosity = 2 + self._warncount = 0 + self.warningiserror = False + + +@pytest.fixture +def mock_app(): + app = MockApp() + status, warning = StringIO(), StringIO() + logging.setup(app, status, warning) + + yield app diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 831cdd91..f993058f 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -934,32 +934,42 @@ def test_no_summary(): ----------""")) -def test_missing_blank_line_after_summary(): - doc = NumpyDocString(""" - Parameters without separating blank line: - Parameters - ---------- - data - Some parameter. - """) - assert len(doc["Parameters"]) == 1 - assert doc["Parameters"][0].name == "data" - - -def test_missing_blank_line_between_sections(): - doc = NumpyDocString(""" - Parameters - ---------- - data - Some parameter. - Returns - ------- - int - """) - assert len(doc["Parameters"]) == 1 - assert doc["Parameters"][0].name == "data" - - assert len(doc["Returns"]) == 1 +@pytest.mark.parametrize( + ["docstring", "expected_warnings"], + ( + ( + """\ + Parameters without separating blank line: + Parameters + ---------- + data + some parameter + """, + ["missing blank line before the Parameters section"], + ), + ( + """\ + Parameters + ---------- + data + some parameter + Returns + ------- + int + """, + ["missing blank line before the Returns section"], + ), + ), + ids=[ + "missing blank line after summary", + "missing blank line between sections", + ], +) +def test_section_detection_warnings(mock_app, caplog, docstring, expected_warnings): + _ = NumpyDocString(docstring) + messages = [record.msg for record in caplog.records] + for w in expected_warnings: + assert w in messages def test_unicode(): From 845f0e06848305804c6ce1ecd221408733f44543 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 17:14:04 +0100 Subject: [PATCH 08/15] expect python warnings instead of sphinx warning log messages --- numpydoc/tests/test_docscrape.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index bae9d92a..6de7e654 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -1011,7 +1011,7 @@ def test_no_summary(): @pytest.mark.parametrize( - ["docstring", "expected_warnings"], + ["docstring", "expected_warning"], ( ( """\ @@ -1021,7 +1021,7 @@ def test_no_summary(): data some parameter """, - ["missing blank line before the Parameters section"], + "missing blank line before the Parameters section", ), ( """\ @@ -1033,7 +1033,7 @@ def test_no_summary(): ------- int """, - ["missing blank line before the Returns section"], + "missing blank line before the Returns section", ), ), ids=[ @@ -1041,11 +1041,10 @@ def test_no_summary(): "missing blank line between sections", ], ) -def test_section_detection_warnings(mock_app, caplog, docstring, expected_warnings): - _ = NumpyDocString(docstring) - messages = [record.msg for record in caplog.records] - for w in expected_warnings: - assert w in messages +def test_section_detection_warnings(docstring, expected_warning): + warning_re = ".*%s.*" % expected_warning + with pytest.warns(UserWarning, match=warning_re): + _ = NumpyDocString(docstring) def test_unicode(): From d0a2c1eca9ab6e2d26d8da8fed27233d89efdb97 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 17:15:00 +0100 Subject: [PATCH 09/15] remove the logging setup fixture --- numpydoc/tests/conftest.py | 50 -------------------------------------- 1 file changed, 50 deletions(-) delete mode 100644 numpydoc/tests/conftest.py diff --git a/numpydoc/tests/conftest.py b/numpydoc/tests/conftest.py deleted file mode 100644 index 9240ac88..00000000 --- a/numpydoc/tests/conftest.py +++ /dev/null @@ -1,50 +0,0 @@ -from copy import deepcopy -from io import StringIO - -import pytest -from numpydoc.xref import DEFAULT_LINKS -from sphinx.util import logging - - -class MockConfig(): - numpydoc_use_plots = False - numpydoc_use_blockquotes = True - numpydoc_show_class_members = True - numpydoc_show_inherited_class_members = True - numpydoc_class_members_toctree = True - numpydoc_xref_param_type = False - numpydoc_xref_aliases = {} - numpydoc_xref_aliases_complete = deepcopy(DEFAULT_LINKS) - numpydoc_xref_ignore = set() - templates_path = [] - numpydoc_edit_link = False - numpydoc_citation_re = '[a-z0-9_.-]+' - numpydoc_attributes_as_param_list = True - numpydoc_validation_checks = set() - numpydoc_validation_exclude = set() - - -class MockBuilder(): - config = MockConfig() - - -class MockApp(): - config = MockConfig() - builder = MockBuilder() - translator = None - - def __init__(self): - self.builder.app = self - # Attrs required for logging - self.verbosity = 2 - self._warncount = 0 - self.warningiserror = False - - -@pytest.fixture -def mock_app(): - app = MockApp() - status, warning = StringIO(), StringIO() - logging.setup(app, status, warning) - - yield app From cc83cb5cf153891bf13e7f30afe72999083b4e2f Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 17:16:30 +0100 Subject: [PATCH 10/15] "read to next empty line" function that warns about missing empty lines --- numpydoc/docscrape.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index e5c07f59..7db5c8f5 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -205,13 +205,33 @@ def _strip(self, doc): return doc[i : len(doc) - j] def _read_to_next_section(self): - section = self._doc.read_to_next_empty_line() + def read_to_next_empty_line(): + data = [] + while True: + old_line = self._doc._l + if self._is_at_section() and self._doc.peek(-1).strip(): + section_name = self._doc.peek() + self._error_location( + "missing blank line before the %s section" % section_name, + error=False, + ) + self._doc._l = old_line + + current = self._doc.read() + if not current.strip(): + break + + data.append(current) + + return data + + section = read_to_next_empty_line() while not self._is_at_section() and not self._doc.eof(): if not self._doc.peek(-1).strip(): # previous line was empty section += [""] - section += self._doc.read_to_next_empty_line() + section += read_to_next_empty_line() return section From 59414770cda5926376cb6e1e117a2aca672e9198 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 17:19:07 +0100 Subject: [PATCH 11/15] make the custom function a method --- numpydoc/docscrape.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 7db5c8f5..190da53b 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -204,34 +204,34 @@ def _strip(self, doc): return doc[i : len(doc) - j] - def _read_to_next_section(self): - def read_to_next_empty_line(): - data = [] - while True: - old_line = self._doc._l - if self._is_at_section() and self._doc.peek(-1).strip(): - section_name = self._doc.peek() - self._error_location( - "missing blank line before the %s section" % section_name, - error=False, - ) - self._doc._l = old_line + def read_to_next_empty_line(self): + data = [] + while True: + old_line = self._doc._l + if self._is_at_section() and self._doc.peek(-1).strip(): + section_name = self._doc.peek() + self._error_location( + "missing blank line before the %s section" % section_name, + error=False, + ) + self._doc._l = old_line - current = self._doc.read() - if not current.strip(): - break + current = self._doc.read() + if not current.strip(): + break - data.append(current) + data.append(current) - return data + return data - section = read_to_next_empty_line() + def _read_to_next_section(self): + section = self.read_to_next_empty_line() while not self._is_at_section() and not self._doc.eof(): if not self._doc.peek(-1).strip(): # previous line was empty section += [""] - section += read_to_next_empty_line() + section += self.read_to_next_empty_line() return section From 21655e86b9532dfc0aa4dcbd06570a2e5daba2e5 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 17:20:29 +0100 Subject: [PATCH 12/15] detect missing empty lines between summary and the first section --- numpydoc/docscrape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 190da53b..d1d8e6d0 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -396,7 +396,7 @@ def _parse_summary(self): # If several signatures present, take the last one while True: - summary = self._doc.read_to_next_empty_line() + summary = self.read_to_next_empty_line() summary_str = " ".join([s.strip() for s in summary]).strip() compiled = re.compile(r"^([\w., ]+=)?\s*[\w\.]+\(.*\)$") if compiled.match(summary_str): From 022b4b95c97dea955a1b2fe07b09125dbf4238fb Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 17:27:00 +0100 Subject: [PATCH 13/15] make the condition a bit clearer --- numpydoc/docscrape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index d1d8e6d0..090aab09 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -208,7 +208,7 @@ def read_to_next_empty_line(self): data = [] while True: old_line = self._doc._l - if self._is_at_section() and self._doc.peek(-1).strip(): + if self._is_at_section() and self._doc.peek(-1).strip() != "": section_name = self._doc.peek() self._error_location( "missing blank line before the %s section" % section_name, From fa621ae4398933230490e9ecbcda7517323f0252 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 17:27:34 +0100 Subject: [PATCH 14/15] return an empty string if peeking results in a negative list index --- numpydoc/docscrape.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 090aab09..8681351f 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -95,7 +95,7 @@ def is_unindented(line): return self.read_to_condition(is_unindented) def peek(self, n=0): - if self._l + n < len(self._str): + if 0 <= self._l + n < len(self._str): return self[self._l + n] else: return "" From 08e2ee2b75b2f62cdc268b7e845c02be723a7959 Mon Sep 17 00:00:00 2001 From: Keewis Date: Tue, 3 Jan 2023 17:35:23 +0100 Subject: [PATCH 15/15] =?UTF-8?q?blank=20=E2=86=92=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- numpydoc/docscrape.py | 2 +- numpydoc/tests/test_docscrape.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/numpydoc/docscrape.py b/numpydoc/docscrape.py index 8681351f..348f63a5 100644 --- a/numpydoc/docscrape.py +++ b/numpydoc/docscrape.py @@ -211,7 +211,7 @@ def read_to_next_empty_line(self): if self._is_at_section() and self._doc.peek(-1).strip() != "": section_name = self._doc.peek() self._error_location( - "missing blank line before the %s section" % section_name, + "missing empty line before the %s section" % section_name, error=False, ) self._doc._l = old_line diff --git a/numpydoc/tests/test_docscrape.py b/numpydoc/tests/test_docscrape.py index 6de7e654..762887f7 100644 --- a/numpydoc/tests/test_docscrape.py +++ b/numpydoc/tests/test_docscrape.py @@ -1015,13 +1015,13 @@ def test_no_summary(): ( ( """\ - Parameters without separating blank line: + Parameters without separating empty line: Parameters ---------- data some parameter """, - "missing blank line before the Parameters section", + "missing empty line before the Parameters section", ), ( """\ @@ -1033,12 +1033,12 @@ def test_no_summary(): ------- int """, - "missing blank line before the Returns section", + "missing empty line before the Returns section", ), ), ids=[ - "missing blank line after summary", - "missing blank line between sections", + "missing empty line after summary", + "missing empty line between sections", ], ) def test_section_detection_warnings(docstring, expected_warning):