From d373ddcd7c93c53ab182b26255fa1ecfae0f28fb Mon Sep 17 00:00:00 2001 From: cielavenir Date: Mon, 18 Mar 2024 15:21:36 +0900 Subject: [PATCH 1/4] Raise A002 for lambda --- flake8_builtins.py | 4 ++-- run_tests.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/flake8_builtins.py b/flake8_builtins.py index 727c471..a5dd046 100644 --- a/flake8_builtins.py +++ b/flake8_builtins.py @@ -74,7 +74,7 @@ def run(self): for child in ast.iter_child_nodes(statement): child.__flake8_builtins_parent = statement - function_nodes = [ast.FunctionDef] + function_nodes = [ast.FunctionDef, ast.Lambda] if getattr(ast, 'AsyncFunctionDef', None): function_nodes.append(ast.AsyncFunctionDef) function_nodes = tuple(function_nodes) @@ -153,7 +153,7 @@ def check_assignment(self, statement): stack.extend(list(item.value.elts)) def check_function_definition(self, statement): - if statement.name in self.names: + if not isinstance(statement, ast.Lambda) and statement.name in self.names: msg = self.assign_msg if type(statement.__flake8_builtins_parent) is ast.ClassDef: msg = self.class_attribute_msg diff --git a/run_tests.py b/run_tests.py index 112936c..167042a 100644 --- a/run_tests.py +++ b/run_tests.py @@ -152,6 +152,11 @@ def bla(list): check_code(source, 'A002') +def test_lambda_argument_message(): + source = 'takefirst = lambda list: list[0]' + check_code(source, 'A002') + + def test_keyword_argument_message(): source = """ def bla(dict=3): From 551f45ee169361aba84c884afbe298c933f5a058 Mon Sep 17 00:00:00 2001 From: cielavenir Date: Wed, 20 Mar 2024 11:01:01 +0900 Subject: [PATCH 2/4] introduce A005 for lambda --- flake8_builtins.py | 27 +++++++++++++++++++++++++-- run_tests.py | 11 ++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/flake8_builtins.py b/flake8_builtins.py index a5dd046..a901954 100644 --- a/flake8_builtins.py +++ b/flake8_builtins.py @@ -28,6 +28,7 @@ class BuiltinsChecker(object): 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' + lambda_argument_msg = 'A005 lambda argument "{0}" is shadowing a Python builtin' names = [] ignore_list = { @@ -74,7 +75,7 @@ def run(self): for child in ast.iter_child_nodes(statement): child.__flake8_builtins_parent = statement - function_nodes = [ast.FunctionDef, ast.Lambda] + function_nodes = [ast.FunctionDef] if getattr(ast, 'AsyncFunctionDef', None): function_nodes.append(ast.AsyncFunctionDef) function_nodes = tuple(function_nodes) @@ -104,6 +105,9 @@ def run(self): elif isinstance(statement, function_nodes): value = self.check_function_definition(statement) + elif isinstance(statement, ast.Lambda): + value = self.check_lambda_definition(statement) + elif isinstance(statement, for_nodes): value = self.check_for_loop(statement) @@ -153,7 +157,7 @@ def check_assignment(self, statement): stack.extend(list(item.value.elts)) def check_function_definition(self, statement): - if not isinstance(statement, ast.Lambda) and statement.name in self.names: + if statement.name in self.names: msg = self.assign_msg if type(statement.__flake8_builtins_parent) is ast.ClassDef: msg = self.class_attribute_msg @@ -178,6 +182,25 @@ def check_function_definition(self, statement): if isinstance(arg, ast.Name) and arg.id in self.names: yield self.error(arg, message=self.argument_msg, variable=arg.id) + def check_lambda_definition(self, statement): + if PY3: + all_arguments = [] + all_arguments.extend(statement.args.args) + all_arguments.extend(getattr(statement.args, 'kwonlyargs', [])) + all_arguments.extend(getattr(statement.args, 'posonlyargs', [])) + + for arg in all_arguments: + if isinstance(arg, ast.arg) and arg.arg in self.names: + yield self.error( + arg, + message=self.lambda_argument_msg, + variable=arg.arg, + ) + else: + for arg in statement.args.args: + if isinstance(arg, ast.Name) and arg.id in self.names: + yield self.error(arg, message=self.lambda_argument_msg, variable=arg.id) + def check_for_loop(self, statement): stack = [statement.target] while stack: diff --git a/run_tests.py b/run_tests.py index 167042a..199990a 100644 --- a/run_tests.py +++ b/run_tests.py @@ -154,7 +154,7 @@ def bla(list): def test_lambda_argument_message(): source = 'takefirst = lambda list: list[0]' - check_code(source, 'A002') + check_code(source, 'A005') def test_keyword_argument_message(): @@ -187,6 +187,15 @@ def bla(list, /): """ check_code(source, 'A002') +@pytest.mark.skipif( + sys.version_info < (3, 8), + reason='This syntax is only valid in Python 3.8+', +) +def test_lambda_posonly_argument_message(): + source = """ + takefirst = lambda list, /: list[0] + """ + check_code(source, 'A005') def test_no_error(): source = """def bla(first):\n b = 4""" From e031e7e756cec12ed9925ed10e31dbdfae66d1bf Mon Sep 17 00:00:00 2001 From: cielavenir Date: Tue, 2 Apr 2024 11:24:49 +0900 Subject: [PATCH 3/4] adopted 2.4.0 release --- flake8_builtins.py | 50 ++++++++++++++++++++++++++++++++--- run_tests.py | 66 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 100 insertions(+), 16 deletions(-) diff --git a/flake8_builtins.py b/flake8_builtins.py index a901954..164939c 100644 --- a/flake8_builtins.py +++ b/flake8_builtins.py @@ -1,5 +1,10 @@ from flake8 import utils as stdin_utils +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + import ast import inspect import sys @@ -28,7 +33,8 @@ class BuiltinsChecker(object): 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' - lambda_argument_msg = 'A005 lambda argument "{0}" is shadowing a Python builtin' + module_name_msg = 'A005 the module is shadowing a Python builtin module "{0}"' + lambda_argument_msg = 'A006 lambda argument "{0}" is shadowing a Python builtin' names = [] ignore_list = { @@ -37,6 +43,7 @@ class BuiltinsChecker(object): 'credits', '_', } + ignored_module_names = set() def __init__(self, tree, filename): self.tree = tree @@ -51,6 +58,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): @@ -64,12 +78,27 @@ 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: + for err in self.check_module_name(self.filename): + yield err for statement in ast.walk(tree): for child in ast.iter_child_nodes(statement): @@ -292,13 +321,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): + 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, + ) diff --git a/run_tests.py b/run_tests.py index 199990a..0dce5b8 100644 --- a/run_tests.py +++ b/run_tests.py @@ -14,15 +14,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, @@ -41,8 +51,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) @@ -154,7 +170,7 @@ def bla(list): def test_lambda_argument_message(): source = 'takefirst = lambda list: list[0]' - check_code(source, 'A005') + check_code(source, 'A006') def test_keyword_argument_message(): @@ -187,6 +203,7 @@ def bla(list, /): """ check_code(source, 'A002') + @pytest.mark.skipif( sys.version_info < (3, 8), reason='This syntax is only valid in Python 3.8+', @@ -195,7 +212,8 @@ def test_lambda_posonly_argument_message(): source = """ takefirst = lambda list, /: list[0] """ - check_code(source, 'A005') + check_code(source, 'A006') + def test_no_error(): source = """def bla(first):\n b = 4""" @@ -525,10 +543,7 @@ 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') @pytest.mark.skipif( @@ -538,3 +553,30 @@ def test_stdin(stdin_get_value): 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') From 0e94f3187d4a7f922acf13dfde75b3a68acc0db8 Mon Sep 17 00:00:00 2001 From: cielavenir Date: Tue, 2 Apr 2024 11:31:47 +0900 Subject: [PATCH 4/4] updated metadata --- README.rst | 18 ++++++++++++++++-- requirements.in | 5 +++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c0a31e5..577df5b 100644 --- a/README.rst +++ b/README.rst @@ -71,11 +71,19 @@ Install ------- Install with pip:: - $ pip install flake8-builtins + $ python -m pip install flake8-builtins + +Options +------- + +One can use `--builtins-ignorelist` option, or configuration option, +to ignore a custom list of builtins:: + + $ flake8 --builtins-ignorelist id,copyright *.py Requirements ------------ -- Python 2.7, 3.6, 3.7, 3.8, 3.9 +- Python 2.7, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and pypy3 - flake8 Rules @@ -93,6 +101,12 @@ A003: A004: An import statement is shadowing a Python builtin. +A005: + A module is shadowing a Python builtin module (e.g: `logging` or `socket`) + +A006: + A lambda argument is shadowing a Python builtin. + License ------- GPL 2.0 diff --git a/requirements.in b/requirements.in index 0ee1338..1f0c251 100644 --- a/requirements.in +++ b/requirements.in @@ -13,8 +13,9 @@ flake8-string-format flake8-todo futures; python_version < '3.0' mock ; python_version < '3.0' +pathlib2; python_version < '3.0' pytest<5; python_version < '3.0' -pytest>5; python_version >= '3.0' +pytest; python_version >= '3.0' pytest-cov -more-itertools==5.0.0 +more-itertools>=4 zipp ; python_version >= '3.0'