Skip to content

Commit 83b6c85

Browse files
committed
Merge remote-tracking branch 'upstream/master' into remove-myunit
2 parents 491bac9 + fc2c678 commit 83b6c85

39 files changed

+1059
-178
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ sure you've found a bug please search our issue trackers for a
1414
duplicate before filing a new issue:
1515

1616
- [mypy tracker](https://github.com/python/mypy/issues)
17-
for mypy isues
17+
for mypy issues
1818
- [typeshed tracker](https://github.com/python/typeshed/issues)
1919
for issues with specific modules
2020
- [typing tracker](https://github.com/python/typing/issues)

mypy/build.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from mypy.version import __version__
5353
from mypy.plugin import Plugin, DefaultPlugin, ChainedPlugin
5454
from mypy.defaults import PYTHON3_VERSION_MIN
55+
from mypy.server.deps import get_dependencies
5556

5657

5758
PYTHON_EXTENSIONS = ['.pyi', '.py']
@@ -1183,6 +1184,7 @@ def compute_hash(text: str) -> str:
11831184

11841185

11851186
def write_cache(id: str, path: str, tree: MypyFile,
1187+
serialized_fine_grained_deps: Dict[str, List[str]],
11861188
dependencies: List[str], suppressed: List[str],
11871189
child_modules: List[str], dep_prios: List[int],
11881190
old_interface_hash: str, source_hash: str,
@@ -1221,7 +1223,9 @@ def write_cache(id: str, path: str, tree: MypyFile,
12211223
assert os.path.dirname(meta_json) == parent
12221224

12231225
# Serialize data and analyze interface
1224-
data = tree.serialize()
1226+
data = {'tree': tree.serialize(),
1227+
'fine_grained_deps': serialized_fine_grained_deps,
1228+
}
12251229
if manager.options.debug_cache:
12261230
data_str = json.dumps(data, indent=2, sort_keys=True)
12271231
else:
@@ -1523,6 +1527,8 @@ class State:
15231527
# Whether the module has an error or any of its dependencies have one.
15241528
transitive_error = False
15251529

1530+
fine_grained_deps = None # type: Dict[str, Set[str]]
1531+
15261532
# Type checker used for checking this file. Use type_checker() for
15271533
# access and to construct this on demand.
15281534
_type_checker = None # type: Optional[TypeChecker]
@@ -1551,6 +1557,7 @@ def __init__(self,
15511557
self.id = id or '__main__'
15521558
self.options = manager.options.clone_for_module(self.id)
15531559
self._type_checker = None
1560+
self.fine_grained_deps = {}
15541561
if not path and source is None:
15551562
assert id is not None
15561563
file_id = id
@@ -1734,7 +1741,9 @@ def load_tree(self) -> None:
17341741
with open(self.meta.data_json) as f:
17351742
data = json.load(f)
17361743
# TODO: Assert data file wasn't changed.
1737-
self.tree = MypyFile.deserialize(data)
1744+
self.tree = MypyFile.deserialize(data['tree'])
1745+
self.fine_grained_deps = {k: set(v) for k, v in data['fine_grained_deps'].items()}
1746+
17381747
self.manager.modules[self.id] = self.tree
17391748
self.manager.add_stats(fresh_trees=1)
17401749

@@ -1977,6 +1986,19 @@ def _patch_indirect_dependencies(self,
19771986
elif dep not in self.suppressed and dep in self.manager.missing_modules:
19781987
self.suppressed.append(dep)
19791988

1989+
def compute_fine_grained_deps(self) -> None:
1990+
assert self.tree is not None
1991+
if '/typeshed/' in self.xpath or self.xpath.startswith('typeshed/'):
1992+
# We don't track changes to typeshed -- the assumption is that they are only changed
1993+
# as part of mypy updates, which will invalidate everything anyway.
1994+
#
1995+
# TODO: Not a reliable test, as we could have a package named typeshed.
1996+
# TODO: Consider relaxing this -- maybe allow some typeshed changes to be tracked.
1997+
return
1998+
self.fine_grained_deps = get_dependencies(target=self.tree,
1999+
type_map=self.type_map(),
2000+
python_version=self.options.python_version)
2001+
19802002
def valid_references(self) -> Set[str]:
19812003
assert self.ancestors is not None
19822004
valid_refs = set(self.dependencies + self.suppressed + self.ancestors)
@@ -2003,6 +2025,7 @@ def write_cache(self) -> None:
20032025
dep_prios = self.dependency_priorities()
20042026
new_interface_hash, self.meta = write_cache(
20052027
self.id, self.path, self.tree,
2028+
{k: list(v) for k, v in self.fine_grained_deps.items()},
20062029
list(self.dependencies), list(self.suppressed), list(self.child_modules),
20072030
dep_prios, self.interface_hash, self.source_hash, self.ignore_all,
20082031
self.manager)
@@ -2534,6 +2557,8 @@ def process_stale_scc(graph: Graph, scc: List[str], manager: BuildManager) -> No
25342557
graph[id].transitive_error = True
25352558
for id in stale:
25362559
graph[id].finish_passes()
2560+
if manager.options.cache_fine_grained:
2561+
graph[id].compute_fine_grained_deps()
25372562
graph[id].generate_unused_ignore_notes()
25382563
manager.flush_errors(manager.errors.file_messages(graph[id].xpath), False)
25392564
graph[id].write_cache()

mypy/dmypy_server.py

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import mypy.server.update
2424
from mypy.dmypy_util import STATUS_FILE, receive
2525
from mypy.gclogger import GcLogger
26+
from mypy.fscache import FileSystemCache
27+
from mypy.fswatcher import FileSystemWatcher
2628

2729

2830
def daemonize(func: Callable[[], None], log_file: Optional[str] = None) -> int:
@@ -84,13 +86,11 @@ class Server:
8486
def __init__(self, flags: List[str]) -> None:
8587
"""Initialize the server with the desired mypy flags."""
8688
self.saved_cache = {} # type: mypy.build.SavedCache
87-
if '--experimental' in flags:
88-
self.fine_grained = True
89-
self.fine_grained_initialized = False
90-
flags.remove('--experimental')
91-
else:
92-
self.fine_grained = False
93-
sources, options = mypy.main.process_options(['-i'] + flags, False)
89+
self.fine_grained_initialized = False
90+
sources, options = mypy.main.process_options(['-i'] + flags,
91+
require_targets=False,
92+
server_options=True)
93+
self.fine_grained = options.fine_grained_incremental
9494
if sources:
9595
sys.exit("dmypy: start/restart does not accept sources")
9696
if options.report_dirs:
@@ -243,14 +243,11 @@ def check_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict[str,
243243
return self.fine_grained_increment(sources)
244244

245245
def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
246-
self.file_modified = {} # type: Dict[str, float]
247-
for source in sources:
248-
assert source.path
249-
try:
250-
self.file_modified[source.path] = os.stat(source.path).st_mtime
251-
except FileNotFoundError:
252-
# Don't crash if passed a non-existent file.
253-
pass
246+
self.fscache = FileSystemCache(self.options.python_version)
247+
self.fswatcher = FileSystemWatcher(self.fscache)
248+
self.update_sources(sources)
249+
# Stores the initial state of sources as a side effect.
250+
self.fswatcher.find_changed()
254251
try:
255252
# TODO: alt_lib_path
256253
result = mypy.build.build(sources=sources,
@@ -270,9 +267,11 @@ def initialize_fine_grained(self, sources: List[mypy.build.BuildSource]) -> Dict
270267
self.previous_messages = messages[:]
271268
self.fine_grained_initialized = True
272269
self.previous_sources = sources
270+
self.fscache.flush()
273271
return {'out': ''.join(s + '\n' for s in messages), 'err': '', 'status': status}
274272

275273
def fine_grained_increment(self, sources: List[mypy.build.BuildSource]) -> Dict[str, Any]:
274+
self.update_sources(sources)
276275
changed = self.find_changed(sources)
277276
if not changed:
278277
# Nothing changed -- just produce the same result as before.
@@ -282,36 +281,26 @@ def fine_grained_increment(self, sources: List[mypy.build.BuildSource]) -> Dict[
282281
status = 1 if messages else 0
283282
self.previous_messages = messages[:]
284283
self.previous_sources = sources
284+
self.fscache.flush()
285285
return {'out': ''.join(s + '\n' for s in messages), 'err': '', 'status': status}
286286

287+
def update_sources(self, sources: List[mypy.build.BuildSource]) -> None:
288+
paths = [source.path for source in sources if source.path is not None]
289+
self.fswatcher.add_watched_paths(paths)
290+
287291
def find_changed(self, sources: List[mypy.build.BuildSource]) -> List[Tuple[str, str]]:
288-
changed = []
289-
for source in sources:
290-
path = source.path
291-
assert path
292-
try:
293-
mtime = os.stat(path).st_mtime
294-
except FileNotFoundError:
295-
# A non-existent file was included on the command line.
296-
#
297-
# TODO: Generate error if file is missing (if not ignoring missing imports)
298-
if path in self.file_modified:
299-
changed.append((source.module, path))
300-
else:
301-
if path not in self.file_modified or self.file_modified[path] != mtime:
302-
self.file_modified[path] = mtime
303-
changed.append((source.module, path))
292+
changed_paths = self.fswatcher.find_changed()
293+
changed = [(source.module, source.path)
294+
for source in sources
295+
if source.path in changed_paths]
304296
modules = {source.module for source in sources}
305297
omitted = [source for source in self.previous_sources if source.module not in modules]
306298
for source in omitted:
307299
path = source.path
308300
assert path
309-
# Note that a file could be removed from the list of root sources but still continue
310-
# to exist on the file system.
311-
if not os.path.isfile(path):
301+
# Note that a file could be removed from the list of root sources but have no changes.
302+
if path in changed_paths:
312303
changed.append((source.module, path))
313-
if source.path in self.file_modified:
314-
del self.file_modified[source.path]
315304
return changed
316305

317306
def cmd_hang(self) -> Dict[str, object]:

mypy/fastparse.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -960,8 +960,12 @@ def visit_Name(self, n: ast3.Name) -> NameExpr:
960960

961961
# List(expr* elts, expr_context ctx)
962962
@with_line
963-
def visit_List(self, n: ast3.List) -> ListExpr:
964-
return ListExpr([self.visit(e) for e in n.elts])
963+
def visit_List(self, n: ast3.List) -> Union[ListExpr, TupleExpr]:
964+
expr_list = [self.visit(e) for e in n.elts] # type: List[Expression]
965+
if isinstance(n.ctx, ast3.Store):
966+
# [x, y] = z and (x, y) = z means exactly the same thing
967+
return TupleExpr(expr_list)
968+
return ListExpr(expr_list)
965969

966970
# Tuple(expr* elts, expr_context ctx)
967971
@with_line
@@ -1155,6 +1159,7 @@ def visit_Ellipsis(self, n: ast3.Ellipsis) -> Type:
11551159

11561160
# List(expr* elts, expr_context ctx)
11571161
def visit_List(self, n: ast3.List) -> Type:
1162+
assert isinstance(n.ctx, ast3.Load)
11581163
return self.translate_argument_list(n.elts)
11591164

11601165

mypy/fastparse2.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,8 +912,12 @@ def visit_Name(self, n: ast27.Name) -> NameExpr:
912912

913913
# List(expr* elts, expr_context ctx)
914914
@with_line
915-
def visit_List(self, n: ast27.List) -> ListExpr:
916-
return ListExpr([self.visit(e) for e in n.elts])
915+
def visit_List(self, n: ast27.List) -> Union[ListExpr, TupleExpr]:
916+
expr_list = [self.visit(e) for e in n.elts] # type: List[Expression]
917+
if isinstance(n.ctx, ast27.Store):
918+
# [x, y] = z and (x, y) = z means exactly the same thing
919+
return TupleExpr(expr_list)
920+
return ListExpr(expr_list)
917921

918922
# Tuple(expr* elts, expr_context ctx)
919923
@with_line

mypy/fscache.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
"""Interface for accessing the file system with automatic caching.
2+
3+
The idea is to cache the results of any file system state reads during
4+
a single transaction. This has two main benefits:
5+
6+
* This avoids redundant syscalls, as we won't perform the same OS
7+
operations multiple times.
8+
9+
* This makes it easier to reason about concurrent FS updates, as different
10+
operations targeting the same paths can't report different state during
11+
a transaction.
12+
13+
Note that this only deals with reading state, not writing.
14+
15+
Properties maintained by the API:
16+
17+
* The contents of the file are always from the same or later time compared
18+
to the reported mtime of the file, even if mtime is queried after reading
19+
a file.
20+
21+
* Repeating an operation produces the same result as the first one during
22+
a transaction.
23+
24+
* Call flush() to start a new transaction (flush the caches).
25+
26+
The API is a bit limited. It's easy to add new cached operations, however.
27+
You should perform all file system reads through the API to actually take
28+
advantage of the benefits.
29+
"""
30+
31+
import os
32+
import stat
33+
from typing import Tuple, Dict, List
34+
35+
from mypy.build import read_with_python_encoding
36+
from mypy.errors import DecodeError
37+
38+
39+
class FileSystemCache:
40+
def __init__(self, pyversion: Tuple[int, int]) -> None:
41+
self.pyversion = pyversion
42+
self.flush()
43+
44+
def flush(self) -> None:
45+
"""Start another transaction and empty all caches."""
46+
self.stat_cache = {} # type: Dict[str, os.stat_result]
47+
self.stat_error_cache = {} # type: Dict[str, Exception]
48+
self.read_cache = {} # type: Dict[str, str]
49+
self.read_error_cache = {} # type: Dict[str, Exception]
50+
self.hash_cache = {} # type: Dict[str, str]
51+
self.listdir_cache = {} # type: Dict[str, List[str]]
52+
self.listdir_error_cache = {} # type: Dict[str, Exception]
53+
54+
def read_with_python_encoding(self, path: str) -> str:
55+
if path in self.read_cache:
56+
return self.read_cache[path]
57+
if path in self.read_error_cache:
58+
raise self.read_error_cache[path]
59+
60+
# Need to stat first so that the contents of file are from no
61+
# earlier instant than the mtime reported by self.stat().
62+
self.stat(path)
63+
64+
try:
65+
data, md5hash = read_with_python_encoding(path, self.pyversion)
66+
except Exception as err:
67+
self.read_error_cache[path] = err
68+
raise
69+
self.read_cache[path] = data
70+
self.hash_cache[path] = md5hash
71+
return data
72+
73+
def stat(self, path: str) -> os.stat_result:
74+
if path in self.stat_cache:
75+
return self.stat_cache[path]
76+
if path in self.stat_error_cache:
77+
raise self.stat_error_cache[path]
78+
try:
79+
st = os.stat(path)
80+
except Exception as err:
81+
self.stat_error_cache[path] = err
82+
raise
83+
self.stat_cache[path] = st
84+
return st
85+
86+
def listdir(self, path: str) -> List[str]:
87+
if path in self.listdir_cache:
88+
return self.listdir_cache[path]
89+
if path in self.listdir_error_cache:
90+
raise self.listdir_error_cache[path]
91+
try:
92+
results = os.listdir(path)
93+
except Exception as err:
94+
self.listdir_error_cache[path] = err
95+
raise err
96+
self.listdir_cache[path] = results
97+
return results
98+
99+
def isfile(self, path: str) -> bool:
100+
st = self.stat(path)
101+
return stat.S_ISREG(st.st_mode)
102+
103+
def isdir(self, path: str) -> bool:
104+
st = self.stat(path)
105+
return stat.S_ISDIR(st.st_mode)
106+
107+
def exists(self, path: str) -> bool:
108+
try:
109+
self.stat(path)
110+
except FileNotFoundError:
111+
return False
112+
return True
113+
114+
def md5(self, path: str) -> str:
115+
if path not in self.hash_cache:
116+
self.read_with_python_encoding(path)
117+
return self.hash_cache[path]

0 commit comments

Comments
 (0)