Skip to content

Commit 34c1440

Browse files
committed
Fix a bug in pytype's import resolution for relative imports.
We made the (erroneous) assumption that "from . import X" can only import a module, which was causing trouble for typeshed. See python/typeshed#5762 (comment) for context. Fixing this exposed another bug where load_pytd.Module.is_package didn't work correctly for stubs that ship with pytype, which I also fixed. PiperOrigin-RevId: 388369899
1 parent c104260 commit 34c1440

File tree

3 files changed

+30
-20
lines changed

3 files changed

+30
-20
lines changed

pytype/load_pytd.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -384,11 +384,18 @@ def _parse_predefined(self, pytd_subdir, module, as_package=False):
384384
return ast
385385

386386
def get_builtin(self, subdir, module_name):
387+
"""Load a stub that ships with pytype."""
387388
builtin_dir = file_utils.get_versioned_path(subdir, self.python_version)
388389
mod = self._parse_predefined(builtin_dir, module_name)
389-
if not mod:
390+
# For stubs in pytype's stubs/ directory, we use the module name prefixed
391+
# with "pytd:" for the filename. Package filenames need an "/__init__.pyi"
392+
# suffix for Module.is_package to recognize them.
393+
if mod:
394+
filename = module_name
395+
else:
390396
mod = self._parse_predefined(builtin_dir, module_name, as_package=True)
391-
return mod
397+
filename = os.path.join(module_name, "__init__.pyi")
398+
return filename, mod
392399

393400

394401
class Loader:
@@ -695,9 +702,9 @@ def _load_builtin(self, subdir, module_name, third_party_only=False):
695702
"""Load a pytd/pyi that ships with pytype or typeshed."""
696703
# Try our own type definitions first.
697704
if not third_party_only:
698-
mod_ast = self._builtin_loader.get_builtin(subdir, module_name)
705+
filename, mod_ast = self._builtin_loader.get_builtin(subdir, module_name)
699706
if mod_ast:
700-
return self.load_file(filename=self.PREFIX + module_name,
707+
return self.load_file(filename=self.PREFIX + filename,
701708
module_name=module_name, mod_ast=mod_ast)
702709
if self.use_typeshed:
703710
return self._load_typeshed_builtin(subdir, module_name)

pytype/load_pytd_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,16 @@ def f(x: T) -> T: ...
483483
from b import *
484484
""")
485485

486+
def test_import_class_from_parent_module(self):
487+
with file_utils.Tempdir() as d:
488+
d.create_file("foo/__init__.pyi", "class Foo: ...")
489+
d.create_file("foo/bar.pyi", """
490+
from . import Foo
491+
class Bar(Foo): ...
492+
""")
493+
loader = load_pytd.Loader(None, self.python_version, pythonpath=[d.path])
494+
loader.import_name("foo.bar")
495+
486496

487497
class ImportTypeMacroTest(_LoaderTest):
488498

pytype/pyi/modules.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""Handling of module and package related details."""
22

3-
from typing import Any
4-
53
import dataclasses
4+
from typing import Any
65

76
from pytype import file_utils
87
from pytype import module_utils
@@ -91,20 +90,14 @@ def process_from_import(self, from_package, item):
9190
else:
9291
name = new_name = item
9392
qualified_name = self.qualify_name("%s.%s" % (from_package, name))
94-
if (from_package in ["__PACKAGE__", "__PARENT__"]
95-
and isinstance(item, str)):
96-
# This will always be a simple module import (from . cannot import a
97-
# NamedType, and without 'as' the name will not be reexported).
98-
t = pytd.Module(name=new_name, module_name=qualified_name)
99-
else:
100-
# We should ideally not need this check, but we have typing
101-
# special-cased in some places.
102-
if not qualified_name.startswith("typing.") and name != "*":
103-
# Mark this as an externally imported type, so that AddNamePrefix
104-
# does not prefix it with the current package name.
105-
qualified_name = (parser_constants.EXTERNAL_NAME_PREFIX +
106-
qualified_name)
107-
t = pytd.NamedType(qualified_name)
93+
# We should ideally not need this check, but we have typing
94+
# special-cased in some places.
95+
if not qualified_name.startswith("typing.") and name != "*":
96+
# Mark this as an externally imported type, so that AddNamePrefix
97+
# does not prefix it with the current package name.
98+
qualified_name = (parser_constants.EXTERNAL_NAME_PREFIX +
99+
qualified_name)
100+
t = pytd.NamedType(qualified_name)
108101
if name == "*":
109102
# A star import is stored as
110103
# 'imported_mod.* = imported_mod.*'. The imported module needs to be

0 commit comments

Comments
 (0)