Skip to content

Commit

Permalink
Fix bug regarding multiline docstrings
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jonafato committed Mar 22, 2021
1 parent 9665722 commit 2ccb497
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 1 deletion.
4 changes: 3 additions & 1 deletion isort/core.py
Expand Up @@ -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",
Expand Down Expand Up @@ -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:
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/test_isort.py
Expand Up @@ -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"])
Expand Down

0 comments on commit 2ccb497

Please sign in to comment.