From 6f4e77f38aaba21d9ba3c272a4b46043ad30baed Mon Sep 17 00:00:00 2001 From: Pavel savchenko Date: Mon, 5 Feb 2024 12:59:31 +0100 Subject: [PATCH] feat: detect builtin module name shadowing --- flake8_builtins.py | 47 +++++++++++++++++++++++++++++++++++++++++++--- run_tests.py | 21 +++++++++++++++++++-- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/flake8_builtins.py b/flake8_builtins.py index 1473b9f..d942c56 100644 --- a/flake8_builtins.py +++ b/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: @@ -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 = { @@ -20,6 +23,7 @@ class BuiltinsChecker: 'credits', '_', } + ignored_module_names = {} def __init__(self, tree, filename): self.tree = tree @@ -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): @@ -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): @@ -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, + ) diff --git a/run_tests.py b/run_tests.py index a297caa..9e5280b 100644 --- a/run_tests.py +++ b/run_tests.py @@ -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, @@ -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')