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
Add case sensitive directory checks #6684
Changes from all commits
062c783
b5e720a
cc00451
4056e23
2d540fa
09a1fcd
bb347c0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -215,28 +215,31 @@ def _find_module(self, id: str) -> Optional[str]: | |
near_misses = [] # Collect near misses for namespace mode (see below). | ||
for base_dir, verify in candidate_base_dirs: | ||
base_path = base_dir + seplast # so e.g. '/usr/lib/python3.4/foo/bar/baz' | ||
dir_prefix = base_dir | ||
for _ in range(len(components) - 1): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be better to strip off There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't work (also it looks like you forgot minus there). I also tried with plus or minus one and some tests fail. |
||
dir_prefix = os.path.dirname(dir_prefix) | ||
# Prefer package over module, i.e. baz/__init__.py* over baz.py*. | ||
for extension in PYTHON_EXTENSIONS: | ||
path = base_path + sepinit + extension | ||
path_stubs = base_path + '-stubs' + sepinit + extension | ||
if fscache.isfile_case(path): | ||
if verify and not verify_module(fscache, id, path): | ||
near_misses.append(path) | ||
if fscache.isfile_case(path, dir_prefix): | ||
if verify and not verify_module(fscache, id, path, dir_prefix): | ||
near_misses.append((path, dir_prefix)) | ||
continue | ||
return path | ||
elif fscache.isfile_case(path_stubs): | ||
if verify and not verify_module(fscache, id, path_stubs): | ||
near_misses.append(path_stubs) | ||
elif fscache.isfile_case(path_stubs, dir_prefix): | ||
if verify and not verify_module(fscache, id, path_stubs, dir_prefix): | ||
near_misses.append((path_stubs, dir_prefix)) | ||
continue | ||
return path_stubs | ||
elif self.options and self.options.namespace_packages and fscache.isdir(base_path): | ||
near_misses.append(base_path) | ||
near_misses.append((base_path, dir_prefix)) | ||
# No package, look for module. | ||
for extension in PYTHON_EXTENSIONS: | ||
path = base_path + extension | ||
if fscache.isfile_case(path): | ||
if verify and not verify_module(fscache, id, path): | ||
near_misses.append(path) | ||
if fscache.isfile_case(path, dir_prefix): | ||
if verify and not verify_module(fscache, id, path, dir_prefix): | ||
near_misses.append((path, dir_prefix)) | ||
continue | ||
return path | ||
|
||
|
@@ -262,9 +265,10 @@ def _find_module(self, id: str) -> Optional[str]: | |
# foo/__init__.py it returns 2 (regardless of what's in | ||
# foo/bar). It doesn't look higher than that. | ||
if self.options and self.options.namespace_packages and near_misses: | ||
levels = [highest_init_level(fscache, id, path) for path in near_misses] | ||
levels = [highest_init_level(fscache, id, path, dir_prefix) | ||
for path, dir_prefix in near_misses] | ||
index = levels.index(max(levels)) | ||
return near_misses[index] | ||
return near_misses[index][0] | ||
|
||
# Finally, we may be asked to produce an ancestor for an | ||
# installed package with a py.typed marker that is a | ||
|
@@ -303,26 +307,28 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]: | |
return result | ||
|
||
|
||
def verify_module(fscache: FileSystemCache, id: str, path: str) -> bool: | ||
def verify_module(fscache: FileSystemCache, id: str, path: str, prefix: str) -> bool: | ||
"""Check that all packages containing id have a __init__ file.""" | ||
if path.endswith(('__init__.py', '__init__.pyi')): | ||
path = os.path.dirname(path) | ||
for i in range(id.count('.')): | ||
path = os.path.dirname(path) | ||
if not any(fscache.isfile_case(os.path.join(path, '__init__{}'.format(extension))) | ||
if not any(fscache.isfile_case(os.path.join(path, '__init__{}'.format(extension)), | ||
prefix) | ||
for extension in PYTHON_EXTENSIONS): | ||
return False | ||
return True | ||
|
||
|
||
def highest_init_level(fscache: FileSystemCache, id: str, path: str) -> int: | ||
def highest_init_level(fscache: FileSystemCache, id: str, path: str, prefix: str) -> int: | ||
"""Compute the highest level where an __init__ file is found.""" | ||
if path.endswith(('__init__.py', '__init__.pyi')): | ||
path = os.path.dirname(path) | ||
level = 0 | ||
for i in range(id.count('.')): | ||
path = os.path.dirname(path) | ||
if any(fscache.isfile_case(os.path.join(path, '__init__{}'.format(extension))) | ||
if any(fscache.isfile_case(os.path.join(path, '__init__{}'.format(extension)), | ||
prefix) | ||
for extension in PYTHON_EXTENSIONS): | ||
level = i + 1 | ||
return level | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
-- Type checker test cases dealing with modules and imports on case-insensitive filesystems. | ||
|
||
[case testCaseSensitivityDir] | ||
from a import B # E: Module 'a' has no attribute 'B' | ||
|
||
[file a/__init__.py] | ||
[file a/b/__init__.py] | ||
|
||
[case testCaseInsensitivityDir] | ||
# flags: --config-file tmp/mypy.ini | ||
|
||
from a import B # E: Module 'a' has no attribute 'B' | ||
from other import x | ||
reveal_type(x) # E: Revealed type is 'builtins.int' | ||
|
||
[file a/__init__.py] | ||
[file a/b/__init__.py] | ||
[file FuNkY_CaSe/other.py] | ||
x = 1 | ||
|
||
[file mypy.ini] | ||
[[mypy] | ||
mypy_path = tmp/funky_case | ||
|
||
[case testPreferPackageOverFileCase] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the test name is backwards. But don't we also need a test for the reverse? (Where the package has the right case and so is preferred.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a similar test case |
||
# flags: --config-file tmp/mypy.ini | ||
import a | ||
[file funky/a.py] | ||
/ # Deliberate syntax error, this file should not be parsed. | ||
[file FuNkY/a/__init__.py] | ||
pass | ||
|
||
[file mypy.ini] | ||
[[mypy] | ||
mypy_path = tmp/funky | ||
|
||
[case testNotPreferPackageOverFileCase] | ||
import a | ||
[file a.py] | ||
'no'() # E: "str" not callable | ||
[file A/__init__.py] | ||
/ # Deliberate syntax error, this file should not be parsed. | ||
|
||
[case testNamespacePackagePickFirstOnMypyPathCase] | ||
# flags: --namespace-packages --config-file tmp/mypy.ini | ||
from foo.bar import x | ||
reveal_type(x) # E: Revealed type is 'builtins.int' | ||
[file XX/foo/bar.py] | ||
x = 0 | ||
[file yy/foo/bar.py] | ||
x = '' | ||
[file mypy.ini] | ||
[[mypy] | ||
mypy_path = tmp/xx, tmp/yy | ||
|
||
[case testClassicPackageInsideNamespacePackageCase] | ||
# flags: --namespace-packages --config-file tmp/mypy.ini | ||
from foo.bar.baz.boo import x | ||
reveal_type(x) # E: Revealed type is 'builtins.int' | ||
[file xx/foo/bar/baz/boo.py] | ||
x = '' | ||
[file xx/foo/bar/baz/__init__.py] | ||
[file yy/foo/bar/baz/boo.py] | ||
x = 0 | ||
[file yy/foo/bar/__init__.py] | ||
|
||
[file mypy.ini] | ||
[[mypy] | ||
mypy_path = TmP/xX, TmP/yY |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems good enough. Perhaps you could see if adding
assert head.startswith(prefix)
before line 211 always passes? OTOH perhaps that's risking too much.I have a faint worry that e.g.
head = '/foo/bar'
andprefix = '/foo/b'
but that's not possible given howprefix
is constructed by the caller right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the assert, and all tests still passed. I think however it is better to not put it there, just in case.
Yes. I will probably just clarify this in the docstring.