diff --git a/mypy/checker.py b/mypy/checker.py index 0e29e337764a6..2d57dbf1c3753 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -431,6 +431,15 @@ def visit_file(self, file_node: MypyFile, path: str) -> None: self.errors.set_ignored_lines(set()) self.current_node_deferred = False + all_ = self.globals.get('__all__') + if all_ is not None and all_.type is not None: + seq_str = self.named_generic_type('typing.Sequence', + [self.named_type('builtins.str')]) + if not is_subtype(all_.type, seq_str): + str_seq_s, all_s = self.msg.format_distinctly(seq_str, all_.type) + self.fail(messages.ALL_MUST_BE_SEQ_STR.format(str_seq_s, all_s), + all_.node) + def check_second_pass(self): """Run second pass of type checking which goes through deferred nodes.""" self.pass_num = 1 diff --git a/mypy/messages.py b/mypy/messages.py index 568b5a93c81d7..362121f3ecdb0 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -81,6 +81,7 @@ ARGUMENT_TYPE_EXPECTED = "Function is missing a type annotation for one or more arguments" KEYWORD_ARGUMENT_REQUIRES_STR_KEY_TYPE = \ 'Keyword argument only valid with "str" key type in call to "dict"' +ALL_MUST_BE_SEQ_STR = 'Type of __all__ must be {}, not {}' class MessageBuilder: diff --git a/mypy/semanal.py b/mypy/semanal.py index 0326fc3a33968..b13f0b6ad409c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -222,6 +222,7 @@ def __init__(self, self.check_untyped_defs = check_untyped_defs self.postpone_nested_functions_stack = [FUNCTION_BOTH_PHASES] self.postponed_functions_stack = [] + self.all_exports = set() # type: Set[str] def visit_file(self, file_node: MypyFile, fnam: str) -> None: self.errors.set_file(fnam) @@ -250,6 +251,11 @@ def visit_file(self, file_node: MypyFile, fnam: str) -> None: self.errors.set_ignored_lines(set()) + if '__all__' in self.globals: + for name, g in self.globals.items(): + if name not in self.all_exports: + g.module_public = False + def visit_func_def(self, defn: FuncDef) -> None: phase_info = self.postpone_nested_functions_stack[-1] if phase_info != FUNCTION_SECOND_PHASE: @@ -1069,6 +1075,11 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: self.process_typevar_declaration(s) self.process_namedtuple_definition(s) + if (len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and + s.lvalues[0].name == '__all__' and s.lvalues[0].kind == GDEF and + isinstance(s.rvalue, (ListExpr, TupleExpr))): + self.add_exports(*s.rvalue.items) + def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: """Check if assignment creates a type alias and set it up as needed.""" # For now, type aliases only work at the top level of a module. @@ -1622,6 +1633,9 @@ def visit_operator_assignment_stmt(self, s: OperatorAssignmentStmt) -> None: s.lvalue.accept(self) s.rvalue.accept(self) + if (isinstance(s.lvalue, NameExpr) and s.lvalue.name == '__all__' and + s.lvalue.kind == GDEF and isinstance(s.rvalue, (ListExpr, TupleExpr))): + self.add_exports(*s.rvalue.items) def visit_while_stmt(self, s: WhileStmt) -> None: s.expr.accept(self) @@ -1837,6 +1851,17 @@ def visit_call_expr(self, expr: CallExpr) -> None: for a in expr.args: a.accept(self) + if (isinstance(expr.callee, MemberExpr) and + isinstance(expr.callee.expr, NameExpr) and + expr.callee.expr.name == '__all__' and + expr.callee.expr.kind == GDEF and + expr.callee.name in ('append', 'extend')): + if expr.callee.name == 'append' and expr.args: + self.add_exports(expr.args[0]) + elif (expr.callee.name == 'extend' and expr.args and + isinstance(expr.args[0], (ListExpr, TupleExpr))): + self.add_exports(*expr.args[0].items) + def translate_dict_call(self, call: CallExpr) -> Optional[DictExpr]: """Translate 'dict(x=y, ...)' to {'x': y, ...}. @@ -2206,6 +2231,11 @@ def add_local(self, node: Union[Var, FuncBase], ctx: Context) -> None: node._fullname = name self.locals[-1][name] = SymbolTableNode(LDEF, node) + def add_exports(self, *exps: Node) -> None: + for exp in exps: + if isinstance(exp, StrExpr): + self.all_exports.add(exp.value) + def check_no_global(self, n: str, ctx: Context, is_func: bool = False) -> None: if n in self.globals: diff --git a/mypy/test/data/check-modules.test b/mypy/test/data/check-modules.test index 265f46d2d682d..d2f863e6205d0 100644 --- a/mypy/test/data/check-modules.test +++ b/mypy/test/data/check-modules.test @@ -392,6 +392,32 @@ class B: pass x = A() y = B() +[case testExportedValuesInImportAll] +import typing +from m import * +_ = a +_ = b +_ = c +_ = d +_ = e +_ = f # E: Name 'f' is not defined +_ = _g # E: Name '_g' is not defined +[file m.py] +__all__ = ['a'] +__all__ += ('b',) +__all__.append('c') +__all__.extend(('d', 'e')) + +a = b = c = d = e = f = _g = 1 +[builtins fixtures/module_all.py] + +[case testAllMustBeSequenceStr] +import typing +__all__ = [1, 2, 3] +[builtins fixtures/module_all.py] +[out] +main: error: Type of __all__ must be Sequence[str], not List[int] + [case testEllipsisInitializerInStubFileWithType] import m m.x = '' # E: Incompatible types in assignment (expression has type "str", variable has type "int") diff --git a/mypy/test/data/fixtures/module_all.py b/mypy/test/data/fixtures/module_all.py new file mode 100644 index 0000000000000..cc1b552bcf088 --- /dev/null +++ b/mypy/test/data/fixtures/module_all.py @@ -0,0 +1,15 @@ +from typing import Generic, Sequence, TypeVar +_T = TypeVar('_T') + +class object: + def __init__(self) -> None: pass +class module: pass +class type: pass +class function: pass +class int: pass +class str: pass +class list(Generic[_T], Sequence[_T]): + def append(self, x: _T): pass + def extend(self, x: Sequence[_T]): pass + def __add__(self, rhs: Sequence[_T]) -> list[_T]: pass +class tuple: pass