Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions custom_dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ hmac
html
idgeneratormixin
ifexpr
igetattr
importedname
importfrom
importnode
Expand Down
4 changes: 2 additions & 2 deletions doc/user_guide/configuration/all-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ Standard Checkers
""""""""""""
*Good variable names which should always be accepted, separated by a comma.*

**Default:** ``('i', 'j', 'k', 'ex', 'Run', '_')``
**Default:** ``('i', 'j', 'k', 'logger', 'ex', 'Run', '_')``


--good-names-rgxs
Expand Down Expand Up @@ -588,7 +588,7 @@ Standard Checkers

# function-rgx =

good-names = ["i", "j", "k", "ex", "Run", "_"]
good-names = ["i", "j", "k", "logger", "ex", "Run", "_"]

good-names-rgxs = []

Expand Down
2 changes: 1 addition & 1 deletion doc/whatsnew/2/2.5/summary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Other Changes
* Add new ``good-names-rgx`` and ``bad-names-rgx`` to enable permitting or disallowing of names via regular expressions

To enable better handling of permitted/disallowed names, we added two new config options: good-names-rgxs: a comma-
separated list of regexes, that if a name matches will be exempt of naming-checking. bad-names-rgxs: a comma-
separated list of regexes, that if a name matches will be exempt from naming-checking. bad-names-rgxs: a comma-
separated list of regexes, that if a name matches will be always marked as a disallowed name.

* Mutable ``collections.*`` are now flagged as dangerous defaults.
Expand Down
10 changes: 10 additions & 0 deletions doc/whatsnew/fragments/3585.breaking
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
`invalid-name` now distinguishes module-level names that are assigned only once
from those that are reassigned and now applies `--variable-rgx` to the latter.

Remember that `--good-names` or `--good-names-rgxs` can be provided to explicitly
allow good names. `logger` has been added to the default `--good-names`.

Also, `invalid-name` is triggered for module-level names for additional types
(e.g. lists and sets).

Closes #3585
1 change: 1 addition & 0 deletions examples/pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ function-naming-style=snake_case
good-names=i,
j,
k,
logger,
ex,
Run,
_
Expand Down
2 changes: 1 addition & 1 deletion examples/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ function-naming-style = "snake_case"
# function-rgx =

# Good variable names which should always be accepted, separated by a comma.
good-names = ["i", "j", "k", "ex", "Run", "_"]
good-names = ["i", "j", "k", "logger", "ex", "Run", "_"]

# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
Expand Down
37 changes: 30 additions & 7 deletions pylint/checkers/base/name_checker/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ class NameChecker(_BasicChecker):
(
"good-names",
{
"default": ("i", "j", "k", "ex", "Run", "_"),
"default": ("i", "j", "k", "logger", "ex", "Run", "_"),
"type": "csv",
"metavar": "<names>",
"help": "Good variable names which should always be accepted,"
Expand Down Expand Up @@ -461,14 +461,37 @@ def visit_assignname( # pylint: disable=too-many-branches
elif isinstance(inferred_assign_type, nodes.ClassDef):
self._check_name("class", node.name, node)

# Don't emit if the name redefines an import in an ImportError except handler.
elif not _redefines_import(node) and isinstance(
inferred_assign_type, nodes.Const
elif inferred_assign_type in (None, astroid.util.Uninferable):
return

# Don't emit if the name redefines an import in an ImportError except handler
# nor any other reassignment.
elif (
not (redefines_import := _redefines_import(node))
and not isinstance(
inferred_assign_type, (nodes.FunctionDef, nodes.Lambda)
)
and not utils.is_reassigned_before_current(node, node.name)
and not utils.is_reassigned_after_current(node, node.name)
and not utils.get_node_first_ancestor_of_type(
node, (nodes.For, nodes.While)
)
):
self._check_name("const", node.name, node)
else:
node_type = "variable"
if (
(iattrs := tuple(node.frame().igetattr(node.name)))
and astroid.util.Uninferable not in iattrs
and len(iattrs) == 2
and astroid.are_exclusive(*iattrs)
):
node_type = "const"
self._check_name(
"variable", node.name, node, disallowed_check_only=True
node_type,
node.name,
node,
disallowed_check_only=redefines_import,
)

# Check names defined in AnnAssign nodes
Expand Down Expand Up @@ -608,11 +631,11 @@ def _assigns_typevar(node: nodes.NodeNG | None) -> bool:
def _assigns_typealias(node: nodes.NodeNG | None) -> bool:
"""Check if a node is assigning a TypeAlias."""
inferred = utils.safe_infer(node)
if isinstance(inferred, nodes.ClassDef):
if isinstance(inferred, (nodes.ClassDef, astroid.bases.UnionType)):
qname = inferred.qname()
if qname == "typing.TypeAlias":
return True
if qname == ".Union":
if qname in {".Union", "builtins.UnionType"}:
# Union is a special case because it can be used as a type alias
# or as a type annotation. We only want to check the former.
assert node is not None
Expand Down
20 changes: 19 additions & 1 deletion pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,18 @@ def is_sys_guard(node: nodes.If) -> bool:
return False


def is_reassigned_before_current(node: nodes.NodeNG, varname: str) -> bool:
"""Check if the given variable name is reassigned in the same scope before the
current node.
"""
return any(
a.name == varname and a.lineno < node.lineno
for a in node.scope().nodes_of_class(
(nodes.AssignName, nodes.ClassDef, nodes.FunctionDef)
)
)


def is_reassigned_after_current(node: nodes.NodeNG, varname: str) -> bool:
"""Check if the given variable name is reassigned in the same scope after the
current node.
Expand Down Expand Up @@ -2135,7 +2147,13 @@ def is_augmented_assign(node: nodes.Assign) -> tuple[bool, str]:
binop.op in COMMUTATIVE_OPERATORS
and _is_target_name_in_binop_side(target, binop.right)
):
inferred_left = safe_infer(binop.left)
if isinstance(binop.left, nodes.Const):
# This bizarrely became necessary after an unrelated call to igetattr().
# Seems like a code smell uncovered in #10212.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I understand what the code smell is here, would you mind explaining a little more ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that I called igetattr() in my PR seemed to cause a cache-creation or cache-eviction side effect somewhere someplace causing a test to fail. It failed in a way where binop.left was already a Const and didn't need to be inferred, so I just handled for it. Debugging the cached side effect sounds like a nightmare, I didn't get that far.

I'd leave a more helpful comment if I could think of one, I'm definitely open to suggestions! Wanted to at least leave some kind of breadcrumb for the next developer.

# tuple(node.frame().igetattr(node.name))
inferred_left = binop.left
else:
inferred_left = safe_infer(binop.left)
if isinstance(inferred_left, nodes.Const) and isinstance(
inferred_left.value, int
):
Expand Down
2 changes: 1 addition & 1 deletion pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ expected-line-ending-format=
[BASIC]

# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
good-names=i,j,k,logger,ex,Run,_

# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
Expand Down
12 changes: 6 additions & 6 deletions tests/functional/a/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ def function_default_arg(one=1, two=2):
function_default_arg(1, one=5) # [redundant-keyword-arg]

# Remaining tests are for coverage of correct names in messages.
LAMBDA = lambda arg: 1
my_lambda = lambda arg: 1

LAMBDA() # [no-value-for-parameter]
my_lambda() # [no-value-for-parameter]

def method_tests():
"""Method invocations."""
Expand Down Expand Up @@ -260,7 +260,7 @@ def func(one, two, three):
return one + two + three


CALL = lambda *args: func(*args)
call = lambda *args: func(*args)


# Ensure `too-many-function-args` is not emitted when a function call is assigned
Expand All @@ -275,9 +275,9 @@ def _print_selection(self):
pick_apple = _pick_fruit("apple")
pick_pear = _pick_fruit("pear")

picker = FruitPicker()
picker.pick_apple()
picker.pick_pear()
PICKER = FruitPicker()
PICKER.pick_apple()
PICKER.pick_pear()


def name1(apple, /, **kwargs):
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/a/arguments.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ no-value-for-parameter:59:0:59:21::No value for argument 'first_argument' in fun
unexpected-keyword-arg:59:0:59:21::Unexpected keyword argument 'bob' in function call:UNDEFINED
unexpected-keyword-arg:60:0:60:40::Unexpected keyword argument 'coin' in function call:UNDEFINED
redundant-keyword-arg:62:0:62:30::Argument 'one' passed by position and keyword in function call:UNDEFINED
no-value-for-parameter:67:0:67:8::No value for argument 'arg' in lambda call:UNDEFINED
no-value-for-parameter:67:0:67:11::No value for argument 'arg' in lambda call:UNDEFINED
no-value-for-parameter:72:4:72:24:method_tests:No value for argument 'arg' in staticmethod call:UNDEFINED
no-value-for-parameter:73:4:73:29:method_tests:No value for argument 'arg' in staticmethod call:UNDEFINED
no-value-for-parameter:75:4:75:23:method_tests:No value for argument 'arg' in classmethod call:UNDEFINED
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# pylint: disable=useless-return, condition-evals-to-constant
# pylint: disable=useless-return, condition-evals-to-constant, invalid-name
"""check assignment to function call where the function doesn't return
'E1111': ('Assigning to function call which doesn\'t return',
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/c/condition_evals_to_constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def func(_):
while CONSTANT and False: # [condition-evals-to-constant]
break
1 if CONSTANT or True else 2 # [condition-evals-to-constant]
z = [x for x in range(10) if x or True] # [condition-evals-to-constant]
Z = [x for x in range(10) if x or True] # [condition-evals-to-constant]

# Simplifies recursively
assert True or CONSTANT or OTHER # [condition-evals-to-constant]
Expand Down
44 changes: 22 additions & 22 deletions tests/functional/c/consider/consider_iterating_dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,17 @@ def keys(self):
(key for key in {}.keys()) # [consider-iterating-dictionary]
{key for key in {}.keys()} # [consider-iterating-dictionary]
{key: key for key in {}.keys()} # [consider-iterating-dictionary]
COMP1 = [key for key in {}.keys()] # [consider-iterating-dictionary]
COMP2 = (key for key in {}.keys()) # [consider-iterating-dictionary]
COMP3 = {key for key in {}.keys()} # [consider-iterating-dictionary]
comp1 = [key for key in {}.keys()] # [consider-iterating-dictionary]
comp2 = (key for key in {}.keys()) # [consider-iterating-dictionary]
comp3 = {key for key in {}.keys()} # [consider-iterating-dictionary]
COMP4 = {key: key for key in {}.keys()} # [consider-iterating-dictionary]
for key in {}.keys(): # [consider-iterating-dictionary]
pass

# Issue #1247
DICT = {'a': 1, 'b': 2}
COMP1 = [k * 2 for k in DICT.keys()] + [k * 3 for k in DICT.keys()] # [consider-iterating-dictionary,consider-iterating-dictionary]
COMP2, COMP3 = [k * 2 for k in DICT.keys()], [k * 3 for k in DICT.keys()] # [consider-iterating-dictionary,consider-iterating-dictionary]
comp1 = [k * 2 for k in DICT.keys()] + [k * 3 for k in DICT.keys()] # [consider-iterating-dictionary,consider-iterating-dictionary]
comp2, comp3 = [k * 2 for k in DICT.keys()], [k * 3 for k in DICT.keys()] # [consider-iterating-dictionary,consider-iterating-dictionary]
SOME_TUPLE = ([k * 2 for k in DICT.keys()], [k * 3 for k in DICT.keys()]) # [consider-iterating-dictionary,consider-iterating-dictionary]

# Checks for membership checks
Expand All @@ -62,21 +62,21 @@ def keys(self):
pass
if [1] == dict():
pass
VAR = 1 in {}.keys() # [consider-iterating-dictionary]
VAR = 1 in {}
VAR = 1 in dict()
VAR = [1, 2] == {}.keys() in {False}
var = 1 in {}.keys() # [consider-iterating-dictionary]
var = 1 in {}
var = 1 in dict()
var = [1, 2] == {}.keys() in {False}

# Additional membership checks
# See: https://github.com/pylint-dev/pylint/issues/5323
metadata = {}
if "a" not in list(metadata.keys()): # [consider-iterating-dictionary]
METADATA = {}
if "a" not in list(METADATA.keys()): # [consider-iterating-dictionary]
print(1)
if "a" not in metadata.keys(): # [consider-iterating-dictionary]
if "a" not in METADATA.keys(): # [consider-iterating-dictionary]
print(1)
if "a" in list(metadata.keys()): # [consider-iterating-dictionary]
if "a" in list(METADATA.keys()): # [consider-iterating-dictionary]
print(1)
if "a" in metadata.keys(): # [consider-iterating-dictionary]
if "a" in METADATA.keys(): # [consider-iterating-dictionary]
print(1)


Expand All @@ -93,24 +93,24 @@ def inner_function():
return inner_function()
return InnerClass().another_function()

a_dict = {"a": 1, "b": 2, "c": 3}
a_set = {"c", "d"}
A_DICT = {"a": 1, "b": 2, "c": 3}
A_SET = {"c", "d"}

# Test bitwise operations. These should not raise msg because removing `.keys()`
# either gives error or ends in a different result
print(a_dict.keys() | a_set)
print(A_DICT.keys() | A_SET)

if "a" in a_dict.keys() | a_set:
if "a" in A_DICT.keys() | A_SET:
pass

if "a" in a_dict.keys() & a_set:
if "a" in A_DICT.keys() & A_SET:
pass

if 1 in a_dict.keys() ^ [1, 2]:
if 1 in A_DICT.keys() ^ [1, 2]:
pass

if "a" in a_dict.keys() or a_set: # [consider-iterating-dictionary]
if "a" in A_DICT.keys() or A_SET: # [consider-iterating-dictionary]
pass

if "a" in a_dict.keys() and a_set: # [consider-iterating-dictionary]
if "a" in A_DICT.keys() and A_SET: # [consider-iterating-dictionary]
pass
Loading