From 1b57c61b27dd782f1304a7a67b33d19f0c14cef2 Mon Sep 17 00:00:00 2001 From: finswimmer Date: Fri, 23 Jan 2026 15:09:14 +0100 Subject: [PATCH] fix: add multi-line window creation for section header detection --- src/docformatter/patterns/lists.py | 47 +++++++++++++++++++-- tests/_data/string_files/list_patterns.toml | 32 ++++++++++++++ tests/patterns/test_list_patterns.py | 2 + 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/docformatter/patterns/lists.py b/src/docformatter/patterns/lists.py index 48acbdf..a3579cd 100644 --- a/src/docformatter/patterns/lists.py +++ b/src/docformatter/patterns/lists.py @@ -26,7 +26,6 @@ # SOFTWARE. """This module provides docformatter's list pattern recognition functions.""" - # Standard Library Imports import re from re import Match @@ -53,6 +52,42 @@ from .misc import is_inline_math, is_literal_block +def _create_multiline_windows(lines: list[str], window_size: int = 3) -> list[str]: + r"""Create overlapping windows of consecutive lines. + + This allows pattern matching against multi-line constructs like + NumPy section headers (e.g., "Parameters\\n----------") and reST + section headers (e.g., "Title\\n====="). + + Parameters + ---------- + lines : list[str] + The list of individual lines. + window_size : int + Number of consecutive lines to join (default 3). + + Returns + ------- + list[str] + List of multi-line strings, each containing window_size consecutive + lines joined with newlines. + + Notes + ----- + Example: + lines = ['A', 'B', 'C', 'D'] + _create_multiline_windows(lines, 2) + # Returns: ['A\\nB', 'B\\nC', 'C\\nD'] + """ + if len(lines) < window_size: + return ["\n".join(lines)] if lines else [] + + return [ + "\n".join(lines[i : i + window_size]) + for i in range(len(lines) - window_size + 1) + ] + + def is_type_of_list( text: str, strict: bool, @@ -83,16 +118,22 @@ def is_type_of_list( if is_field_list(text, style): return False + # Check for multi-line patterns (section headers) first. + # These require looking at consecutive lines together. + multiline_windows = _create_multiline_windows(split_lines, window_size=2) + for window in multiline_windows: + if is_rest_section_header(window) or is_numpy_section_header(window): + return True + + # Check single-line patterns. return any( ( is_bullet_list(line) or is_enumerated_list(line) - or is_rest_section_header(line) or is_option_list(line) or is_epytext_field_list(line) or is_sphinx_field_list(line) or is_numpy_field_list(line) - or is_numpy_section_header(line) or is_google_field_list(line) or is_user_defined_field_list(line) or is_literal_block(line) diff --git a/tests/_data/string_files/list_patterns.toml b/tests/_data/string_files/list_patterns.toml index 92ff5f2..b7ab8d9 100644 --- a/tests/_data/string_files/list_patterns.toml +++ b/tests/_data/string_files/list_patterns.toml @@ -190,3 +190,35 @@ This is a description. strict = false style = "epytext" expected = false + +[is_numpy_section_in_docstring_issue_338] # See GitHub issue #338 +instring = """Do a number of things. + +Parameters +---------- +n + How many things to do. +colors + True to paint the things in bright colors, False to keep them + dull and grey. + +Returns +------- +int + How many things were actually done. +""" +strict = false +style = "numpy" +expected = true + +[is_rest_section_in_docstring] +instring = """This is a description. + +Section Title +============= + +Some content under the section. +""" +strict = false +style = "numpy" +expected = true diff --git a/tests/patterns/test_list_patterns.py b/tests/patterns/test_list_patterns.py index 4085a8c..b402389 100644 --- a/tests/patterns/test_list_patterns.py +++ b/tests/patterns/test_list_patterns.py @@ -73,6 +73,8 @@ "is_type_of_list_alembic_header", "is_epytext_field_list", "is_sphinx_field_list", + "is_numpy_section_in_docstring_issue_338", + "is_rest_section_in_docstring", ], ) def test_is_type_of_list(test_key):