Skip to content

Hide imported names in stubs unless 'as id' is used (PEP 484) #3706

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jul 31, 2017
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
10 changes: 9 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2286,6 +2286,8 @@ class SymbolTableNode:
# If False, this name won't be imported via 'from <module> import *'.
# This has no effect on names within classes.
module_public = True
# If True, the name will be never exported (needed for stub files)
module_hidden = False
# For deserialized MODULE_REF nodes, the referenced module name;
# for other nodes, optionally the name of the referenced object.
cross_ref = None # type: Optional[str]
Expand All @@ -2302,11 +2304,13 @@ def __init__(self,
module_public: bool = True,
normalized: bool = False,
alias_tvars: Optional[List[str]] = None,
implicit: bool = False) -> None:
implicit: bool = False,
module_hidden: bool = False) -> None:
self.kind = kind
self.node = node
self.type_override = typ
self.mod_id = mod_id
self.module_hidden = module_hidden
self.module_public = module_public
self.normalized = normalized
self.alias_tvars = alias_tvars
Expand Down Expand Up @@ -2352,6 +2356,8 @@ def serialize(self, prefix: str, name: str) -> JsonDict:
data = {'.class': 'SymbolTableNode',
'kind': node_kinds[self.kind],
} # type: JsonDict
if self.module_hidden:
data['module_hidden'] = True
if not self.module_public:
data['module_public'] = False
if self.normalized:
Expand Down Expand Up @@ -2393,6 +2399,8 @@ def deserialize(cls, data: JsonDict) -> 'SymbolTableNode':
stnode = SymbolTableNode(kind, node, typ=typ)
if 'alias_tvars' in data:
stnode.alias_tvars = data['alias_tvars']
if 'module_hidden' in data:
stnode.module_hidden = data['module_hidden']
if 'module_public' in data:
stnode.module_public = data['module_public']
if 'normalized' in data:
Expand Down
26 changes: 16 additions & 10 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1317,12 +1317,11 @@ def visit_import(self, i: Import) -> None:
if as_id is not None:
self.add_module_symbol(id, as_id, module_public=True, context=i)
else:
# Modules imported in a stub file without using 'as x' won't get exported when
# doing 'from m import *'.
# Modules imported in a stub file without using 'as x' won't get exported
module_public = not self.is_stub_file
base = id.split('.')[0]
self.add_module_symbol(base, base, module_public=module_public,
context=i)
context=i, module_hidden=not module_public)
self.add_submodules_to_parent_modules(id, module_public)

def add_submodules_to_parent_modules(self, id: str, module_public: bool) -> None:
Expand Down Expand Up @@ -1351,11 +1350,12 @@ def add_submodules_to_parent_modules(self, id: str, module_public: bool) -> None
id = parent

def add_module_symbol(self, id: str, as_id: str, module_public: bool,
context: Context) -> None:
context: Context, module_hidden: bool = False) -> None:
if id in self.modules:
m = self.modules[id]
self.add_symbol(as_id, SymbolTableNode(MODULE_REF, m, self.cur_mod_id,
module_public=module_public), context)
module_public=module_public,
module_hidden=module_hidden), context)
else:
self.add_unknown_symbol(as_id, context, is_import=True)

Expand All @@ -1366,11 +1366,11 @@ def visit_import_from(self, imp: ImportFrom) -> None:
for id, as_id in imp.names:
node = module.names.get(id) if module else None
missing = False
possible_module_id = import_id + '.' + id

# If the module does not contain a symbol with the name 'id',
# try checking if it's a module instead.
if not node or node.kind == UNBOUND_IMPORTED:
possible_module_id = import_id + '.' + id
mod = self.modules.get(possible_module_id)
if mod is not None:
node = SymbolTableNode(MODULE_REF, mod, import_id)
Expand All @@ -1394,7 +1394,7 @@ def visit_import_from(self, imp: ImportFrom) -> None:
symbol = SymbolTableNode(GDEF, ast_node, name)
self.add_symbol(name, symbol, imp)
return
if node and node.kind != UNBOUND_IMPORTED:
if node and node.kind != UNBOUND_IMPORTED and not node.module_hidden:
node = self.normalize_type_alias(node, imp)
if not node:
return
Expand All @@ -1407,12 +1407,14 @@ def visit_import_from(self, imp: ImportFrom) -> None:
continue
# 'from m import x as x' exports x in a stub file.
module_public = not self.is_stub_file or as_id is not None
module_hidden = not module_public and possible_module_id not in self.modules
symbol = SymbolTableNode(node.kind, node.node,
self.cur_mod_id,
node.type_override,
module_public=module_public,
normalized=node.normalized,
alias_tvars=node.alias_tvars)
alias_tvars=node.alias_tvars,
module_hidden=module_hidden)
self.add_symbol(imported_id, symbol, imp)
elif module and not missing:
# Missing attribute.
Expand Down Expand Up @@ -3164,7 +3166,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
# bar in its namespace. This must be done for all types of bar.
file = cast(Optional[MypyFile], base.node) # can't use isinstance due to issue #2999
n = file.names.get(expr.name, None) if file is not None else None
if n:
if n and not n.module_hidden:
n = self.normalize_type_alias(n, expr)
if not n:
return
Expand Down Expand Up @@ -3508,7 +3510,11 @@ def lookup_qualified(self, name: str, ctx: Context,
break
if n:
n = self.normalize_type_alias(n, ctx)
return n
if n and n.module_hidden:
self.name_not_defined(name, ctx)
if n and not n.module_hidden:
return n
return None

def builtin_type(self, fully_qualified_name: str) -> Instance:
sym = self.lookup_fully_qualified(fully_qualified_name)
Expand Down
124 changes: 123 additions & 1 deletion test-data/unit/check-modules.test
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ x + '' # No error here
y + '' # No error here
z + '' # Error here
[file stub.pyi]
from non_stub import x # this import is not followed
from non_stub import x as x # this import is not followed

z = 42
[file non_stub.py]
Expand Down Expand Up @@ -1665,6 +1665,128 @@ m = n # E: Cannot assign multiple modules to name 'm' without explicit 'types.M

[builtins fixtures/module.pyi]

[case testNoReExportFromStubs]
from stub import Iterable # E: Module 'stub' has no attribute 'Iterable'
from stub import C

c = C()
reveal_type(c.x) # E: Revealed type is 'builtins.int'
it: Iterable[int]
reveal_type(it) # E: Revealed type is 'Any'

[file stub.pyi]
from typing import Iterable
from substub import C as C

def fun(x: Iterable[str]) -> Iterable[int]: pass

[file substub.pyi]
class C:
x: int

[builtins fixtures/module.pyi]

[case testNoReExportFromStubsMemberType]
import stub

c = stub.C()
reveal_type(c.x) # E: Revealed type is 'builtins.int'
it: stub.Iterable[int] # E: Name 'stub.Iterable' is not defined
reveal_type(it) # E: Revealed type is 'Any'

[file stub.pyi]
from typing import Iterable
from substub import C as C

def fun(x: Iterable[str]) -> Iterable[int]: pass

[file substub.pyi]
class C:
x: int

[builtins fixtures/module.pyi]

[case testNoReExportFromStubsMemberVar]
import stub

reveal_type(stub.y) # E: Revealed type is 'builtins.int'
reveal_type(stub.z) # E: Revealed type is 'Any' \
# E: Module has no attribute "z"

[file stub.pyi]
from substub import y as y
from substub import z

[file substub.pyi]
y = 42
z: int

[builtins fixtures/module.pyi]

[case testReExportChildStubs]
import mod
from mod import submod

reveal_type(mod.x) # E: Revealed type is 'mod.submod.C'
y = submod.C()
reveal_type(y.a) # E: Revealed type is 'builtins.str'

[file mod/__init__.pyi]
from . import submod
x: submod.C

[file mod/submod.pyi]
class C:
a: str

[builtins fixtures/module.pyi]

[case testReExportChildStubs2]
import mod.submod

y = mod.submod.C()
reveal_type(y.a) # E: Revealed type is 'builtins.str'

[file mod/__init__.pyi]
from . import submod
x: submod.C

[file mod/submod.pyi]
class C:
a: str

[builtins fixtures/module.pyi]

[case testNoReExportChildStubs]
import mod
from mod import C, D # E: Module 'mod' has no attribute 'C'

reveal_type(mod.x) # E: Revealed type is 'mod.submod.C'
mod.C # E: Module has no attribute "C"
y = mod.D()
reveal_type(y.a) # E: Revealed type is 'builtins.str'

[file mod/__init__.pyi]
from .submod import C, D as D
x: C

[file mod/submod.pyi]
class C: pass
class D:
a: str
[builtins fixtures/module.pyi]

[case testNoReExportNestedStub]
from stub import substub # E: Module 'stub' has no attribute 'substub'

[file stub.pyi]
import substub

[file substub.pyi]
x = 42

[file mod/submod.pyi]

[case testModuleAliasToQualifiedImport]
import package.module
alias = package.module
Expand Down