Skip to content

Commit 6936615

Browse files
committed
Merge remote-tracking branch 'origin/main' into raiseA002ForLambda
2 parents 5b3cbc3 + 5f02040 commit 6936615

6 files changed

+105
-16
lines changed

CHANGES.rst

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@
33
Changelog
44
=========
55

6-
2.2.1 (unreleased)
6+
2.3.1 (unreleased)
77
------------------
88

99
- Nothing changed yet.
1010

1111

12+
2.3.0 (2024-03-29)
13+
------------------
14+
15+
- Add rule for builtin module name shadowing (`A005`).
16+
[asfaltboy]
17+
18+
1219
2.2.0 (2023-11-03)
1320
------------------
1421

README.rst

+3
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ A004:
102102
An import statement is shadowing a Python builtin.
103103

104104
A005:
105+
A module is shadowing a Python builtin module (e.g: `logging` or `socket`)
106+
107+
A006:
105108
A lambda argument is shadowing a Python builtin.
106109

107110
License

flake8_builtins.py

+42-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from flake8 import utils as stdin_utils
2+
from pathlib import Path
23

34
import ast
45
import builtins
56
import inspect
7+
import sys
68

79

810
class BuiltinsChecker:
@@ -12,7 +14,8 @@ class BuiltinsChecker:
1214
argument_msg = 'A002 argument "{0}" is shadowing a Python builtin'
1315
class_attribute_msg = 'A003 class attribute "{0}" is shadowing a Python builtin'
1416
import_msg = 'A004 import statement "{0}" is shadowing a Python builtin'
15-
lambda_argument_msg = 'A005 lambda argument "{0}" is shadowing a Python builtin'
17+
module_name_msg = 'A005 the module is shadowing a Python builtin module "{0}"'
18+
lambda_argument_msg = 'A006 lambda argument "{0}" is shadowing a Python builtin'
1619

1720
names = []
1821
ignore_list = {
@@ -21,6 +24,7 @@ class BuiltinsChecker:
2124
'credits',
2225
'_',
2326
}
27+
ignored_module_names = set()
2428

2529
def __init__(self, tree, filename):
2630
self.tree = tree
@@ -35,6 +39,13 @@ def add_options(cls, option_manager):
3539
comma_separated_list=True,
3640
help='A comma separated list of builtins to skip checking',
3741
)
42+
option_manager.add_option(
43+
'--builtins-allowed-modules',
44+
metavar='builtins',
45+
parse_from_config=True,
46+
comma_separated_list=True,
47+
help='A comma separated list of builtin module names to allow',
48+
)
3849

3950
@classmethod
4051
def parse_options(cls, options):
@@ -48,12 +59,26 @@ def parse_options(cls, options):
4859
if flake8_builtins:
4960
cls.names.update(flake8_builtins)
5061

62+
if options.builtins_allowed_modules is not None:
63+
cls.ignored_module_names.update(options.builtins_allowed_modules)
64+
65+
if hasattr(sys, 'stdlib_module_names'):
66+
# stdlib_module_names is only available in Python 3.10+
67+
known_module_names = sys.stdlib_module_names
68+
cls.module_names = {
69+
m for m in known_module_names if m not in cls.ignored_module_names
70+
}
71+
else:
72+
cls.module_names = set()
73+
5174
def run(self):
5275
tree = self.tree
5376

5477
if self.filename == 'stdin':
5578
lines = stdin_utils.stdin_get_value()
5679
tree = ast.parse(lines)
80+
else:
81+
yield from self.check_module_name(self.filename)
5782

5883
for statement in ast.walk(tree):
5984
for child in ast.iter_child_nodes(statement):
@@ -252,13 +277,26 @@ def check_class(self, statement):
252277
if statement.name in self.names:
253278
yield self.error(statement, variable=statement.name)
254279

255-
def error(self, statement, variable, message=None):
280+
def error(self, statement=None, variable=None, message=None):
256281
if not message:
257282
message = self.assign_msg
258283

284+
# lineno and col_offset must be integers
259285
return (
260-
statement.lineno,
261-
statement.col_offset,
286+
statement.lineno if statement else 0,
287+
statement.col_offset if statement else 0,
262288
message.format(variable),
263289
type(self),
264290
)
291+
292+
def check_module_name(self, filename: str):
293+
if not self.module_names:
294+
return
295+
path = Path(filename)
296+
module_name = path.name.removesuffix('.py')
297+
if module_name in self.module_names:
298+
yield self.error(
299+
None,
300+
module_name,
301+
message=self.module_name_msg,
302+
)

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "flake8-builtins"
7-
version = "2.2.1.dev0"
7+
version = "2.3.1.dev0"
88
authors = [
99
{ name="Gil Forcada Codinachs", email="[email protected]" },
1010
]

run_tests.py

+50-10
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,25 @@
1010
class FakeOptions:
1111
builtins_ignorelist = []
1212
builtins = None
13+
builtins_allowed_modules = None
1314

14-
def __init__(self, ignore_list='', builtins=None):
15+
def __init__(self, ignore_list='', builtins=None, builtins_allowed_modules=None):
1516
if ignore_list:
1617
self.builtins_ignorelist = ignore_list
1718
if builtins:
1819
self.builtins = builtins
19-
20-
21-
def check_code(source, expected_codes=None, ignore_list=None, builtins=None):
20+
if builtins_allowed_modules:
21+
self.builtins_allowed_modules = builtins_allowed_modules
22+
23+
24+
def check_code(
25+
source,
26+
expected_codes=None,
27+
ignore_list=None,
28+
builtins=None,
29+
builtins_allowed_modules=None,
30+
filename='/home/script.py',
31+
):
2232
"""Check if the given source code generates the given flake8 errors
2333
2434
If `expected_codes` is a string is converted to a list,
@@ -37,8 +47,14 @@ def check_code(source, expected_codes=None, ignore_list=None, builtins=None):
3747
if ignore_list is None:
3848
ignore_list = []
3949
tree = ast.parse(textwrap.dedent(source))
40-
checker = BuiltinsChecker(tree, '/home/script.py')
41-
checker.parse_options(FakeOptions(ignore_list=ignore_list, builtins=builtins))
50+
checker = BuiltinsChecker(tree, filename)
51+
checker.parse_options(
52+
FakeOptions(
53+
ignore_list=ignore_list,
54+
builtins=builtins,
55+
builtins_allowed_modules=builtins_allowed_modules,
56+
)
57+
)
4258
return_statements = list(checker.run())
4359

4460
assert len(return_statements) == len(expected_codes)
@@ -477,12 +493,36 @@ async def bla():
477493
def test_stdin(stdin_get_value):
478494
source = 'max = 4'
479495
stdin_get_value.return_value = source
480-
checker = BuiltinsChecker('', 'stdin')
481-
checker.parse_options(FakeOptions())
482-
ret = list(checker.run())
483-
assert len(ret) == 1
496+
check_code('', expected_codes='A001', filename='stdin')
484497

485498

486499
def test_tuple_unpacking():
487500
source = 'a, *(b, c) = 1, 2, 3'
488501
check_code(source)
502+
503+
504+
@pytest.mark.skipif(
505+
sys.version_info < (3, 10),
506+
reason='Skip A005, module testing is only supported in Python 3.10 and above',
507+
)
508+
def test_module_name():
509+
source = ''
510+
check_code(source, expected_codes='A005', filename='./temp/logging.py')
511+
512+
513+
@pytest.mark.skipif(
514+
sys.version_info < (3, 10),
515+
reason='Skip A005, module testing is only supported in Python 3.10 and above',
516+
)
517+
def test_module_name_ignore_module():
518+
source = ''
519+
check_code(
520+
source,
521+
filename='./temp/logging.py',
522+
builtins_allowed_modules=['logging'],
523+
)
524+
525+
526+
def test_module_name_not_builtin():
527+
source = ''
528+
check_code(source, filename='log_config')

tox.ini

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ description = get a test coverage report
4444
use_develop = true
4545
skip_install = false
4646
deps =
47+
pytest-cov
4748
coverage
4849
commands =
4950
pytest run_tests.py --cov --cov-report term-missing

0 commit comments

Comments
 (0)