@@ -702,6 +702,12 @@ def _has_same_layout_slots(
702
702
"Used when a class tries to extend an inherited Enum class. "
703
703
"Doing so will raise a TypeError at runtime." ,
704
704
),
705
+ "E0245" : (
706
+ "No such name %r in __slots__" ,
707
+ "declare-non-slot" ,
708
+ "Raised when a type annotation on a class is absent from the list of names in __slots__, "
709
+ "and __slots__ does not contain a __dict__ entry." ,
710
+ ),
705
711
"R0202" : (
706
712
"Consider using a decorator instead of calling classmethod" ,
707
713
"no-classmethod-decorator" ,
@@ -870,6 +876,7 @@ def _dummy_rgx(self) -> Pattern[str]:
870
876
"invalid-enum-extension" ,
871
877
"subclassed-final-class" ,
872
878
"implicit-flag-alias" ,
879
+ "declare-non-slot" ,
873
880
)
874
881
def visit_classdef (self , node : nodes .ClassDef ) -> None :
875
882
"""Init visit variable _accessed."""
@@ -878,6 +885,50 @@ def visit_classdef(self, node: nodes.ClassDef) -> None:
878
885
self ._check_proper_bases (node )
879
886
self ._check_typing_final (node )
880
887
self ._check_consistent_mro (node )
888
+ self ._check_declare_non_slot (node )
889
+
890
+ def _check_declare_non_slot (self , node : nodes .ClassDef ) -> None :
891
+ if not self ._has_valid_slots (node ):
892
+ return
893
+
894
+ slot_names = self ._get_classdef_slots_names (node )
895
+
896
+ # Stop if empty __slots__ in the class body, this likely indicates that
897
+ # this class takes part in multiple inheritance with other slotted classes.
898
+ if not slot_names :
899
+ return
900
+
901
+ # Stop if we find __dict__, since this means attributes can be set
902
+ # dynamically
903
+ if "__dict__" in slot_names :
904
+ return
905
+
906
+ for base in node .bases :
907
+ ancestor = safe_infer (base )
908
+ if not isinstance (ancestor , nodes .ClassDef ):
909
+ continue
910
+ # if any base doesn't have __slots__, attributes can be set dynamically, so stop
911
+ if not self ._has_valid_slots (ancestor ):
912
+ return
913
+ for slot_name in self ._get_classdef_slots_names (ancestor ):
914
+ if slot_name == "__dict__" :
915
+ return
916
+ slot_names .append (slot_name )
917
+
918
+ # Every class in bases has __slots__, our __slots__ is non-empty and there is no __dict__
919
+
920
+ for child in node .body :
921
+ if isinstance (child , nodes .AnnAssign ):
922
+ if child .value is not None :
923
+ continue
924
+ if isinstance (child .target , nodes .AssignName ):
925
+ if child .target .name not in slot_names :
926
+ self .add_message (
927
+ "declare-non-slot" ,
928
+ args = child .target .name ,
929
+ node = child .target ,
930
+ confidence = INFERENCE ,
931
+ )
881
932
882
933
def _check_consistent_mro (self , node : nodes .ClassDef ) -> None :
883
934
"""Detect that a class has a consistent mro or duplicate bases."""
@@ -1482,6 +1533,24 @@ def _check_functools_or_not(self, decorator: nodes.Attribute) -> bool:
1482
1533
1483
1534
return "functools" in dict (import_node .names )
1484
1535
1536
+ def _has_valid_slots (self , node : nodes .ClassDef ) -> bool :
1537
+ if "__slots__" not in node .locals :
1538
+ return False
1539
+
1540
+ for slots in node .ilookup ("__slots__" ):
1541
+ # check if __slots__ is a valid type
1542
+ if isinstance (slots , util .UninferableBase ):
1543
+ return False
1544
+ if not is_iterable (slots ) and not is_comprehension (slots ):
1545
+ return False
1546
+ if isinstance (slots , nodes .Const ):
1547
+ return False
1548
+ if not hasattr (slots , "itered" ):
1549
+ # we can't obtain the values, maybe a .deque?
1550
+ return False
1551
+
1552
+ return True
1553
+
1485
1554
def _check_slots (self , node : nodes .ClassDef ) -> None :
1486
1555
if "__slots__" not in node .locals :
1487
1556
return
@@ -1515,13 +1584,19 @@ def _check_slots(self, node: nodes.ClassDef) -> None:
1515
1584
continue
1516
1585
self ._check_redefined_slots (node , slots , values )
1517
1586
1518
- def _check_redefined_slots (
1519
- self ,
1520
- node : nodes .ClassDef ,
1521
- slots_node : nodes .NodeNG ,
1522
- slots_list : list [nodes .NodeNG ],
1523
- ) -> None :
1524
- """Check if `node` redefines a slot which is defined in an ancestor class."""
1587
+ def _get_classdef_slots_names (self , node : nodes .ClassDef ) -> list [str ]:
1588
+
1589
+ slots_names = []
1590
+ for slots in node .ilookup ("__slots__" ):
1591
+ if isinstance (slots , nodes .Dict ):
1592
+ values = [item [0 ] for item in slots .items ]
1593
+ else :
1594
+ values = slots .itered ()
1595
+ slots_names .extend (self ._get_slots_names (values ))
1596
+
1597
+ return slots_names
1598
+
1599
+ def _get_slots_names (self , slots_list : list [nodes .NodeNG ]) -> list [str ]:
1525
1600
slots_names : list [str ] = []
1526
1601
for slot in slots_list :
1527
1602
if isinstance (slot , nodes .Const ):
@@ -1531,6 +1606,16 @@ def _check_redefined_slots(
1531
1606
inferred_slot_value = getattr (inferred_slot , "value" , None )
1532
1607
if isinstance (inferred_slot_value , str ):
1533
1608
slots_names .append (inferred_slot_value )
1609
+ return slots_names
1610
+
1611
+ def _check_redefined_slots (
1612
+ self ,
1613
+ node : nodes .ClassDef ,
1614
+ slots_node : nodes .NodeNG ,
1615
+ slots_list : list [nodes .NodeNG ],
1616
+ ) -> None :
1617
+ """Check if `node` redefines a slot which is defined in an ancestor class."""
1618
+ slots_names : list [str ] = self ._get_slots_names (slots_list )
1534
1619
1535
1620
# Slots of all parent classes
1536
1621
ancestors_slots_names = {
0 commit comments