From 586fc5af2caf21a370f9a4f527dd3f2549fc622a Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 28 Sep 2018 16:23:52 -0700 Subject: [PATCH 01/12] Tentative solution for namespace packages --- mypy/modulefinder.py | 50 +++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index b9b79e3b4f09..d243413db522 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -170,27 +170,35 @@ def _find_module(self, id: str) -> Optional[str]: # Now just look for 'baz.pyi', 'baz/__init__.py', etc., inside those directories. seplast = os.sep + components[-1] # so e.g. '/baz' sepinit = os.sep + '__init__' - for base_dir, verify in candidate_base_dirs: - base_path = base_dir + seplast # so e.g. '/usr/lib/python3.4/foo/bar/baz' - # 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): - continue - return path - elif fscache.isfile_case(path_stubs): - if verify and not verify_module(fscache, id, path_stubs): - continue - return path_stubs - # 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): - continue - return path + verify_flags = [True] + if self.options is not None and self.options.namespace_packages: + verify_flags.append(False) + # If --namespace-packages, we do the whole thing twice: + # - once with classic rules (verify if requested) + # - once looking for namespace packages (never verify) + for verify_flag in verify_flags: + for base_dir, verify in candidate_base_dirs: + verify = verify and verify_flag + base_path = base_dir + seplast # so e.g. '/usr/lib/python3.4/foo/bar/baz' + # 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): + continue + return path + elif fscache.isfile_case(path_stubs): + if verify and not verify_module(fscache, id, path_stubs): + continue + return path_stubs + # 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): + continue + return path return None def find_modules_recursive(self, module: str) -> List[BuildSource]: From ecac1b5dd8675121b61770bc95b46b8ccadead77 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Fri, 28 Sep 2018 16:58:54 -0700 Subject: [PATCH 02/12] Add tests for namespace packages --- test-data/unit/check-modules.test | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 36cd21b25189..19cb5d2f6c02 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -1,4 +1,5 @@ -- Type checker test cases dealing with modules and imports. +-- Towards the end there are tests for PEP 420 (namespace packages, i.e. __init__.py-less packages). [case testAccessImportedDefinitions] import m @@ -2525,3 +2526,51 @@ def __radd__(self) -> int: ... [case testFunctionWithInPlaceDunderName] def __iadd__(self) -> int: ... + +-- Tests for PEP 420 namespace packages. +[case testClassicPackage] +from foo.bar import x +[file foo/__init__.py] +# empty +[file foo/bar.py] +x = 0 + +[case testClassicNotPackage] +from foo.bar import x +[file foo/bar.py] +x = 0 +[out] +main:1: error: Cannot find module named 'foo.bar' +main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" flag would help) + +[case testNamespacePackage] +# flags: --namespace-packages +from foo.bar import x +[file foo/bar.py] +x = 0 + +[case testNamespacePackageWithMypyPath] +# flags: --namespace-packages --config-file tmp/mypy.ini +from foo.bax import x +from foo.bay import y +from foo.baz import z +[file xx/foo/bax.py] +x = 0 +[file yy/foo/bay.py] +y = 0 +[file foo/baz.py] +z = 0 +[file mypy.ini] +[[mypy] +mypy_path = tmp/xx, tmp/yy + +[case testClassicPackageIgnoresEarlierNamespacePackage] +# flags: --namespace-packages --config-file tmp/mypy.ini +from foo.bar import y +[file xx/foo/bar.py] +[file yy/foo/bar.py] +y = 0 +[file yy/foo/__init__.py] +[file mypy.ini] +[[mypy] +mypy_path = tmp/xx, tmp/yy From 2da72cc57bf1994a6a5bd801e0f6470310686ae5 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sun, 30 Sep 2018 21:13:59 -0700 Subject: [PATCH 03/12] Add reveal_type() calls to tests --- test-data/unit/check-modules.test | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 19cb5d2f6c02..fc0ac5f9ca8c 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2528,6 +2528,7 @@ def __radd__(self) -> int: ... def __iadd__(self) -> int: ... -- Tests for PEP 420 namespace packages. + [case testClassicPackage] from foo.bar import x [file foo/__init__.py] @@ -2546,6 +2547,7 @@ main:1: note: (Perhaps setting MYPYPATH or using the "--ignore-missing-imports" [case testNamespacePackage] # flags: --namespace-packages from foo.bar import x +reveal_type(x) # E: Revealed type is 'builtins.int' [file foo/bar.py] x = 0 @@ -2554,6 +2556,9 @@ x = 0 from foo.bax import x from foo.bay import y from foo.baz import z +reveal_type(x) # E: Revealed type is 'builtins.int' +reveal_type(y) # E: Revealed type is 'builtins.int' +reveal_type(z) # E: Revealed type is 'builtins.int' [file xx/foo/bax.py] x = 0 [file yy/foo/bay.py] @@ -2567,7 +2572,9 @@ mypy_path = tmp/xx, tmp/yy [case testClassicPackageIgnoresEarlierNamespacePackage] # flags: --namespace-packages --config-file tmp/mypy.ini from foo.bar import y +reveal_type(y) # E: Revealed type is 'builtins.int' [file xx/foo/bar.py] +x = '' [file yy/foo/bar.py] y = 0 [file yy/foo/__init__.py] From 9d8f94d1659e04a00a9d645b669dba22720b22ae Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sun, 30 Sep 2018 21:14:30 -0700 Subject: [PATCH 04/12] Add test showing the first hit is used --- test-data/unit/check-modules.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index fc0ac5f9ca8c..99db9cbd4eb4 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2581,3 +2581,15 @@ y = 0 [file mypy.ini] [[mypy] mypy_path = tmp/xx, tmp/yy + +[case testNamespacePackagePickFirstOnMypyPath] +# 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 From de9049cd30d28afbc30a972b200677b2bd2cf2a0 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Sun, 30 Sep 2018 21:15:52 -0700 Subject: [PATCH 05/12] Add two tests for classic in namespace and vice versa -- both are BROKEN --- test-data/unit/check-modules.test | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index 99db9cbd4eb4..f6a3afa35106 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2593,3 +2593,29 @@ x = '' [file mypy.ini] [[mypy] mypy_path = tmp/xx, tmp/yy + +[case testNamespacePackageInsideClassicPackage] +# flags: --namespace-packages --config-file tmp/mypy.ini +from foo.bar.baz import x +reveal_type(x) # E: Revealed type is 'builtins.int' +[file xx/foo/bar/baz.py] +x = '' +[file yy/foo/bar/baz.py] +x = 0 +[file yy/foo/__init__.py] +[file mypy.ini] +[[mypy] +mypy_path = tmp/xx, tmp/yy + +[case testClassicPackageInsideNamespacePackage] +# flags: --namespace-packages --config-file tmp/mypy.ini +from foo.bar.baz import x +reveal_type(x) # E: Revealed type is 'builtins.int' +[file xx/foo/bar/baz.py] +x = '' +[file yy/foo/bar/baz.py] +x = 0 +[file xx/foo/bar/__init__.py] +[file mypy.ini] +[[mypy] +mypy_path = tmp/xx, tmp/yy From 6ffd9b73a39fbb6986a796c85f5852f45457c96a Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 1 Oct 2018 15:23:10 -0700 Subject: [PATCH 06/12] Correct typo in final test case --- test-data/unit/check-modules.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index f6a3afa35106..fc6514739096 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2615,7 +2615,7 @@ reveal_type(x) # E: Revealed type is 'builtins.int' x = '' [file yy/foo/bar/baz.py] x = 0 -[file xx/foo/bar/__init__.py] +[file yy/foo/bar/__init__.py] [file mypy.ini] [[mypy] mypy_path = tmp/xx, tmp/yy From e65795f9db1a2efae3e62842e8432e63e182c09c Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 1 Oct 2018 15:37:52 -0700 Subject: [PATCH 07/12] Proper fix for nesting of namespace and classic packages --- mypy/modulefinder.py | 99 ++++++++++++++++++++----------- test-data/unit/check-modules.test | 7 ++- 2 files changed, 69 insertions(+), 37 deletions(-) diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index d243413db522..75fa94f93de6 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -170,35 +170,50 @@ def _find_module(self, id: str) -> Optional[str]: # Now just look for 'baz.pyi', 'baz/__init__.py', etc., inside those directories. seplast = os.sep + components[-1] # so e.g. '/baz' sepinit = os.sep + '__init__' - verify_flags = [True] - if self.options is not None and self.options.namespace_packages: - verify_flags.append(False) - # If --namespace-packages, we do the whole thing twice: - # - once with classic rules (verify if requested) - # - once looking for namespace packages (never verify) - for verify_flag in verify_flags: - for base_dir, verify in candidate_base_dirs: - verify = verify and verify_flag - base_path = base_dir + seplast # so e.g. '/usr/lib/python3.4/foo/bar/baz' - # 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): - continue - return path - elif fscache.isfile_case(path_stubs): - if verify and not verify_module(fscache, id, path_stubs): - continue - return path_stubs - # 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): - continue - return path + 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' + # 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) + continue + return path + elif fscache.isfile_case(path_stubs): + if verify and not verify_module(fscache, id, path_stubs): + near_misses.append(path_stubs) + continue + return path_stubs + # 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) + continue + return path + + # In namespace mode, re-check those entries that had 'verify' + # set using different rules. This time we allow missing + # __init__.py[i] files and we don't look for -stubs. It's + # complicated because if there's a classic subpackage + # somewhere it should still be preferred over a namespace + # subpackage with the same name earlier on the path. + if self.options.namespace_packages and near_misses: + for index in range(id.count('.')): + if len(near_misses) > 1: + reduced_list = [path + for path in near_misses + if verify_module(fscache, id, path, + index, index + 1)] + if reduced_list: + near_misses = reduced_list + if near_misses: + return near_misses[0] + return None def find_modules_recursive(self, module: str) -> List[BuildSource]: @@ -232,14 +247,30 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]: return result -def verify_module(fscache: FileSystemCache, id: str, path: str) -> bool: - """Check that all packages containing id have a __init__ file.""" +def verify_module(fscache: FileSystemCache, id: str, path: str, + head: int = 0, tail: int = 999999999) -> bool: + """Check that all packages containing id have a __init__ file. + + Optional arguments head and tail constrain the check to the slice + [head : tail] of the paths to try. For example, if the toplevel + package is allowed to be a namespace, pass head=1; if the + innermost package is allowed to be a namespace, pass tail=-1. + """ if path.endswith(('__init__.py', '__init__.pyi')): path = os.path.dirname(path) - for i in range(id.count('.')): + # Counting is a bit tricky. If id == 'foo.bar.baz.boo', then + # count == 3, so the index takes on the values 0, 1 and 2. But + # the algorithm strips dirnames from the end, so we make the index + # go backwards: 2, 1, 0. Then specifying head=1 would skip index + # value 0, while specifying head=-1 would skip index value 2. + count = id.count('.') + if tail < 0: + tail += count + for index in reversed(range(count)): path = os.path.dirname(path) - if not any(fscache.isfile_case(os.path.join(path, '__init__{}'.format(extension))) - for extension in PYTHON_EXTENSIONS): + if (head <= index < tail + and not any(fscache.isfile_case(os.path.join(path, '__init__{}'.format(extension))) + for extension in PYTHON_EXTENSIONS)): return False return True diff --git a/test-data/unit/check-modules.test b/test-data/unit/check-modules.test index fc6514739096..23284c23bfb0 100644 --- a/test-data/unit/check-modules.test +++ b/test-data/unit/check-modules.test @@ -2609,11 +2609,12 @@ mypy_path = tmp/xx, tmp/yy [case testClassicPackageInsideNamespacePackage] # flags: --namespace-packages --config-file tmp/mypy.ini -from foo.bar.baz import x +from foo.bar.baz.boo import x reveal_type(x) # E: Revealed type is 'builtins.int' -[file xx/foo/bar/baz.py] +[file xx/foo/bar/baz/boo.py] x = '' -[file yy/foo/bar/baz.py] +[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] From f1ac04aebb217202b1a05a8168fca38d1b30e554 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 1 Oct 2018 16:34:41 -0700 Subject: [PATCH 08/12] Check that self.options isn't None --- mypy/modulefinder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index 75fa94f93de6..ef9c80568aab 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -202,7 +202,7 @@ def _find_module(self, id: str) -> Optional[str]: # complicated because if there's a classic subpackage # somewhere it should still be preferred over a namespace # subpackage with the same name earlier on the path. - if self.options.namespace_packages and near_misses: + if self.options and self.options.namespace_packages and near_misses: for index in range(id.count('.')): if len(near_misses) > 1: reduced_list = [path From c91c9c1e8087ce435d80c861b5a369c0725d77ec Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 2 Oct 2018 11:24:36 -0700 Subject: [PATCH 09/12] Different way of handling nested classic/namespace packages; restore original verify_module --- mypy/modulefinder.py | 54 ++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index ef9c80568aab..2f2c1ef6a098 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -203,17 +203,10 @@ def _find_module(self, id: str) -> Optional[str]: # somewhere it should still be preferred over a namespace # subpackage with the same name earlier on the path. if self.options and self.options.namespace_packages and near_misses: - for index in range(id.count('.')): - if len(near_misses) > 1: - reduced_list = [path - for path in near_misses - if verify_module(fscache, id, path, - index, index + 1)] - if reduced_list: - near_misses = reduced_list - if near_misses: - return near_misses[0] - + levels = [highest_init_level(fscache, id, path) for path in near_misses] + index = levels.index(max(levels)) + return near_misses[index] + return None def find_modules_recursive(self, module: str) -> List[BuildSource]: @@ -247,34 +240,31 @@ def find_modules_recursive(self, module: str) -> List[BuildSource]: return result -def verify_module(fscache: FileSystemCache, id: str, path: str, - head: int = 0, tail: int = 999999999) -> bool: - """Check that all packages containing id have a __init__ file. - - Optional arguments head and tail constrain the check to the slice - [head : tail] of the paths to try. For example, if the toplevel - package is allowed to be a namespace, pass head=1; if the - innermost package is allowed to be a namespace, pass tail=-1. - """ +def verify_module(fscache: FileSystemCache, id: str, path: str) -> bool: + """Check that all packages containing id have a __init__ file.""" if path.endswith(('__init__.py', '__init__.pyi')): path = os.path.dirname(path) - # Counting is a bit tricky. If id == 'foo.bar.baz.boo', then - # count == 3, so the index takes on the values 0, 1 and 2. But - # the algorithm strips dirnames from the end, so we make the index - # go backwards: 2, 1, 0. Then specifying head=1 would skip index - # value 0, while specifying head=-1 would skip index value 2. - count = id.count('.') - if tail < 0: - tail += count - for index in reversed(range(count)): + for i in range(id.count('.')): path = os.path.dirname(path) - if (head <= index < tail - and not any(fscache.isfile_case(os.path.join(path, '__init__{}'.format(extension))) - for extension in PYTHON_EXTENSIONS)): + if not any(fscache.isfile_case(os.path.join(path, '__init__{}'.format(extension))) + for extension in PYTHON_EXTENSIONS): return False return True +def highest_init_level(fscache: FileSystemCache, id: str, path: 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))) + for extension in PYTHON_EXTENSIONS): + level = i + 1 + return level + + def mypy_path() -> List[str]: path_env = os.getenv('MYPYPATH') if not path_env: From 46b85d4cd0526290c735d0c767dce465dc16a9b0 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 2 Oct 2018 12:20:50 -0700 Subject: [PATCH 10/12] Update the comment, explaining the algorithm --- mypy/modulefinder.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index 2f2c1ef6a098..cacf94d8ce55 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -196,12 +196,27 @@ def _find_module(self, id: str) -> Optional[str]: continue return path - # In namespace mode, re-check those entries that had 'verify' - # set using different rules. This time we allow missing - # __init__.py[i] files and we don't look for -stubs. It's - # complicated because if there's a classic subpackage - # somewhere it should still be preferred over a namespace - # subpackage with the same name earlier on the path. + # In namespace mode, re-check those entries that had 'verify'. + # Assume search path entries xxx, yyy and zzz, and we're + # looking for foo.bar.baz. Suppose near_misses has: + # + # - xxx/foo/bar/baz.py + # - yyy/foo/bar/baz/__init__.py + # - zzz/foo/bar/baz.pyi + # + # If any of the foo directories has __init__.py[i], it wins. + # Else, we look for foo/bar/__init__.py[i], etc. If there are + # none, the first hit wins. Note that this does not take into + # account whether the lowest-level module is a file (baz.py), + # a package (baz/__init__.py), or a stub file (baz.pyi) -- for + # these the first one encountered along the search path wins. + # + # The helper function highest_init_level() returns an int that + # indicates the highest level at which a __init__.py[i] file + # is found; if no __init__ was found it returns 0, if we find + # only foo/bar/__init__.py it returns 1, and if we have + # foo/__init__.py it returns 2 (regardless of what's un + # 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] index = levels.index(max(levels)) From bde1b0feafc251eda3279aac07393769039b317f Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 2 Oct 2018 12:22:41 -0700 Subject: [PATCH 11/12] fix lint --- mypy/modulefinder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index cacf94d8ce55..b95579772732 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -221,7 +221,7 @@ def _find_module(self, id: str) -> Optional[str]: levels = [highest_init_level(fscache, id, path) for path in near_misses] index = levels.index(max(levels)) return near_misses[index] - + return None def find_modules_recursive(self, module: str) -> List[BuildSource]: From 644752a9cb791af8781865c1f19f57238219b341 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Wed, 3 Oct 2018 10:43:34 -0700 Subject: [PATCH 12/12] Fix typo --- mypy/modulefinder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/modulefinder.py b/mypy/modulefinder.py index b95579772732..3eb7dec89d83 100644 --- a/mypy/modulefinder.py +++ b/mypy/modulefinder.py @@ -215,7 +215,7 @@ def _find_module(self, id: str) -> Optional[str]: # indicates the highest level at which a __init__.py[i] file # is found; if no __init__ was found it returns 0, if we find # only foo/bar/__init__.py it returns 1, and if we have - # foo/__init__.py it returns 2 (regardless of what's un + # 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]