Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce A006 for lambda shadowing #1

Merged
merged 4 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
71 changes: 68 additions & 3 deletions flake8_builtins.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -28,6 +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'
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 = {
Expand All @@ -36,6 +43,7 @@ class BuiltinsChecker(object):
'credits',
'_',
}
ignored_module_names = set()

def __init__(self, tree, filename):
self.tree = tree
Expand All @@ -50,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):
Expand All @@ -63,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):
Expand Down Expand Up @@ -104,6 +134,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)

Expand Down Expand Up @@ -178,6 +211,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:
Expand Down Expand Up @@ -269,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,
)
5 changes: 3 additions & 2 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -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'
76 changes: 66 additions & 10 deletions run_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -152,6 +168,11 @@ def bla(list):
check_code(source, 'A002')


def test_lambda_argument_message():
source = 'takefirst = lambda list: list[0]'
check_code(source, 'A006')


def test_keyword_argument_message():
source = """
def bla(dict=3):
Expand Down Expand Up @@ -183,6 +204,17 @@ 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, 'A006')


def test_no_error():
source = """def bla(first):\n b = 4"""
check_code(source)
Expand Down Expand Up @@ -511,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(
Expand All @@ -524,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')