Skip to content

Fix caching behavior of PEP561-installed namespace packages #10937

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 2 commits into from
Aug 6, 2021
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
23 changes: 16 additions & 7 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,6 @@ def correct_rel_imp(imp: Union[ImportFrom, ImportAll]) -> str:
res.append((ancestor_pri, ".".join(ancestors), imp.line))
elif isinstance(imp, ImportFrom):
cur_id = correct_rel_imp(imp)
pos = len(res)
all_are_submodules = True
# Also add any imported names that are submodules.
pri = import_priority(imp, PRI_MED)
Expand All @@ -768,7 +767,10 @@ def correct_rel_imp(imp: Union[ImportFrom, ImportAll]) -> str:
# if all of the imports are submodules, do the import at a lower
# priority.
pri = import_priority(imp, PRI_HIGH if not all_are_submodules else PRI_LOW)
res.insert(pos, ((pri, cur_id, imp.line)))
# The imported module goes in after the
Copy link
Collaborator

@hauntsaninja hauntsaninja Aug 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I was curious why this was an res.insert to start with, since insert is nobody's first choice. It looks like it was introduced in the dawn of incremental mode #1292 and was previously a res.append before the loop. It was moved after the loop to change the priority, and so I guess res.insert was to maintain the previous position)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, interesting!

# submodules, for the same namespace related
# reasons discussed in the Import case.
res.append((pri, cur_id, imp.line))
elif isinstance(imp, ImportAll):
pri = import_priority(imp, PRI_HIGH)
res.append((pri, correct_rel_imp(imp), imp.line))
Expand Down Expand Up @@ -1317,7 +1319,7 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
st = manager.get_stat(path)
except OSError:
return None
if not stat.S_ISREG(st.st_mode):
if not (stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode)):
manager.log('Metadata abandoned for {}: file {} does not exist'.format(id, path))
return None

Expand Down Expand Up @@ -1360,7 +1362,11 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],

t0 = time.time()
try:
source_hash = manager.fscache.hash_digest(path)
# dir means it is a namespace package
if stat.S_ISDIR(st.st_mode):
source_hash = ''
else:
source_hash = manager.fscache.hash_digest(path)
except (OSError, UnicodeDecodeError, DecodeError):
return None
manager.add_stats(validate_hash_time=time.time() - t0)
Expand Down Expand Up @@ -1835,15 +1841,15 @@ def __init__(self,
if path:
self.abspath = os.path.abspath(path)
self.xpath = path or '<string>'
if path and source is None and self.manager.fscache.isdir(path):
source = ''
self.source = source
if path and source is None and self.manager.cache_enabled:
self.meta = find_cache_meta(self.id, path, manager)
# TODO: Get mtime if not cached.
if self.meta is not None:
self.interface_hash = self.meta.interface_hash
self.meta_source_hash = self.meta.hash
if path and source is None and self.manager.fscache.isdir(path):
source = ''
self.source = source
self.add_ancestors()
t0 = time.time()
self.meta = validate_meta(self.meta, self.id, self.path, self.ignore_all, manager)
Expand Down Expand Up @@ -2038,6 +2044,9 @@ def parse_file(self) -> None:
else:
err = "mypy: can't decode file '{}': {}".format(self.path, str(decodeerr))
raise CompileError([err], module_with_blocker=self.id) from decodeerr
elif self.path and self.manager.fscache.isdir(self.path):
source = ''
self.source_hash = ''
else:
assert source is not None
self.source_hash = compute_hash(source)
Expand Down
22 changes: 22 additions & 0 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -5563,3 +5563,25 @@ class D:
[out2]
tmp/a.py:2: note: Revealed type is "builtins.list[builtins.int]"
tmp/a.py:3: note: Revealed type is "builtins.list[builtins.str]"

[case testIncrementalNamespacePackage1]
# flags: --namespace-packages
import m
[file m.py]
from foo.bar import x
x + 0
[file foo/bar.py]
x = 0
[rechecked]
[stale]

[case testIncrementalNamespacePackage2]
# flags: --namespace-packages
import m
[file m.py]
from foo import bar
bar.x + 0
[file foo/bar.py]
x = 0
[rechecked]
[stale]
5 changes: 2 additions & 3 deletions test-data/unit/pep561.test
Original file line number Diff line number Diff line change
Expand Up @@ -187,9 +187,8 @@ import a
[out2]
a.py:1: error: Unsupported operand types for + ("int" and "str")

-- Test for issue #9852, among others
[case testTypedPkgNamespaceRegFromImportTwice-xfail]
# pkgs: typedpkg, typedpkg_ns
[case testTypedPkgNamespaceRegFromImportTwice]
# pkgs: typedpkg_ns
from typedpkg_ns import ns
-- dummy should trigger a second iteration
[file dummy.py.2]
Expand Down