Skip to content

Commit c87e413

Browse files
authored
Hide imported names in stubs unless 'as id' is used (PEP 484) (#3706)
Fixes #2927 PEP 484 specifies that only names imported as ``from mod import name as name`` should be re-exported. However, mypy didn't follow this rule so that this code passed without errors but obviously fails at runtime: from collections import TypeVar from weakref import Generic from getopt import List T = TypeVar('T') class C(Generic[T], List[int]): ... This PR makes all these errors.
1 parent 6366a02 commit c87e413

File tree

3 files changed

+148
-12
lines changed

3 files changed

+148
-12
lines changed

mypy/nodes.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2286,6 +2286,8 @@ class SymbolTableNode:
22862286
# If False, this name won't be imported via 'from <module> import *'.
22872287
# This has no effect on names within classes.
22882288
module_public = True
2289+
# If True, the name will be never exported (needed for stub files)
2290+
module_hidden = False
22892291
# For deserialized MODULE_REF nodes, the referenced module name;
22902292
# for other nodes, optionally the name of the referenced object.
22912293
cross_ref = None # type: Optional[str]
@@ -2302,11 +2304,13 @@ def __init__(self,
23022304
module_public: bool = True,
23032305
normalized: bool = False,
23042306
alias_tvars: Optional[List[str]] = None,
2305-
implicit: bool = False) -> None:
2307+
implicit: bool = False,
2308+
module_hidden: bool = False) -> None:
23062309
self.kind = kind
23072310
self.node = node
23082311
self.type_override = typ
23092312
self.mod_id = mod_id
2313+
self.module_hidden = module_hidden
23102314
self.module_public = module_public
23112315
self.normalized = normalized
23122316
self.alias_tvars = alias_tvars
@@ -2352,6 +2356,8 @@ def serialize(self, prefix: str, name: str) -> JsonDict:
23522356
data = {'.class': 'SymbolTableNode',
23532357
'kind': node_kinds[self.kind],
23542358
} # type: JsonDict
2359+
if self.module_hidden:
2360+
data['module_hidden'] = True
23552361
if not self.module_public:
23562362
data['module_public'] = False
23572363
if self.normalized:
@@ -2393,6 +2399,8 @@ def deserialize(cls, data: JsonDict) -> 'SymbolTableNode':
23932399
stnode = SymbolTableNode(kind, node, typ=typ)
23942400
if 'alias_tvars' in data:
23952401
stnode.alias_tvars = data['alias_tvars']
2402+
if 'module_hidden' in data:
2403+
stnode.module_hidden = data['module_hidden']
23962404
if 'module_public' in data:
23972405
stnode.module_public = data['module_public']
23982406
if 'normalized' in data:

mypy/semanal.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,12 +1317,11 @@ def visit_import(self, i: Import) -> None:
13171317
if as_id is not None:
13181318
self.add_module_symbol(id, as_id, module_public=True, context=i)
13191319
else:
1320-
# Modules imported in a stub file without using 'as x' won't get exported when
1321-
# doing 'from m import *'.
1320+
# Modules imported in a stub file without using 'as x' won't get exported
13221321
module_public = not self.is_stub_file
13231322
base = id.split('.')[0]
13241323
self.add_module_symbol(base, base, module_public=module_public,
1325-
context=i)
1324+
context=i, module_hidden=not module_public)
13261325
self.add_submodules_to_parent_modules(id, module_public)
13271326

13281327
def add_submodules_to_parent_modules(self, id: str, module_public: bool) -> None:
@@ -1351,11 +1350,12 @@ def add_submodules_to_parent_modules(self, id: str, module_public: bool) -> None
13511350
id = parent
13521351

13531352
def add_module_symbol(self, id: str, as_id: str, module_public: bool,
1354-
context: Context) -> None:
1353+
context: Context, module_hidden: bool = False) -> None:
13551354
if id in self.modules:
13561355
m = self.modules[id]
13571356
self.add_symbol(as_id, SymbolTableNode(MODULE_REF, m, self.cur_mod_id,
1358-
module_public=module_public), context)
1357+
module_public=module_public,
1358+
module_hidden=module_hidden), context)
13591359
else:
13601360
self.add_unknown_symbol(as_id, context, is_import=True)
13611361

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

13701371
# If the module does not contain a symbol with the name 'id',
13711372
# try checking if it's a module instead.
13721373
if not node or node.kind == UNBOUND_IMPORTED:
1373-
possible_module_id = import_id + '.' + id
13741374
mod = self.modules.get(possible_module_id)
13751375
if mod is not None:
13761376
node = SymbolTableNode(MODULE_REF, mod, import_id)
@@ -1394,7 +1394,7 @@ def visit_import_from(self, imp: ImportFrom) -> None:
13941394
symbol = SymbolTableNode(GDEF, ast_node, name)
13951395
self.add_symbol(name, symbol, imp)
13961396
return
1397-
if node and node.kind != UNBOUND_IMPORTED:
1397+
if node and node.kind != UNBOUND_IMPORTED and not node.module_hidden:
13981398
node = self.normalize_type_alias(node, imp)
13991399
if not node:
14001400
return
@@ -1407,12 +1407,14 @@ def visit_import_from(self, imp: ImportFrom) -> None:
14071407
continue
14081408
# 'from m import x as x' exports x in a stub file.
14091409
module_public = not self.is_stub_file or as_id is not None
1410+
module_hidden = not module_public and possible_module_id not in self.modules
14101411
symbol = SymbolTableNode(node.kind, node.node,
14111412
self.cur_mod_id,
14121413
node.type_override,
14131414
module_public=module_public,
14141415
normalized=node.normalized,
1415-
alias_tvars=node.alias_tvars)
1416+
alias_tvars=node.alias_tvars,
1417+
module_hidden=module_hidden)
14161418
self.add_symbol(imported_id, symbol, imp)
14171419
elif module and not missing:
14181420
# Missing attribute.
@@ -3164,7 +3166,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
31643166
# bar in its namespace. This must be done for all types of bar.
31653167
file = cast(Optional[MypyFile], base.node) # can't use isinstance due to issue #2999
31663168
n = file.names.get(expr.name, None) if file is not None else None
3167-
if n:
3169+
if n and not n.module_hidden:
31683170
n = self.normalize_type_alias(n, expr)
31693171
if not n:
31703172
return
@@ -3508,7 +3510,11 @@ def lookup_qualified(self, name: str, ctx: Context,
35083510
break
35093511
if n:
35103512
n = self.normalize_type_alias(n, ctx)
3511-
return n
3513+
if n and n.module_hidden:
3514+
self.name_not_defined(name, ctx)
3515+
if n and not n.module_hidden:
3516+
return n
3517+
return None
35123518

35133519
def builtin_type(self, fully_qualified_name: str) -> Instance:
35143520
sym = self.lookup_fully_qualified(fully_qualified_name)

test-data/unit/check-modules.test

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -980,7 +980,7 @@ x + '' # No error here
980980
y + '' # No error here
981981
z + '' # Error here
982982
[file stub.pyi]
983-
from non_stub import x # this import is not followed
983+
from non_stub import x as x # this import is not followed
984984

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

16661666
[builtins fixtures/module.pyi]
16671667

1668+
[case testNoReExportFromStubs]
1669+
from stub import Iterable # E: Module 'stub' has no attribute 'Iterable'
1670+
from stub import C
1671+
1672+
c = C()
1673+
reveal_type(c.x) # E: Revealed type is 'builtins.int'
1674+
it: Iterable[int]
1675+
reveal_type(it) # E: Revealed type is 'Any'
1676+
1677+
[file stub.pyi]
1678+
from typing import Iterable
1679+
from substub import C as C
1680+
1681+
def fun(x: Iterable[str]) -> Iterable[int]: pass
1682+
1683+
[file substub.pyi]
1684+
class C:
1685+
x: int
1686+
1687+
[builtins fixtures/module.pyi]
1688+
1689+
[case testNoReExportFromStubsMemberType]
1690+
import stub
1691+
1692+
c = stub.C()
1693+
reveal_type(c.x) # E: Revealed type is 'builtins.int'
1694+
it: stub.Iterable[int] # E: Name 'stub.Iterable' is not defined
1695+
reveal_type(it) # E: Revealed type is 'Any'
1696+
1697+
[file stub.pyi]
1698+
from typing import Iterable
1699+
from substub import C as C
1700+
1701+
def fun(x: Iterable[str]) -> Iterable[int]: pass
1702+
1703+
[file substub.pyi]
1704+
class C:
1705+
x: int
1706+
1707+
[builtins fixtures/module.pyi]
1708+
1709+
[case testNoReExportFromStubsMemberVar]
1710+
import stub
1711+
1712+
reveal_type(stub.y) # E: Revealed type is 'builtins.int'
1713+
reveal_type(stub.z) # E: Revealed type is 'Any' \
1714+
# E: Module has no attribute "z"
1715+
1716+
[file stub.pyi]
1717+
from substub import y as y
1718+
from substub import z
1719+
1720+
[file substub.pyi]
1721+
y = 42
1722+
z: int
1723+
1724+
[builtins fixtures/module.pyi]
1725+
1726+
[case testReExportChildStubs]
1727+
import mod
1728+
from mod import submod
1729+
1730+
reveal_type(mod.x) # E: Revealed type is 'mod.submod.C'
1731+
y = submod.C()
1732+
reveal_type(y.a) # E: Revealed type is 'builtins.str'
1733+
1734+
[file mod/__init__.pyi]
1735+
from . import submod
1736+
x: submod.C
1737+
1738+
[file mod/submod.pyi]
1739+
class C:
1740+
a: str
1741+
1742+
[builtins fixtures/module.pyi]
1743+
1744+
[case testReExportChildStubs2]
1745+
import mod.submod
1746+
1747+
y = mod.submod.C()
1748+
reveal_type(y.a) # E: Revealed type is 'builtins.str'
1749+
1750+
[file mod/__init__.pyi]
1751+
from . import submod
1752+
x: submod.C
1753+
1754+
[file mod/submod.pyi]
1755+
class C:
1756+
a: str
1757+
1758+
[builtins fixtures/module.pyi]
1759+
1760+
[case testNoReExportChildStubs]
1761+
import mod
1762+
from mod import C, D # E: Module 'mod' has no attribute 'C'
1763+
1764+
reveal_type(mod.x) # E: Revealed type is 'mod.submod.C'
1765+
mod.C # E: Module has no attribute "C"
1766+
y = mod.D()
1767+
reveal_type(y.a) # E: Revealed type is 'builtins.str'
1768+
1769+
[file mod/__init__.pyi]
1770+
from .submod import C, D as D
1771+
x: C
1772+
1773+
[file mod/submod.pyi]
1774+
class C: pass
1775+
class D:
1776+
a: str
1777+
[builtins fixtures/module.pyi]
1778+
1779+
[case testNoReExportNestedStub]
1780+
from stub import substub # E: Module 'stub' has no attribute 'substub'
1781+
1782+
[file stub.pyi]
1783+
import substub
1784+
1785+
[file substub.pyi]
1786+
x = 42
1787+
1788+
[file mod/submod.pyi]
1789+
16681790
[case testModuleAliasToQualifiedImport]
16691791
import package.module
16701792
alias = package.module

0 commit comments

Comments
 (0)