diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_comment.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_comment.py.snap index 9a2d25a230736..d06b9d2ecf9b7 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_comment.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_comment.py.snap @@ -5,6 +5,6 @@ I002 [*] Missing required import: `from __future__ import annotations` --> comment.py:1:1 help: Insert required import: `from __future__ import annotations` 1 | #!/usr/bin/env python3 -2 + from __future__ import annotations -3 | +2 | +3 + from __future__ import annotations 4 | x = 1 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_comments_and_newlines.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_comments_and_newlines.py.snap index d1911923c2e2d..eade912ca1724 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_comments_and_newlines.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_comments_and_newlines.py.snap @@ -4,9 +4,8 @@ source: crates/ruff_linter/src/rules/isort/mod.rs I002 [*] Missing required import: `from __future__ import annotations` --> comments_and_newlines.py:1:1 help: Insert required import: `from __future__ import annotations` -2 | # A copyright notice could go here 3 | 4 | # A linter directive could go here -5 + from __future__ import annotations -6 | +5 | +6 + from __future__ import annotations 7 | x = 1 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_off.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_off.py.snap index ea3ab8c897399..ac98c2981c09a 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_off.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_off.py.snap @@ -5,7 +5,7 @@ I002 [*] Missing required import: `from __future__ import annotations` --> off.py:1:1 help: Insert required import: `from __future__ import annotations` 1 | # isort: off -2 + from __future__ import annotations -3 | +2 | +3 + from __future__ import annotations 4 | x = 1 5 | # isort: on diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_comment.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_comment.py.snap index dcfb94ea27f47..c4d7139308bdc 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_comment.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_comment.py.snap @@ -5,6 +5,6 @@ I002 [*] Missing required import: `from __future__ import annotations as _annota --> comment.py:1:1 help: Insert required import: `from __future__ import annotations as _annotations` 1 | #!/usr/bin/env python3 -2 + from __future__ import annotations as _annotations -3 | +2 | +3 + from __future__ import annotations as _annotations 4 | x = 1 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_comments_and_newlines.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_comments_and_newlines.py.snap index 0253eee38e4a5..a2655714d5620 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_comments_and_newlines.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_comments_and_newlines.py.snap @@ -4,9 +4,8 @@ source: crates/ruff_linter/src/rules/isort/mod.rs I002 [*] Missing required import: `from __future__ import annotations as _annotations` --> comments_and_newlines.py:1:1 help: Insert required import: `from __future__ import annotations as _annotations` -2 | # A copyright notice could go here 3 | 4 | # A linter directive could go here -5 + from __future__ import annotations as _annotations -6 | +5 | +6 + from __future__ import annotations as _annotations 7 | x = 1 diff --git a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_off.py.snap b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_off.py.snap index 886d740520bb1..f1e73e33b4e41 100644 --- a/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_off.py.snap +++ b/crates/ruff_linter/src/rules/isort/snapshots/ruff_linter__rules__isort__tests__required_import_with_alias_off.py.snap @@ -5,7 +5,7 @@ I002 [*] Missing required import: `from __future__ import annotations as _annota --> off.py:1:1 help: Insert required import: `from __future__ import annotations as _annotations` 1 | # isort: off -2 + from __future__ import annotations as _annotations -3 | +2 | +3 + from __future__ import annotations as _annotations 4 | x = 1 5 | # isort: on diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_2.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_2.py.snap index 00d2098e90620..db27c4ab5d36a 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_2.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101_2.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/refurb/mod.rs -assertion_line: 65 --- FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf-8")` --> FURB101_2.py:2:6 diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB156_FURB156.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB156_FURB156.py.snap index dee2d3685acbf..28da8cde0d2d7 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB156_FURB156.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB156_FURB156.py.snap @@ -13,9 +13,9 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.digits` 1 | # Errors -2 + import string -3 | +2 | - _ = "0123456789" +3 + import string 4 + _ = string.digits 5 | _ = "01234567" 6 | _ = "0123456789abcdefABCDEF" @@ -32,8 +32,8 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.octdigits` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" - _ = "01234567" 5 + _ = string.octdigits @@ -53,8 +53,8 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.hexdigits` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" - _ = "0123456789abcdefABCDEF" @@ -75,8 +75,8 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.ascii_lowercase` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" 6 | _ = "0123456789abcdefABCDEF" @@ -98,8 +98,8 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.ascii_uppercase` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" 6 | _ = "0123456789abcdefABCDEF" @@ -122,8 +122,8 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.ascii_letters` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" 6 | _ = "0123456789abcdefABCDEF" @@ -146,11 +146,11 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.punctuation` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" --------------------------------------------------------------------------------- +6 | _ = "0123456789abcdefABCDEF" 7 | _ = "abcdefghijklmnopqrstuvwxyz" 8 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 9 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -172,10 +172,11 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.whitespace` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" +6 | _ = "0123456789abcdefABCDEF" -------------------------------------------------------------------------------- 8 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" 9 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -198,10 +199,11 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.printable` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" +6 | _ = "0123456789abcdefABCDEF" -------------------------------------------------------------------------------- 10 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" 11 | _ = " \t\n\r\v\f" @@ -225,10 +227,11 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.printable` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" +6 | _ = "0123456789abcdefABCDEF" -------------------------------------------------------------------------------- 12 | 13 | _ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c' @@ -255,10 +258,11 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.digits` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" +6 | _ = "0123456789abcdefABCDEF" -------------------------------------------------------------------------------- 15 | '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&' 16 | "'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c" @@ -281,10 +285,11 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.digits` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" +6 | _ = "0123456789abcdefABCDEF" -------------------------------------------------------------------------------- 20 | "89") 21 | @@ -306,10 +311,11 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.digits` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" +6 | _ = "0123456789abcdefABCDEF" -------------------------------------------------------------------------------- 24 | ).capitalize() 25 | @@ -331,10 +337,11 @@ FURB156 [*] Use of hardcoded string charset | help: Replace hardcoded charset with `string.digits` 1 | # Errors -2 + import string -3 | +2 | +3 + import string 4 | _ = "0123456789" 5 | _ = "01234567" +6 | _ = "0123456789abcdefABCDEF" -------------------------------------------------------------------------------- 29 | ).capitalize() 30 | diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap index 63e5b089eb89f..c0507ebdf61bb 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__PY39_RUF013_RUF013_1.py.snap @@ -10,10 +10,10 @@ RUF013 [*] PEP 484 prohibits implicit `Optional` | help: Convert to `Optional[T]` 1 | # No `typing.Optional` import -2 + from typing import Optional +2 | 3 | -4 | - def f(arg: int = None): # RUF013 +4 + from typing import Optional 5 + def f(arg: Optional[int] = None): # RUF013 6 | pass note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_1.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_1.py.snap index dfcdf2c124ab9..3b2e6595b5412 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_1.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_1.py.snap @@ -10,10 +10,10 @@ RUF013 [*] PEP 484 prohibits implicit `Optional` | help: Convert to `T | None` 1 | # No `typing.Optional` import -2 + from __future__ import annotations +2 | 3 | -4 | - def f(arg: int = None): # RUF013 +4 + from __future__ import annotations 5 + def f(arg: int | None = None): # RUF013 6 | pass note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap index d1a4078a12874..053d7f6d944bd 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__add_future_import_RUF013_4.py.snap @@ -9,10 +9,11 @@ RUF013 [*] PEP 484 prohibits implicit `Optional` | help: Convert to `T | None` 1 | # https://github.com/astral-sh/ruff/issues/13833 -2 + from __future__ import annotations -3 | +2 | +3 + from __future__ import annotations 4 | from typing import Optional 5 | +6 | -------------------------------------------------------------------------------- 13 | def multiple_1(arg1: Optional, arg2: Optional = None): ... 14 | diff --git a/crates/ruff_python_importer/src/insertion.rs b/crates/ruff_python_importer/src/insertion.rs index 15b0f8d67429f..0f695b392ee97 100644 --- a/crates/ruff_python_importer/src/insertion.rs +++ b/crates/ruff_python_importer/src/insertion.rs @@ -93,16 +93,24 @@ impl<'a> Insertion<'a> { contents.bom_start_offset() }; - // Skip over commented lines, with whitespace separation. + // Skip over commented lines, with whitespace separation. Track blank + // lines after comments so we can preserve them between comments and + // the first statement. + let mut seen_comment = false; for line in UniversalNewlineIterator::with_offset(&contents[location.to_usize()..], location) { let trimmed_line = line.trim_whitespace_start(); if trimmed_line.is_empty() { + if seen_comment { + location = line.full_end(); + } continue; } + if trimmed_line.starts_with('#') { location = line.full_end(); + seen_comment = true; } else { break; } @@ -525,6 +533,35 @@ x = 1 Insertion::inline(" ", TextSize::from(20), ";") ); + // Script metadata comments followed by a blank line and imports. + // The blank line between the comments and the import should be preserved. + let contents = r" +# /// script +# dependencies = ['anyio'] +# /// + +import datetime as dt +" + .trim_start(); + assert_eq!( + insert(contents)?, + Insertion::own_line("", TextSize::from(47), "\n") + ); + + // Comments without a blank line before imports should insert right + // after the comments (no blank line to preserve). + let contents = r" +# /// script +# dependencies = ['anyio'] +# /// +import datetime as dt +" + .trim_start(); + assert_eq!( + insert(contents)?, + Insertion::own_line("", TextSize::from(46), "\n") + ); + Ok(()) }