From 0db427960c6652a7db97ea6d2fb5a1c9cc83e167 Mon Sep 17 00:00:00 2001 From: Charlie Jenkins Date: Thu, 30 Jan 2025 11:24:55 -0800 Subject: [PATCH] Skip newline before python docstring 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 --- AUTHORS.rst | 1 + .../changed/removed-space-before-docstring.md | 2 + src/reuse/header.py | 20 ++++- tests/test_header.py | 78 +++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 changelog.d/changed/removed-space-before-docstring.md diff --git a/AUTHORS.rst b/AUTHORS.rst index 498c62253..2f92b81d3 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -144,3 +144,4 @@ Contributors - Skyler Grey - Emil Velikov - Linnea Gräf +- Charlie Jenkins diff --git a/changelog.d/changed/removed-space-before-docstring.md b/changelog.d/changed/removed-space-before-docstring.md new file mode 100644 index 000000000..8b30c2007 --- /dev/null +++ b/changelog.d/changed/removed-space-before-docstring.md @@ -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) diff --git a/src/reuse/header.py b/src/reuse/header.py index 0538155a3..4877c543a 100644 --- a/src/reuse/header.py +++ b/src/reuse/header.py @@ -9,6 +9,7 @@ # SPDX-FileCopyrightText: 2022 Florian Snow # SPDX-FileCopyrightText: 2022 Yaman Qalieh # SPDX-FileCopyrightText: 2022 Carmen Bianca Bakker +# SPDX-FileCopyrightText: 2025 Charles Jenkins # # SPDX-License-Identifier: GPL-3.0-or-later @@ -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'"""[\S\s]*"""', re.MULTILINE) class _TextSections(NamedTuple): @@ -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, @@ -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 @@ -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 diff --git a/tests/test_header.py b/tests/test_header.py index 15d5507db..364d37230 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -228,6 +228,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() @@ -459,4 +501,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