Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ jiff = { version = "0.2.0" }
jod-thread = { version = "1.0.0" }
js-sys = { version = "0.3.69" }
libc = { version = "0.2.153" }
libcst = { version = "1.8.4", default-features = false }
libcst = { version = "1.9.0", default-features = false }
log = { version = "0.4.17" }
lsp-server = { version = "0.10.0" }
lsp-types = { package = "gen-lsp-types", version = "0.11.0", features = ["url"] }
Expand Down
123 changes: 123 additions & 0 deletions crates/ruff_linter/resources/mdtest/fixes/imports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Import fixes

The shared import-editing helpers preserve explicit `lazy` syntax when extending, moving, or replacing
imports.

```toml
target-version = "py315"

[lint]
preview = true
select = ["UP017", "TC004", "AIR321"]
```

## Extending an import

`UP017` adds `UTC` to the existing lazy import.

```py
lazy from datetime import timezone

timezone.utc # snapshot: datetime-timezone-utc
```

```snapshot
error[UP017]: Use `datetime.UTC` alias
--> src/mdtest_snippet.py:3:1
|
3 | timezone.utc # snapshot: datetime-timezone-utc
| ^^^^^^^^^^^^
help: Convert to `datetime.UTC` alias
|
- lazy from datetime import timezone
1 + lazy from datetime import timezone, UTC
2 |
- timezone.utc # snapshot: datetime-timezone-utc
3 + UTC # snapshot: datetime-timezone-utc
|
```

## Moving part of an import

`TC004` preserves `lazy` on both the imports moved to runtime and those remaining in the type-checking
block.

```py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
lazy import os, sys # snapshot: runtime-import-in-type-checking-block
lazy from pathlib import Path, PurePath # snapshot: runtime-import-in-type-checking-block

print(os, Path)
```

```snapshot
error[TC004]: Move import `os` out of type-checking block. Import is used for more than type hinting.
--> src/mdtest_snippet.py:4:17
|
4 | lazy import os, sys # snapshot: runtime-import-in-type-checking-block
| ^^
5 | lazy from pathlib import Path, PurePath # snapshot: runtime-import-in-type-checking-block
6 |
7 | print(os, Path)
| -- Used at runtime here
help: Move out of type-checking block
|
1 | from typing import TYPE_CHECKING
2 + lazy import os
3 |
4 | if TYPE_CHECKING:
- lazy import os, sys # snapshot: runtime-import-in-type-checking-block
5 + lazy import sys # snapshot: runtime-import-in-type-checking-block
6 | lazy from pathlib import Path, PurePath # snapshot: runtime-import-in-type-checking-block
|
note: This is an unsafe fix and may change runtime behavior


error[TC004]: Move import `pathlib.Path` out of type-checking block. Import is used for more than type hinting.
--> src/mdtest_snippet.py:5:30
|
5 | lazy from pathlib import Path, PurePath # snapshot: runtime-import-in-type-checking-block
| ^^^^
6 |
7 | print(os, Path)
| ---- Used at runtime here
help: Move out of type-checking block
|
1 | from typing import TYPE_CHECKING
2 + lazy from pathlib import Path
3 |
4 | if TYPE_CHECKING:
5 | lazy import os, sys # snapshot: runtime-import-in-type-checking-block
- lazy from pathlib import Path, PurePath # snapshot: runtime-import-in-type-checking-block
6 + lazy from pathlib import PurePath # snapshot: runtime-import-in-type-checking-block
7 |
|
note: This is an unsafe fix and may change runtime behavior
```

## Replacing an import

`AIR321` preserves `lazy` when generating an import from the replacement module.

```py
lazy from airflow.utils.timezone import convert_to_utc

convert_to_utc # snapshot: airflow31-moved
```

```snapshot
error[AIR321]: `airflow.utils.timezone.convert_to_utc` is moved in Airflow 3.1
--> src/mdtest_snippet.py:3:1
|
3 | convert_to_utc # snapshot: airflow31-moved
| ^^^^^^^^^^^^^^
help: `convert_to_utc` has been moved to `airflow.sdk.timezone` since Airflow 3.1 (with apache-airflow-task-sdk>=1.1.0).
|
- lazy from airflow.utils.timezone import convert_to_utc
1 + lazy from airflow.sdk.timezone import convert_to_utc
2 |
|
note: This is an unsafe fix and may change runtime behavior
```
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,37 @@ help: Import from `unittest.mock` instead

## Explicit lazy imports

An explicit lazy import is diagnosed, but the fixer does not support the `lazy` keyword, so no fix
is offered.
The replacement imports retain `lazy`, including when a `from` import is split.

```py
lazy from mock import patch # snapshot: deprecated-mock-import
lazy from mock import mock, patch # snapshot: deprecated-mock-import
lazy import mock # snapshot: deprecated-mock-import
```

```snapshot
error[UP026]: `mock` is deprecated, use `unittest.mock`
--> src/mdtest_snippet.py:1:1
|
1 | lazy from mock import patch # snapshot: deprecated-mock-import
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
1 | lazy from mock import mock, patch # snapshot: deprecated-mock-import
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
help: Import from `unittest.mock` instead
|
- lazy from mock import mock, patch # snapshot: deprecated-mock-import
1 + lazy from unittest.mock import patch
2 + lazy from unittest import mock # snapshot: deprecated-mock-import
3 | lazy import mock # snapshot: deprecated-mock-import
|


error[UP026]: `mock` is deprecated, use `unittest.mock`
--> src/mdtest_snippet.py:2:13
|
2 | lazy import mock # snapshot: deprecated-mock-import
| ^^^^
help: Import from `unittest.mock` instead
|
1 | lazy from mock import mock, patch # snapshot: deprecated-mock-import
- lazy import mock # snapshot: deprecated-mock-import
2 + lazy from unittest import mock # snapshot: deprecated-mock-import
|
```
48 changes: 28 additions & 20 deletions crates/ruff_linter/src/cst/matchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use anyhow::{Result, bail};
use libcst_native::{
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString,
FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import,
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, SmallStatement,
Statement, Suite, Tuple, With,
ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, LazyImport, LazyImportFrom,
ListComp, Module, NameOrAttribute, SmallStatement, Statement, Suite, Tuple, With,
};
use ruff_python_codegen::Stylist;

Expand All @@ -22,36 +22,44 @@ pub(crate) fn match_statement(statement_text: &str) -> Result<Statement<'_>> {
}
}

pub(crate) fn match_import<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a mut Import<'b>> {
if let Statement::Simple(expr) = statement {
if let Some(SmallStatement::Import(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::Import")
}
} else {
pub(crate) fn match_import<'a, 'b>(
statement: &'a mut Statement<'b>,
) -> Result<(&'a mut Vec<ImportAlias<'b>>, bool)> {
let Statement::Simple(statement) = statement else {
bail!("Expected Statement::Simple")
};
match statement.body.first_mut() {
Some(SmallStatement::Import(Import { names, .. })) => Ok((names, false)),
Some(SmallStatement::LazyImport(LazyImport { names, .. })) => Ok((names, true)),
_ => bail!("Expected SmallStatement::Import | SmallStatement::LazyImport"),
}
}

pub(crate) fn match_import_from<'a, 'b>(
statement: &'a mut Statement<'b>,
) -> Result<&'a mut ImportFrom<'b>> {
if let Statement::Simple(expr) = statement {
if let Some(SmallStatement::ImportFrom(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::ImportFrom")
}
} else {
) -> Result<(
&'a mut Option<NameOrAttribute<'b>>,
&'a mut ImportNames<'b>,
bool,
)> {
let Statement::Simple(statement) = statement else {
bail!("Expected Statement::Simple")
};
match statement.body.first_mut() {
Some(SmallStatement::ImportFrom(ImportFrom { module, names, .. })) => {
Ok((module, names, false))
}
Some(SmallStatement::LazyImportFrom(LazyImportFrom { module, names, .. })) => {
Ok((module, names, true))
}
_ => bail!("Expected SmallStatement::ImportFrom | SmallStatement::LazyImportFrom"),
}
}

pub(crate) fn match_aliases<'a, 'b>(
import_from: &'a mut ImportFrom<'b>,
names: &'a mut ImportNames<'b>,
) -> Result<&'a mut Vec<ImportAlias<'b>>> {
if let ImportNames::Aliases(aliases) = &mut import_from.names {
if let ImportNames::Aliases(aliases) = names {
Ok(aliases)
} else {
bail!("Expected ImportNames::Aliases")
Expand Down
38 changes: 25 additions & 13 deletions crates/ruff_linter/src/fix/codemods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use std::borrow::Cow;

use anyhow::{Result, bail};
use libcst_native::{
Codegen, CodegenState, Expression, ImportNames, NameOrAttribute, ParenthesizableWhitespace,
SmallStatement, Statement,
Codegen, CodegenState, Expression, Import, ImportFrom, ImportNames, LazyImport, LazyImportFrom,
NameOrAttribute, ParenthesizableWhitespace, SmallStatement, Statement,
};
use rustc_hash::{FxHashMap, FxHashSet};
use smallvec::{SmallVec, smallvec};
Expand Down Expand Up @@ -53,11 +53,17 @@ pub(crate) fn remove_imports<'a>(
};

let aliases = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => &mut import_body.names,
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
names
} else if let ImportNames::Star(..) = &import_body.names {
Some(
SmallStatement::Import(Import { names, .. })
| SmallStatement::LazyImport(LazyImport { names, .. }),
) => names,
Some(
SmallStatement::ImportFrom(ImportFrom { names, .. })
| SmallStatement::LazyImportFrom(LazyImportFrom { names, .. }),
) => {
if let ImportNames::Aliases(aliases) = names {
aliases
} else if let ImportNames::Star(..) = names {
// Special-case: if the import is a `from ... import *`, then we delete the
// entire statement.
let mut found_star = false;
Expand All @@ -76,7 +82,7 @@ pub(crate) fn remove_imports<'a>(
bail!("Expected: ImportNames::Aliases | ImportNames::Star");
}
}
_ => bail!("Expected: SmallStatement::ImportFrom | SmallStatement::Import"),
_ => bail!("Expected import statement"),
};

// Preserve the trailing comma (or not) from the last entry.
Expand Down Expand Up @@ -139,15 +145,21 @@ pub(crate) fn retain_imports(
};

let aliases = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => &mut import_body.names,
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
names
Some(
SmallStatement::Import(Import { names, .. })
| SmallStatement::LazyImport(LazyImport { names, .. }),
) => names,
Some(
SmallStatement::ImportFrom(ImportFrom { names, .. })
| SmallStatement::LazyImportFrom(LazyImportFrom { names, .. }),
) => {
if let ImportNames::Aliases(aliases) = names {
aliases
} else {
bail!("Expected: ImportNames::Aliases");
}
}
_ => bail!("Expected: SmallStatement::ImportFrom | SmallStatement::Import"),
_ => bail!("Expected import statement"),
};

// Preserve the trailing comma (or not) from the last entry.
Expand Down
Loading
Loading