Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Current usage of ``pydocstringformatter``:
[--exit-code] [--max-summary-lines int]
[--summary-quotes-same-line]
[--max-line-length int]
[--style {pep257} [{pep257} ...]]
[--style {pep257,numpydoc} [{pep257,numpydoc} ...]]
[--strip-whitespaces --no-strip-whitespaces]
[--split-summary-body --no-split-summary-body]
[--linewrap-full-docstring --no-linewrap-full-docstring]
Expand All @@ -18,6 +18,10 @@ Current usage of ``pydocstringformatter``:
[--capitalize-first-letter --no-capitalize-first-letter]
[--final-period --no-final-period]
[--quotes-type --no-quotes-type]
[--numpydoc-section-order --no-numpydoc-section-order]
[--numpydoc-name-type-spacing --no-numpydoc-name-type-spacing]
[--numpydoc-section-spacing --no-numpydoc-section-spacing]
[--numpydoc-section-hyphen-length --no-numpydoc-section-hyphen-length]
[files ...]

positional arguments:
Expand Down Expand Up @@ -45,7 +49,7 @@ Current usage of ``pydocstringformatter``:
is enforced for single line docstrings.
--max-line-length int
Maximum line length of docstrings.
--style {pep257} [{pep257} ...]
--style {pep257,numpydoc} [{pep257,numpydoc} ...]
Docstring styles that are used in the project. Can be
more than one.

Expand Down Expand Up @@ -82,6 +86,22 @@ Current usage of ``pydocstringformatter``:
Activate or deactivate quotes-type: Change all opening
and closing quotes to be triple quotes. Styles:
default. (default: True)
--numpydoc-section-order, --no-numpydoc-section-order
Activate or deactivate numpydoc-section-order: Change
section order to match numpydoc guidelines. Styles:
numpydoc. (default: True)
--numpydoc-name-type-spacing, --no-numpydoc-name-type-spacing
Activate or deactivate numpydoc-name-type-spacing:
Ensure proper spacing around the colon separating
names from types. Styles: numpydoc. (default: True)
--numpydoc-section-spacing, --no-numpydoc-section-spacing
Activate or deactivate numpydoc-section-spacing:
Ensure proper spacing between sections. Styles:
numpydoc. (default: True)
--numpydoc-section-hyphen-length, --no-numpydoc-section-hyphen-length
Activate or deactivate numpydoc-section-hyphen-length:
Ensure hyphens after section header lines are proper
length. Styles: numpydoc. (default: True)

optional formatters:
these formatters are turned off by default
Expand Down
2 changes: 1 addition & 1 deletion pydocstringformatter/_configuration/arguments_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def register_arguments(self, version: str) -> None:
action="extend",
type=str,
nargs="+",
choices=["pep257"],
choices=["pep257", "numpydoc"],
help="Docstring styles that are used in the project. Can be more than one.",
)

Expand Down
10 changes: 10 additions & 0 deletions pydocstringformatter/_formatting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
QuotesTypeFormatter,
StripWhitespacesFormatter,
)
from pydocstringformatter._formatting.formatters_numpydoc import (
NumpydocNameColonTypeFormatter,
NumpydocSectionHyphenLengthFormatter,
NumpydocSectionOrderingFormatter,
NumpydocSectionSpacingFormatter,
)
from pydocstringformatter._formatting.formatters_pep257 import (
SplitSummaryAndDocstringFormatter,
)
Expand All @@ -31,4 +37,8 @@
CapitalizeFirstLetterFormatter(),
FinalPeriodFormatter(),
QuotesTypeFormatter(),
NumpydocSectionOrderingFormatter(),
NumpydocNameColonTypeFormatter(),
NumpydocSectionSpacingFormatter(),
NumpydocSectionHyphenLengthFormatter(),
]
96 changes: 95 additions & 1 deletion pydocstringformatter/_formatting/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import functools
import re
import tokenize
from typing import Literal
from collections import OrderedDict
from collections.abc import Iterator
from itertools import tee
from typing import Literal, TypeVar

_T = TypeVar("_T")


class Formatter:
Expand Down Expand Up @@ -202,3 +207,92 @@ def treat_summary(

def treat_description(self, description: str, indent_length: int) -> str:
return description


def _pairwise(iterator: Iterator[_T]) -> Iterator[tuple[_T, _T]]:
"""Create an iterator over pairs of successive elements."""
first, second = tee(iterator)
next(second)
return zip(first, second)


class NumpydocSectionFormatter(StringAndQuotesFormatter, metaclass=abc.ABCMeta):
"""Base class for formatters working on numpydoc sections."""

style = ["numpydoc"]

@abc.abstractmethod
def treat_sections(
self, sections: OrderedDict[str, list[str]]
) -> OrderedDict[str, list[str]]:
"""Process the individual numpydoc sections."""

def treat_string(
self,
tokeninfo: tokenize.TokenInfo,
indent_length: int,
quotes: str,
quotes_length: Literal[1, 3],
) -> str:
"""Split numpydoc sections, pass them for processing, then rejoin them."""
lines = tokeninfo.string[quotes_length:-quotes_length].split("\n")
# Handle the spaces before the closing quotes
last_line = lines[-1]
if lines[-1].isspace():
lines[-1] = ""

# Split sections
section_hyphen_lines = [
index
for index, line in enumerate(lines)
if "-" in line and all(char in " \t-" for char in line)
]
section_starts = (
[0] + [index - 1 for index in section_hyphen_lines] + [len(lines)]
)
sections = OrderedDict(
[
(
lines[curr_section_start].lstrip(),
lines[curr_section_start:next_section_start],
)
for curr_section_start, next_section_start in _pairwise(
iter(section_starts)
)
]
)

if not section_hyphen_lines or section_hyphen_lines[0] > 1:
# The "Summary" section here includes the numpydoc
# summary, deprecation warning, and extended summary
# sections. There's not an easy split for those the way
# there is for the other sections.
_, summary_section = sections.popitem(last=False)
sections["Summary"] = summary_section
sections.move_to_end("Summary", last=False)

# Process sections
new_sections = self.treat_sections(sections)

# Check that indent on first line of section didn't get weird
first_section = True
for section in new_sections.values():
if first_section:
section[0] = section[0].lstrip()
first_section = False
elif not section[0][0].isspace():
section[0] = f"{' ' * indent_length:s}{section[0]:s}"

# Rejoin sections
lines = [line for section in new_sections.values() for line in section]
# Ensure the last line puts the quotes in the right spot
if lines and lines[-1] == "":
if (last_line == "") or last_line.isspace():
# Try to preserve unindented closing quotes
lines[-1] = last_line
else:
# Quotes weren't on a different line before formatting
# but are now
lines[-1] = indent_length * " "
body = "\n".join(lines)
return f"{quotes:s}{body:s}{quotes:s}"
131 changes: 131 additions & 0 deletions pydocstringformatter/_formatting/formatters_numpydoc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
from __future__ import annotations

from collections import OrderedDict

from pydocstringformatter._formatting.base import NumpydocSectionFormatter


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",
)

def treat_sections(
self, sections: OrderedDict[str, list[str]]
) -> OrderedDict[str, list[str]]:
"""Sort the numpydoc sections into the numpydoc order."""
for sec_name in reversed(self.numpydoc_section_order):
try:
sections.move_to_end(sec_name, last=False)
except KeyError:
pass
return sections


class NumpydocNameColonTypeFormatter(NumpydocSectionFormatter):
"""Ensure proper spacing around the colon separating names from types."""

name = "numpydoc-name-type-spacing"

numpydoc_sections_with_parameters = (
"Parameters",
"Attributes",
"Returns",
"Yields",
"Receives",
"Other Parameters",
"See Also",
)

def treat_sections(
self, sections: OrderedDict[str, list[str]]
) -> OrderedDict[str, list[str]]:
"""Ensure proper spacing around the colon separating names from types."""
for section_name, section_lines in sections.items():
if section_name in self.numpydoc_sections_with_parameters:
# Any section that gets here has a line of hyphens
initial_indent = section_lines[1].index("-")
for index, line in enumerate(section_lines):
if (
# There is content on this line (at least the initial indent)
len(line) > initial_indent
# and the first character after the indent for
# the docstring is a name, not an additional
# indent indicating a description rather than
# a line with name and type
and not line[initial_indent].isspace()
# and there is a colon to separate the name
# from the type (functions returning only one
# thing don't put a name in their "Returns"
# section)
and ":" in line
):
line_name, line_type = line.split(":", 1)
if line_type:
# Avoid adding trailing whitespace
# Colon ending first line is suggested for long
# "See Also" links
section_lines[
index
] = f"{line_name.rstrip():s} : {line_type.lstrip():s}"
return sections


class NumpydocSectionSpacingFormatter(NumpydocSectionFormatter):
"""Ensure proper spacing between sections."""

name = "numpydoc-section-spacing"

def treat_sections(
self, sections: OrderedDict[str, list[str]]
) -> OrderedDict[str, list[str]]:
"""Ensure proper spacing between sections."""
for section_lines in sections.values():
last_line = section_lines[-1]
if not (last_line == "" or last_line.isspace()):
section_lines.append("")
return sections


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

name = "numpydoc-section-hyphen-length"

def treat_sections(
self, sections: OrderedDict[str, list[str]]
) -> OrderedDict[str, list[str]]:
"""Ensure section header lines are proper length."""
first_section = True
for section_name, section_lines in sections.items():
if section_name != "Summary":
# Skip the summary, deprecation warning and extended
# summary. They have neither a section header nor the
# line of hyphens after it.
indent_length = section_lines[1].index("-")
section_lines[1] = " " * indent_length + "-" * len(section_name)
if first_section:
# If the second line were not hyphens, the section name
# would be summary. This assumes triple quotes, but that
# seems fine for a multi-line docstring.
section_lines[1] = f"{section_lines[1]:s}---"
first_section = False
return sections
6 changes: 6 additions & 0 deletions tests/data/format/numpydoc/numpydoc_header_line.args
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
--style=numpydoc
--no-numpydoc-name-type-spacing
--no-numpydoc-section-order
--no-numpydoc-section-spacing
--no-final-period
--no-closing-quotes
1 change: 1 addition & 0 deletions tests/data/format/numpydoc/numpydoc_header_line.py
63 changes: 63 additions & 0 deletions tests/data/format/numpydoc/numpydoc_header_line.py.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Example module for numpydoc docstring style.
References
----------
NumPy docstring style guide:
https://numpydoc.readthedocs.io/en/latest/format.html#documenting-modules"""
import math

EULER_NUMBER = math.e
"""Euler's number.

Not related to Euler's constant (sometimes called the Euler-Mascheroni
constant.
References
----------
E (mathematical constant)
https://en.wikipedia.org/wiki/E_(mathematical_constant)
Notes
-----
It is the limit of ``(1 + 1/n)**n`` as n approaches infinity, so it
is used in the equation for continuously-compouned interest.

It is also the sum of the reciprocals of the whole numbers starting
with zero, which is related to some calculus-related properties
mathemeticians find elegant.
"""


def sincos(theta):
"""Returns
----------
sin: float
the sine of theta
cos: float
the cosine of theta
Raises
------
TypeError
If `theta` is not a float.
Parameters
----------
theta: float
the angle at which to calculate the sine and cosine.
"""
return math.sin(theta), math.cos(theta)


def fibbonacci():
"""Generate the Fibonacci sequence.

Each term is the sum of the two previous; conventionally starts
with two ones.
References
----------
Fibonacci numbers
https://en.wikipedia.org/wiki/Fibonacci_number
Yields
------
int"""
curr = 1
last = 0
while True:
yield curr
curr, last = curr + last, curr
Loading