Skip to content

Commit af6ea2f

Browse files
authored
[pycodestyle]: Make blank lines in typing stub files optional (E3*) (#10098)
## Summary Fixes #10039 The [recommendation for typing stub files](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines) is to use **one** blank line to group related definitions and otherwise omit blank lines. The newly added blank line rules (`E3*`) didn't account for typing stub files and enforced two empty lines at the top level and one empty line otherwise, making it impossible to group related definitions. This PR implements the `E3*` rules to: * Not enforce blank lines. The use of blank lines in typing definitions is entirely up to the user. * Allow at most one empty line, including between top level statements. ## Test Plan Added unit tests (It may look odd that many snapshots are empty but the point is that the rule should no longer emit diagnostics)
1 parent 46ab9de commit af6ea2f

12 files changed

+577
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import json
2+
3+
from typing import Any, Sequence
4+
5+
class MissingCommand(TypeError): ...
6+
class AnoherClass: ...
7+
8+
def a(): ...
9+
10+
@overload
11+
def a(arg: int): ...
12+
13+
@overload
14+
def a(arg: int, name: str): ...
15+
16+
17+
def grouped1(): ...
18+
def grouped2(): ...
19+
def grouped3( ): ...
20+
21+
22+
class BackendProxy:
23+
backend_module: str
24+
backend_object: str | None
25+
backend: Any
26+
27+
def grouped1(): ...
28+
def grouped2(): ...
29+
def grouped3( ): ...
30+
@decorated
31+
32+
def with_blank_line(): ...
33+
34+
35+
def ungrouped(): ...
36+
a = "test"
37+
38+
def function_def():
39+
pass
40+
b = "test"
41+
42+
43+
def outer():
44+
def inner():
45+
pass
46+
def inner2():
47+
pass
48+
49+
class Foo: ...
50+
class Bar: ...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import json
2+
3+
4+
5+
from typing import Any, Sequence
6+
7+
8+
class MissingCommand(TypeError): ... # noqa: N818
9+
10+
11+
class BackendProxy:
12+
backend_module: str
13+
backend_object: str | None
14+
backend: Any
15+
16+
17+
if __name__ == "__main__":
18+
import abcd
19+
20+
21+
abcd.foo()
22+
23+
def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
24+
25+
if TYPE_CHECKING:
26+
import os
27+
28+
29+
30+
from typing_extensions import TypeAlias
31+
32+
33+
abcd.foo()
34+
35+
def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any:
36+
...
37+
38+
if TYPE_CHECKING:
39+
from typing_extensions import TypeAlias
40+
41+
def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any:
42+
...
43+
44+
45+
def _exit(self) -> None: ...
46+
47+
48+
def _optional_commands(self) -> dict[str, bool]: ...
49+
50+
51+
def run(argv: Sequence[str]) -> int: ...
52+
53+
54+
def read_line(fd: int = 0) -> bytearray: ...
55+
56+
57+
def flush() -> None: ...
58+
59+
60+
from typing import Any, Sequence
61+
62+
class MissingCommand(TypeError): ... # noqa: N818

crates/ruff_linter/src/checkers/tokens.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ pub(crate) fn check_tokens(
4141
Rule::BlankLinesAfterFunctionOrClass,
4242
Rule::BlankLinesBeforeNestedDefinition,
4343
]) {
44-
BlankLinesChecker::new(locator, stylist, settings).check_lines(tokens, &mut diagnostics);
44+
BlankLinesChecker::new(locator, stylist, settings, source_type)
45+
.check_lines(tokens, &mut diagnostics);
4546
}
4647

4748
if settings.rules.enabled(Rule::BlanketNOQA) {

crates/ruff_linter/src/rules/pycodestyle/mod.rs

+32
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,38 @@ mod tests {
222222
Ok(())
223223
}
224224

225+
#[test_case(Rule::BlankLineBetweenMethods)]
226+
#[test_case(Rule::BlankLinesTopLevel)]
227+
#[test_case(Rule::TooManyBlankLines)]
228+
#[test_case(Rule::BlankLineAfterDecorator)]
229+
#[test_case(Rule::BlankLinesAfterFunctionOrClass)]
230+
#[test_case(Rule::BlankLinesBeforeNestedDefinition)]
231+
fn blank_lines_typing_stub(rule_code: Rule) -> Result<()> {
232+
let snapshot = format!("blank_lines_{}_typing_stub", rule_code.noqa_code());
233+
let diagnostics = test_path(
234+
Path::new("pycodestyle").join("E30.pyi"),
235+
&settings::LinterSettings::for_rule(rule_code),
236+
)?;
237+
assert_messages!(snapshot, diagnostics);
238+
Ok(())
239+
}
240+
241+
#[test]
242+
fn blank_lines_typing_stub_isort() -> Result<()> {
243+
let diagnostics = test_path(
244+
Path::new("pycodestyle").join("E30_isort.pyi"),
245+
&settings::LinterSettings {
246+
..settings::LinterSettings::for_rules([
247+
Rule::TooManyBlankLines,
248+
Rule::BlankLinesTopLevel,
249+
Rule::UnsortedImports,
250+
])
251+
},
252+
)?;
253+
assert_messages!(diagnostics);
254+
Ok(())
255+
}
256+
225257
#[test]
226258
fn constant_literals() -> Result<()> {
227259
let diagnostics = test_path(

crates/ruff_linter/src/rules/pycodestyle/rules/blank_lines.rs

+52-11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use ruff_diagnostics::Diagnostic;
88
use ruff_diagnostics::Edit;
99
use ruff_diagnostics::Fix;
1010
use ruff_macros::{derive_message_formats, violation};
11+
use ruff_python_ast::PySourceType;
1112
use ruff_python_codegen::Stylist;
1213
use ruff_python_parser::lexer::LexResult;
1314
use ruff_python_parser::lexer::LexicalError;
@@ -51,9 +52,14 @@ const BLANK_LINES_NESTED_LEVEL: u32 = 1;
5152
/// pass
5253
/// ```
5354
///
55+
/// ## Typing stub files (`.pyi`)
56+
/// The typing style guide recommends to not use blank lines between methods except to group
57+
/// them. That's why this rule is not enabled in typing stub files.
58+
///
5459
/// ## References
5560
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
5661
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html)
62+
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
5763
#[violation]
5864
pub struct BlankLineBetweenMethods;
5965

@@ -96,9 +102,14 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods {
96102
/// pass
97103
/// ```
98104
///
105+
/// ## Typing stub files (`.pyi`)
106+
/// The typing style guide recommends to not use blank lines between classes and functions except to group
107+
/// them. That's why this rule is not enabled in typing stub files.
108+
///
99109
/// ## References
100110
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
101111
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html)
112+
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
102113
#[violation]
103114
pub struct BlankLinesTopLevel {
104115
actual_blank_lines: u32,
@@ -150,13 +161,17 @@ impl AlwaysFixableViolation for BlankLinesTopLevel {
150161
/// pass
151162
/// ```
152163
///
164+
/// ## Typing stub files (`.pyi`)
165+
/// The rule allows at most one blank line in typing stub files in accordance to the typing style guide recommendation.
166+
///
153167
/// Note: The rule respects the following `isort` settings when determining the maximum number of blank lines allowed between two statements:
154168
/// * [`lint.isort.lines-after-imports`]: For top-level statements directly following an import statement.
155169
/// * [`lint.isort.lines-between-types`]: For `import` statements directly following a `from ... import ...` statement or vice versa.
156170
///
157171
/// ## References
158172
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
159173
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html)
174+
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
160175
#[violation]
161176
pub struct TooManyBlankLines {
162177
actual_blank_lines: u32,
@@ -246,9 +261,14 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator {
246261
/// user = User()
247262
/// ```
248263
///
264+
/// ## Typing stub files (`.pyi`)
265+
/// The typing style guide recommends to not use blank lines between statements except to group
266+
/// them. That's why this rule is not enabled in typing stub files.
267+
///
249268
/// ## References
250269
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
251270
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html)
271+
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
252272
#[violation]
253273
pub struct BlankLinesAfterFunctionOrClass {
254274
actual_blank_lines: u32,
@@ -295,9 +315,14 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass {
295315
/// pass
296316
/// ```
297317
///
318+
/// ## Typing stub files (`.pyi`)
319+
/// The typing style guide recommends to not use blank lines between classes and functions except to group
320+
/// them. That's why this rule is not enabled in typing stub files.
321+
///
298322
/// ## References
299323
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
300324
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html)
325+
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
301326
#[violation]
302327
pub struct BlankLinesBeforeNestedDefinition;
303328

@@ -628,20 +653,23 @@ pub(crate) struct BlankLinesChecker<'a> {
628653
indent_width: IndentWidth,
629654
lines_after_imports: isize,
630655
lines_between_types: usize,
656+
source_type: PySourceType,
631657
}
632658

633659
impl<'a> BlankLinesChecker<'a> {
634660
pub(crate) fn new(
635661
locator: &'a Locator<'a>,
636662
stylist: &'a Stylist<'a>,
637663
settings: &crate::settings::LinterSettings,
664+
source_type: PySourceType,
638665
) -> BlankLinesChecker<'a> {
639666
BlankLinesChecker {
640667
stylist,
641668
locator,
642669
indent_width: settings.tab_size,
643670
lines_after_imports: settings.isort.lines_after_imports,
644671
lines_between_types: settings.isort.lines_between_types,
672+
source_type,
645673
}
646674
}
647675

@@ -739,6 +767,8 @@ impl<'a> BlankLinesChecker<'a> {
739767
&& !matches!(state.follows, Follows::Docstring | Follows::Decorator)
740768
// Do not trigger when the def follows an if/while/etc...
741769
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
770+
// Blank lines in stub files are only used for grouping. Don't enforce blank lines.
771+
&& !self.source_type.is_stub()
742772
{
743773
// E301
744774
let mut diagnostic = Diagnostic::new(BlankLineBetweenMethods, line.first_token_range);
@@ -750,20 +780,31 @@ impl<'a> BlankLinesChecker<'a> {
750780
diagnostics.push(diagnostic);
751781
}
752782

783+
// Blank lines in stub files are used to group definitions. Don't enforce blank lines.
784+
let max_lines_level = if self.source_type.is_stub() {
785+
1
786+
} else {
787+
if line.indent_length == 0 {
788+
BLANK_LINES_TOP_LEVEL
789+
} else {
790+
BLANK_LINES_NESTED_LEVEL
791+
}
792+
};
793+
753794
let expected_blank_lines_before_definition = if line.indent_length == 0 {
754795
// Mimic the isort rules for the number of blank lines before classes and functions
755796
if state.follows.is_any_import() {
756797
// Fallback to the default if the value is too large for an u32 or if it is negative.
757798
// A negative value means that isort should determine the blank lines automatically.
758-
// `isort` defaults to 2 if before a class or function definition and 1 otherwise.
759-
// Defaulting to 2 here is correct because the variable is only used when testing the
799+
// `isort` defaults to 2 if before a class or function definition (except in stubs where it is one) and 1 otherwise.
800+
// Defaulting to 2 (or 1 in stubs) here is correct because the variable is only used when testing the
760801
// blank lines before a class or function definition.
761-
u32::try_from(self.lines_after_imports).unwrap_or(BLANK_LINES_TOP_LEVEL)
802+
u32::try_from(self.lines_after_imports).unwrap_or(max_lines_level)
762803
} else {
763-
BLANK_LINES_TOP_LEVEL
804+
max_lines_level
764805
}
765806
} else {
766-
BLANK_LINES_NESTED_LEVEL
807+
max_lines_level
767808
};
768809

769810
if line.preceding_blank_lines < expected_blank_lines_before_definition
@@ -775,6 +816,8 @@ impl<'a> BlankLinesChecker<'a> {
775816
&& line.indent_length == 0
776817
// Only apply to functions or classes.
777818
&& line.kind.is_class_function_or_decorator()
819+
// Blank lines in stub files are used to group definitions. Don't enforce blank lines.
820+
&& !self.source_type.is_stub()
778821
{
779822
// E302
780823
let mut diagnostic = Diagnostic::new(
@@ -804,12 +847,6 @@ impl<'a> BlankLinesChecker<'a> {
804847
diagnostics.push(diagnostic);
805848
}
806849

807-
let max_lines_level = if line.indent_length == 0 {
808-
BLANK_LINES_TOP_LEVEL
809-
} else {
810-
BLANK_LINES_NESTED_LEVEL
811-
};
812-
813850
// If between `import` and `from .. import ..` or the other way round,
814851
// allow up to `lines_between_types` newlines for isort compatibility.
815852
// We let `isort` remove extra blank lines when the imports belong
@@ -893,6 +930,8 @@ impl<'a> BlankLinesChecker<'a> {
893930
&& line.indent_length == 0
894931
&& !line.is_comment_only
895932
&& !line.kind.is_class_function_or_decorator()
933+
// Blank lines in stub files are used for grouping, don't enforce blank lines.
934+
&& !self.source_type.is_stub()
896935
{
897936
// E305
898937
let mut diagnostic = Diagnostic::new(
@@ -933,6 +972,8 @@ impl<'a> BlankLinesChecker<'a> {
933972
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
934973
// Allow groups of one-liners.
935974
&& !(matches!(state.follows, Follows::Def) && line.last_token != TokenKind::Colon)
975+
// Blank lines in stub files are only used for grouping. Don't enforce blank lines.
976+
&& !self.source_type.is_stub()
936977
{
937978
// E306
938979
let mut diagnostic =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
3+
---
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
3+
---
4+

0 commit comments

Comments
 (0)