[pycodestyle]: Make blank lines in typing stub files optional (E3*) (#10098)

## Summary

Fixes https://github.com/astral-sh/ruff/issues/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)
This commit is contained in:
Micha Reiser 2024-03-05 12:48:50 +01:00 committed by GitHub
parent 46ab9dec18
commit af6ea2f5e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 577 additions and 12 deletions

View file

@ -0,0 +1,50 @@
import json
from typing import Any, Sequence
class MissingCommand(TypeError): ...
class AnoherClass: ...
def a(): ...
@overload
def a(arg: int): ...
@overload
def a(arg: int, name: str): ...
def grouped1(): ...
def grouped2(): ...
def grouped3( ): ...
class BackendProxy:
backend_module: str
backend_object: str | None
backend: Any
def grouped1(): ...
def grouped2(): ...
def grouped3( ): ...
@decorated
def with_blank_line(): ...
def ungrouped(): ...
a = "test"
def function_def():
pass
b = "test"
def outer():
def inner():
pass
def inner2():
pass
class Foo: ...
class Bar: ...

View file

@ -0,0 +1,62 @@
import json
from typing import Any, Sequence
class MissingCommand(TypeError): ... # noqa: N818
class BackendProxy:
backend_module: str
backend_object: str | None
backend: Any
if __name__ == "__main__":
import abcd
abcd.foo()
def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
if TYPE_CHECKING:
import os
from typing_extensions import TypeAlias
abcd.foo()
def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any:
...
if TYPE_CHECKING:
from typing_extensions import TypeAlias
def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any:
...
def _exit(self) -> None: ...
def _optional_commands(self) -> dict[str, bool]: ...
def run(argv: Sequence[str]) -> int: ...
def read_line(fd: int = 0) -> bytearray: ...
def flush() -> None: ...
from typing import Any, Sequence
class MissingCommand(TypeError): ... # noqa: N818

View file

@ -41,7 +41,8 @@ pub(crate) fn check_tokens(
Rule::BlankLinesAfterFunctionOrClass,
Rule::BlankLinesBeforeNestedDefinition,
]) {
BlankLinesChecker::new(locator, stylist, settings).check_lines(tokens, &mut diagnostics);
BlankLinesChecker::new(locator, stylist, settings, source_type)
.check_lines(tokens, &mut diagnostics);
}
if settings.rules.enabled(Rule::BlanketNOQA) {

View file

@ -222,6 +222,38 @@ mod tests {
Ok(())
}
#[test_case(Rule::BlankLineBetweenMethods)]
#[test_case(Rule::BlankLinesTopLevel)]
#[test_case(Rule::TooManyBlankLines)]
#[test_case(Rule::BlankLineAfterDecorator)]
#[test_case(Rule::BlankLinesAfterFunctionOrClass)]
#[test_case(Rule::BlankLinesBeforeNestedDefinition)]
fn blank_lines_typing_stub(rule_code: Rule) -> Result<()> {
let snapshot = format!("blank_lines_{}_typing_stub", rule_code.noqa_code());
let diagnostics = test_path(
Path::new("pycodestyle").join("E30.pyi"),
&settings::LinterSettings::for_rule(rule_code),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn blank_lines_typing_stub_isort() -> Result<()> {
let diagnostics = test_path(
Path::new("pycodestyle").join("E30_isort.pyi"),
&settings::LinterSettings {
..settings::LinterSettings::for_rules([
Rule::TooManyBlankLines,
Rule::BlankLinesTopLevel,
Rule::UnsortedImports,
])
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn constant_literals() -> Result<()> {
let diagnostics = test_path(

View file

@ -8,6 +8,7 @@ use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Edit;
use ruff_diagnostics::Fix;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::lexer::LexicalError;
@ -51,9 +52,14 @@ const BLANK_LINES_NESTED_LEVEL: u32 = 1;
/// pass
/// ```
///
/// ## Typing stub files (`.pyi`)
/// The typing style guide recommends to not use blank lines between methods except to group
/// them. That's why this rule is not enabled in typing stub files.
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html)
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
#[violation]
pub struct BlankLineBetweenMethods;
@ -96,9 +102,14 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods {
/// pass
/// ```
///
/// ## Typing stub files (`.pyi`)
/// The typing style guide recommends to not use blank lines between classes and functions except to group
/// them. That's why this rule is not enabled in typing stub files.
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html)
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
#[violation]
pub struct BlankLinesTopLevel {
actual_blank_lines: u32,
@ -150,6 +161,9 @@ impl AlwaysFixableViolation for BlankLinesTopLevel {
/// pass
/// ```
///
/// ## Typing stub files (`.pyi`)
/// The rule allows at most one blank line in typing stub files in accordance to the typing style guide recommendation.
///
/// Note: The rule respects the following `isort` settings when determining the maximum number of blank lines allowed between two statements:
/// * [`lint.isort.lines-after-imports`]: For top-level statements directly following an import statement.
/// * [`lint.isort.lines-between-types`]: For `import` statements directly following a `from ... import ...` statement or vice versa.
@ -157,6 +171,7 @@ impl AlwaysFixableViolation for BlankLinesTopLevel {
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html)
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
#[violation]
pub struct TooManyBlankLines {
actual_blank_lines: u32,
@ -246,9 +261,14 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator {
/// user = User()
/// ```
///
/// ## Typing stub files (`.pyi`)
/// The typing style guide recommends to not use blank lines between statements except to group
/// them. That's why this rule is not enabled in typing stub files.
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html)
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
#[violation]
pub struct BlankLinesAfterFunctionOrClass {
actual_blank_lines: u32,
@ -295,9 +315,14 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass {
/// pass
/// ```
///
/// ## Typing stub files (`.pyi`)
/// The typing style guide recommends to not use blank lines between classes and functions except to group
/// them. That's why this rule is not enabled in typing stub files.
///
/// ## References
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html)
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
#[violation]
pub struct BlankLinesBeforeNestedDefinition;
@ -628,6 +653,7 @@ pub(crate) struct BlankLinesChecker<'a> {
indent_width: IndentWidth,
lines_after_imports: isize,
lines_between_types: usize,
source_type: PySourceType,
}
impl<'a> BlankLinesChecker<'a> {
@ -635,6 +661,7 @@ impl<'a> BlankLinesChecker<'a> {
locator: &'a Locator<'a>,
stylist: &'a Stylist<'a>,
settings: &crate::settings::LinterSettings,
source_type: PySourceType,
) -> BlankLinesChecker<'a> {
BlankLinesChecker {
stylist,
@ -642,6 +669,7 @@ impl<'a> BlankLinesChecker<'a> {
indent_width: settings.tab_size,
lines_after_imports: settings.isort.lines_after_imports,
lines_between_types: settings.isort.lines_between_types,
source_type,
}
}
@ -739,6 +767,8 @@ impl<'a> BlankLinesChecker<'a> {
&& !matches!(state.follows, Follows::Docstring | Follows::Decorator)
// Do not trigger when the def follows an if/while/etc...
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
// Blank lines in stub files are only used for grouping. Don't enforce blank lines.
&& !self.source_type.is_stub()
{
// E301
let mut diagnostic = Diagnostic::new(BlankLineBetweenMethods, line.first_token_range);
@ -750,20 +780,31 @@ impl<'a> BlankLinesChecker<'a> {
diagnostics.push(diagnostic);
}
// Blank lines in stub files are used to group definitions. Don't enforce blank lines.
let max_lines_level = if self.source_type.is_stub() {
1
} else {
if line.indent_length == 0 {
BLANK_LINES_TOP_LEVEL
} else {
BLANK_LINES_NESTED_LEVEL
}
};
let expected_blank_lines_before_definition = if line.indent_length == 0 {
// Mimic the isort rules for the number of blank lines before classes and functions
if state.follows.is_any_import() {
// Fallback to the default if the value is too large for an u32 or if it is negative.
// A negative value means that isort should determine the blank lines automatically.
// `isort` defaults to 2 if before a class or function definition and 1 otherwise.
// Defaulting to 2 here is correct because the variable is only used when testing the
// `isort` defaults to 2 if before a class or function definition (except in stubs where it is one) and 1 otherwise.
// Defaulting to 2 (or 1 in stubs) here is correct because the variable is only used when testing the
// blank lines before a class or function definition.
u32::try_from(self.lines_after_imports).unwrap_or(BLANK_LINES_TOP_LEVEL)
u32::try_from(self.lines_after_imports).unwrap_or(max_lines_level)
} else {
BLANK_LINES_TOP_LEVEL
max_lines_level
}
} else {
BLANK_LINES_NESTED_LEVEL
max_lines_level
};
if line.preceding_blank_lines < expected_blank_lines_before_definition
@ -775,6 +816,8 @@ impl<'a> BlankLinesChecker<'a> {
&& line.indent_length == 0
// Only apply to functions or classes.
&& line.kind.is_class_function_or_decorator()
// Blank lines in stub files are used to group definitions. Don't enforce blank lines.
&& !self.source_type.is_stub()
{
// E302
let mut diagnostic = Diagnostic::new(
@ -804,12 +847,6 @@ impl<'a> BlankLinesChecker<'a> {
diagnostics.push(diagnostic);
}
let max_lines_level = if line.indent_length == 0 {
BLANK_LINES_TOP_LEVEL
} else {
BLANK_LINES_NESTED_LEVEL
};
// If between `import` and `from .. import ..` or the other way round,
// allow up to `lines_between_types` newlines for isort compatibility.
// We let `isort` remove extra blank lines when the imports belong
@ -893,6 +930,8 @@ impl<'a> BlankLinesChecker<'a> {
&& line.indent_length == 0
&& !line.is_comment_only
&& !line.kind.is_class_function_or_decorator()
// Blank lines in stub files are used for grouping, don't enforce blank lines.
&& !self.source_type.is_stub()
{
// E305
let mut diagnostic = Diagnostic::new(
@ -933,6 +972,8 @@ impl<'a> BlankLinesChecker<'a> {
&& prev_indent_length.is_some_and(|prev_indent_length| prev_indent_length >= line.indent_length)
// Allow groups of one-liners.
&& !(matches!(state.follows, Follows::Def) && line.last_token != TokenKind::Colon)
// Blank lines in stub files are only used for grouping. Don't enforce blank lines.
&& !self.source_type.is_stub()
{
// E306
let mut diagnostic =

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

View file

@ -0,0 +1,73 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.pyi:17:1: E303 [*] Too many blank lines (2)
|
17 | def grouped1(): ...
| ^^^ E303
18 | def grouped2(): ...
19 | def grouped3( ): ...
|
= help: Remove extraneous blank line(s)
Safe fix
13 13 | @overload
14 14 | def a(arg: int, name: str): ...
15 15 |
16 |-
17 16 | def grouped1(): ...
18 17 | def grouped2(): ...
19 18 | def grouped3( ): ...
E30.pyi:22:1: E303 [*] Too many blank lines (2)
|
22 | class BackendProxy:
| ^^^^^ E303
23 | backend_module: str
24 | backend_object: str | None
|
= help: Remove extraneous blank line(s)
Safe fix
18 18 | def grouped2(): ...
19 19 | def grouped3( ): ...
20 20 |
21 |-
22 21 | class BackendProxy:
23 22 | backend_module: str
24 23 | backend_object: str | None
E30.pyi:35:5: E303 [*] Too many blank lines (2)
|
35 | def ungrouped(): ...
| ^^^ E303
36 | a = "test"
|
= help: Remove extraneous blank line(s)
Safe fix
31 31 |
32 32 | def with_blank_line(): ...
33 33 |
34 |-
35 34 | def ungrouped(): ...
36 35 | a = "test"
37 36 |
E30.pyi:43:1: E303 [*] Too many blank lines (2)
|
43 | def outer():
| ^^^ E303
44 | def inner():
45 | pass
|
= help: Remove extraneous blank line(s)
Safe fix
39 39 | pass
40 40 | b = "test"
41 41 |
42 |-
43 42 | def outer():
44 43 | def inner():
45 44 | pass

View file

@ -0,0 +1,20 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30.pyi:32:5: E304 [*] Blank lines found after function decorator (1)
|
30 | @decorated
31 |
32 | def with_blank_line(): ...
| ^^^ E304
|
= help: Remove extraneous blank line(s)
Safe fix
28 28 | def grouped2(): ...
29 29 | def grouped3( ): ...
30 30 | @decorated
31 |-
32 31 | def with_blank_line(): ...
33 32 |
34 33 |

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

View file

@ -0,0 +1,270 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E30_isort.pyi:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import json
2 | |
3 | |
4 | |
5 | | from typing import Any, Sequence
6 | |
7 | |
8 | | class MissingCommand(TypeError): ... # noqa: N818
| |_^ I001
|
= help: Organize imports
Safe fix
1 1 | import json
2 |-
3 |-
4 |-
5 2 | from typing import Any, Sequence
6 |-
7 3 |
8 4 | class MissingCommand(TypeError): ... # noqa: N818
9 5 |
E30_isort.pyi:5:1: E303 [*] Too many blank lines (3)
|
5 | from typing import Any, Sequence
| ^^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
1 1 | import json
2 2 |
3 |-
4 |-
5 3 | from typing import Any, Sequence
6 4 |
7 5 |
E30_isort.pyi:8:1: E303 [*] Too many blank lines (2)
|
8 | class MissingCommand(TypeError): ... # noqa: N818
| ^^^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
4 4 |
5 5 | from typing import Any, Sequence
6 6 |
7 |-
8 7 | class MissingCommand(TypeError): ... # noqa: N818
9 8 |
10 9 |
E30_isort.pyi:11:1: E303 [*] Too many blank lines (2)
|
11 | class BackendProxy:
| ^^^^^ E303
12 | backend_module: str
13 | backend_object: str | None
|
= help: Remove extraneous blank line(s)
Safe fix
7 7 |
8 8 | class MissingCommand(TypeError): ... # noqa: N818
9 9 |
10 |-
11 10 | class BackendProxy:
12 11 | backend_module: str
13 12 | backend_object: str | None
E30_isort.pyi:17:1: E303 [*] Too many blank lines (2)
|
17 | if __name__ == "__main__":
| ^^ E303
18 | import abcd
|
= help: Remove extraneous blank line(s)
Safe fix
13 13 | backend_object: str | None
14 14 | backend: Any
15 15 |
16 |-
17 16 | if __name__ == "__main__":
18 17 | import abcd
19 18 |
E30_isort.pyi:21:5: E303 [*] Too many blank lines (2)
|
21 | abcd.foo()
| ^^^^ E303
22 |
23 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
|
= help: Remove extraneous blank line(s)
Safe fix
17 17 | if __name__ == "__main__":
18 18 | import abcd
19 19 |
20 |-
21 20 | abcd.foo()
22 21 |
23 22 | def __init__(self, backend_module: str, backend_obj: str | None) -> None: ...
E30_isort.pyi:26:1: I001 [*] Import block is un-sorted or un-formatted
|
25 | if TYPE_CHECKING:
26 | / import os
27 | |
28 | |
29 | |
30 | | from typing_extensions import TypeAlias
31 | |
| |_^ I001
32 |
33 | abcd.foo()
|
= help: Organize imports
Safe fix
25 25 | if TYPE_CHECKING:
26 26 | import os
27 27 |
28 |-
29 |-
30 28 | from typing_extensions import TypeAlias
31 29 |
32 30 |
E30_isort.pyi:30:5: E303 [*] Too many blank lines (3)
|
30 | from typing_extensions import TypeAlias
| ^^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
25 25 | if TYPE_CHECKING:
26 26 | import os
27 27 |
28 |-
29 |-
30 28 | from typing_extensions import TypeAlias
31 29 |
32 30 |
E30_isort.pyi:33:5: E303 [*] Too many blank lines (2)
|
33 | abcd.foo()
| ^^^^ E303
34 |
35 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
= help: Remove extraneous blank line(s)
Safe fix
29 29 |
30 30 | from typing_extensions import TypeAlias
31 31 |
32 |-
33 32 | abcd.foo()
34 33 |
35 34 | def __call__(self, name: str, *args: Any, **kwargs: Any) -> Any:
E30_isort.pyi:45:1: E303 [*] Too many blank lines (2)
|
45 | def _exit(self) -> None: ...
| ^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
41 41 | def __call__2(self, name: str, *args: Any, **kwargs: Any) -> Any:
42 42 | ...
43 43 |
44 |-
45 44 | def _exit(self) -> None: ...
46 45 |
47 46 |
E30_isort.pyi:48:1: E303 [*] Too many blank lines (2)
|
48 | def _optional_commands(self) -> dict[str, bool]: ...
| ^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
44 44 |
45 45 | def _exit(self) -> None: ...
46 46 |
47 |-
48 47 | def _optional_commands(self) -> dict[str, bool]: ...
49 48 |
50 49 |
E30_isort.pyi:51:1: E303 [*] Too many blank lines (2)
|
51 | def run(argv: Sequence[str]) -> int: ...
| ^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
47 47 |
48 48 | def _optional_commands(self) -> dict[str, bool]: ...
49 49 |
50 |-
51 50 | def run(argv: Sequence[str]) -> int: ...
52 51 |
53 52 |
E30_isort.pyi:54:1: E303 [*] Too many blank lines (2)
|
54 | def read_line(fd: int = 0) -> bytearray: ...
| ^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
50 50 |
51 51 | def run(argv: Sequence[str]) -> int: ...
52 52 |
53 |-
54 53 | def read_line(fd: int = 0) -> bytearray: ...
55 54 |
56 55 |
E30_isort.pyi:57:1: E303 [*] Too many blank lines (2)
|
57 | def flush() -> None: ...
| ^^^ E303
|
= help: Remove extraneous blank line(s)
Safe fix
53 53 |
54 54 | def read_line(fd: int = 0) -> bytearray: ...
55 55 |
56 |-
57 56 | def flush() -> None: ...
58 57 |
59 58 |
E30_isort.pyi:60:1: E303 [*] Too many blank lines (2)
|
60 | from typing import Any, Sequence
| ^^^^ E303
61 |
62 | class MissingCommand(TypeError): ... # noqa: N818
|
= help: Remove extraneous blank line(s)
Safe fix
56 56 |
57 57 | def flush() -> None: ...
58 58 |
59 |-
60 59 | from typing import Any, Sequence
61 60 |
62 61 | class MissingCommand(TypeError): ... # noqa: N818