From 8c69531c59c26d1869bbe1aa543e58ef7cab2122 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Fri, 3 Jun 2016 16:39:43 -0500 Subject: [PATCH 1/4] Acknowledge __all__ (fixes #1606) --- mypy/checker.py | 9 +++++++++ mypy/messages.py | 1 + mypy/semanal.py | 32 +++++++++++++++++++++++++++++++ mypy/test/data/check-modules.test | 26 +++++++++++++++++++++++++ mypy/test/data/fixtures/module.py | 8 ++++++++ 5 files changed, 76 insertions(+) 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..54781ef1e74b2 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -159,6 +159,8 @@ class SemanticAnalyzer(NodeVisitor): modules = None # type: Dict[str, MypyFile] # Global name space for current module globals = None # type: SymbolTable + # Globals that were mentioned in __all__ and should be module_public. + global_exports = set() # Names declared using "global" (separate set for each scope) global_decls = None # type: List[Set[str]] # Names declated using "nonlocal" (separate set for each scope) @@ -222,6 +224,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() def visit_file(self, file_node: MypyFile, fnam: str) -> None: self.errors.set_file(fnam) @@ -250,6 +253,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 +1077,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 +1635,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 +1853,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 +2233,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..3f95d1da91557 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.py] + +[case testAllMustBeSequenceStr] +import typing +__all__ = [1, 2, 3] +[builtins fixtures/module.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.py b/mypy/test/data/fixtures/module.py index 38fe4cc318a3b..cc1b552bcf088 100644 --- a/mypy/test/data/fixtures/module.py +++ b/mypy/test/data/fixtures/module.py @@ -1,3 +1,6 @@ +from typing import Generic, Sequence, TypeVar +_T = TypeVar('_T') + class object: def __init__(self) -> None: pass class module: pass @@ -5,3 +8,8 @@ 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 From 721ff116caee7f786f50db139183ef4a605d797d Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Sat, 4 Jun 2016 14:44:49 -0500 Subject: [PATCH 2/4] Cleanup/style tweaks --- mypy/semanal.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 54781ef1e74b2..01acda8d78e07 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -159,8 +159,6 @@ class SemanticAnalyzer(NodeVisitor): modules = None # type: Dict[str, MypyFile] # Global name space for current module globals = None # type: SymbolTable - # Globals that were mentioned in __all__ and should be module_public. - global_exports = set() # Names declared using "global" (separate set for each scope) global_decls = None # type: List[Set[str]] # Names declated using "nonlocal" (separate set for each scope) @@ -224,7 +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() + self.all_exports = set() # type: Set[str] def visit_file(self, file_node: MypyFile, fnam: str) -> None: self.errors.set_file(fnam) @@ -1077,9 +1075,9 @@ 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)): + 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: @@ -1635,9 +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) + 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) @@ -1853,15 +1851,15 @@ 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 (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)): + 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]: From d333e12028e4c539f747d497cce1dc85490f5cd5 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Sat, 4 Jun 2016 15:00:28 -0500 Subject: [PATCH 3/4] Fix the tests and fixtures --- mypy/test/data/check-modules.test | 4 ++-- mypy/test/data/fixtures/module.py | 8 -------- mypy/test/data/fixtures/module_all.py | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 mypy/test/data/fixtures/module_all.py diff --git a/mypy/test/data/check-modules.test b/mypy/test/data/check-modules.test index 3f95d1da91557..d2f863e6205d0 100644 --- a/mypy/test/data/check-modules.test +++ b/mypy/test/data/check-modules.test @@ -409,12 +409,12 @@ __all__.append('c') __all__.extend(('d', 'e')) a = b = c = d = e = f = _g = 1 -[builtins fixtures/module.py] +[builtins fixtures/module_all.py] [case testAllMustBeSequenceStr] import typing __all__ = [1, 2, 3] -[builtins fixtures/module.py] +[builtins fixtures/module_all.py] [out] main: error: Type of __all__ must be Sequence[str], not List[int] diff --git a/mypy/test/data/fixtures/module.py b/mypy/test/data/fixtures/module.py index cc1b552bcf088..38fe4cc318a3b 100644 --- a/mypy/test/data/fixtures/module.py +++ b/mypy/test/data/fixtures/module.py @@ -1,6 +1,3 @@ -from typing import Generic, Sequence, TypeVar -_T = TypeVar('_T') - class object: def __init__(self) -> None: pass class module: pass @@ -8,8 +5,3 @@ 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 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 From 3f5f2eb8b9a263c6b76b78628e57db59c40fc602 Mon Sep 17 00:00:00 2001 From: Ryan Gonzalez Date: Sat, 4 Jun 2016 15:43:09 -0500 Subject: [PATCH 4/4] Fix linter errors --- mypy/semanal.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 01acda8d78e07..b13f0b6ad409c 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -222,7 +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] + self.all_exports = set() # type: Set[str] def visit_file(self, file_node: MypyFile, fnam: str) -> None: self.errors.set_file(fnam) @@ -1076,8 +1076,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: 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))): + 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: @@ -1634,8 +1634,8 @@ def visit_operator_assignment_stmt(self, 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) + 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) @@ -1852,14 +1852,14 @@ def visit_call_expr(self, expr: CallExpr) -> None: 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')): + 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))): + isinstance(expr.args[0], (ListExpr, TupleExpr))): self.add_exports(*expr.args[0].items) def translate_dict_call(self, call: CallExpr) -> Optional[DictExpr]: