Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
30 changes: 30 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!


def visit_func_def(self, defn: FuncDef) -> None:
phase_info = self.postpone_nested_functions_stack[-1]
if phase_info != FUNCTION_SECOND_PHASE:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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, ...}.

Expand Down Expand Up @@ -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:
Expand Down
26 changes: 26 additions & 0 deletions mypy/test/data/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
15 changes: 15 additions & 0 deletions mypy/test/data/fixtures/module_all.py
Original file line number Diff line number Diff line change
@@ -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