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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ My docstring
"""
```

**PEP 257: _Multi-line docstrings consist of a summary line just like a one-line
docstring, followed by a blank line, followed by a more elaborate description._**

```python
# Bad
"""Summary. Body."""

"""Summary.
Body.
"""

# Good
"""Summary.

Body.
"""
```

## Development

For development and contributing guidelines please see
Expand Down
7 changes: 7 additions & 0 deletions pydocstringformatter/formatting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@
BeginningQuotesFormatter,
ClosingQuotesFormatter,
FinalPeriodFormatter,
SplitSummaryAndDocstringFormatter,
)

# The order of these formatters is important as they are called in order.
# The order is currently:
# String manipulation such as adding extra new lines
# Determine if multi-line or single line and position quotes accordingly
# String manipulation in which being multi-line or single line matters
FORMATTERS: List[Formatter] = [
SplitSummaryAndDocstringFormatter(),
BeginningQuotesFormatter(),
ClosingQuotesFormatter(),
FinalPeriodFormatter(),
Expand Down
4 changes: 2 additions & 2 deletions pydocstringformatter/formatting/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ class StringFormatter(Formatter):
"""Base class for formatter that only modifies the string content."""

@abc.abstractmethod
def _treat_string(self, tokeninfo: tokenize.TokenInfo) -> str:
def _treat_string(self, tokeninfo: tokenize.TokenInfo, indent_length: int) -> str:
"""Return a modified string."""

def treat_token(self, tokeninfo: tokenize.TokenInfo) -> tokenize.TokenInfo:
return tokenize.TokenInfo(
tokeninfo.type,
self._treat_string(tokeninfo),
self._treat_string(tokeninfo, tokeninfo.start[1]),
tokeninfo.start,
tokeninfo.end,
tokeninfo.line,
Expand Down
44 changes: 41 additions & 3 deletions pydocstringformatter/formatting/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class BeginningQuotesFormatter(StringFormatter):

name = "beginning-quotes"

def _treat_string(self, tokeninfo: tokenize.TokenInfo) -> str:
def _treat_string(self, tokeninfo: tokenize.TokenInfo, _: int) -> str:
new_string = tokeninfo.string
if new_string[3] == "\n":
new_string = re.sub(r"\n *", "", new_string, 1)
Expand All @@ -21,7 +21,7 @@ class ClosingQuotesFormatter(StringFormatter):

name = "closing-quotes"

def _treat_string(self, tokeninfo: tokenize.TokenInfo) -> str:
def _treat_string(self, tokeninfo: tokenize.TokenInfo, _: int) -> str:
"""Fix the position of end quotes for multi-line docstrings."""
new_string = tokeninfo.string
if "\n" not in new_string:
Expand All @@ -44,7 +44,7 @@ class FinalPeriodFormatter(StringFormatter):

name = "final-period"

def _treat_string(self, tokeninfo: tokenize.TokenInfo) -> str:
def _treat_string(self, tokeninfo: tokenize.TokenInfo, _: int) -> str:
"""Add a period to the end of single-line docstrings and summaries."""
# Handle single line docstrings
if not tokeninfo.string.count("\n"):
Expand All @@ -65,3 +65,41 @@ def _treat_string(self, tokeninfo: tokenize.TokenInfo) -> str:
# This is obviously dependent on whether 'pydocstringformatter' will
# start enforcing summaries :)
return tokeninfo.string


class SplitSummaryAndDocstringFormatter(StringFormatter):
"""Split the summary and body of a docstring based on a period in between them.

This formatter is currently optional as its considered somwehat opinionated
and might require major refactoring for existing projects.
"""

name = "split-summary-body"
optional = True

def _treat_string(self, tokeninfo: tokenize.TokenInfo, indent_length: int) -> str:
"""Split a summary and body if there is a period after the summary."""
if index := tokeninfo.string.find("."):
if (
index not in (-1, len(tokeninfo.string) - 4)
and "\n" not in tokeninfo.string[:index] # Skip multi-line summaries
):
# Handle summary with part of docstring body on same line
if tokeninfo.string[index + 1] == " ":
return (
tokeninfo.string[:index]
+ f".\n\n{' ' * indent_length}"
+ tokeninfo.string[index + 2 :]
)

# Handle summary with part of docstring body on same line
if (
tokeninfo.string[index + 1] == "\n"
and tokeninfo.string[index + 2] != "\n"
):
return (
tokeninfo.string[:index]
+ ".\n\n"
+ tokeninfo.string[index + 2 :]
)
return tokeninfo.string
1 change: 1 addition & 0 deletions tests/data/format/summary_splitter/class_docstring.args
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--split-summary-body
34 changes: 34 additions & 0 deletions tests/data/format/summary_splitter/class_docstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class MyClass:
"""Summary. Body."""

class InnerClass:
"""Summary. Body."""


class MyClass:
"""Summary without period Body."""

class InnerClass:
"""Summary without period Body."""


class MyClass:
"""Summary. Body without period"""

class InnerClass:
"""Summary. Body without period"""


class MyClass:
"""Summary.
Body without period"""

class InnerClass:
"""Summary.
Body without period"""


class MyClass:
"""Summary over multiple
lines.
"""
50 changes: 50 additions & 0 deletions tests/data/format/summary_splitter/class_docstring.py.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
class MyClass:
"""Summary.

Body.
"""

class InnerClass:
"""Summary.

Body.
"""


class MyClass:
"""Summary without period Body."""

class InnerClass:
"""Summary without period Body."""


class MyClass:
"""Summary.

Body without period
"""

class InnerClass:
"""Summary.

Body without period
"""


class MyClass:
"""Summary.

Body without period
"""

class InnerClass:
"""Summary.

Body without period
"""


class MyClass:
"""Summary over multiple
lines.
"""
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--split-summary-body
28 changes: 28 additions & 0 deletions tests/data/format/summary_splitter/function_docstrings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
def func():
"""Summary. Body."""

def inner_func():
"""Summary. Body."""


def func():
"""Summary without period Body."""

def inner_func():
"""Summary without period Body."""


def func():
"""Summary. Body without period"""

def inner_func():
"""Summary. Body without period"""


def func():
"""Summary.
Body without period"""

def inner_func():
"""Summary.
Body without period"""
44 changes: 44 additions & 0 deletions tests/data/format/summary_splitter/function_docstrings.py.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
def func():
"""Summary.

Body.
"""

def inner_func():
"""Summary.

Body.
"""


def func():
"""Summary without period Body."""

def inner_func():
"""Summary without period Body."""


def func():
"""Summary.

Body without period
"""

def inner_func():
"""Summary.

Body without period
"""


def func():
"""Summary.

Body without period
"""

def inner_func():
"""Summary.

Body without period
"""
1 change: 1 addition & 0 deletions tests/data/format/summary_splitter/module_docstring.args
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--split-summary-body
8 changes: 8 additions & 0 deletions tests/data/format/summary_splitter/module_docstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Summary. Body."""

"""Summary without period Body."""

"""Summary. Body without period"""

"""Summary.
Body without period"""
16 changes: 16 additions & 0 deletions tests/data/format/summary_splitter/module_docstring.py.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Summary.

Body.
"""

"""Summary without period Body."""

"""Summary.

Body without period
"""

"""Summary.

Body without period
"""
1 change: 1 addition & 0 deletions tests/data/format/summary_splitter/variable_docstring.args
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--split-summary-body
12 changes: 12 additions & 0 deletions tests/data/format/summary_splitter/variable_docstring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
MYVAR = 1
"""Summary. Body."""

MYVAR = 1
"""Summary without period Body."""

MYVAR = 1
"""Summary. Body without period"""

MYVAR = 1
"""Summary.
Body without period"""
20 changes: 20 additions & 0 deletions tests/data/format/summary_splitter/variable_docstring.py.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
MYVAR = 1
"""Summary.

Body.
"""

MYVAR = 1
"""Summary without period Body."""

MYVAR = 1
"""Summary.

Body without period
"""

MYVAR = 1
"""Summary.

Body without period
"""
10 changes: 9 additions & 1 deletion tests/test_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,15 @@ def test_formatting(
with open(temp_file_name, "w", encoding="utf-8") as temp_file:
temp_file.writelines(original_lines)

pydocstringformatter.run_docstring_formatter([temp_file_name, "--write"])
# Get any additional args as specified by an .args file
additional_args: List[str] = []
if os.path.exists(test_file.replace(".py", ".args")):
with open(test_file.replace(".py", ".args"), encoding="utf-8") as args_file:
additional_args = args_file.readlines()[0].split()

pydocstringformatter.run_docstring_formatter(
[temp_file_name, "--write"] + additional_args
)

output = capsys.readouterr()
assert not output.err
Expand Down
12 changes: 12 additions & 0 deletions tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pydocstringformatter
from pydocstringformatter.formatting import FORMATTERS
from pydocstringformatter.formatting.formatter import SplitSummaryAndDocstringFormatter
from pydocstringformatter.testutils import FormatterAsserter


Expand Down Expand Up @@ -125,3 +126,14 @@ def test_begin_quote_formatters(
) as asserter:
asserter.assert_format_when_activated()
asserter.assert_no_change_when_deactivated()


def test_optional_formatters_argument(
capsys: pytest.CaptureFixture[str], tmp_path: Path
) -> None:
"""Test that an optional formatter is correctly turned on and off with arguments."""
with FormatterAsserter(
'"""Summary. Body."""', [SplitSummaryAndDocstringFormatter()], capsys, tmp_path
) as asserter:
asserter.assert_format_when_activated()
asserter.assert_no_change_when_deactivated()