Skip to content

Commit

Permalink
Backport (#7987) (#8005)
Browse files Browse the repository at this point in the history
* class attrs should not emit assigning-non-slot msg (#7987)

* Create `TERMINATING_FUNCS_QNAMES` (#7825)

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
  • Loading branch information
clavedeluna and Pierre-Sassoulas authored Jan 2, 2023
1 parent bb4a567 commit 1673aa6
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ repos:
args: [--prose-wrap=always, --print-width=88]
exclude: tests(/\w*)*data/
- repo: https://github.com/DanielNoord/pydocstringformatter
rev: v0.7.0
rev: v0.7.2
hooks:
- id: pydocstringformatter
exclude: *fixtures
Expand Down
3 changes: 3 additions & 0 deletions doc/whatsnew/fragments/6001.false_positive
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix false positive ``assigning-non-slot`` when a class attribute is re-assigned.

Closes #6001
20 changes: 13 additions & 7 deletions pylint/checkers/classes/class_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1627,6 +1627,10 @@ def _check_in_slots(self, node: nodes.AssignAttr) -> None:
# Properties circumvent the slots mechanism,
# so we should not emit a warning for them.
return
if node.attrname != "__class__" and utils.is_class_attr(
node.attrname, klass
):
return
if node.attrname in klass.locals:
for local_name in klass.locals.get(node.attrname):
statement = local_name.statement(future=True)
Expand All @@ -1642,7 +1646,12 @@ def _check_in_slots(self, node: nodes.AssignAttr) -> None:
slots, node.parent.value
):
return
self.add_message("assigning-non-slot", args=(node.attrname,), node=node)
self.add_message(
"assigning-non-slot",
args=(node.attrname,),
node=node,
confidence=INFERENCE,
)

@only_required_for_messages(
"protected-access", "no-classmethod-decorator", "no-staticmethod-decorator"
Expand Down Expand Up @@ -1777,7 +1786,7 @@ def _check_protected_attribute_access(
if (
self._is_classmethod(node.frame(future=True))
and self._is_inferred_instance(node.expr, klass)
and self._is_class_attribute(attrname, klass)
and self._is_class_or_instance_attribute(attrname, klass)
):
return

Expand Down Expand Up @@ -1824,19 +1833,16 @@ def _is_inferred_instance(expr, klass: nodes.ClassDef) -> bool:
return inferred._proxied is klass

@staticmethod
def _is_class_attribute(name: str, klass: nodes.ClassDef) -> bool:
def _is_class_or_instance_attribute(name: str, klass: nodes.ClassDef) -> bool:
"""Check if the given attribute *name* is a class or instance member of the
given *klass*.
Returns ``True`` if the name is a property in the given klass,
``False`` otherwise.
"""

try:
klass.getattr(name)
if utils.is_class_attr(name, klass):
return True
except astroid.NotFoundError:
pass

try:
klass.instance_attr(name)
Expand Down
42 changes: 42 additions & 0 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@
)
)

SINGLETON_VALUES = {True, False, None}

TERMINATING_FUNCS_QNAMES = frozenset(
{"_sitebuiltins.Quitter", "sys.exit", "posix._exit", "nt._exit"}
)


class NoSuchArgumentError(Exception):
pass
Expand Down Expand Up @@ -2028,3 +2034,39 @@ def is_module_ignored(
return True

return False


def is_singleton_const(node: nodes.NodeNG) -> bool:
return isinstance(node, nodes.Const) and any(
node.value is value for value in SINGLETON_VALUES
)


def is_terminating_func(node: nodes.Call) -> bool:
"""Detect call to exit(), quit(), os._exit(), or sys.exit()."""
if (
not isinstance(node.func, nodes.Attribute)
and not (isinstance(node.func, nodes.Name))
or isinstance(node.parent, nodes.Lambda)
):
return False

try:
for inferred in node.func.infer():
if (
hasattr(inferred, "qname")
and inferred.qname() in TERMINATING_FUNCS_QNAMES
):
return True
except (StopIteration, astroid.InferenceError):
pass

return False


def is_class_attr(name: str, klass: nodes.ClassDef) -> bool:
try:
klass.getattr(name)
return True
except astroid.NotFoundError:
return False
43 changes: 41 additions & 2 deletions tests/functional/a/assigning/assigning_non_slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
will trigger assigning-non-slot warning.
"""
# pylint: disable=too-few-public-methods, missing-docstring, import-error, redundant-u-string-prefix, unnecessary-dunder-call
# pylint: disable=attribute-defined-outside-init

from collections import deque

from missing import Unknown
Expand Down Expand Up @@ -129,7 +131,7 @@ def dont_emit_for_descriptors():
# This should not emit, because attr is
# a data descriptor
inst.data_descriptor = 'foo'
inst.non_data_descriptor = 'lala' # [assigning-non-slot]
inst.non_data_descriptor = 'lala'


class ClassWithSlots:
Expand All @@ -147,7 +149,8 @@ class ClassReassingingInvalidLayoutClass:
__slots__ = []

def release(self):
self.__class__ = ClassWithSlots # [assigning-non-slot]
self.__class__ = ClassWithSlots # [assigning-non-slot]
self.test = 'test' # [assigning-non-slot]


# pylint: disable=attribute-defined-outside-init
Expand Down Expand Up @@ -200,3 +203,39 @@ def dont_emit_for_defined_setattr():

child = ClassWithParentDefiningSetattr()
child.non_existent = "non-existent"

class ColorCls:
__slots__ = ()
COLOR = "red"


class Child(ColorCls):
__slots__ = ()


repro = Child()
Child.COLOR = "blue"

class MyDescriptor:
"""Basic descriptor."""

def __get__(self, instance, owner):
return 42

def __set__(self, instance, value):
pass


# Regression test from https://github.com/PyCQA/pylint/issues/6001
class Base:
__slots__ = ()

attr2 = MyDescriptor()


class Repro(Base):
__slots__ = ()


repro = Repro()
repro.attr2 = "anything"
10 changes: 5 additions & 5 deletions tests/functional/a/assigning/assigning_non_slot.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
assigning-non-slot:18:8:18:20:Bad.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED
assigning-non-slot:26:8:26:20:Bad2.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED
assigning-non-slot:36:8:36:20:Bad3.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED
assigning-non-slot:132:4:132:28:dont_emit_for_descriptors:Assigning to attribute 'non_data_descriptor' not defined in class slots:UNDEFINED
assigning-non-slot:150:8:150:22:ClassReassingingInvalidLayoutClass.release:Assigning to attribute '__class__' not defined in class slots:UNDEFINED
assigning-non-slot:20:8:20:20:Bad.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE
assigning-non-slot:28:8:28:20:Bad2.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE
assigning-non-slot:38:8:38:20:Bad3.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE
assigning-non-slot:152:8:152:22:ClassReassingingInvalidLayoutClass.release:Assigning to attribute '__class__' not defined in class slots:INFERENCE
assigning-non-slot:153:8:153:17:ClassReassingingInvalidLayoutClass.release:Assigning to attribute 'test' not defined in class slots:INFERENCE
2 changes: 1 addition & 1 deletion tests/functional/a/assigning/assigning_non_slot_4509.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
assigning-non-slot:18:8:18:17:Foo.__init__:Assigning to attribute '_bar' not defined in class slots:UNDEFINED
assigning-non-slot:18:8:18:17:Foo.__init__:Assigning to attribute '_bar' not defined in class slots:INFERENCE

0 comments on commit 1673aa6

Please sign in to comment.