Skip to content

Commit 3327563

Browse files
Michael0x2agvanrossum
authored andcommitted
Make parent patching occur only on imports (#2061)
* Remove the patch parent phase This commit removes the "patch_parent" phase. The logic within those phases are now run only when a file is actually importing something, instead of being done automatically. This fixes some irregularities with how imports are handled. In particular, the "patch parent" stage was interacting poorly with implicit module references since the automatic patch would add back in an explicit module reference even when it was removed, which could preserve an implicit reference that should have instead been broken. That is, the presense of "patch parent" was sometimes concealing errors that should have been caught. * Fix bugs revealed by patch_parent fix This commit tweaks some subtly broken imports that were not caught before due to the automatic patching patch_parent performed. * Test check parent patching works for all imports Previously, my modifications to the parent patching process only took place when loading imports of the form `import a.b.c`. This commit adds more extensive tests to confirm the parent patching happens in all kinds of imports (i.e. `from a.b import c`). * Correct parent patching to work for all imports My previous modification to parent patching took place only when doing imports of the form `import a.b.c`. This commit changes this so parent patching happens when doing any kind of import.
1 parent 675e074 commit 3327563

File tree

6 files changed

+203
-20
lines changed

6 files changed

+203
-20
lines changed

mypy/build.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,19 +1400,6 @@ def parse_file(self) -> None:
14001400
self.dep_line_map = dep_line_map
14011401
self.check_blockers()
14021402

1403-
def patch_parent(self) -> None:
1404-
# Include module in the symbol table of the enclosing package.
1405-
if '.' not in self.id:
1406-
return
1407-
manager = self.manager
1408-
modules = manager.modules
1409-
parent, child = self.id.rsplit('.', 1)
1410-
if parent in modules:
1411-
manager.trace("Added %s.%s" % (parent, child))
1412-
modules[parent].names[child] = SymbolTableNode(MODULE_REF, self.tree, parent)
1413-
else:
1414-
manager.log("Hm... couldn't add %s.%s" % (parent, child))
1415-
14161403
def semantic_analysis(self) -> None:
14171404
with self.wrap_context():
14181405
self.manager.semantic_analyzer.visit_file(self.tree, self.xpath)
@@ -1683,8 +1670,6 @@ def process_fresh_scc(graph: Graph, scc: List[str]) -> None:
16831670
"""Process the modules in one SCC from their cached data."""
16841671
for id in scc:
16851672
graph[id].load_tree()
1686-
for id in scc:
1687-
graph[id].patch_parent()
16881673
for id in scc:
16891674
graph[id].fix_cross_refs()
16901675
for id in scc:
@@ -1698,8 +1683,6 @@ def process_stale_scc(graph: Graph, scc: List[str]) -> None:
16981683
# If the former, parse_file() is a no-op.
16991684
graph[id].parse_file()
17001685
graph[id].fix_suppressed_dependencies(graph)
1701-
for id in scc:
1702-
graph[id].patch_parent()
17031686
for id in scc:
17041687
graph[id].semantic_analysis()
17051688
for id in scc:

mypy/checkexpr.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
from mypy.constraints import get_actual_type
3737
from mypy.checkstrformat import StringFormatterChecker
3838
from mypy.expandtype import expand_type
39-
import mypy.checkexpr
4039

4140
from mypy import experiments
4241

@@ -61,15 +60,15 @@ class ExpressionChecker:
6160
# This is shared with TypeChecker, but stored also here for convenience.
6261
msg = None # type: MessageBuilder
6362

64-
strfrm_checker = None # type: mypy.checkstrformat.StringFormatterChecker
63+
strfrm_checker = None # type: StringFormatterChecker
6564

6665
def __init__(self,
6766
chk: 'mypy.checker.TypeChecker',
6867
msg: MessageBuilder) -> None:
6968
"""Construct an expression type checker."""
7069
self.chk = chk
7170
self.msg = msg
72-
self.strfrm_checker = mypy.checkexpr.StringFormatterChecker(self, self.chk, self.msg)
71+
self.strfrm_checker = StringFormatterChecker(self, self.chk, self.msg)
7372

7473
def visit_name_expr(self, e: NameExpr) -> Type:
7574
"""Type check a name expression.

mypy/checkstrformat.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
if False:
1414
# break import cycle only needed for mypy
1515
import mypy.checker
16+
import mypy.checkexpr
1617
from mypy import messages
1718
from mypy.messages import MessageBuilder
1819

mypy/semanal.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,30 @@ def visit_import(self, i: Import) -> None:
894894
base = id.split('.')[0]
895895
self.add_module_symbol(base, base, module_public=module_public,
896896
context=i)
897+
self.add_submodules_to_parent_modules(id, module_public)
898+
899+
def add_submodules_to_parent_modules(self, id: str, module_public: bool) -> None:
900+
"""Recursively adds a reference to a newly loaded submodule to its parent.
901+
902+
When you import a submodule in any way, Python will add a reference to that
903+
submodule to its parent. So, if you do something like `import A.B` or
904+
`from A import B` or `from A.B import Foo`, Python will add a reference to
905+
module A.B to A's namespace.
906+
907+
Note that this "parent patching" process is completely independent from any
908+
changes made to the *importer's* namespace. For example, if you have a file
909+
named `foo.py` where you do `from A.B import Bar`, then foo's namespace will
910+
be modified to contain a reference to only Bar. Independently, A's namespace
911+
will be modified to contain a reference to `A.B`.
912+
"""
913+
while '.' in id:
914+
parent, child = id.rsplit('.', 1)
915+
modules_loaded = parent in self.modules and id in self.modules
916+
if modules_loaded and child not in self.modules[parent].names:
917+
sym = SymbolTableNode(MODULE_REF, self.modules[id], parent,
918+
module_public=module_public)
919+
self.modules[parent].names[child] = sym
920+
id = parent
897921

898922
def add_module_symbol(self, id: str, as_id: str, module_public: bool,
899923
context: Context) -> None:
@@ -908,8 +932,19 @@ def visit_import_from(self, imp: ImportFrom) -> None:
908932
import_id = self.correct_relative_import(imp)
909933
if import_id in self.modules:
910934
module = self.modules[import_id]
935+
self.add_submodules_to_parent_modules(import_id, True)
911936
for id, as_id in imp.names:
912937
node = module.names.get(id)
938+
939+
# If the module does not contain a symbol with the name 'id',
940+
# try checking if it's a module instead.
941+
if id not in module.names or node.kind == UNBOUND_IMPORTED:
942+
possible_module_id = import_id + '.' + id
943+
mod = self.modules.get(possible_module_id)
944+
if mod is not None:
945+
node = SymbolTableNode(MODULE_REF, mod, import_id)
946+
self.add_submodules_to_parent_modules(possible_module_id, True)
947+
913948
if node and node.kind != UNBOUND_IMPORTED:
914949
node = self.normalize_type_alias(node, imp)
915950
if not node:
@@ -991,6 +1026,7 @@ def visit_import_all(self, i: ImportAll) -> None:
9911026
i_id = self.correct_relative_import(i)
9921027
if i_id in self.modules:
9931028
m = self.modules[i_id]
1029+
self.add_submodules_to_parent_modules(i_id, True)
9941030
for name, node in m.names.items():
9951031
node = self.normalize_type_alias(node, i)
9961032
if not name.startswith('_') and node.module_public:

test-data/unit/check-modules.test

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,169 @@ import m.a
711711
[out]
712712

713713

714+
-- Checks dealing with submodules and different kinds of imports
715+
-- -------------------------------------------------------------
716+
717+
[case testSubmoduleRegularImportAddsAllParents]
718+
import a.b.c
719+
reveal_type(a.value) # E: Revealed type is 'builtins.int'
720+
reveal_type(a.b.value) # E: Revealed type is 'builtins.str'
721+
reveal_type(a.b.c.value) # E: Revealed type is 'builtins.float'
722+
b.value # E: Name 'b' is not defined
723+
c.value # E: Name 'c' is not defined
724+
725+
[file a/__init__.py]
726+
value = 3
727+
[file a/b/__init__.py]
728+
value = "a"
729+
[file a/b/c.py]
730+
value = 3.2
731+
[out]
732+
733+
[case testSubmoduleImportAsDoesNotAddParents]
734+
import a.b.c as foo
735+
reveal_type(foo.value) # E: Revealed type is 'builtins.float'
736+
a.value # E: Name 'a' is not defined
737+
b.value # E: Name 'b' is not defined
738+
c.value # E: Name 'c' is not defined
739+
740+
[file a/__init__.py]
741+
value = 3
742+
[file a/b/__init__.py]
743+
value = "a"
744+
[file a/b/c.py]
745+
value = 3.2
746+
[out]
747+
748+
[case testSubmoduleImportFromDoesNotAddParents]
749+
from a import b
750+
reveal_type(b.value) # E: Revealed type is 'builtins.str'
751+
b.c.value # E: "module" has no attribute "c"
752+
a.value # E: Name 'a' is not defined
753+
754+
[file a/__init__.py]
755+
value = 3
756+
[file a/b/__init__.py]
757+
value = "a"
758+
[file a/b/c.py]
759+
value = 3.2
760+
[builtins fixtures/module.pyi]
761+
[out]
762+
763+
[case testSubmoduleImportFromDoesNotAddParents2]
764+
from a.b import c
765+
reveal_type(c.value) # E: Revealed type is 'builtins.float'
766+
a.value # E: Name 'a' is not defined
767+
b.value # E: Name 'b' is not defined
768+
769+
[file a/__init__.py]
770+
value = 3
771+
[file a/b/__init__.py]
772+
value = "a"
773+
[file a/b/c.py]
774+
value = 3.2
775+
[out]
776+
777+
[case testSubmoduleRegularImportNotDirectlyAddedToParent]
778+
import a.b.c
779+
def accept_float(x: float) -> None: pass
780+
accept_float(a.b.c.value)
781+
782+
[file a/__init__.py]
783+
value = 3
784+
b.value
785+
a.b.value
786+
787+
[file a/b/__init__.py]
788+
value = "a"
789+
c.value
790+
a.b.c.value
791+
792+
[file a/b/c.py]
793+
value = 3.2
794+
[out]
795+
main:1: note: In module imported here:
796+
tmp/a/b/__init__.py:2: error: Name 'c' is not defined
797+
tmp/a/b/__init__.py:3: error: Name 'a' is not defined
798+
tmp/a/__init__.py:2: error: Name 'b' is not defined
799+
tmp/a/__init__.py:3: error: Name 'a' is not defined
800+
801+
[case testSubmoduleMixingLocalAndQualifiedNames]
802+
from a.b import MyClass
803+
val1 = None # type: a.b.MyClass # E: Name 'a' is not defined
804+
val2 = None # type: MyClass
805+
806+
[file a/__init__.py]
807+
[file a/b.py]
808+
class MyClass: pass
809+
[out]
810+
811+
[case testSubmoduleMixingImportFrom]
812+
import parent.child
813+
814+
[file parent/__init__.py]
815+
816+
[file parent/common.py]
817+
class SomeClass: pass
818+
819+
[file parent/child.py]
820+
from parent.common import SomeClass
821+
from parent import common
822+
foo = parent.common.SomeClass()
823+
824+
[builtins fixtures/module.pyi]
825+
[out]
826+
main:1: note: In module imported here:
827+
tmp/parent/child.py:3: error: Name 'parent' is not defined
828+
829+
[case testSubmoduleMixingImportFromAndImport]
830+
import parent.child
831+
832+
[file parent/__init__.py]
833+
834+
[file parent/common.py]
835+
class SomeClass: pass
836+
837+
[file parent/unrelated.py]
838+
class ShouldNotLoad: pass
839+
840+
[file parent/child.py]
841+
from parent.common import SomeClass
842+
import parent
843+
844+
# Note, since this might be unintuitive -- when `parent.common` is loaded in any way,
845+
# shape, or form, it's added to `parent`'s namespace, which is why the below line
846+
# succeeds.
847+
foo = parent.common.SomeClass()
848+
reveal_type(foo)
849+
bar = parent.unrelated.ShouldNotLoad()
850+
851+
[builtins fixtures/module.pyi]
852+
[out]
853+
main:1: note: In module imported here:
854+
tmp/parent/child.py:8: error: Revealed type is 'parent.common.SomeClass'
855+
tmp/parent/child.py:9: error: "module" has no attribute "unrelated"
856+
857+
[case testSubmoduleMixingImportFromAndImport2]
858+
import parent.child
859+
860+
[file parent/__init__.py]
861+
862+
[file parent/common.py]
863+
class SomeClass: pass
864+
865+
[file parent/child.py]
866+
from parent import common
867+
import parent
868+
foo = parent.common.SomeClass()
869+
reveal_type(foo)
870+
871+
[builtins fixtures/module.pyi]
872+
[out]
873+
main:1: note: In module imported here:
874+
tmp/parent/child.py:4: error: Revealed type is 'parent.common.SomeClass'
875+
876+
714877
-- Misc
715878

716879
[case testInheritFromBadImport]

test-data/unit/fixtures/module.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ class type: pass
55
class function: pass
66
class int: pass
77
class str: pass
8+
class bool: pass

0 commit comments

Comments
 (0)