Skip to content

Commit 1673aa6

Browse files
Backport (#7987) (#8005)
* class attrs should not emit assigning-non-slot msg (#7987) * Create `TERMINATING_FUNCS_QNAMES` (#7825) Co-authored-by: Pierre Sassoulas <[email protected]>
1 parent bb4a567 commit 1673aa6

File tree

7 files changed

+106
-16
lines changed

7 files changed

+106
-16
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ repos:
118118
args: [--prose-wrap=always, --print-width=88]
119119
exclude: tests(/\w*)*data/
120120
- repo: https://github.com/DanielNoord/pydocstringformatter
121-
rev: v0.7.0
121+
rev: v0.7.2
122122
hooks:
123123
- id: pydocstringformatter
124124
exclude: *fixtures
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix false positive ``assigning-non-slot`` when a class attribute is re-assigned.
2+
3+
Closes #6001

pylint/checkers/classes/class_checker.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -1627,6 +1627,10 @@ def _check_in_slots(self, node: nodes.AssignAttr) -> None:
16271627
# Properties circumvent the slots mechanism,
16281628
# so we should not emit a warning for them.
16291629
return
1630+
if node.attrname != "__class__" and utils.is_class_attr(
1631+
node.attrname, klass
1632+
):
1633+
return
16301634
if node.attrname in klass.locals:
16311635
for local_name in klass.locals.get(node.attrname):
16321636
statement = local_name.statement(future=True)
@@ -1642,7 +1646,12 @@ def _check_in_slots(self, node: nodes.AssignAttr) -> None:
16421646
slots, node.parent.value
16431647
):
16441648
return
1645-
self.add_message("assigning-non-slot", args=(node.attrname,), node=node)
1649+
self.add_message(
1650+
"assigning-non-slot",
1651+
args=(node.attrname,),
1652+
node=node,
1653+
confidence=INFERENCE,
1654+
)
16461655

16471656
@only_required_for_messages(
16481657
"protected-access", "no-classmethod-decorator", "no-staticmethod-decorator"
@@ -1777,7 +1786,7 @@ def _check_protected_attribute_access(
17771786
if (
17781787
self._is_classmethod(node.frame(future=True))
17791788
and self._is_inferred_instance(node.expr, klass)
1780-
and self._is_class_attribute(attrname, klass)
1789+
and self._is_class_or_instance_attribute(attrname, klass)
17811790
):
17821791
return
17831792

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

18261835
@staticmethod
1827-
def _is_class_attribute(name: str, klass: nodes.ClassDef) -> bool:
1836+
def _is_class_or_instance_attribute(name: str, klass: nodes.ClassDef) -> bool:
18281837
"""Check if the given attribute *name* is a class or instance member of the
18291838
given *klass*.
18301839
18311840
Returns ``True`` if the name is a property in the given klass,
18321841
``False`` otherwise.
18331842
"""
18341843

1835-
try:
1836-
klass.getattr(name)
1844+
if utils.is_class_attr(name, klass):
18371845
return True
1838-
except astroid.NotFoundError:
1839-
pass
18401846

18411847
try:
18421848
klass.instance_attr(name)

pylint/checkers/utils.py

+42
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@
231231
)
232232
)
233233

234+
SINGLETON_VALUES = {True, False, None}
235+
236+
TERMINATING_FUNCS_QNAMES = frozenset(
237+
{"_sitebuiltins.Quitter", "sys.exit", "posix._exit", "nt._exit"}
238+
)
239+
234240

235241
class NoSuchArgumentError(Exception):
236242
pass
@@ -2028,3 +2034,39 @@ def is_module_ignored(
20282034
return True
20292035

20302036
return False
2037+
2038+
2039+
def is_singleton_const(node: nodes.NodeNG) -> bool:
2040+
return isinstance(node, nodes.Const) and any(
2041+
node.value is value for value in SINGLETON_VALUES
2042+
)
2043+
2044+
2045+
def is_terminating_func(node: nodes.Call) -> bool:
2046+
"""Detect call to exit(), quit(), os._exit(), or sys.exit()."""
2047+
if (
2048+
not isinstance(node.func, nodes.Attribute)
2049+
and not (isinstance(node.func, nodes.Name))
2050+
or isinstance(node.parent, nodes.Lambda)
2051+
):
2052+
return False
2053+
2054+
try:
2055+
for inferred in node.func.infer():
2056+
if (
2057+
hasattr(inferred, "qname")
2058+
and inferred.qname() in TERMINATING_FUNCS_QNAMES
2059+
):
2060+
return True
2061+
except (StopIteration, astroid.InferenceError):
2062+
pass
2063+
2064+
return False
2065+
2066+
2067+
def is_class_attr(name: str, klass: nodes.ClassDef) -> bool:
2068+
try:
2069+
klass.getattr(name)
2070+
return True
2071+
except astroid.NotFoundError:
2072+
return False

tests/functional/a/assigning/assigning_non_slot.py

+41-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
will trigger assigning-non-slot warning.
33
"""
44
# pylint: disable=too-few-public-methods, missing-docstring, import-error, redundant-u-string-prefix, unnecessary-dunder-call
5+
# pylint: disable=attribute-defined-outside-init
6+
57
from collections import deque
68

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

134136

135137
class ClassWithSlots:
@@ -147,7 +149,8 @@ class ClassReassingingInvalidLayoutClass:
147149
__slots__ = []
148150

149151
def release(self):
150-
self.__class__ = ClassWithSlots # [assigning-non-slot]
152+
self.__class__ = ClassWithSlots # [assigning-non-slot]
153+
self.test = 'test' # [assigning-non-slot]
151154

152155

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

201204
child = ClassWithParentDefiningSetattr()
202205
child.non_existent = "non-existent"
206+
207+
class ColorCls:
208+
__slots__ = ()
209+
COLOR = "red"
210+
211+
212+
class Child(ColorCls):
213+
__slots__ = ()
214+
215+
216+
repro = Child()
217+
Child.COLOR = "blue"
218+
219+
class MyDescriptor:
220+
"""Basic descriptor."""
221+
222+
def __get__(self, instance, owner):
223+
return 42
224+
225+
def __set__(self, instance, value):
226+
pass
227+
228+
229+
# Regression test from https://github.com/PyCQA/pylint/issues/6001
230+
class Base:
231+
__slots__ = ()
232+
233+
attr2 = MyDescriptor()
234+
235+
236+
class Repro(Base):
237+
__slots__ = ()
238+
239+
240+
repro = Repro()
241+
repro.attr2 = "anything"
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
assigning-non-slot:18:8:18:20:Bad.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED
2-
assigning-non-slot:26:8:26:20:Bad2.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED
3-
assigning-non-slot:36:8:36:20:Bad3.__init__:Assigning to attribute 'missing' not defined in class slots:UNDEFINED
4-
assigning-non-slot:132:4:132:28:dont_emit_for_descriptors:Assigning to attribute 'non_data_descriptor' not defined in class slots:UNDEFINED
5-
assigning-non-slot:150:8:150:22:ClassReassingingInvalidLayoutClass.release:Assigning to attribute '__class__' not defined in class slots:UNDEFINED
1+
assigning-non-slot:20:8:20:20:Bad.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE
2+
assigning-non-slot:28:8:28:20:Bad2.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE
3+
assigning-non-slot:38:8:38:20:Bad3.__init__:Assigning to attribute 'missing' not defined in class slots:INFERENCE
4+
assigning-non-slot:152:8:152:22:ClassReassingingInvalidLayoutClass.release:Assigning to attribute '__class__' not defined in class slots:INFERENCE
5+
assigning-non-slot:153:8:153:17:ClassReassingingInvalidLayoutClass.release:Assigning to attribute 'test' not defined in class slots:INFERENCE
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
assigning-non-slot:18:8:18:17:Foo.__init__:Assigning to attribute '_bar' not defined in class slots:UNDEFINED
1+
assigning-non-slot:18:8:18:17:Foo.__init__:Assigning to attribute '_bar' not defined in class slots:INFERENCE

0 commit comments

Comments
 (0)