Skip to content

Commit 7a07a5b

Browse files
authored
Silence B009/B010 for non-identifiers (#116)
If an object implements __getattr__ and/or __setattr__, the second parameter to getattr() or setattr() may not be a valid Python identifier (e.g. getattr(foo, "123abc"). In this case bugbear should not emit B009/B010, since changing to "foo.123abc" would be a SyntaxError. Update the B009/B010 detection to check using a regex.
1 parent 76ee744 commit 7a07a5b

File tree

3 files changed

+30
-5
lines changed

3 files changed

+30
-5
lines changed

bugbear.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from functools import lru_cache, partial
66
import itertools
77
import logging
8+
import re
89

910
import attr
1011
import pycodestyle
@@ -110,6 +111,16 @@ def should_warn(self, code):
110111
return False
111112

112113

114+
def _is_identifier(arg):
115+
# Return True if arg is a valid identifier, per
116+
# https://docs.python.org/2/reference/lexical_analysis.html#identifiers
117+
118+
if not isinstance(arg, ast.Str):
119+
return False
120+
121+
return re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", arg.s) is not None
122+
123+
113124
def _to_name_str(node):
114125
# Turn Name and Attribute nodes to strings, e.g "ValueError" or
115126
# "pkg.mod.error", handling any depth of attribute accesses.
@@ -213,13 +224,13 @@ def visit_Call(self, node):
213224
if (
214225
node.func.id == "getattr"
215226
and len(node.args) == 2 # noqa: W503
216-
and isinstance(node.args[1], ast.Str) # noqa: W503
227+
and _is_identifier(node.args[1]) # noqa: W503
217228
):
218229
self.errors.append(B009(node.lineno, node.col_offset))
219230
elif (
220231
node.func.id == "setattr"
221232
and len(node.args) == 3 # noqa: W503
222-
and isinstance(node.args[1], ast.Str) # noqa: W503
233+
and _is_identifier(node.args[1]) # noqa: W503
223234
):
224235
self.errors.append(B010(node.lineno, node.col_offset))
225236

tests/b009_b010.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Should emit:
3-
B009 - Line 15
4-
B010 - Line 22
3+
B009 - Line 16, 17, 18
4+
B010 - Line 26, 27, 28
55
"""
66

77
# Valid getattr usage
@@ -10,13 +10,19 @@
1010
getattr(foo, "bar{foo}".format(foo="a"), None)
1111
getattr(foo, "bar{foo}".format(foo="a"))
1212
getattr(foo, bar, None)
13+
getattr(foo, "123abc")
1314

1415
# Invalid usage
1516
getattr(foo, "bar")
17+
getattr(foo, "_123abc")
18+
getattr(foo, "abc123")
1619

1720
# Valid setattr usage
1821
setattr(foo, bar, None)
1922
setattr(foo, "bar{foo}".format(foo="a"), None)
23+
setattr(foo, "123abc", None)
2024

2125
# Invalid usage
2226
setattr(foo, "bar", None)
27+
setattr(foo, "_123abc", None)
28+
setattr(foo, "abc123", None)

tests/test_bugbear.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,15 @@ def test_b009_b010(self):
123123
filename = Path(__file__).absolute().parent / "b009_b010.py"
124124
bbc = BugBearChecker(filename=str(filename))
125125
errors = list(bbc.run())
126-
self.assertEqual(errors, self.errors(B009(15, 0), B010(22, 0)))
126+
all_errors = [
127+
B009(16, 0),
128+
B009(17, 0),
129+
B009(18, 0),
130+
B010(26, 0),
131+
B010(27, 0),
132+
B010(28, 0),
133+
]
134+
self.assertEqual(errors, self.errors(*all_errors))
127135

128136
def test_b011(self):
129137
filename = Path(__file__).absolute().parent / "b011.py"

0 commit comments

Comments
 (0)