Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement import sorting #633

Merged
merged 10 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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