Skip to content

Commit

Permalink
Merge pull request #121 from asfaltboy/ps/module-name-shadowing
Browse files Browse the repository at this point in the history
feat: module name shadowing
  • Loading branch information
gforcada committed Mar 29, 2024
2 parents 54dc44e + 58b8527 commit df6c1d2
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 14 deletions.
3 changes: 2 additions & 1 deletion CHANGES.rst
Expand Up @@ -6,7 +6,8 @@ Changelog
2.2.1 (unreleased)
------------------

- Nothing changed yet.
- Add rule for builtin module name shadowing (`A005`).
[asfaltboy]


2.2.0 (2023-11-03)
Expand Down
3 changes: 3 additions & 0 deletions README.rst
Expand Up @@ -101,6 +101,9 @@ A003:
A004:
An import statement is shadowing a Python builtin.

A005:
A module is shadowing a Python builtin module (e.g: `logging` or `socket`)

License
-------
GPL 2.0
44 changes: 41 additions & 3 deletions flake8_builtins.py
@@ -1,8 +1,10 @@
from flake8 import utils as stdin_utils
from pathlib import Path

import ast
import builtins
import inspect
import sys


class BuiltinsChecker:
Expand All @@ -12,6 +14,7 @@ class BuiltinsChecker:
argument_msg = 'A002 argument "{0}" is shadowing a Python builtin'
class_attribute_msg = 'A003 class attribute "{0}" is shadowing a Python builtin'
import_msg = 'A004 import statement "{0}" is shadowing a Python builtin'
module_name_msg = 'A005 the module is shadowing a Python builtin module "{0}"'

names = []
ignore_list = {
Expand All @@ -20,6 +23,7 @@ class BuiltinsChecker:
'credits',
'_',
}
ignored_module_names = set()

def __init__(self, tree, filename):
self.tree = tree
Expand All @@ -34,6 +38,13 @@ def add_options(cls, option_manager):
comma_separated_list=True,
help='A comma separated list of builtins to skip checking',
)
option_manager.add_option(
'--builtins-allowed-modules',
metavar='builtins',
parse_from_config=True,
comma_separated_list=True,
help='A comma separated list of builtin module names to allow',
)

@classmethod
def parse_options(cls, options):
Expand All @@ -47,12 +58,26 @@ def parse_options(cls, options):
if flake8_builtins:
cls.names.update(flake8_builtins)

if options.builtins_allowed_modules is not None:
cls.ignored_module_names.update(options.builtins_allowed_modules)

if hasattr(sys, 'stdlib_module_names'):
# stdlib_module_names is only available in Python 3.10+
known_module_names = sys.stdlib_module_names
cls.module_names = {
m for m in known_module_names if m not in cls.ignored_module_names
}
else:
cls.module_names = set()

def run(self):
tree = self.tree

if self.filename == 'stdin':
lines = stdin_utils.stdin_get_value()
tree = ast.parse(lines)
else:
yield from self.check_module_name(self.filename)

for statement in ast.walk(tree):
for child in ast.iter_child_nodes(statement):
Expand Down Expand Up @@ -234,13 +259,26 @@ def check_class(self, statement):
if statement.name in self.names:
yield self.error(statement, variable=statement.name)

def error(self, statement, variable, message=None):
def error(self, statement=None, variable=None, message=None):
if not message:
message = self.assign_msg

# lineno and col_offset must be integers
return (
statement.lineno,
statement.col_offset,
statement.lineno if statement else 0,
statement.col_offset if statement else 0,
message.format(variable),
type(self),
)

def check_module_name(self, filename: str):
if not self.module_names:
return
path = Path(filename)
module_name = path.name.removesuffix('.py')
if module_name in self.module_names:
yield self.error(
None,
module_name,
message=self.module_name_msg,
)
60 changes: 50 additions & 10 deletions run_tests.py
Expand Up @@ -10,15 +10,25 @@
class FakeOptions:
builtins_ignorelist = []
builtins = None
builtins_allowed_modules = None

def __init__(self, ignore_list='', builtins=None):
def __init__(self, ignore_list='', builtins=None, builtins_allowed_modules=None):
if ignore_list:
self.builtins_ignorelist = ignore_list
if builtins:
self.builtins = builtins


def check_code(source, expected_codes=None, ignore_list=None, builtins=None):
if builtins_allowed_modules:
self.builtins_allowed_modules = builtins_allowed_modules


def check_code(
source,
expected_codes=None,
ignore_list=None,
builtins=None,
builtins_allowed_modules=None,
filename='/home/script.py',
):
"""Check if the given source code generates the given flake8 errors
If `expected_codes` is a string is converted to a list,
Expand All @@ -37,8 +47,14 @@ def check_code(source, expected_codes=None, ignore_list=None, builtins=None):
if ignore_list is None:
ignore_list = []
tree = ast.parse(textwrap.dedent(source))
checker = BuiltinsChecker(tree, '/home/script.py')
checker.parse_options(FakeOptions(ignore_list=ignore_list, builtins=builtins))
checker = BuiltinsChecker(tree, filename)
checker.parse_options(
FakeOptions(
ignore_list=ignore_list,
builtins=builtins,
builtins_allowed_modules=builtins_allowed_modules,
)
)
return_statements = list(checker.run())

assert len(return_statements) == len(expected_codes)
Expand Down Expand Up @@ -463,12 +479,36 @@ async def bla():
def test_stdin(stdin_get_value):
source = 'max = 4'
stdin_get_value.return_value = source
checker = BuiltinsChecker('', 'stdin')
checker.parse_options(FakeOptions())
ret = list(checker.run())
assert len(ret) == 1
check_code('', expected_codes='A001', filename='stdin')


def test_tuple_unpacking():
source = 'a, *(b, c) = 1, 2, 3'
check_code(source)


@pytest.mark.skipif(
sys.version_info < (3, 10),
reason='Skip A005, module testing is only supported in Python 3.10 and above',
)
def test_module_name():
source = ''
check_code(source, expected_codes='A005', filename='./temp/logging.py')


@pytest.mark.skipif(
sys.version_info < (3, 10),
reason='Skip A005, module testing is only supported in Python 3.10 and above',
)
def test_module_name_ignore_module():
source = ''
check_code(
source,
filename='./temp/logging.py',
builtins_allowed_modules=['logging'],
)


def test_module_name_not_builtin():
source = ''
check_code(source, filename='log_config')

0 comments on commit df6c1d2

Please sign in to comment.