Skip to content

Commit ecbed7f

Browse files
committed
feat(patterns): improve handling of field and section lists for mixed-style docstrings
Improve handling of field and section lists for mixed-style docstrings - Refactor field list detection to preserve Google/NumPy sections when using Sphinx/Epytext styles. - Update list pattern recognition to distinguish between field-based and section-based styles. - Ensure only the appropriate sections are wrapped, preserving formatting for others. - Update description splitting logic to wrap only the description before a preserved section. - Add force_wrap shortcut to always wrap as regular text. - Update test data to reflect new logic (NumPy sections in Sphinx-style docs are now preserved). - Improves compliance with requirements for mixed-style docstrings and prevents unwanted wrapping.
1 parent 1b3f03d commit ecbed7f

File tree

6 files changed

+98
-16
lines changed

6 files changed

+98
-16
lines changed

src/docformatter/patterns/fields.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,39 @@ def do_find_field_lists(
7777
for _field in re.finditer(SPHINX_REGEX, text)
7878
]
7979
_wrap_parameters = True
80-
80+
elif style == "google":
81+
_field_idx = [
82+
(_field.start(0), _field.end(0))
83+
for _field in re.finditer(GOOGLE_REGEX, text, re.MULTILINE)
84+
]
85+
_wrap_parameters = False # Don't wrap Google-style field lists
86+
elif style == "numpy":
87+
_field_idx = [
88+
(_field.start(0), _field.end(0))
89+
for _field in re.finditer(NUMPY_REGEX, text, re.MULTILINE)
90+
]
91+
_wrap_parameters = False # Don't wrap NumPy-style field lists
92+
93+
# If no field lists were found for the current style, check for field lists
94+
# from other styles and preserve them as-is (don't wrap).
95+
if not _field_idx:
96+
# Check for Google-style field lists (e.g., "Args:", "Returns:").
97+
# Use a more specific pattern that only matches known Google section names.
98+
google_sections = r'^ *(Args|Arguments|Attributes|Example|Examples|Note|Notes|' \
99+
r'See Also|References|Returns|Return|Raises|Raise|Yields|Yield|' \
100+
r'Warns|Warning|Warnings|Receives|Receive|Other Parameters):$'
101+
google_matches = list(re.finditer(google_sections, text, re.MULTILINE))
102+
if google_matches:
103+
_field_idx = [(_field.start(0), _field.end(0)) for _field in google_matches]
104+
_wrap_parameters = False
105+
106+
# If still nothing, check for NumPy-style field lists
107+
if not _field_idx:
108+
numpy_matches = list(re.finditer(NUMPY_REGEX, text, re.MULTILINE))
109+
if numpy_matches:
110+
_field_idx = [(_field.start(0), _field.end(0)) for _field in numpy_matches]
111+
_wrap_parameters = False
112+
81113
return _field_idx, _wrap_parameters
82114

83115

src/docformatter/patterns/lists.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,48 @@ def is_type_of_list(
8383
if is_field_list(text, style):
8484
return False
8585

86-
return any(
87-
(
86+
# Check for various list patterns
87+
for line in split_lines:
88+
# Always check for non-field-list patterns
89+
if (
8890
is_bullet_list(line)
8991
or is_enumerated_list(line)
9092
or is_rest_section_header(line)
9193
or is_option_list(line)
92-
or is_epytext_field_list(line)
93-
or is_sphinx_field_list(line)
94-
or is_numpy_field_list(line)
95-
or is_numpy_section_header(line)
96-
or is_google_field_list(line)
97-
or is_user_defined_field_list(line)
9894
or is_literal_block(line)
9995
or is_inline_math(line)
10096
or is_alembic_header(line)
101-
)
102-
for line in split_lines
103-
)
97+
or is_user_defined_field_list(line)
98+
):
99+
return True
100+
101+
# For field list patterns from other styles:
102+
# - When using epytext or sphinx (field-based styles), do NOT treat
103+
# section-based styles (Google/NumPy) as lists to skip. Instead, return
104+
# False so that do_split_description can wrap the description while
105+
# preserving the field sections.
106+
# - When using numpy or google (section-based styles), check for all field
107+
# list patterns to maintain backward compatibility.
108+
if style in ("numpy", "google"):
109+
# For numpy and google styles, check all field list patterns
110+
if (
111+
is_epytext_field_list(line)
112+
or is_sphinx_field_list(line)
113+
or is_numpy_field_list(line)
114+
or is_numpy_section_header(line)
115+
or is_google_field_list(line)
116+
):
117+
return True
118+
elif style in ("epytext", "sphinx"):
119+
# For field-based styles, only check for OTHER field-based styles
120+
if style != "epytext" and is_epytext_field_list(line):
121+
return True
122+
if style != "sphinx" and is_sphinx_field_list(line):
123+
return True
124+
# Do NOT check for Google/NumPy patterns - they'll be preserved by
125+
# do_split_description
126+
127+
return False
104128

105129

106130
def is_bullet_list(line: str) -> Union[Match[str], None]:

src/docformatter/strings.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ def do_split_description(
298298
_url_idx,
299299
)
300300

301-
if not _url_idx and not (_field_idx and _wrap_fields):
301+
if not _url_idx and not _field_idx:
302302
return description_to_list(
303303
text,
304304
indentation,
@@ -314,7 +314,7 @@ def do_split_description(
314314
wrap_length,
315315
)
316316

317-
if _field_idx:
317+
if _field_idx and _wrap_fields:
318318
_lines, _text_idx = _wrappers.do_wrap_field_lists(
319319
text,
320320
_field_idx,
@@ -323,6 +323,24 @@ def do_split_description(
323323
indentation,
324324
wrap_length,
325325
)
326+
elif _field_idx and not _wrap_fields:
327+
# Field lists were found but should not be wrapped (e.g., Google/NumPy style
328+
# when using a different style). Wrap the text before the first field list,
329+
# then preserve the rest as-is.
330+
_lines.extend(
331+
description_to_list(
332+
text[_text_idx : _field_idx[0][0]],
333+
indentation,
334+
wrap_length,
335+
)
336+
)
337+
# Add the field list section as-is, preserving original formatting.
338+
# The text has already been reindented by do_wrap_description, so we
339+
# just preserve the lines as they are.
340+
_field_section = text[_field_idx[0][0]:].splitlines()
341+
for line in _field_section:
342+
_lines.append(line if line.strip() else "")
343+
_text_idx = len(text)
326344
else:
327345
# Finally, add everything after the last URL or field list directive.
328346
_lines += _wrappers.do_close_description(text, _text_idx, indentation)

src/docformatter/wrappers/description.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ def do_wrap_description( # noqa: PLR0913
9595
):
9696
return text
9797

98+
# When force_wrap is True, wrap everything as regular text without special
99+
# handling for field lists.
100+
if force_wrap:
101+
return indentation + "\n".join(
102+
_strings.description_to_list(text, indentation, wrap_length)
103+
).strip()
104+
98105
lines = _strings.do_split_description(text, indentation, wrap_length, style)
99106

100107
return indentation + "\n".join(lines).strip()

tests/_data/string_files/description_wrappers.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ This is a long description from a docstring that will contain an heuristic list.
5454
Item 3
5555
"""
5656
expected = """
57-
This is a long description from a docstring that will contain an heuristic list. The description shouldn't get wrapped at all.
57+
This is a long description from a docstring that will contain an
58+
heuristic list. The description shouldn't get wrapped at all.
5859
5960
Example:
6061
Item one

tests/_data/string_files/list_patterns.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ Parameters
126126
"""
127127
strict = false
128128
style = "sphinx"
129-
expected = true
129+
expected = false # Changed from true: NumPy sections in Sphinx docs should be preserved, not skip wrapping
130130

131131
[is_google_list_numpy_style]
132132
instring = """\

0 commit comments

Comments
 (0)