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 476d7ad commit 69304b3
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 2 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,4 @@ Contributors
- Сергій <sergiy.goncharuk.1@gmail.com>
- Mersho <code.rezaei@gmail.com>
- Skyler Grey <sky@a.starrysky.fyi>
- 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 @@ -43,6 +44,7 @@
DEFAULT_TEMPLATE = _ENV.get_template("default_template.jinja2")

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


class _TextSections(NamedTuple):
Expand Down Expand Up @@ -221,6 +223,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 @@ -294,7 +310,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 @@ -340,5 +356,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
78 changes: 78 additions & 0 deletions tests/test_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,48 @@ def test_find_and_replace_no_header():
)


def test_find_and_replace_no_header_docstring():
"""Given text with docstring and 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_no_header_multiline_docstring():
"""Given text with multiline docstring and without header, add a header."""
info = ReuseInfo({"GPL-3.0-or-later"}, {"SPDX-FileCopyrightText: Jane Doe"})
text = '"""docstring\ndocstring continued\n"""'
expected = cleandoc(
"""
# SPDX-FileCopyrightText: Jane Doe
#
# SPDX-License-Identifier: GPL-3.0-or-later
\"\"\"docstring
docstring continued
\"\"\"
"""
)

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 @@ -463,4 +505,40 @@ 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


def test_find_and_replace_preserve_no_newline_mulitline_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
docstring continued
\"\"\"
"""
)

assert find_and_replace_header(text, info) == text


# REUSE-IgnoreEnd

0 comments on commit 69304b3

Please sign in to comment.