Skip to content

Add numpydoc section name checker #278

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

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pydocstringformatter/_formatting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pydocstringformatter._formatting.formatters_numpydoc import (
NumpydocNameColonTypeFormatter,
NumpydocSectionHyphenLengthFormatter,
NumpydocSectionNameFormatter,
NumpydocSectionOrderingFormatter,
NumpydocSectionSpacingFormatter,
)
Expand All @@ -35,6 +36,7 @@
NumpydocNameColonTypeFormatter(),
NumpydocSectionSpacingFormatter(),
NumpydocSectionHyphenLengthFormatter(),
NumpydocSectionNameFormatter(),
LineWrapperFormatter(),
BeginningQuotesFormatter(),
ClosingQuotesFormatter(),
Expand Down
53 changes: 36 additions & 17 deletions pydocstringformatter/_formatting/formatters_numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,33 @@
from collections import OrderedDict

from pydocstringformatter._formatting.base import NumpydocSectionFormatter
from pydocstringformatter._utils.exceptions import ActionRequiredError

NUMPYDOC_SECTIONS = (
"Summary",
"Parameters",
"Attributes",
"Methods",
"Returns",
"Yields",
"Receives",
"Other Parameters",
"Raises",
"Warns",
"Warnings",
"See Also",
"Notes",
"References",
"Examples",
) # Order must match numpydoc


class NumpydocSectionOrderingFormatter(NumpydocSectionFormatter):
"""Change section order to match numpydoc guidelines."""

name = "numpydoc-section-order"

numpydoc_section_order = (
"Summary",
"Parameters",
"Attributes",
"Methods",
"Returns",
"Yields",
"Receives",
"Other Parameters",
"Raises",
"Warns",
"Warnings",
"See Also",
"Notes",
"References",
"Examples",
)
numpydoc_section_order = NUMPYDOC_SECTIONS

def treat_sections(
self, sections: OrderedDict[str, list[str]]
Expand Down Expand Up @@ -105,6 +108,22 @@ def treat_sections(
return sections


class NumpydocSectionNameFormatter(NumpydocSectionFormatter):
"""Check if sections are named correctly."""

name = "numpydoc-section-name-checker"

def treat_sections(
self, sections: OrderedDict[str, list[str]]
) -> OrderedDict[str, list[str]]:
"""Ensure proper spacing between sections."""
for section_name in sections:
if section_name not in NUMPYDOC_SECTIONS:
raise ActionRequiredError(f"Invalid section_name '{section_name}'")

return sections


class NumpydocSectionHyphenLengthFormatter(NumpydocSectionFormatter):
"""Ensure hyphens after section header lines are proper length."""

Expand Down
2 changes: 1 addition & 1 deletion pydocstringformatter/_testutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def __init__(
) -> None:
self.formatters = formatters
file_name = "_".join([f.name for f in self.formatters])
self.file_to_format = tmp_path / f"test_{file_name}.py"
self.file_to_format = tmp_path / f"test_{hash(file_name)}.py"
self.file_to_format.write_text(docstring)
self.capsys = capsys
names = [f"'{f.name}'" for f in formatters]
Expand Down
8 changes: 8 additions & 0 deletions pydocstringformatter/_utils/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ class TomlParsingError(PydocstringFormatterError):

class UnstableResultError(PydocstringFormatterError):
"""Raised when the result of the formatting is unstable."""


class ActionRequiredError(PydocstringFormatterError):
"""Raised when an action is required from the user."""

def __init__(self, message: str) -> None:
super().__init__(message)
self.message = message
34 changes: 28 additions & 6 deletions pydocstringformatter/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@

from pydocstringformatter import __version__, _formatting, _utils
from pydocstringformatter._configuration.arguments_manager import ArgumentsManager
from pydocstringformatter._utils.exceptions import UnstableResultError
from pydocstringformatter._utils.exceptions import (
ActionRequiredError,
UnstableResultError,
)


class _Run:
Expand All @@ -31,16 +34,17 @@ def __init__(self, argv: list[str] | None) -> None:
formatter.set_config_namespace(self.config)

self.enabled_formatters = self.get_enabled_formatters()
self.is_changed = False
self.check_files(self.config.files)

# pylint: disable-next=inconsistent-return-statements
def check_files(self, files: list[str]) -> None:
"""Find all files and perform the formatting."""
filepaths = _utils.find_python_files(files, self.config.exclude)

is_changed = self.format_files(filepaths)
self.format_files(filepaths)

if is_changed: # pylint: disable=consider-using-assignment-expr
if self.is_changed:
return _utils.sys_exit(32, self.config.exit_code)

files_string = f"{len(filepaths)} "
Expand Down Expand Up @@ -128,6 +132,9 @@ def format_file_tokens(
UnstableResultError::
If the formatters are not able to get to a stable result.
It reports what formatters are still modifying the tokens.
RuntimeError::
If a formatter fails to apply on a docstring. It reports which
file and which line numbers where the formatter failed.
"""
formatted_tokens: list[tokenize.TokenInfo] = []
is_changed = False
Expand All @@ -136,7 +143,22 @@ def format_file_tokens(
new_tokeninfo = tokeninfo

if _utils.is_docstring(new_tokeninfo, tokens[index - 1]):
new_tokeninfo, changers = self.apply_formatters(new_tokeninfo)
try:
new_tokeninfo, changers = self.apply_formatters(new_tokeninfo)
except ActionRequiredError as err:
start, end = new_tokeninfo.start[0], new_tokeninfo.end[0]
_utils.print_to_console(
f"{filename}:{start}:{end}:\n\n{err.message}", False
)
self.is_changed = True
formatted_tokens.append(new_tokeninfo)
continue
except Exception as err:
start, end = new_tokeninfo.start[0], new_tokeninfo.end[0]
raise RuntimeError(
f"In {filename} L{start}-L{end}:\n\n{err}"
) from None

is_changed = is_changed or bool(changers)

# Run formatters again (3rd time) to check if the result is stable
Expand Down Expand Up @@ -197,7 +219,7 @@ def _apply_formatters_once(

return token, changers

def format_files(self, filepaths: list[Path]) -> bool:
def format_files(self, filepaths: list[Path]) -> None:
"""Format a list of files."""
is_changed = [self.format_file(file) for file in filepaths]
return any(is_changed)
self.is_changed = any(is_changed) or self.is_changed
1 change: 1 addition & 0 deletions tests/data/format/numpydoc/numpydoc_bad_section_name.args
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--style=numpydoc
12 changes: 12 additions & 0 deletions tests/data/format/numpydoc/numpydoc_bad_section_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
def func():
"""Sample function with an invalid section name.

This should raise an error.

Exaplmes
--------
Run this func.

>>> func()

"""
3 changes: 3 additions & 0 deletions tests/data/format/numpydoc/numpydoc_bad_section_name.py.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RuntimeError: In tests/data/format/numpydoc/numpydoc_bad_section_name.py L2-L12:

Invalid section_name 'Exaplmes'