Skip to content

Commit 1091321

Browse files
authored
Infer empty list without annotation for __slots__ and module __all__ (#19348)
Fixes #10870, fixes #10103. This adds a fake `Iterable[str]` context when checking the following: * `__all__ = []` at top level * `__slots__ = []` at class level (also works for sets but not for dicts) Additionally, this fixes a bug with `__slots__` being mistakenly checked in other contexts (at top level or in function bodies), so e.g. the following is now accepted: ```python def foo() -> None: __slots__ = 1 ```
1 parent 5d59e43 commit 1091321

File tree

4 files changed

+63
-2
lines changed

4 files changed

+63
-2
lines changed

mypy/checker.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3138,7 +3138,7 @@ def check_assignment(
31383138
else:
31393139
self.check_getattr_method(signature, lvalue, name)
31403140

3141-
if name == "__slots__":
3141+
if name == "__slots__" and self.scope.active_class() is not None:
31423142
typ = lvalue_type or self.expr_checker.accept(rvalue)
31433143
self.check_slots_definition(typ, lvalue)
31443144
if name == "__match_args__" and inferred is not None:
@@ -3317,6 +3317,12 @@ def get_variable_type_context(self, inferred: Var, rvalue: Expression) -> Type |
33173317
type_contexts.append(base_type)
33183318
# Use most derived supertype as type context if available.
33193319
if not type_contexts:
3320+
if inferred.name == "__slots__" and self.scope.active_class() is not None:
3321+
str_type = self.named_type("builtins.str")
3322+
return self.named_generic_type("typing.Iterable", [str_type])
3323+
if inferred.name == "__all__" and self.scope.is_top_level():
3324+
str_type = self.named_type("builtins.str")
3325+
return self.named_generic_type("typing.Sequence", [str_type])
33203326
return None
33213327
candidate = type_contexts[0]
33223328
for other in type_contexts:

mypy/checker_shared.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ def current_self_type(self) -> Instance | TupleType | None:
334334
return fill_typevars(item)
335335
return None
336336

337+
def is_top_level(self) -> bool:
338+
"""Is current scope top-level (no classes or functions)?"""
339+
return len(self.stack) == 1
340+
337341
@contextmanager
338342
def push_function(self, item: FuncItem) -> Iterator[None]:
339343
self.stack.append(item)

test-data/unit/check-modules.test

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,35 @@ import typing
423423
__all__ = [1, 2, 3]
424424
[builtins fixtures/module_all.pyi]
425425
[out]
426-
main:2: error: Type of __all__ must be "Sequence[str]", not "list[int]"
426+
main:2: error: List item 0 has incompatible type "int"; expected "str"
427+
main:2: error: List item 1 has incompatible type "int"; expected "str"
428+
main:2: error: List item 2 has incompatible type "int"; expected "str"
429+
430+
[case testAllMustBeSequenceStr2]
431+
import typing
432+
__all__ = 1 # E: Type of __all__ must be "Sequence[str]", not "int"
433+
reveal_type(__all__) # N: Revealed type is "builtins.int"
434+
[builtins fixtures/module_all.pyi]
435+
436+
[case testAllMustBeSequenceStr3]
437+
import typing
438+
__all__ = set() # E: Need type annotation for "__all__" (hint: "__all__: set[<type>] = ...") \
439+
# E: Type of __all__ must be "Sequence[str]", not "set[Any]"
440+
reveal_type(__all__) # N: Revealed type is "builtins.set[Any]"
441+
[builtins fixtures/set.pyi]
442+
443+
[case testModuleAllEmptyList]
444+
__all__ = []
445+
reveal_type(__all__) # N: Revealed type is "builtins.list[builtins.str]"
446+
[builtins fixtures/module_all.pyi]
447+
448+
[case testDunderAllNotGlobal]
449+
class A:
450+
__all__ = 1
451+
452+
def foo() -> None:
453+
__all__ = 1
454+
[builtins fixtures/module_all.pyi]
427455

428456
[case testUnderscoreExportedValuesInImportAll]
429457
import typing

test-data/unit/check-slots.test

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,29 @@ class A:
496496
self.missing = 3
497497
[builtins fixtures/dict.pyi]
498498

499+
[case testSlotsNotInClass]
500+
# Shouldn't be triggered
501+
__slots__ = [1, 2]
502+
reveal_type(__slots__) # N: Revealed type is "builtins.list[builtins.int]"
503+
504+
def foo() -> None:
505+
__slots__ = 1
506+
reveal_type(__slots__) # N: Revealed type is "builtins.int"
507+
508+
[case testSlotsEmptyList]
509+
class A:
510+
__slots__ = []
511+
reveal_type(__slots__) # N: Revealed type is "builtins.list[builtins.str]"
512+
513+
reveal_type(A.__slots__) # N: Revealed type is "builtins.list[builtins.str]"
514+
515+
[case testSlotsEmptySet]
516+
class A:
517+
__slots__ = set()
518+
reveal_type(__slots__) # N: Revealed type is "builtins.set[builtins.str]"
519+
520+
reveal_type(A.__slots__) # N: Revealed type is "builtins.set[builtins.str]"
521+
[builtins fixtures/set.pyi]
499522

500523
[case testSlotsWithAny]
501524
from typing import Any

0 commit comments

Comments
 (0)