From 2ccb49776957dbcb8b05ac4cc5eb9ef20a54a25d Mon Sep 17 00:00:00 2001 From: Jon Banafato Date: Mon, 22 Mar 2021 13:24:15 -0400 Subject: [PATCH] Fix bug regarding multiline docstrings If a multiline docstring has its closing quotes at the end of a line instead of on a separate line, isort fails to properly add imports using the `add_imports` config option; instead, it adds the desired imports into the middle of the docstring as illustrated below. While PEP 257 (and other guides) advises that closing quotes appear on their own line, `isort` should not fail here. This change adds a check for closing docstrings at the end of a line in addition to the existing line start check for all comment indicators. A new section of the `test_add_imports` test explicitly tests multiline imports and this failure scenario specifically. --- A working example: ```python """My module. Provides example functionality. """ print("hello, world") ``` Running `isort --add-import "from __future__ import annotations"` produces the following as expected: ```python """My module. Provides example functionality. """ from __future__ import annotations print("hello, world") ``` --- The failure behavior described: ```python """My module. Provides example functionality.""" print("hello, world") ``` Running `isort --add-import "from __future__ import annotations"` as above produces the following result: ```python """My module. from __future__ import annotations Provides example functionality.""" print("hello, world") ``` Subsequent executions add more import lines into the docstring. This behavior occurs even if the file already has the desired imports. --- isort/core.py | 4 +++- tests/unit/test_isort.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/isort/core.py b/isort/core.py index d763ad469..9f577dac6 100644 --- a/isort/core.py +++ b/isort/core.py @@ -13,7 +13,8 @@ CIMPORT_IDENTIFIERS = ("cimport ", "cimport*", "from.cimport") IMPORT_START_IDENTIFIERS = ("from ", "from.import", "import ", "import*") + CIMPORT_IDENTIFIERS -COMMENT_INDICATORS = ('"""', "'''", "'", '"', "#") +DOCSTRING_INDICATORS = ('"""', "'''") +COMMENT_INDICATORS = DOCSTRING_INDICATORS + ("'", '"', "#") CODE_SORT_COMMENTS = ( "# isort: list", "# isort: dict", @@ -317,6 +318,7 @@ def process( and not in_quote and not import_section and not line.lstrip().startswith(COMMENT_INDICATORS) + and not line.rstrip().endswith(DOCSTRING_INDICATORS) ): import_section = line_separator.join(add_imports) + line_separator if end_of_file and index != 0: diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 19452db6d..7a02884ba 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -873,6 +873,41 @@ def test_add_imports() -> None: " pass\n" ) + # On a file that has no pre-existing imports and a multiline docstring + test_input = ( + '"""Module docstring\n\nWith a second line\n"""\n' "class MyClass(object):\n pass\n" + ) + test_output = isort.code(code=test_input, add_imports=["from __future__ import print_function"]) + assert test_output == ( + '"""Module docstring\n' + "\n" + "With a second line\n" + '"""\n' + "from __future__ import print_function\n" + "\n" + "\n" + "class MyClass(object):\n" + " pass\n" + ) + + # On a file that has no pre-existing imports and a multiline docstring. + # In this example, the closing quotes for the docstring are on the final + # line rather than a separate one. + test_input = ( + '"""Module docstring\n\nWith a second line"""\n' "class MyClass(object):\n pass\n" + ) + test_output = isort.code(code=test_input, add_imports=["from __future__ import print_function"]) + assert test_output == ( + '"""Module docstring\n' + "\n" + 'With a second line"""\n' + "from __future__ import print_function\n" + "\n" + "\n" + "class MyClass(object):\n" + " pass\n" + ) + # On a file that has no pre-existing imports, and no doc-string test_input = "class MyClass(object):\n pass\n" test_output = isort.code(code=test_input, add_imports=["from __future__ import print_function"])