-
Notifications
You must be signed in to change notification settings - Fork 881
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
[pycodestyle
] Avoid blank line rules for the first logical line in cell
#10291
Changes from 5 commits
122136e
1d6a35d
89964b9
a2842a3
8d0776b
f29a53d
3f703af
05a7473
1374e23
6196e91
dd8c32e
d543e72
bce6989
559b030
a2cea4a
a0946de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
{ | ||
"cells": [ | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": { | ||
"id": "palRUQyD-U6u" | ||
}, | ||
"outputs": [], | ||
"source": [ | ||
"some_string = \"123123\"" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": { | ||
"id": "UWdDLRyf-Zz0" | ||
}, | ||
"outputs": [], | ||
"source": [ | ||
"some_computation = 1 + 1" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": { | ||
"id": "YreT1sTr-c32" | ||
}, | ||
"outputs": [], | ||
"source": [ | ||
"some_computation" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": { | ||
"id": "V48ppml7-h0f" | ||
}, | ||
"outputs": [], | ||
"source": [ | ||
"def fn():\n", | ||
" print(\"Hey!\")" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": { | ||
"id": "cscw_8Xv-lYQ" | ||
}, | ||
"outputs": [], | ||
"source": [ | ||
"fn()" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# E301\n", | ||
"class Class:\n", | ||
" \"\"\"Class for minimal repo.\"\"\"\n", | ||
"\n", | ||
" def method(cls) -> None:\n", | ||
" pass\n", | ||
" @classmethod\n", | ||
" def cls_method(cls) -> None:\n", | ||
" pass\n", | ||
"# end" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# E302\n", | ||
"def a():\n", | ||
" pass\n", | ||
"\n", | ||
"def b():\n", | ||
" pass\n", | ||
"# end" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# E303\n", | ||
"def fn():\n", | ||
" _ = None\n", | ||
"\n", | ||
"\n", | ||
" # arbitrary comment\n", | ||
"\n", | ||
" def inner(): # E306 not expected (pycodestyle detects E306)\n", | ||
" pass\n", | ||
"# end" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# E304\n", | ||
"@decorator\n", | ||
"\n", | ||
"def function():\n", | ||
" pass\n", | ||
"# end" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# E305:7:1\n", | ||
"def fn():\n", | ||
" print()\n", | ||
"\n", | ||
" # comment\n", | ||
"\n", | ||
" # another comment\n", | ||
"fn()\n", | ||
"# end" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"# E306:3:5\n", | ||
"def a():\n", | ||
" x = 1\n", | ||
" def b():\n", | ||
" pass\n", | ||
"# end" | ||
] | ||
} | ||
], | ||
"metadata": { | ||
"colab": { | ||
"provenance": [] | ||
}, | ||
"kernelspec": { | ||
"display_name": "Python 3 (ipykernel)", | ||
"language": "python", | ||
"name": "python3" | ||
}, | ||
"language_info": { | ||
"codemirror_mode": { | ||
"name": "ipython", | ||
"version": 3 | ||
}, | ||
"file_extension": ".py", | ||
"mimetype": "text/x-python", | ||
"name": "python", | ||
"nbconvert_exporter": "python", | ||
"pygments_lexer": "ipython3", | ||
"version": "3.11.7" | ||
} | ||
}, | ||
"nbformat": 4, | ||
"nbformat_minor": 4 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
use itertools::Itertools; | ||
use ruff_notebook::CellOffsets; | ||
use std::cmp::Ordering; | ||
use std::num::NonZeroU32; | ||
use std::slice::Iter; | ||
|
@@ -351,6 +352,9 @@ struct LogicalLineInfo { | |
// `true` if this is not a blank but only consists of a comment. | ||
is_comment_only: bool, | ||
|
||
/// If running on a notebook, whether the line is the first non-trivia line of its cell. | ||
is_cell_first_non_comment_line: bool, | ||
|
||
/// `true` if the line is a string only (including trivia tokens) line, which is a docstring if coming right after a class/function definition. | ||
is_docstring: bool, | ||
|
||
|
@@ -379,20 +383,27 @@ struct LinePreprocessor<'a> { | |
/// Maximum number of consecutive blank lines between the current line and the previous non-comment logical line. | ||
/// One of its main uses is to allow a comment to directly precede a class/function definition. | ||
max_preceding_blank_lines: BlankLines, | ||
/// The cell offsets of the notebook (if running on a notebook). | ||
cell_offsets: Option<&'a CellOffsets>, | ||
/// If running on a notebook, whether the line is the first non-trivia line of its cell. | ||
is_cell_first_non_comment_line: bool, | ||
} | ||
|
||
impl<'a> LinePreprocessor<'a> { | ||
fn new( | ||
tokens: &'a [LexResult], | ||
locator: &'a Locator, | ||
indent_width: IndentWidth, | ||
cell_offsets: Option<&'a CellOffsets>, | ||
) -> LinePreprocessor<'a> { | ||
LinePreprocessor { | ||
tokens: tokens.iter(), | ||
locator, | ||
line_start: TextSize::new(0), | ||
max_preceding_blank_lines: BlankLines::Zero, | ||
indent_width, | ||
is_cell_first_non_comment_line: cell_offsets.is_some(), | ||
cell_offsets, | ||
} | ||
} | ||
} | ||
|
@@ -491,6 +502,7 @@ impl<'a> Iterator for LinePreprocessor<'a> { | |
last_token, | ||
logical_line_end: range.end(), | ||
is_comment_only: line_is_comment_only, | ||
is_cell_first_non_comment_line: self.is_cell_first_non_comment_line, | ||
is_docstring, | ||
indent_length, | ||
blank_lines, | ||
|
@@ -505,6 +517,15 @@ impl<'a> Iterator for LinePreprocessor<'a> { | |
// Set the start for the next logical line. | ||
self.line_start = range.end(); | ||
|
||
if self | ||
.cell_offsets | ||
.is_some_and(|cell_offsets| cell_offsets.contains(&self.line_start)) | ||
{ | ||
self.is_cell_first_non_comment_line = true; | ||
} else if !line_is_comment_only { | ||
self.is_cell_first_non_comment_line = false; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it okay that we set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fine since if a cell starts with a comment, we don't want to run the check on that comment (since it's the first line). Edit: renamed in f29a53d. |
||
return Some(logical_line); | ||
} | ||
_ => {} | ||
|
@@ -654,6 +675,7 @@ pub(crate) struct BlankLinesChecker<'a> { | |
lines_after_imports: isize, | ||
lines_between_types: usize, | ||
source_type: PySourceType, | ||
cell_offsets: Option<&'a CellOffsets>, | ||
} | ||
|
||
impl<'a> BlankLinesChecker<'a> { | ||
|
@@ -662,6 +684,7 @@ impl<'a> BlankLinesChecker<'a> { | |
stylist: &'a Stylist<'a>, | ||
settings: &crate::settings::LinterSettings, | ||
source_type: PySourceType, | ||
cell_offsets: Option<&'a CellOffsets>, | ||
) -> BlankLinesChecker<'a> { | ||
BlankLinesChecker { | ||
stylist, | ||
|
@@ -670,14 +693,16 @@ impl<'a> BlankLinesChecker<'a> { | |
lines_after_imports: settings.isort.lines_after_imports, | ||
lines_between_types: settings.isort.lines_between_types, | ||
source_type, | ||
cell_offsets, | ||
} | ||
} | ||
|
||
/// E301, E302, E303, E304, E305, E306 | ||
pub(crate) fn check_lines(&self, tokens: &[LexResult], diagnostics: &mut Vec<Diagnostic>) { | ||
let mut prev_indent_length: Option<usize> = None; | ||
let mut state = BlankLinesState::default(); | ||
let line_preprocessor = LinePreprocessor::new(tokens, self.locator, self.indent_width); | ||
let line_preprocessor = | ||
LinePreprocessor::new(tokens, self.locator, self.indent_width, self.cell_offsets); | ||
|
||
for logical_line in line_preprocessor { | ||
// Reset `follows` after a dedent: | ||
|
@@ -696,7 +721,10 @@ impl<'a> BlankLinesChecker<'a> { | |
state.class_status.update(&logical_line); | ||
state.fn_status.update(&logical_line); | ||
|
||
if state.is_not_first_logical_line { | ||
if state.is_not_first_logical_line | ||
// Ignore the first line of each cell in notebooks. | ||
&& !logical_line.is_cell_first_non_comment_line | ||
{ | ||
self.check_line(&logical_line, &state, prev_indent_length, diagnostics); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs | ||
--- | ||
E30.ipynb:13:5: E301 [*] Expected 1 blank line, found 0 | ||
| | ||
11 | def method(cls) -> None: | ||
12 | pass | ||
13 | @classmethod | ||
| ^ E301 | ||
14 | def cls_method(cls) -> None: | ||
15 | pass | ||
| | ||
= help: Add missing blank line | ||
|
||
ℹ Safe fix | ||
10 10 | | ||
11 11 | def method(cls) -> None: | ||
12 12 | pass | ||
13 |+ | ||
13 14 | @classmethod | ||
14 15 | def cls_method(cls) -> None: | ||
15 16 | pass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs | ||
--- | ||
E30.ipynb:21:1: E302 [*] Expected 2 blank lines, found 1 | ||
| | ||
19 | pass | ||
20 | | ||
21 | def b(): | ||
| ^^^ E302 | ||
22 | pass | ||
23 | # end | ||
| | ||
= help: Add missing blank line(s) | ||
|
||
ℹ Safe fix | ||
18 18 | def a(): | ||
19 19 | pass | ||
20 20 | | ||
21 |+ | ||
21 22 | def b(): | ||
22 23 | pass | ||
23 24 | # end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
--- | ||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs | ||
--- | ||
E30.ipynb:29:5: E303 [*] Too many blank lines (2) | ||
| | ||
29 | # arbitrary comment | ||
| ^^^^^^^^^^^^^^^^^^^ E303 | ||
30 | | ||
31 | def inner(): # E306 not expected (pycodestyle detects E306) | ||
| | ||
= help: Remove extraneous blank line(s) | ||
|
||
ℹ Safe fix | ||
25 25 | def fn(): | ||
26 26 | _ = None | ||
27 27 | | ||
28 |- | ||
29 28 | # arbitrary comment | ||
30 29 | | ||
31 30 | def inner(): # E306 not expected (pycodestyle detects E306) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may want to use
containing_range
here.contains
isO(n)
,containing_range
isO(log(n))
. But I wonder if we could do better by keeping an iterator with the cell offsets (they're sorted) and peek at the first offset and:line_start
, call nextNone
, you know its inside of the cell.This gives you
O(n)
performanceThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MichaReiser It's not exactly what you said, but I gave it a try in 3f703af. What do you think ?