Skip to content

Commit 6b05cb7

Browse files
authored
Cache package names in create_source_list (#4848)
1 parent 9b4d942 commit 6b05cb7

File tree

2 files changed

+100
-70
lines changed

2 files changed

+100
-70
lines changed

mypy/find_sources.py

Lines changed: 97 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import os.path
44

5-
from typing import List, Sequence, Set, Tuple, Optional
5+
from typing import List, Sequence, Set, Tuple, Optional, Dict
66

77
from mypy.build import BuildSource, PYTHON_EXTENSIONS
88
from mypy.fscache import FileSystemMetaCache
@@ -17,20 +17,23 @@ class InvalidSourceList(Exception):
1717

1818

1919
def create_source_list(files: Sequence[str], options: Options,
20-
fscache: Optional[FileSystemMetaCache] = None) -> List[BuildSource]:
20+
fscache: Optional[FileSystemMetaCache] = None,
21+
allow_empty_dir: bool = False) -> List[BuildSource]:
2122
"""From a list of source files/directories, makes a list of BuildSources.
2223
2324
Raises InvalidSourceList on errors.
2425
"""
2526
fscache = fscache or FileSystemMetaCache()
27+
finder = SourceFinder(fscache)
28+
2629
targets = []
2730
for f in files:
2831
if f.endswith(PY_EXTENSIONS):
2932
# Can raise InvalidSourceList if a directory doesn't have a valid module name.
30-
targets.append(BuildSource(f, crawl_up(fscache, f)[1], None))
33+
targets.append(BuildSource(f, finder.crawl_up(f), None))
3134
elif fscache.isdir(f):
32-
sub_targets = expand_dir(fscache, f)
33-
if not sub_targets:
35+
sub_targets = finder.expand_dir(f)
36+
if not sub_targets and not allow_empty_dir:
3437
raise InvalidSourceList("There are no .py[i] files in directory '{}'"
3538
.format(f))
3639
targets.extend(sub_targets)
@@ -52,60 +55,101 @@ def keyfunc(name: str) -> Tuple[int, str]:
5255
return (-1, name)
5356

5457

55-
def expand_dir(fscache: FileSystemMetaCache,
56-
arg: str, mod_prefix: str = '') -> List[BuildSource]:
57-
"""Convert a directory name to a list of sources to build."""
58-
f = get_init_file(fscache, arg)
59-
if mod_prefix and not f:
60-
return []
61-
seen = set() # type: Set[str]
62-
sources = []
63-
if f and not mod_prefix:
64-
top_dir, top_mod = crawl_up(fscache, f)
65-
mod_prefix = top_mod + '.'
66-
if mod_prefix:
67-
sources.append(BuildSource(f, mod_prefix.rstrip('.'), None))
68-
names = fscache.listdir(arg)
69-
names.sort(key=keyfunc)
70-
for name in names:
71-
path = os.path.join(arg, name)
72-
if fscache.isdir(path):
73-
sub_sources = expand_dir(fscache, path, mod_prefix + name + '.')
74-
if sub_sources:
75-
seen.add(name)
76-
sources.extend(sub_sources)
58+
class SourceFinder:
59+
def __init__(self, fscache: FileSystemMetaCache) -> None:
60+
self.fscache = fscache
61+
# A cache for package names, mapping from module id to directory path
62+
self.package_cache = {} # type: Dict[str, str]
63+
64+
def expand_dir(self, arg: str, mod_prefix: str = '') -> List[BuildSource]:
65+
"""Convert a directory name to a list of sources to build."""
66+
f = self.get_init_file(arg)
67+
if mod_prefix and not f:
68+
return []
69+
seen = set() # type: Set[str]
70+
sources = []
71+
if f and not mod_prefix:
72+
top_mod = self.crawl_up(f)
73+
mod_prefix = top_mod + '.'
74+
if mod_prefix:
75+
sources.append(BuildSource(f, mod_prefix.rstrip('.'), None))
76+
names = self.fscache.listdir(arg)
77+
names.sort(key=keyfunc)
78+
for name in names:
79+
path = os.path.join(arg, name)
80+
if self.fscache.isdir(path):
81+
sub_sources = self.expand_dir(path, mod_prefix + name + '.')
82+
if sub_sources:
83+
seen.add(name)
84+
sources.extend(sub_sources)
85+
else:
86+
base, suffix = os.path.splitext(name)
87+
if base == '__init__':
88+
continue
89+
if base not in seen and '.' not in base and suffix in PY_EXTENSIONS:
90+
seen.add(base)
91+
src = BuildSource(path, mod_prefix + base, None)
92+
sources.append(src)
93+
return sources
94+
95+
def crawl_up(self, arg: str) -> str:
96+
"""Given a .py[i] filename, return module.
97+
98+
We crawl up the path until we find a directory without
99+
__init__.py[i], or until we run out of path components.
100+
"""
101+
dir, mod = os.path.split(arg)
102+
mod = strip_py(mod) or mod
103+
base = self.crawl_up_dir(dir)
104+
if mod == '__init__' or not mod:
105+
mod = base
77106
else:
78-
base, suffix = os.path.splitext(name)
79-
if base == '__init__':
80-
continue
81-
if base not in seen and '.' not in base and suffix in PY_EXTENSIONS:
82-
seen.add(base)
83-
src = BuildSource(path, mod_prefix + base, None)
84-
sources.append(src)
85-
return sources
107+
mod = module_join(base, mod)
86108

109+
return mod
87110

88-
def crawl_up(fscache: FileSystemMetaCache, arg: str) -> Tuple[str, str]:
89-
"""Given a .py[i] filename, return (root directory, module).
111+
def crawl_up_dir(self, dir: str) -> str:
112+
"""Given a directory name, return the corresponding module name.
90113
91-
We crawl up the path until we find a directory without
92-
__init__.py[i], or until we run out of path components.
93-
"""
94-
dir, mod = os.path.split(arg)
95-
mod = strip_py(mod) or mod
96-
while dir and get_init_file(fscache, dir):
97-
dir, base = os.path.split(dir)
98-
if not base:
99-
break
100-
# Ensure that base is a valid python module name
101-
if not base.isidentifier():
102-
raise InvalidSourceList('{} is not a valid Python package name'.format(base))
103-
if mod == '__init__' or not mod:
104-
mod = base
114+
Use package_cache to cache results.
115+
"""
116+
if dir in self.package_cache:
117+
return self.package_cache[dir]
118+
119+
parent_dir, base = os.path.split(dir)
120+
if not dir or not self.get_init_file(dir) or not base:
121+
res = ''
105122
else:
106-
mod = base + '.' + mod
123+
# Ensure that base is a valid python module name
124+
if not base.isidentifier():
125+
raise InvalidSourceList('{} is not a valid Python package name'.format(base))
126+
parent = self.crawl_up_dir(parent_dir)
127+
res = module_join(parent, base)
128+
129+
self.package_cache[dir] = res
130+
return res
131+
132+
def get_init_file(self, dir: str) -> Optional[str]:
133+
"""Check whether a directory contains a file named __init__.py[i].
134+
135+
If so, return the file's name (with dir prefixed). If not, return
136+
None.
107137
108-
return dir, mod
138+
This prefers .pyi over .py (because of the ordering of PY_EXTENSIONS).
139+
"""
140+
for ext in PY_EXTENSIONS:
141+
f = os.path.join(dir, '__init__' + ext)
142+
if self.fscache.isfile(f):
143+
return f
144+
return None
145+
146+
147+
def module_join(parent: str, child: str) -> str:
148+
"""Join module ids, accounting for a possibly empty parent."""
149+
if parent:
150+
return parent + '.' + child
151+
else:
152+
return child
109153

110154

111155
def strip_py(arg: str) -> Optional[str]:
@@ -117,18 +161,3 @@ def strip_py(arg: str) -> Optional[str]:
117161
if arg.endswith(ext):
118162
return arg[:-len(ext)]
119163
return None
120-
121-
122-
def get_init_file(fscache: FileSystemMetaCache, dir: str) -> Optional[str]:
123-
"""Check whether a directory contains a file named __init__.py[i].
124-
125-
If so, return the file's name (with dir prefixed). If not, return
126-
None.
127-
128-
This prefers .pyi over .py (because of the ordering of PY_EXTENSIONS).
129-
"""
130-
for ext in PY_EXTENSIONS:
131-
f = os.path.join(dir, '__init__' + ext)
132-
if fscache.isfile(f):
133-
return f
134-
return None

mypy/test/testfinegrained.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from mypy.server.mergecheck import check_consistency
2828
from mypy.dmypy_server import Server
2929
from mypy.main import parse_config_file
30-
from mypy.find_sources import expand_dir, create_source_list
30+
from mypy.find_sources import create_source_list
3131
from mypy.fscache import FileSystemMetaCache
3232

3333
import pytest # type: ignore # no pytest in typeshed
@@ -238,7 +238,8 @@ def parse_sources(self, program_text: str,
238238
base = BuildSource(os.path.join(test_temp_dir, 'main'), '__main__', None)
239239
# Use expand_dir instead of create_source_list to avoid complaints
240240
# when there aren't any .py files in an increment
241-
return [base] + expand_dir(FileSystemMetaCache(), test_temp_dir)
241+
return [base] + create_source_list([test_temp_dir], options,
242+
allow_empty_dir=True)
242243

243244

244245
def normalize_messages(messages: List[str]) -> List[str]:

0 commit comments

Comments
 (0)