Skip to content

Commit 3d6bd68

Browse files
author
hauntsaninja
committed
namespace packages files
1 parent 0fb671c commit 3d6bd68

File tree

5 files changed

+67
-144
lines changed

5 files changed

+67
-144
lines changed

mypy/find_sources.py

Lines changed: 42 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from mypy.modulefinder import BuildSource, PYTHON_EXTENSIONS
99
from mypy.fscache import FileSystemCache
1010
from mypy.options import Options
11+
from mypy.util import normalise_package_root
1112

1213
PY_EXTENSIONS = tuple(PYTHON_EXTENSIONS) # type: Final
1314

@@ -24,7 +25,7 @@ def create_source_list(paths: Sequence[str], options: Options,
2425
Raises InvalidSourceList on errors.
2526
"""
2627
fscache = fscache or FileSystemCache()
27-
finder = SourceFinder(fscache)
28+
finder = SourceFinder(fscache, explicit_package_roots=options.package_root or None)
2829

2930
sources = []
3031
for path in paths:
@@ -34,7 +35,7 @@ def create_source_list(paths: Sequence[str], options: Options,
3435
name, base_dir = finder.crawl_up(path)
3536
sources.append(BuildSource(path, name, None, base_dir))
3637
elif fscache.isdir(path):
37-
sub_sources = finder.find_sources_in_dir(path, explicit_package_roots=None)
38+
sub_sources = finder.find_sources_in_dir(path)
3839
if not sub_sources and not allow_empty_dir:
3940
raise InvalidSourceList(
4041
"There are no .py[i] files in directory '{}'".format(path)
@@ -59,26 +60,26 @@ def keyfunc(name: str) -> Tuple[int, str]:
5960

6061

6162
class SourceFinder:
62-
def __init__(self, fscache: FileSystemCache) -> None:
63+
def __init__(self, fscache: FileSystemCache, explicit_package_roots: Optional[List[str]]) -> None:
6364
self.fscache = fscache
64-
# A cache for package names, mapping from directory path to module id and base dir
65-
self.package_cache = {} # type: Dict[str, Tuple[str, str]]
65+
self.explicit_package_roots = explicit_package_roots
6666

67-
def find_sources_in_dir(
68-
self, path: str, explicit_package_roots: Optional[List[str]]
69-
) -> List[BuildSource]:
70-
if explicit_package_roots is None:
67+
def is_package_root(self, path: str) -> bool:
68+
assert self.explicit_package_roots
69+
return normalise_package_root(path) in self.explicit_package_roots
70+
71+
def find_sources_in_dir(self, path: str) -> List[BuildSource]:
72+
if self.explicit_package_roots is None:
7173
mod_prefix, root_dir = self.crawl_up_dir(path)
7274
else:
7375
mod_prefix = os.path.basename(path)
7476
root_dir = os.path.dirname(path) or "."
7577
if mod_prefix:
7678
mod_prefix += "."
77-
return self.find_sources_in_dir_helper(path, mod_prefix, root_dir, explicit_package_roots)
79+
return self.find_sources_in_dir_helper(path, mod_prefix, root_dir)
7880

7981
def find_sources_in_dir_helper(
80-
self, dir_path: str, mod_prefix: str, root_dir: str,
81-
explicit_package_roots: Optional[List[str]]
82+
self, dir_path: str, mod_prefix: str, root_dir: str
8283
) -> List[BuildSource]:
8384
assert not mod_prefix or mod_prefix.endswith(".")
8485

@@ -87,8 +88,8 @@ def find_sources_in_dir_helper(
8788
# Alternatively, if we aren't given explicit package roots and we don't have an __init__
8889
# file, recursively explore this directory as a new package root.
8990
if (
90-
(explicit_package_roots is not None and dir_path in explicit_package_roots)
91-
or (explicit_package_roots is None and init_file is None)
91+
(self.explicit_package_roots is not None and self.is_package_root(dir_path))
92+
or (self.explicit_package_roots is None and init_file is None)
9293
):
9394
mod_prefix = ""
9495
root_dir = dir_path
@@ -109,7 +110,7 @@ def find_sources_in_dir_helper(
109110

110111
if self.fscache.isdir(path):
111112
sub_sources = self.find_sources_in_dir_helper(
112-
path, mod_prefix + name + '.', root_dir, explicit_package_roots
113+
path, mod_prefix + name + '.', root_dir
113114
)
114115
if sub_sources:
115116
seen.add(name)
@@ -126,10 +127,12 @@ def find_sources_in_dir_helper(
126127
return sources
127128

128129
def crawl_up(self, path: str) -> Tuple[str, str]:
129-
"""Given a .py[i] filename, return module and base directory
130+
"""Given a .py[i] filename, return module and base directory.
130131
131-
We crawl up the path until we find a directory without
132-
__init__.py[i], or until we run out of path components.
132+
If we are given explicit package roots, we crawl up until we find one (or run out of
133+
path components).
134+
135+
Otherwise, we crawl up the path until we find an directory without __init__.py[i]
133136
"""
134137
parent, filename = os.path.split(path)
135138
module_name = strip_py(filename) or os.path.basename(filename)
@@ -142,27 +145,30 @@ def crawl_up(self, path: str) -> Tuple[str, str]:
142145
return module, base_dir
143146

144147
def crawl_up_dir(self, dir: str) -> Tuple[str, str]:
145-
"""Given a directory name, return the corresponding module name and base directory
146-
147-
Use package_cache to cache results.
148-
"""
149-
if dir in self.package_cache:
150-
return self.package_cache[dir]
151-
148+
"""Given a directory name, return the corresponding module name and base directory."""
152149
parent_dir, base = os.path.split(dir)
153-
if not dir or not self.get_init_file(dir) or not base:
150+
if not dir or not base:
154151
module = ''
155152
base_dir = dir or '.'
156-
else:
157-
# Ensure that base is a valid python module name
158-
if base.endswith('-stubs'):
159-
base = base[:-6] # PEP-561 stub-only directory
160-
if not base.isidentifier():
161-
raise InvalidSourceList('{} is not a valid Python package name'.format(base))
162-
parent_module, base_dir = self.crawl_up_dir(parent_dir)
163-
module = module_join(parent_module, base)
164-
165-
self.package_cache[dir] = module, base_dir
153+
return module, base_dir
154+
155+
if self.explicit_package_roots is None and not self.get_init_file(dir):
156+
module = ''
157+
base_dir = dir or '.'
158+
return module, base_dir
159+
160+
# Ensure that base is a valid python module name
161+
if base.endswith('-stubs'):
162+
base = base[:-6] # PEP-561 stub-only directory
163+
if not base.isidentifier():
164+
raise InvalidSourceList('{} is not a valid Python package name'.format(base))
165+
166+
if self.explicit_package_roots is not None:
167+
if self.is_package_root(parent_dir):
168+
return base, parent_dir
169+
170+
parent_module, base_dir = self.crawl_up_dir(parent_dir)
171+
module = module_join(parent_module, base)
166172
return module, base_dir
167173

168174
def get_init_file(self, dir: str) -> Optional[str]:
@@ -176,8 +182,6 @@ def get_init_file(self, dir: str) -> Optional[str]:
176182
f = os.path.join(dir, '__init__' + ext)
177183
if self.fscache.isfile(f):
178184
return f
179-
if ext == '.py' and self.fscache.init_under_package_root(f):
180-
return f
181185
return None
182186

183187

mypy/fscache.py

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,8 @@
3636

3737
class FileSystemCache:
3838
def __init__(self) -> None:
39-
# The package root is not flushed with the caches.
40-
# It is set by set_package_root() below.
41-
self.package_root = [] # type: List[str]
4239
self.flush()
4340

44-
def set_package_root(self, package_root: List[str]) -> None:
45-
self.package_root = package_root
46-
4741
def flush(self) -> None:
4842
"""Start another transaction and empty all caches."""
4943
self.stat_cache = {} # type: Dict[str, os.stat_result]
@@ -64,92 +58,13 @@ def stat(self, path: str) -> os.stat_result:
6458
try:
6559
st = os.stat(path)
6660
except OSError as err:
67-
if self.init_under_package_root(path):
68-
try:
69-
return self._fake_init(path)
70-
except OSError:
71-
pass
7261
# Take a copy to get rid of associated traceback and frame objects.
7362
# Just assigning to __traceback__ doesn't free them.
7463
self.stat_error_cache[path] = copy_os_error(err)
7564
raise err
7665
self.stat_cache[path] = st
7766
return st
7867

79-
def init_under_package_root(self, path: str) -> bool:
80-
"""Is this path an __init__.py under a package root?
81-
82-
This is used to detect packages that don't contain __init__.py
83-
files, which is needed to support Bazel. The function should
84-
only be called for non-existing files.
85-
86-
It will return True if it refers to a __init__.py file that
87-
Bazel would create, so that at runtime Python would think the
88-
directory containing it is a package. For this to work you
89-
must pass one or more package roots using the --package-root
90-
flag.
91-
92-
As an exceptional case, any directory that is a package root
93-
itself will not be considered to contain a __init__.py file.
94-
This is different from the rules Bazel itself applies, but is
95-
necessary for mypy to properly distinguish packages from other
96-
directories.
97-
98-
See https://docs.bazel.build/versions/master/be/python.html,
99-
where this behavior is described under legacy_create_init.
100-
"""
101-
if not self.package_root:
102-
return False
103-
dirname, basename = os.path.split(path)
104-
if basename != '__init__.py':
105-
return False
106-
try:
107-
st = self.stat(dirname)
108-
except OSError:
109-
return False
110-
else:
111-
if not stat.S_ISDIR(st.st_mode):
112-
return False
113-
ok = False
114-
drive, path = os.path.splitdrive(path) # Ignore Windows drive name
115-
path = os.path.normpath(path)
116-
for root in self.package_root:
117-
if path.startswith(root):
118-
if path == root + basename:
119-
# A package root itself is never a package.
120-
ok = False
121-
break
122-
else:
123-
ok = True
124-
return ok
125-
126-
def _fake_init(self, path: str) -> os.stat_result:
127-
"""Prime the cache with a fake __init__.py file.
128-
129-
This makes code that looks for path believe an empty file by
130-
that name exists. Should only be called after
131-
init_under_package_root() returns True.
132-
"""
133-
dirname, basename = os.path.split(path)
134-
assert basename == '__init__.py', path
135-
assert not os.path.exists(path), path # Not cached!
136-
dirname = os.path.normpath(dirname)
137-
st = self.stat(dirname) # May raise OSError
138-
# Get stat result as a sequence so we can modify it.
139-
# (Alas, typeshed's os.stat_result is not a sequence yet.)
140-
tpl = tuple(st) # type: ignore[arg-type, var-annotated]
141-
seq = list(tpl) # type: List[float]
142-
seq[stat.ST_MODE] = stat.S_IFREG | 0o444
143-
seq[stat.ST_INO] = 1
144-
seq[stat.ST_NLINK] = 1
145-
seq[stat.ST_SIZE] = 0
146-
tpl = tuple(seq)
147-
st = os.stat_result(tpl)
148-
self.stat_cache[path] = st
149-
# Make listdir() and read() also pretend this file exists.
150-
self.fake_package_cache.add(dirname)
151-
return st
152-
15368
def listdir(self, path: str) -> List[str]:
15469
path = os.path.normpath(path)
15570
if path in self.listdir_cache:

mypy/main.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@ def set_strict_flags() -> None:
905905

906906
# Process --package-root.
907907
if options.package_root:
908-
process_package_roots(fscache, parser, options)
908+
process_package_roots(parser, options)
909909

910910
# Process --cache-map.
911911
if special_opts.cache_map:
@@ -955,39 +955,25 @@ def set_strict_flags() -> None:
955955
return targets, options
956956

957957

958-
def process_package_roots(fscache: Optional[FileSystemCache],
959-
parser: argparse.ArgumentParser,
958+
def process_package_roots(parser: argparse.ArgumentParser,
960959
options: Options) -> None:
961960
"""Validate and normalize package_root."""
962-
if fscache is None:
963-
parser.error("--package-root does not work here (no fscache)")
964-
assert fscache is not None # Since mypy doesn't know parser.error() raises.
965961
# Do some stuff with drive letters to make Windows happy (esp. tests).
966962
current_drive, _ = os.path.splitdrive(os.getcwd())
967-
dot = os.curdir
968-
dotslash = os.curdir + os.sep
969963
dotdotslash = os.pardir + os.sep
970-
trivial_paths = {dot, dotslash}
971964
package_root = []
972965
for root in options.package_root:
973966
if os.path.isabs(root):
974967
parser.error("Package root cannot be absolute: %r" % root)
975968
drive, root = os.path.splitdrive(root)
976969
if drive and drive != current_drive:
977970
parser.error("Package root must be on current drive: %r" % (drive + root))
978-
# Empty package root is always okay.
979-
if root:
980-
root = os.path.relpath(root) # Normalize the heck out of it.
981-
if root.startswith(dotdotslash):
982-
parser.error("Package root cannot be above current directory: %r" % root)
983-
if root in trivial_paths:
984-
root = ''
985-
elif not root.endswith(os.sep):
986-
root = root + os.sep
971+
972+
root = util.normalise_package_root(root)
973+
if root.startswith(dotdotslash):
974+
parser.error("Package root cannot be above current directory: %r" % root)
987975
package_root.append(root)
988976
options.package_root = package_root
989-
# Pass the package root on the the filesystem cache.
990-
fscache.set_package_root(package_root)
991977

992978

993979
def process_cache_map(parser: argparse.ArgumentParser,

mypy/suggestions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,9 @@ def __init__(self, fgmanager: FineGrainedBuildManager,
220220
self.manager = fgmanager.manager
221221
self.plugin = self.manager.plugin
222222
self.graph = fgmanager.graph
223-
self.finder = SourceFinder(self.manager.fscache)
223+
self.finder = SourceFinder(
224+
self.manager.fscache, self.manager.options.package_root or None
225+
)
224226

225227
self.give_json = json
226228
self.no_errors = no_errors

mypy/util.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,22 @@ def parse_gray_color(cup: bytes) -> str:
475475
return gray
476476

477477

478+
def normalise_package_root(root: str) -> str:
479+
# Empty package root is always okay.
480+
if not root:
481+
return ''
482+
483+
dot = os.curdir
484+
dotslash = os.curdir + os.sep
485+
trivial_paths = {dot, dotslash}
486+
root = os.path.relpath(root) # Normalize the heck out of it.
487+
if root in trivial_paths:
488+
return ''
489+
if root.endswith(os.sep):
490+
root = root[:-1]
491+
return root
492+
493+
478494
class FancyFormatter:
479495
"""Apply color and bold font to terminal output.
480496

0 commit comments

Comments
 (0)