Skip to content

Commit

Permalink
feat: detect builtin module name shadowing
Browse files Browse the repository at this point in the history
  • Loading branch information
asfaltboy committed Feb 20, 2024
1 parent 5bd1537 commit f2b1455
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 5 deletions.
47 changes: 44 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 = {}

def __init__(self, tree, filename):
self.tree = tree
Expand All @@ -34,6 +38,20 @@ 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',
)
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 +65,24 @@ 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)

known_module_names = getattr(
sys, 'stdlib_module_names', sys.builtin_module_names
)
cls.module_names = {
m for m in known_module_names if m not in cls.ignored_module_names
}

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 +264,24 @@ 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):
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,
)
21 changes: 19 additions & 2 deletions run_tests.py
Expand Up @@ -10,15 +10,24 @@
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
if builtins_allowed_modules:
self.builtins_allowed_modules = builtins_allowed_modules


def check_code(source, expected_codes=None, ignore_list=None, builtins=None, filename='/home/script.py'):
def check_code(
source,
expected_codes=None,
ignore_list=None,
builtins=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 Down Expand Up @@ -471,3 +480,11 @@ def test_tuple_unpacking():
check_code(source)


def test_module_name():
source = ''
check_code(source, expected_codes='A005', filename='./temp/logging.py')


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

0 comments on commit f2b1455

Please sign in to comment.