Skip to content

Commit

Permalink
Allow sys.path modifications between imports
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Dec 7, 2023
1 parent fcc0889 commit 1eb6fec
Show file tree
Hide file tree
Showing 8 changed files with 107 additions and 19 deletions.
17 changes: 11 additions & 6 deletions crates/ruff_linter/resources/test/fixtures/pycodestyle/E402.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,26 @@
else:
import e

__some__magic = 1
import sys
sys.path.insert(0, "some/path")

import f

__some__magic = 1

import g


def foo() -> None:
import e
import h


if __name__ == "__main__":
import g
import i

import h; import i
import j; import k


if __name__ == "__main__":
import j; \
import k
import l; \
import m
4 changes: 3 additions & 1 deletion crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Quote, Stylist};
use ruff_python_index::Indexer;
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_semantic::analyze::{typing, visibility};
use ruff_python_semantic::analyze::{imports, typing, visibility};
use ruff_python_semantic::{
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, Snapshot,
Expand Down Expand Up @@ -306,6 +306,8 @@ where
if !self.semantic.seen_import_boundary()
&& !helpers::is_assignment_to_a_dunder(stmt)
&& !helpers::in_nested_block(self.semantic.current_statements())
&& !(self.settings.preview.is_enabled()
&& imports::is_sys_path_modification(stmt, self.semantic()))
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}
Expand Down
1 change: 1 addition & 0 deletions crates/ruff_linter/src/rules/pycodestyle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ mod tests {
}

#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.py"))]
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff_linter/src/rules/pycodestyle/rules/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ impl Violation for MultipleImportsOnOneLine {
/// According to [PEP 8], "imports are always put at the top of the file, just after any
/// module comments and docstrings, and before module globals and constants."
///
/// In [preview], this rule makes an exception for `sys.path` modifications,
/// allowing for `sys.path.insert`, `sys.path.append`, and similar
/// modifications between import statements.
///
/// ## Example
/// ```python
/// "One string"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402.py:24:1: E402 Module level import not at top of file
E402.py:25:1: E402 Module level import not at top of file
|
22 | __some__magic = 1
23 |
24 | import f
23 | sys.path.insert(0, "some/path")
24 |
25 | import f
| ^^^^^^^^ E402
26 |
27 | __some__magic = 1
|

E402.py:34:1: E402 Module level import not at top of file
E402.py:29:1: E402 Module level import not at top of file
|
32 | import g
33 |
34 | import h; import i
27 | __some__magic = 1
28 |
29 | import g
| ^^^^^^^^ E402
|

E402.py:34:11: E402 Module level import not at top of file
E402.py:39:1: E402 Module level import not at top of file
|
32 | import g
33 |
34 | import h; import i
37 | import i
38 |
39 | import j; import k
| ^^^^^^^^ E402
|

E402.py:39:11: E402 Module level import not at top of file
|
37 | import i
38 |
39 | import j; import k
| ^^^^^^^^ E402
|

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402.py:29:1: E402 Module level import not at top of file
|
27 | __some__magic = 1
28 |
29 | import g
| ^^^^^^^^ E402
|

E402.py:39:1: E402 Module level import not at top of file
|
37 | import i
38 |
39 | import j; import k
| ^^^^^^^^ E402
|

E402.py:39:11: E402 Module level import not at top of file
|
37 | import i
38 |
39 | import j; import k
| ^^^^^^^^ E402
|


37 changes: 37 additions & 0 deletions crates/ruff_python_semantic/src/analyze/imports.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use ruff_python_ast::{self as ast, Expr, Stmt};

use crate::SemanticModel;

/// Returns `true` if a [`Stmt`] is a `sys.path` modification, as in:
/// ```python
/// import sys
///
/// sys.path.append("../")
/// ```
pub fn is_sys_path_modification(stmt: &Stmt, semantic: &SemanticModel) -> bool {
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
return false;
};
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
return false;
};
semantic
.resolve_call_path(func.as_ref())
.is_some_and(|call_path| {
matches!(
call_path.as_slice(),
[
"sys",
"path",
"append"
| "insert"
| "extend"
| "remove"
| "pop"
| "clear"
| "reverse"
| "sort"
]
)
})
}
1 change: 1 addition & 0 deletions crates/ruff_python_semantic/src/analyze/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod function_type;
pub mod imports;
pub mod logging;
pub mod type_inference;
pub mod typing;
Expand Down

0 comments on commit 1eb6fec

Please sign in to comment.