Skip to content

Commit

Permalink
Skip newline before python docstring
Browse files Browse the repository at this point in the history
Docstrings are defined in PEP 257 as "A docstring is a string literal
that occurs as the first statement in a module, function, class, or
method definition." When reuse adds a header, it forcibly adds a
space before the docstring violating this principle.

When the style is set to Python and the first line in the file is a
docstring, do not add a space between the license and the docstring.

Signed-off-by: Charlie Jenkins <charlie@rivosinc.com>
  • Loading branch information
charlie-rivos committed Jan 30, 2025
1 parent 7a3a7c1 commit 318cbe1
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 2 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,4 @@ Contributors
- Skyler Grey <sky@a.starrysky.fyi>
- Emil Velikov <emil.l.velikov@gmail.com>
- Linnea Gräf <nea@nea.moe>
- Charlie Jenkins <charlie@rivosinc.com>
2 changes: 2 additions & 0 deletions changelog.d/changed/removed-space-before-docstring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- To conform to PEP 257, do not put a space before a docstring at the beginning
of python formatted files. (#1136)
20 changes: 18 additions & 2 deletions src/reuse/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# SPDX-FileCopyrightText: 2022 Florian Snow <florian@familysnow.net>
# SPDX-FileCopyrightText: 2022 Yaman Qalieh
# SPDX-FileCopyrightText: 2022 Carmen Bianca Bakker <carmenbianca@fsfe.org>
# SPDX-FileCopyrightText: 2025 Charles Jenkins <charlie@rivosinc.com>
#
# SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -39,6 +40,7 @@
DEFAULT_TEMPLATE = _ENV.get_template("default_template.jinja2")

_NEWLINE_PATTERN = re.compile(r"\n", re.MULTILINE)
_DOCSTRING_PATTERN = re.compile(r'^""".*"""', re.MULTILINE)


class _TextSections(NamedTuple):
Expand Down Expand Up @@ -215,6 +217,20 @@ def _extract_shebang(prefix: str, text: str) -> tuple[str, str]:
return (shebang, text)


def _get_separator(style: Type[CommentStyle], text: str) -> str:
"""Define separator between the license and the body of the text."""
separator = "\n"

if style == PythonCommentStyle:
# PEP 257 requires that docstrings are the first line of the file, so
# there must not be a space after the license.
docstring = _DOCSTRING_PATTERN.match(text)
if docstring:
separator = ""

return separator


# pylint: disable=too-many-arguments
def find_and_replace_header(
text: str,
Expand Down Expand Up @@ -289,7 +305,7 @@ def find_and_replace_header(
if before.strip():
new_text = f"{before.rstrip()}\n\n{new_text}"
if after.strip():
new_text = f"{new_text}\n{after.lstrip()}"
new_text = f"{new_text}{_get_separator(style, after)}{after.lstrip()}"
return new_text


Expand Down Expand Up @@ -335,5 +351,5 @@ def add_new_header(
if shebang.strip():
new_text = f"{shebang.rstrip()}\n\n{new_text}"
if text.strip():
new_text = f"{new_text}\n{text.lstrip()}"
new_text = f"{new_text}{_get_separator(style, text)}{text.lstrip()}"
return new_text
37 changes: 37 additions & 0 deletions tests/test_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,26 @@ def test_find_and_replace_no_header():
)


def test_find_and_replace_no_header_docstring():
"""Given text with docstring and no without header, add a header."""
info = ReuseInfo({"GPL-3.0-or-later"}, {"SPDX-FileCopyrightText: Jane Doe"})
text = '"""docstring"""'
expected = cleandoc(
"""
# SPDX-FileCopyrightText: Jane Doe
#
# SPDX-License-Identifier: GPL-3.0-or-later
\"\"\"docstring\"\"\"
"""
)

assert (
find_and_replace_header(text, info)
== add_new_header(text, info)
== expected
)


def test_find_and_replace_verbatim():
"""Replace a header with itself."""
info = ReuseInfo()
Expand Down Expand Up @@ -459,4 +479,21 @@ def test_find_and_replace_preserve_newline():
assert find_and_replace_header(text, info) == text


def test_find_and_replace_preserve_no_newline_docstring():
"""If the file content doesn't have a newline and has a docstring,
don't add a newline."""

info = ReuseInfo()
text = cleandoc(
"""
# SPDX-FileCopyrightText: Jane Doe
#
# SPDX-License-Identifier: GPL-3.0-or-later
\"\"\"docstring\"\"\"
"""
)

assert find_and_replace_header(text, info) == text


# REUSE-IgnoreEnd

0 comments on commit 318cbe1

Please sign in to comment.