Skip to content

Commit

Permalink
Implement import sorting (#633)
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh authored Nov 11, 2022
1 parent 887b9aa commit 3cc74c0
Show file tree
Hide file tree
Showing 47 changed files with 1,521 additions and 27 deletions.
54 changes: 44 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ An extremely fast Python linter, written in Rust.

Ruff aims to be orders of magnitude faster than alternative tools while integrating more
functionality behind a single, common interface. Ruff can be used to replace Flake8 (plus a variety
of plugins), [`pydocstyle`](https://pypi.org/project/pydocstyle/), [`yesqa`](https://github.com/asottile/yesqa),
and even a subset of [`pyupgrade`](https://pypi.org/project/pyupgrade/) and [`autoflake`](https://pypi.org/project/autoflake/)
all while executing tens or hundreds of times faster than any individual tool.
of plugins), [`isort`](https://pypi.org/project/isort/), [`pydocstyle`](https://pypi.org/project/pydocstyle/),
[`yesqa`](https://github.com/asottile/yesqa), and even a subset of [`pyupgrade`](https://pypi.org/project/pyupgrade/)
and [`autoflake`](https://pypi.org/project/autoflake/) all while executing tens or hundreds of times
faster than any individual tool.

(Coming from Flake8? Try [`flake8-to-ruff`](https://pypi.org/project/flake8-to-ruff/) to
automatically convert your existing configuration.)
Expand Down Expand Up @@ -285,16 +286,16 @@ Ruff supports several workflows to aid in `noqa` management.
First, Ruff provides a special error code, `M001`, to enforce that your `noqa` directives are
"valid", in that the errors they _say_ they ignore are actually being triggered on that line (and
thus suppressed). **You can run `ruff /path/to/file.py --extend-select M001` to flag unused `noqa`
directives.**
thus suppressed). You can run `ruff /path/to/file.py --extend-select M001` to flag unused `noqa`
directives.
Second, Ruff can _automatically remove_ unused `noqa` directives via its autofix functionality.
**You can run `ruff /path/to/file.py --extend-select M001 --fix` to automatically remove unused
`noqa` directives.**
You can run `ruff /path/to/file.py --extend-select M001 --fix` to automatically remove unused
`noqa` directives.
Third, Ruff can _automatically add_ `noqa` directives to all failing lines. This is useful when
migrating a new codebase to Ruff. **You can run `ruff /path/to/file.py --add-noqa` to automatically
add `noqa` directives to all failing lines, with the appropriate error codes.**
migrating a new codebase to Ruff. You can run `ruff /path/to/file.py --add-noqa` to automatically
add `noqa` directives to all failing lines, with the appropriate error codes.
## Supported Rules
Expand Down Expand Up @@ -365,6 +366,14 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | |
### isort
For more, see [isort](https://pypi.org/project/isort/5.10.1/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| I001 | UnsortedImports | Import block is un-sorted or un-formatted | 🛠 |
### pydocstyle
For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
Expand Down Expand Up @@ -681,7 +690,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (19/32)
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa),
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
Expand All @@ -702,6 +711,31 @@ on Rust at all.
Ruff does not yet support third-party plugins, though a plugin system is within-scope for the
project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
### How does Ruff's import sorting compare to [`isort`](https://pypi.org/project/isort/)?
Ruff's import sorting is intended to be equivalent to `isort` when used `profile = "black"` and
`combine_as_imports = true`. Like `isort`, Ruff's import sorting is compatible with Black.
Ruff is less configurable than `isort`, but supports the `known-first-party`, `known-third-party`,
`extra-standard-library`, and `src` settings, like so:
```toml
[tool.ruff]
select = [
# Pyflakes
"F",
# Pycodestyle
"E",
"W",
# isort
"I"
]
src = ["src", "tests"]
[tool.ruff.isort]
known-first-party = ["my_module1", "my_module2"]
```
### Does Ruff support NumPy- or Google-style docstrings?
Yes! To enable a specific docstring convention, start by enabling all `pydocstyle` error codes, and
Expand Down
14 changes: 14 additions & 0 deletions flake8_to_ruff/src/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ mod tests {
let actual = convert(&HashMap::from([]), None)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
Expand All @@ -224,6 +225,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
Expand All @@ -239,6 +241,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: Some(100),
src: None,
fix: None,
exclude: None,
extend_exclude: None,
Expand All @@ -255,6 +258,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
Expand All @@ -270,6 +274,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: Some(100),
src: None,
fix: None,
exclude: None,
extend_exclude: None,
Expand All @@ -286,6 +291,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
Expand All @@ -301,6 +307,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
Expand All @@ -317,6 +324,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
Expand All @@ -332,6 +340,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
Expand All @@ -353,6 +362,7 @@ mod tests {
docstring_quotes: None,
avoid_escape: None,
}),
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
Expand All @@ -371,6 +381,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
Expand Down Expand Up @@ -422,6 +433,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
Expand All @@ -437,6 +449,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
Expand All @@ -459,6 +472,7 @@ mod tests {
docstring_quotes: None,
avoid_escape: None,
}),
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ build-backend = "maturin"
bindings = "bin"
sdist-include = ["Cargo.lock"]
strip = true

[tool.isort]
profile = "black"
known_third_party = ["fastapi", "pydantic", "starlette"]
5 changes: 5 additions & 0 deletions resources/test/fixtures/isort/combine_import_froms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from collections import Awaitable
from collections import AsyncIterable
from collections import Collection
from collections import ChainMap
from collections import MutableSequence, MutableMapping
4 changes: 4 additions & 0 deletions resources/test/fixtures/isort/deduplicate_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import os
import os
import os as os1
import os as os2
1 change: 1 addition & 0 deletions resources/test/fixtures/isort/fit_line_length.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from collections import Collection
2 changes: 2 additions & 0 deletions resources/test/fixtures/isort/import_from_after_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from collections import Collection
import os
6 changes: 6 additions & 0 deletions resources/test/fixtures/isort/leading_prefix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
x = 1; import sys
import os

if True:
x = 1; import sys
import os
3 changes: 3 additions & 0 deletions resources/test/fixtures/isort/no_reorder_within_section.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# OK
import os
import sys
6 changes: 6 additions & 0 deletions resources/test/fixtures/isort/preserve_indentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
if True:
import sys
import os
else:
import sys
import os
2 changes: 2 additions & 0 deletions resources/test/fixtures/isort/reorder_within_section.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import sys
import os
5 changes: 5 additions & 0 deletions resources/test/fixtures/isort/separate_first_party_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import sys
import leading_prefix
import numpy as np
import os
from leading_prefix import Class
3 changes: 3 additions & 0 deletions resources/test/fixtures/isort/separate_future_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import sys
import os
from __future__ import annotations
4 changes: 4 additions & 0 deletions resources/test/fixtures/isort/separate_third_party_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import pandas as pd
import sys
import numpy as np
import os
6 changes: 6 additions & 0 deletions resources/test/fixtures/isort/trailing_suffix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import sys
import os; x = 1

if True:
import sys
import os; x = 1
13 changes: 13 additions & 0 deletions src/check_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::ast::{helpers, operations, visitor};
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
use crate::isort::track::ImportTracker;
use crate::python::builtins::{BUILTINS, MAGIC_GLOBALS};
use crate::python::future::ALL_FEATURE_NAMES;
use crate::python::typing;
Expand Down Expand Up @@ -77,6 +78,7 @@ pub struct Checker<'a> {
deferred_functions: Vec<(&'a Stmt, Vec<usize>, Vec<usize>, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, Vec<usize>, Vec<usize>)>,
deferred_assignments: Vec<usize>,
import_tracker: ImportTracker<'a>,
// Internal, derivative state.
visible_scope: VisibleScope,
in_f_string: Option<Range>,
Expand Down Expand Up @@ -115,6 +117,8 @@ impl<'a> Checker<'a> {
deferred_functions: Default::default(),
deferred_lambdas: Default::default(),
deferred_assignments: Default::default(),
import_tracker: ImportTracker::new(),
// Internal, derivative state.
visible_scope: VisibleScope {
modifier: Modifier::Module,
visibility: module_visibility(path),
Expand Down Expand Up @@ -181,6 +185,9 @@ where
'b: 'a,
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Call-through to any composed visitors.
self.import_tracker.visit_stmt(stmt);

self.push_parent(stmt);

// Track whether we've seen docstrings, non-imports, etc.
Expand Down Expand Up @@ -1657,6 +1664,9 @@ where
}

fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) {
// Call-through to any composed visitors.
self.import_tracker.visit_excepthandler(excepthandler);

match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { type_, name, .. } => {
if self.settings.enabled.contains(&CheckCode::E722) && type_.is_none() {
Expand Down Expand Up @@ -2591,5 +2601,8 @@ pub fn check_ast(
// Check docstrings.
checker.check_definitions();

// Check import blocks.
// checker.check_import_blocks();

checker.checks
}
41 changes: 41 additions & 0 deletions src/check_imports.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Lint rules based on import analysis.
use rustpython_parser::ast::Suite;

use crate::ast::visitor::Visitor;
use crate::autofix::fixer;
use crate::checks::Check;
use crate::isort;
use crate::isort::track::ImportTracker;
use crate::settings::Settings;
use crate::source_code_locator::SourceCodeLocator;

fn check_import_blocks(
tracker: ImportTracker,
locator: &SourceCodeLocator,
settings: &Settings,
autofix: &fixer::Mode,
) -> Vec<Check> {
let mut checks = vec![];
for block in tracker.into_iter() {
if !block.is_empty() {
if let Some(check) = isort::plugins::check_imports(block, locator, settings, autofix) {
checks.push(check);
}
}
}
checks
}

pub fn check_imports(
python_ast: &Suite,
locator: &SourceCodeLocator,
settings: &Settings,
autofix: &fixer::Mode,
) -> Vec<Check> {
let mut tracker = ImportTracker::new();
for stmt in python_ast {
tracker.visit_stmt(stmt);
}
check_import_blocks(tracker, locator, settings, autofix)
}
Loading

0 comments on commit 3cc74c0

Please sign in to comment.