Skip to content

Make self-cls-assignment consider tuple assign. & ignore typing.cast() #5254

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

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ Release date: TBA

Closes #4426

* ``self-cls-assignment`` now also considers tuple assignment and does not raise a false positive on
``typing.cast()``

* Fix ``missing-function-docstring`` not being able to check ``__init__`` and other
magic methods even if the ``no-docstring-rgx`` setting was set to do so

Expand Down
3 changes: 3 additions & 0 deletions doc/whatsnew/2.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ Other Changes

Fixes part of #3688

* ``self-cls-assignment`` now also considers tuple assignment and does not raise a false positive on
``typing.cast()``

* ``undefined-variable`` now correctly triggers for assignment expressions in if ... else statements
This includes a basic form of control flow inference for if ... else statements using
constant boolean values
Expand Down
34 changes: 22 additions & 12 deletions pylint/checkers/variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"BinaryIO",
}
)
NON_REASSIGNING_CALLS = {"typing.cast"}


def _is_from_future_import(stmt, name):
Expand Down Expand Up @@ -2024,16 +2025,19 @@ def _store_type_annotation_names(self, node):

def _check_self_cls_assign(self, node):
"""Check that self/cls don't get assigned"""
assign_names = {
target.name
for target in node.targets
if isinstance(target, nodes.AssignName)
}
assign_names = []
for target in node.targets:
if isinstance(target, nodes.AssignName):
assign_names.append(target.name)
elif isinstance(target, nodes.Tuple):
assign_names.extend(
elt.name for elt in target.elts if isinstance(elt, nodes.AssignName)
)
scope = node.scope()
nonlocals_with_same_name = any(
child
for child in scope.body
if isinstance(child, nodes.Nonlocal) and assign_names & set(child.names)
if isinstance(child, nodes.Nonlocal) and assign_names + child.names
)
if nonlocals_with_same_name:
scope = node.scope().parent.scope()
Expand All @@ -2048,12 +2052,18 @@ def _check_self_cls_assign(self, node):
if not argument_names:
return
self_cls_name = argument_names[0]
target_assign_names = (
target.name
for target in node.targets
if isinstance(target, nodes.AssignName)
)
if self_cls_name in target_assign_names:
if self_cls_name in assign_names:
if isinstance(node.value, nodes.Call):
if utils.safe_infer(node.value.func).qname() in NON_REASSIGNING_CALLS:
return
elif isinstance(node.value, nodes.Tuple):
index = assign_names.index(self_cls_name)
if (
isinstance(node.value.elts[index], nodes.Call)
and utils.safe_infer(node.value.elts[index].func).qname()
in NON_REASSIGNING_CALLS
):
return
self.add_message("self-cls-assignment", node=node, args=(self_cls_name,))

def _check_unpacking(self, inferred, node, targets):
Expand Down
16 changes: 16 additions & 0 deletions tests/functional/s/self/self_cls_assignment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Warning about assigning self/cls variable."""
from __future__ import print_function

from typing import cast

# pylint: disable=too-few-public-methods, useless-object-inheritance

class Foo(object):
Expand All @@ -14,6 +17,8 @@ def self_foo(bar_):
def self_foofoo(self, lala):
"""Instance method, should warn for self"""
self = lala # [self-cls-assignment]
self, var = lala, 1 # [self-cls-assignment]
print(var)

@classmethod
def cls_foo(cls):
Expand Down Expand Up @@ -45,3 +50,14 @@ def _set_param(param):

_set_param(param)
return self


class TestTypingCast:
"""Test class for use of typing.cast"""

def function(self):
"""This function uses typing.cast to reassign self"""

self = cast(TestTypingCast, self)
Copy link
Member

Choose a reason for hiding this comment

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

Why would we cast to TestTypingCast here ? Wouldn't it be used in a more complex context ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I just used self's class again because a similar example caused me to make this change. We can change it to something else, perhaps even a more generic Type if you want!

self, var = cast(TestTypingCast, self), 1
print(var)
9 changes: 5 additions & 4 deletions tests/functional/s/self/self_cls_assignment.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
self-cls-assignment:11:8:Foo.self_foo:Invalid assignment to bar_ in method
self-cls-assignment:16:8:Foo.self_foofoo:Invalid assignment to self in method
self-cls-assignment:21:8:Foo.cls_foo:Invalid assignment to cls in method
self-cls-assignment:44:12:TestNonLocal.function._set_param:Invalid assignment to self in method
self-cls-assignment:14:8:Foo.self_foo:Invalid assignment to bar_ in method:HIGH
self-cls-assignment:19:8:Foo.self_foofoo:Invalid assignment to self in method:HIGH
self-cls-assignment:20:8:Foo.self_foofoo:Invalid assignment to self in method:HIGH
self-cls-assignment:26:8:Foo.cls_foo:Invalid assignment to cls in method:HIGH
self-cls-assignment:49:12:TestNonLocal.function._set_param:Invalid assignment to self in method:HIGH