Skip to content

Commit cb179db

Browse files
EljeesDanielNoord
authored andcommitted
Keep add_imports below a prefixed module docstring
The guard that stops isort inserting add_imports above a module docstring tests the raw line against COMMENT_INDICATORS, so it only recognises a docstring that starts with a quote. A docstring carrying a legal string prefix (r, b, f, u or a combination) starts with a letter, the guard misses it, and the import is inserted above the docstring - and, because the file already contained that import, duplicated. Single-line prefixed docstrings escape this by accident, via the neighbouring endswith(DOCSTRING_INDICATORS) guard; multi-line ones do not. Strip a legal string prefix before the quote-based check. Closes #1893.
1 parent 7542f76 commit cb179db

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

‎isort/core.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
)
2121
DOCSTRING_INDICATORS = ('"""', "'''")
2222
COMMENT_INDICATORS = (*DOCSTRING_INDICATORS, "'", '"', "#")
23+
# Every legal Python string prefix, lower-cased; `u` cannot be combined with another.
24+
STRING_PREFIXES = frozenset(("r", "u", "f", "b", "fr", "rf", "br", "rb"))
2325
CODE_SORT_COMMENTS = (
2426
"# isort: list",
2527
"# isort: dict",
@@ -34,6 +36,17 @@
3436
SKIP_IMPORT_COMMENTS = ("isort:skip", "isort: skip")
3537

3638

39+
def _strip_string_prefix(line: str) -> str:
40+
"""Return `line` without a leading Python string prefix (`r`, `b`, `f`, `u`
41+
or a legal combination of them), so quote-based heuristics see the quote itself."""
42+
for prefix_length in (2, 1):
43+
if line[:prefix_length].lower() in STRING_PREFIXES and line[
44+
prefix_length : prefix_length + 1
45+
] in ("'", '"'):
46+
return line[prefix_length:]
47+
return line
48+
49+
3750
def _has_skip_comment(import_statement: str) -> bool:
3851
"""Return whether an import statement carries a per-line ``isort: skip`` directive."""
3952
return any(comment in import_statement for comment in SKIP_IMPORT_COMMENTS)
@@ -427,7 +440,7 @@ def process(
427440
and not in_top_comment
428441
and not was_in_quote
429442
and not import_section
430-
and not line.lstrip().startswith(COMMENT_INDICATORS)
443+
and not _strip_string_prefix(line.lstrip()).startswith(COMMENT_INDICATORS)
431444
and not (line.rstrip().endswith(DOCSTRING_INDICATORS) and "=" not in line)
432445
):
433446
add_line_separator = line_separator or "\n"

‎tests/unit/test_regressions.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2460,3 +2460,29 @@ def test_isort_does_not_drop_aliased_import_when_plain_name_has_a_comment():
24602460
expected = "from . import bar\nfrom . import one # NOQA\nfrom . import one as zzz\n"
24612461
assert relative_sorted == expected
24622462
assert isort.code(relative_sorted) == relative_sorted
2463+
2464+
2465+
def test_add_import_keeps_a_prefixed_module_docstring_first_issue_1893():
2466+
"""``add_imports`` must not move an import above a module docstring that carries a
2467+
string prefix (``r``, ``b``, ``f``, ``u`` or a legal combination of them), as reported
2468+
in issue #1893: https://github.com/pycqa/isort/issues/1893
2469+
"""
2470+
# The exact input from the report: the added import was emitted a second time, above
2471+
# the docstring.
2472+
reported = 'r"""module docstring\n"""\nfrom __future__ import annotations\n'
2473+
assert isort.code(reported, add_imports=["from __future__ import annotations"]) == reported
2474+
2475+
# The import still has to be added, below the docstring -- where an unprefixed
2476+
# docstring already puts it.
2477+
with_code = 'r"""module docstring\n"""\n\nx = 1\n'
2478+
assert (
2479+
isort.code(with_code, add_imports=["import a"])
2480+
== 'r"""module docstring\n"""\n\nimport a\n\nx = 1\n'
2481+
)
2482+
2483+
# Every legal prefix, in both quote flavours and both cases.
2484+
for prefix in ("r", "R", "b", "B", "f", "F", "u", "U", "rb", "bR", "Fr", "rf"):
2485+
for quote in ('"""', "'''"):
2486+
docstring = f"{prefix}{quote}module docstring\n{quote}\n"
2487+
source = docstring + "import a\n"
2488+
assert isort.code(source, add_imports=["import a"]) == source, prefix + quote

0 commit comments

Comments
 (0)