Skip to content

Commit 7b7c7ad

Browse files
carljmilevkivskyi
authored andcommitted
Detect and support module aliasing via assignment. (#3435)
* Detect and support module aliasing via assignment. In semantic analysis, if we find a simple assignment statement where the rvalue is a module, make the lvalue a direct alias to that same module. Fixes #1778. * Handle chained assignment and iterable unpacking assignment. * Remove test case that was only for exploration, not intended for inclusion. * Add some more tests for module assignment. * Also add tests for access/assignment of nonexistent module attribute. * Break down code and add comments for clarity; add test for mismatch lengths. * Naming improvements. * Support tracking module assignment in non-global scope. * Add more tests for unpacking mismatch cases. * Keep rvals always on the right. * Don't use a form of unpacking that is Py35+ only. * It's the zip that is problematic, not just the unpacking. * Add tests for module assignment in class and local scopes. * Simplify to single method. * Go back to annotating genericpath as Any in stdlib-sample. * Respect explicit type annotation and don't propagate module reference. * Backtrack to simple ModuleType var in case of incompatible module aliasing. * Remove stray pdb comment. * Also handle reassignment of an original (non-alias) module reference. * Simplify: fail on assignment of different modules to same variable without explicit annotation. * Style and working tweaks.
1 parent b5fa5d2 commit 7b7c7ad

File tree

4 files changed

+266
-2
lines changed

4 files changed

+266
-2
lines changed

mypy/semanal.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
15471547
self.process_namedtuple_definition(s)
15481548
self.process_typeddict_definition(s)
15491549
self.process_enum_call(s)
1550+
if not s.type:
1551+
self.process_module_assignment(s.lvalues, s.rvalue, s)
15501552

15511553
if (len(s.lvalues) == 1 and isinstance(s.lvalues[0], NameExpr) and
15521554
s.lvalues[0].name == '__all__' and s.lvalues[0].kind == GDEF and
@@ -2383,6 +2385,66 @@ def is_classvar(self, typ: Type) -> bool:
23832385
def fail_invalid_classvar(self, context: Context) -> None:
23842386
self.fail('ClassVar can only be used for assignments in class body', context)
23852387

2388+
def process_module_assignment(self, lvals: List[Expression], rval: Expression,
2389+
ctx: AssignmentStmt) -> None:
2390+
"""Propagate module references across assignments.
2391+
2392+
Recursively handles the simple form of iterable unpacking; doesn't
2393+
handle advanced unpacking with *rest, dictionary unpacking, etc.
2394+
2395+
In an expression like x = y = z, z is the rval and lvals will be [x,
2396+
y].
2397+
2398+
"""
2399+
if all(isinstance(v, (TupleExpr, ListExpr)) for v in lvals + [rval]):
2400+
# rval and all lvals are either list or tuple, so we are dealing
2401+
# with unpacking assignment like `x, y = a, b`. Mypy didn't
2402+
# understand our all(isinstance(...)), so cast them as
2403+
# Union[TupleExpr, ListExpr] so mypy knows it is safe to access
2404+
# their .items attribute.
2405+
seq_lvals = cast(List[Union[TupleExpr, ListExpr]], lvals)
2406+
seq_rval = cast(Union[TupleExpr, ListExpr], rval)
2407+
# given an assignment like:
2408+
# (x, y) = (m, n) = (a, b)
2409+
# we now have:
2410+
# seq_lvals = [(x, y), (m, n)]
2411+
# seq_rval = (a, b)
2412+
# We now zip this into:
2413+
# elementwise_assignments = [(a, x, m), (b, y, n)]
2414+
# where each elementwise assignment includes one element of rval and the
2415+
# corresponding element of each lval. Basically we unpack
2416+
# (x, y) = (m, n) = (a, b)
2417+
# into elementwise assignments
2418+
# x = m = a
2419+
# y = n = b
2420+
# and then we recursively call this method for each of those assignments.
2421+
# If the rval and all lvals are not all of the same length, zip will just ignore
2422+
# extra elements, so no error will be raised here; mypy will later complain
2423+
# about the length mismatch in type-checking.
2424+
elementwise_assignments = zip(seq_rval.items, *[v.items for v in seq_lvals])
2425+
for rv, *lvs in elementwise_assignments:
2426+
self.process_module_assignment(lvs, rv, ctx)
2427+
elif isinstance(rval, NameExpr):
2428+
rnode = self.lookup(rval.name, ctx)
2429+
if rnode and rnode.kind == MODULE_REF:
2430+
for lval in lvals:
2431+
if not isinstance(lval, NameExpr):
2432+
continue
2433+
# respect explicitly annotated type
2434+
if (isinstance(lval.node, Var) and lval.node.type is not None):
2435+
continue
2436+
lnode = self.lookup(lval.name, ctx)
2437+
if lnode:
2438+
if lnode.kind == MODULE_REF and lnode.node is not rnode.node:
2439+
self.fail(
2440+
"Cannot assign multiple modules to name '{}' "
2441+
"without explicit 'types.ModuleType' annotation".format(lval.name),
2442+
ctx)
2443+
# never create module alias except on initial var definition
2444+
elif lval.is_def:
2445+
lnode.kind = MODULE_REF
2446+
lnode.node = rnode.node
2447+
23862448
def process_enum_call(self, s: AssignmentStmt) -> None:
23872449
"""Check if s defines an Enum; if yes, store the definition in symbol table."""
23882450
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):

test-data/stdlib-samples/3.2/test/test_genericpath.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def safe_rmdir(dirname: str) -> None:
2323

2424
class GenericTest(unittest.TestCase):
2525
# The path module to be tested
26-
pathmodule = genericpath # type: Any
26+
pathmodule = genericpath # type: Any
2727
common_attributes = ['commonprefix', 'getsize', 'getatime', 'getctime',
2828
'getmtime', 'exists', 'isdir', 'isfile']
2929
attributes = [] # type: List[str]

test-data/unit/check-modules.test

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,3 +1439,204 @@ class C:
14391439
a = 'foo'
14401440

14411441
[builtins fixtures/module.pyi]
1442+
1443+
[case testModuleAlias]
1444+
import m
1445+
m2 = m
1446+
reveal_type(m2.a) # E: Revealed type is 'builtins.str'
1447+
m2.b # E: Module has no attribute "b"
1448+
m2.c = 'bar' # E: Module has no attribute "c"
1449+
1450+
[file m.py]
1451+
a = 'foo'
1452+
1453+
[builtins fixtures/module.pyi]
1454+
1455+
[case testClassModuleAlias]
1456+
import m
1457+
1458+
class C:
1459+
x = m
1460+
def foo(self) -> None:
1461+
reveal_type(self.x.a) # E: Revealed type is 'builtins.str'
1462+
1463+
[file m.py]
1464+
a = 'foo'
1465+
1466+
[builtins fixtures/module.pyi]
1467+
1468+
[case testLocalModuleAlias]
1469+
import m
1470+
1471+
def foo() -> None:
1472+
x = m
1473+
reveal_type(x.a) # E: Revealed type is 'builtins.str'
1474+
1475+
class C:
1476+
def foo(self) -> None:
1477+
x = m
1478+
reveal_type(x.a) # E: Revealed type is 'builtins.str'
1479+
1480+
[file m.py]
1481+
a = 'foo'
1482+
1483+
[builtins fixtures/module.pyi]
1484+
1485+
[case testChainedModuleAlias]
1486+
import m
1487+
m3 = m2 = m
1488+
m4 = m3
1489+
m5 = m4
1490+
reveal_type(m2.a) # E: Revealed type is 'builtins.str'
1491+
reveal_type(m3.a) # E: Revealed type is 'builtins.str'
1492+
reveal_type(m4.a) # E: Revealed type is 'builtins.str'
1493+
reveal_type(m5.a) # E: Revealed type is 'builtins.str'
1494+
1495+
[file m.py]
1496+
a = 'foo'
1497+
1498+
[builtins fixtures/module.pyi]
1499+
1500+
[case testMultiModuleAlias]
1501+
import m, n
1502+
m2, n2, (m3, n3) = m, n, [m, n]
1503+
reveal_type(m2.a) # E: Revealed type is 'builtins.str'
1504+
reveal_type(n2.b) # E: Revealed type is 'builtins.str'
1505+
reveal_type(m3.a) # E: Revealed type is 'builtins.str'
1506+
reveal_type(n3.b) # E: Revealed type is 'builtins.str'
1507+
1508+
x, y = m # E: 'types.ModuleType' object is not iterable
1509+
x, y, z = m, n # E: Need more than 2 values to unpack (3 expected)
1510+
x, y = m, m, m # E: Too many values to unpack (2 expected, 3 provided)
1511+
x, (y, z) = m, n # E: 'types.ModuleType' object is not iterable
1512+
x, (y, z) = m, (n, n, n) # E: Too many values to unpack (2 expected, 3 provided)
1513+
1514+
[file m.py]
1515+
a = 'foo'
1516+
1517+
[file n.py]
1518+
b = 'bar'
1519+
1520+
[builtins fixtures/module.pyi]
1521+
1522+
[case testModuleAliasWithExplicitAnnotation]
1523+
from typing import Any
1524+
import types
1525+
import m
1526+
mod_mod: types.ModuleType = m
1527+
mod_mod2: types.ModuleType
1528+
mod_mod2 = m
1529+
mod_mod3 = m # type: types.ModuleType
1530+
mod_any: Any = m
1531+
mod_int: int = m # E: Incompatible types in assignment (expression has type Module, variable has type "int")
1532+
1533+
reveal_type(mod_mod) # E: Revealed type is 'types.ModuleType'
1534+
mod_mod.a # E: Module has no attribute "a"
1535+
reveal_type(mod_mod2) # E: Revealed type is 'types.ModuleType'
1536+
mod_mod2.a # E: Module has no attribute "a"
1537+
reveal_type(mod_mod3) # E: Revealed type is 'types.ModuleType'
1538+
mod_mod3.a # E: Module has no attribute "a"
1539+
reveal_type(mod_any) # E: Revealed type is 'Any'
1540+
1541+
[file m.py]
1542+
a = 'foo'
1543+
1544+
[builtins fixtures/module.pyi]
1545+
1546+
[case testModuleAliasPassedToFunction]
1547+
import types
1548+
import m
1549+
1550+
def takes_module(x: types.ModuleType):
1551+
reveal_type(x.__file__) # E: Revealed type is 'builtins.str'
1552+
1553+
n = m
1554+
takes_module(m)
1555+
takes_module(n)
1556+
1557+
[file m.py]
1558+
a = 'foo'
1559+
1560+
[builtins fixtures/module.pyi]
1561+
1562+
[case testModuleAliasRepeated]
1563+
import m, n
1564+
1565+
if bool():
1566+
x = m
1567+
else:
1568+
x = 3 # E: Incompatible types in assignment (expression has type "int", variable has type Module)
1569+
1570+
if bool():
1571+
y = 3
1572+
else:
1573+
y = m # E: Incompatible types in assignment (expression has type Module, variable has type "int")
1574+
1575+
if bool():
1576+
z = m
1577+
else:
1578+
z = n # E: Cannot assign multiple modules to name 'z' without explicit 'types.ModuleType' annotation
1579+
1580+
[file m.py]
1581+
a = 'foo'
1582+
1583+
[file n.py]
1584+
a = 3
1585+
1586+
[builtins fixtures/module.pyi]
1587+
1588+
[case testModuleAliasRepeatedWithAnnotation]
1589+
import types
1590+
import m, n
1591+
1592+
x: types.ModuleType
1593+
if bool():
1594+
x = m
1595+
else:
1596+
x = n
1597+
1598+
x.a # E: Module has no attribute "a"
1599+
reveal_type(x.__file__) # E: Revealed type is 'builtins.str'
1600+
1601+
[file m.py]
1602+
a = 'foo'
1603+
1604+
[file n.py]
1605+
a = 3
1606+
1607+
[builtins fixtures/module.pyi]
1608+
1609+
[case testModuleAliasRepeatedComplex]
1610+
import m, n, o
1611+
1612+
x = m
1613+
x = n # E: Cannot assign multiple modules to name 'x' without explicit 'types.ModuleType' annotation
1614+
x = o # E: Cannot assign multiple modules to name 'x' without explicit 'types.ModuleType' annotation
1615+
1616+
y = o
1617+
y, z = m, n # E: Cannot assign multiple modules to name 'y' without explicit 'types.ModuleType' annotation
1618+
1619+
xx = m
1620+
xx = m
1621+
reveal_type(xx.a) # E: Revealed type is 'builtins.str'
1622+
1623+
[file m.py]
1624+
a = 'foo'
1625+
1626+
[file n.py]
1627+
a = 3
1628+
1629+
[file o.py]
1630+
a = 'bar'
1631+
1632+
[builtins fixtures/module.pyi]
1633+
1634+
[case testModuleAliasToOtherModule]
1635+
import m, n
1636+
m = n # E: Cannot assign multiple modules to name 'm' without explicit 'types.ModuleType' annotation
1637+
1638+
[file m.py]
1639+
1640+
[file n.py]
1641+
1642+
[builtins fixtures/module.pyi]

test-data/unit/lib-stub/types.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ def coroutine(func: _T) -> _T: pass
66

77
class bool: ...
88

9-
class ModuleType: ...
9+
class ModuleType:
10+
__file__ = ... # type: str

0 commit comments

Comments
 (0)