Skip to content

Commit ee687e9

Browse files
committed
Add PLU002: blank lines before return statement
1 parent 7db6361 commit ee687e9

File tree

12 files changed

+217
-20
lines changed

12 files changed

+217
-20
lines changed

README.md

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
[![black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
88

99
Flake8-plus is a plugin for [Flake8](https://github.com/PyCQA/flake8) that detects
10-
incorrect amounts of vertical whitespace before the first toplevel `import` statement.
11-
By default, the plugin issues a warning if there are blank lines immediately preceding
12-
the first toplevel `import`. The plugin can be configured to expect any number of blank
13-
lines.
10+
incorrect amounts of vertical whitespace before the first toplevel `import` statement
11+
and before `return` statements. The plugin can be configured to expect any number of
12+
blank lines. By default, the plugin expects no blank lines before both `import` and
13+
`return`.
1414

1515
## Installation
1616

@@ -30,28 +30,32 @@ $ flake8 --version
3030

3131
## Configuration
3232

33-
You can set the required number of blank lines before the first `import`. This can be
34-
done from the command line:
33+
You can set the required number of blank lines before the first `import` as well as the
34+
number of blank lines required before a `return`. This can be done from the command
35+
line:
3536

3637
```shell
37-
$ flake8 --blanks-before-imports 1
38+
$ flake8 --blanks-before-imports 1 --blanks-before-return 1
3839
```
3940

4041
Or from one of the `setup.cfg`, `tox.ini`, or `.flake8` files:
4142

4243
```ini
4344
[flake8]
4445
blanks-before-imports=1
46+
blanks-before-return=1
4547
```
4648

4749
## Why no blank lines?
4850

49-
Neither Black, Flake8 nor Pylint enforces a specific number of blank lines preceding the
50-
first `import` and consequently there seems to be no consensus or standard. The table
51-
below shows the frequency of the number of blank lines before the first toplevel
52-
`import` statement in the code bases for [Black](https://github.com/psf/black),
53-
[Flake8](https://github.com/PyCQA/flake8) and [Pylint](https://github.com/PyCQA/pylint)
54-
(as of October 2022).
51+
### Before `import`
52+
53+
Neither [Black](https://github.com/psf/black), [Flake8](https://github.com/PyCQA/flake8)
54+
nor [Pylint](https://github.com/PyCQA/pylint) enforces a specific number of blank lines
55+
preceding the first `import` and consequently there seems to be no consensus or
56+
standard. The table below shows the frequency of the number of blank lines before the
57+
first toplevel `import` statement in the code bases for Black, Flake8 and Pylint (as of
58+
October 2022).
5559

5660
| Package | Total files | 0 blanks | 1 blank | 2 blanks | Folder |
5761
| ------- | ----------: | -------: | ------: | -------: | ------------- |
@@ -65,8 +69,26 @@ Pylint code does not consistently include module docstrings (thereby breaking
6569
`pylint(missing-module-docstring)`). For that reason, and also because this is a Flake8
6670
plugin, the plugin follows the style of Flake8 as the default.
6771

72+
### Before `return`
73+
74+
Neither Black, Flake8 nor Pylint enforces a specific number of blank lines preceding
75+
`return`. However, they all use zero blank lines more frequently than they use any
76+
other number of blanks. The table below shows the frequency of the number of blank
77+
lines before a `return` statement in the code bases for Black, Flake8 and Pylint (as of
78+
October 2022).
79+
80+
| Package | Total `return`s | 0 blanks | 1 blank | 2 blanks | Folder |
81+
| ------- | --------------: | -------: | ------: | -------: | ------------- |
82+
| Black | 618 | 544 | 74 | 0 | `src` |
83+
| Flake8 | 174 | 155 | 19 | 0 | `src/flake8/` |
84+
| Pylint | 1941 | 1852 | 89 | 0 | `pylint` |
85+
86+
Since zero blank lines is the style used most frequently, Flake8-plus uses that as that
87+
as the default.
88+
6889
## Reported problems
6990

70-
| Code |  Description |
71-
| ------ | ------------------------------------------------------- |
72-
| PLU001 | "expected {} blank lines before first import, found {}" |
91+
| Code |  Description |
92+
| ------ | ----------------------------------------------------------- |
93+
| PLU001 | "expected {} blank lines before first import, found {}" |
94+
| PLU002 | "expected {} blank lines before return statement, found {}" |

flake8_plus/config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
"""Config class."""
22
# pylint: disable=too-few-public-methods
3+
from . import defaults
34

45

56
class Config:
67
"""Plugin configuration class."""
78

8-
def __init__(self, blanks_before_imports: int):
9+
def __init__(
10+
self,
11+
blanks_before_imports: int = defaults.BLANKS_BEFORE_IMPORTS,
12+
blanks_before_return: int = defaults.BLANKS_BEFORE_RETURN,
13+
):
914
"""
1015
Initialize a `Configuration` instance.
1116
1217
Args:
1318
blanks_before_imports (int): Number of blanks line expected before first
1419
import statements.
20+
blanks_before_return (int): Number of blanks line expected before return
21+
statement.
1522
"""
1623
self.blanks_before_imports = blanks_before_imports
24+
self.blanks_before_return = blanks_before_return

flake8_plus/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
"""Default values."""
22

33
BLANKS_BEFORE_IMPORTS = 0
4+
BLANKS_BEFORE_RETURN = 0

flake8_plus/plugin.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .config import Config
1111
from .version import VERSION
1212
from .visitors.plu001_visitor import PLU001Visitor
13+
from .visitors.plu002_visitor import PLU002Visitor
1314

1415

1516
class Plugin:
@@ -19,6 +20,7 @@ class Plugin:
1920
version = VERSION
2021
visitors = [
2122
PLU001Visitor,
23+
PLU002Visitor,
2224
]
2325

2426
def __init__(self, tree: ast.AST, lines: list[str]):
@@ -59,7 +61,17 @@ def add_options(option_manager: OptionManager) -> None: # pragma: no cover
5961
"(Default: %(default)s)",
6062
)
6163

64+
option_manager.add_option(
65+
"--blanks-before-return",
66+
type="int",
67+
metavar="n",
68+
default=defaults.BLANKS_BEFORE_RETURN,
69+
parse_from_config=True,
70+
help="Expected number of blank lines before return statement. "
71+
"(Default: %(default)s)",
72+
)
73+
6274
@classmethod
6375
def parse_options(cls, options: Namespace) -> None: # pragma: no cover
6476
"""Parse the custom configuration options given to flake8."""
65-
cls.config = Config(options.blanks_before_imports)
77+
cls.config = Config(options.blanks_before_imports, options.blanks_before_return)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Exception classes raised by various operations within pylint."""
2+
# pylint: disable=too-few-public-methods
3+
import ast
4+
from typing import Any
5+
6+
from ..config import Config
7+
from ..exceptions import MultipleStatementsError
8+
from ..problem import Problem
9+
from .base_visitor import BaseVisitor
10+
11+
12+
class PLU002Problem(Problem):
13+
"""Problem 002: Number of blank lines before return statement."""
14+
15+
code = "PLU002"
16+
format_ = "expected {} blank lines before return statement, found {}"
17+
18+
def __init__( # pylint: disable=unused-argument
19+
self,
20+
line_number: int,
21+
col_offset: int,
22+
blanks_actual: int,
23+
blanks_expected: int,
24+
**kwargs: dict[str, Any],
25+
):
26+
"""
27+
Initialize a `PLU002Problem` instance.
28+
29+
Args:
30+
line_number (int): The line number.
31+
col_offset (int): The column offset.
32+
blanks_actual (int): Number of actual blanks before return statement.
33+
blanks_expected (int): Number of expected blanks before return statement.
34+
"""
35+
message = PLU002Problem.format_.format(blanks_expected, blanks_actual)
36+
super().__init__(line_number, col_offset, message)
37+
38+
39+
class PLU002Visitor(BaseVisitor):
40+
"""Visitor class for the PLU002 rule."""
41+
42+
def __init__(self, lines: list[str], config: Config):
43+
"""
44+
Initialize a PLU002Visitor instance.
45+
46+
Args:
47+
lines (list[str]): The physical lines.
48+
config (Config): Configuration instance for the plugin and visitor.
49+
"""
50+
self._last_end: int = 0
51+
super().__init__(lines, config)
52+
53+
def visit(self, node: ast.AST) -> Any:
54+
"""
55+
Visit the specified node.
56+
57+
Args:
58+
node (ast.AST): The node to visit.
59+
60+
Returns:
61+
Any: The value returned by the base class `visit` method.
62+
"""
63+
self._process_node(node)
64+
return super().visit(node)
65+
66+
def _process_node(self, node: ast.AST):
67+
if isinstance(node, ast.Return):
68+
try:
69+
actual = self.compute_blanks_before(node)
70+
except MultipleStatementsError:
71+
return
72+
if actual != self.config.blanks_before_return:
73+
problem = PLU002Problem(
74+
node.lineno,
75+
node.col_offset,
76+
actual,
77+
self.config.blanks_before_return,
78+
)
79+
self.problems.append(problem)
80+
elif hasattr(node, "end_lineno") and (node.end_lineno is not None):
81+
self._last_end = node.end_lineno

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool.black]
2+
extend-exclude="case_files"

setup.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,8 @@ def run(self):
4444
def _validate_version(tag: Optional[str], version: str) -> bool:
4545
if not tag:
4646
return version == "0.0.0"
47-
4847
if tag[0] != "v":
4948
return False
50-
5149
return tag[1:] == version
5250

5351

tests/case_files/plu002/1_blank.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def some_func() -> int:
2+
3+
return 0

tests/case_files/plu002/cases.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[
2+
{
3+
"filename": "1_blank.py",
4+
"cases": [
5+
{
6+
"expectation": { "blanks_expected": 0 },
7+
"problems": [{ "line_number": 3, "col_offset": 4, "blanks_actual": 1 }]
8+
},
9+
{
10+
"expectation": { "blanks_expected": 1 },
11+
"problems": []
12+
}
13+
]
14+
},
15+
{
16+
"filename": "no_blanks.py",
17+
"cases": [
18+
{
19+
"expectation": { "blanks_expected": 0 },
20+
"problems": []
21+
},
22+
{
23+
"expectation": { "blanks_expected": 1 },
24+
"problems": [{ "line_number": 2, "col_offset": 4, "blanks_actual": 0 }]
25+
}
26+
]
27+
},
28+
{
29+
"filename": "print_return_same_line.py",
30+
"cases": [
31+
{
32+
"expectation": { "blanks_expected": 0 },
33+
"problems": []
34+
},
35+
{
36+
"expectation": { "blanks_expected": 1 },
37+
"problems": []
38+
}
39+
]
40+
}
41+
]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def some_func() -> int:
2+
return 0

0 commit comments

Comments
 (0)