Skip to content

Add several undocumented flags (--bazel, --package-root and --cache-map) in support of Bazel #4759

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

Merged
merged 55 commits into from
Jun 5, 2018
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
13a52e1
New --bazel flag
Mar 14, 2018
4618115
Implement behavior changes for --bazel flag.
Mar 14, 2018
d188e9f
Don't raise OSError for makedirs('')
Mar 14, 2018
9d32192
Use zero mtimes in validate_meta() if bazel
Mar 14, 2018
f1c7834
More zero mtime, and don't compare size -> always believe cache file …
Mar 14, 2018
aff2f72
Different tack for cache under Bazel: use --cache-dir but omit pyversion
Mar 16, 2018
8048ee4
Add --cache-map option
Mar 17, 2018
7bc3a0b
Pass cache mapping on command line
Mar 17, 2018
3632c55
Tweak --cache-map arg parsing
Mar 17, 2018
24de3d5
Drop the -i requirement for --bazel; it's not strictly needed
Mar 19, 2018
ec719df
Fix lint
Mar 20, 2018
fcc9604
Somehow mypy/api.py had DOS line endings. Fix this. (#4768)
gvanrossum Mar 20, 2018
c62a22e
Add docstring for relpath()
Mar 22, 2018
72d2657
Merge remote-tracking branch 'upstream/master' into bazel-flag
Mar 26, 2018
4dd90b8
Monumental effort to support packages without __init__.py for Bazel (…
Mar 26, 2018
c667dfc
Fix mypy errors
Mar 27, 2018
a43ecdc
Fix tests
Mar 27, 2018
e17a702
Fix flake8
Mar 27, 2018
0f4c8af
Merge master (except typeshed; resolved one conflict in main.py)
Apr 4, 2018
cd10259
Merge branch 'master' into bazel-flag
Apr 4, 2018
eade116
Make --package-root a separate flag
Apr 5, 2018
e7ffae4
Merge master
Apr 17, 2018
f19892a
Sync typeshed (same as master)
Apr 18, 2018
a0072ab
Add tests for --package-root and fix behavior
Apr 18, 2018
0096ee0
Add rudimentary tests for --cache-map and --bazel.
Apr 18, 2018
cf9f6e0
Merge master
Apr 23, 2018
268b74c
Fix two type errors found by
Apr 23, 2018
dc1f262
Replace relpath() with os.path.relpath() (D'oh)
Apr 23, 2018
e103d63
Use os.path.relpath() in --package-root normalization
Apr 23, 2018
80daeae
Break long line
Apr 23, 2018
b7fcc9e
Another attempt at fixing the Windows test failures
Apr 23, 2018
363f4b6
Merge master
Apr 24, 2018
2314b54
Another attempt at fixing --package-root on Windows
Apr 24, 2018
605b963
Merge master
Apr 24, 2018
a89e7a4
Make Windows tests pass by judicious use of os.path.normpath()
Apr 24, 2018
29f3f59
Merge master (some tests broken)
Apr 25, 2018
052fa08
Change the way FileSystemCache gets the package_root
Apr 25, 2018
fb24a14
Merge remote-tracking branch 'upstream/master' into bazel-flag
Apr 25, 2018
05a448d
Avoid mypy error (see https://github.com/python/typeshed/issues/2081)
Apr 25, 2018
8b88cd2
Merge master
May 1, 2018
aa5facd
Fix fscache.py bug: only generate fake __init__.py, not .pyi
May 2, 2018
755b540
Merge master 211a65470079f9080881db1e069a7577a44c1bb1
May 4, 2018
7884ed4
Avoid updating hash in cache meta
May 7, 2018
01f3893
Merge master (22ee7137a5f21b398b18908cca405d104bc060d4)
May 21, 2018
1f26bdc
Merge upstream/master (464b553a4cf1e8b8a2b78d6cf6ebb6597f13af60)
May 24, 2018
3d7f633
Respond to code review
May 29, 2018
7559fc8
Merge master (0447473de2c00b5429ed0072249ecb4f134e2f38)
May 29, 2018
210de9c
Don't force tree.path absolute in non-bazel -- it breaks Windows
May 29, 2018
15b7d87
Add annotations to new functions, to make self-test pass
May 29, 2018
4135e63
Hrmpf
May 29, 2018
6cbcb08
Merge master
Jun 1, 2018
52649f5
merge master (again)
Jun 2, 2018
3bd329e
Fix bad merge
Jun 2, 2018
389c908
Hopefully fix flaky tests (don't use the fscache in getmtime())
Jun 3, 2018
4665933
Merge master one last time
Jun 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 58 additions & 12 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,38 @@
PYTHON_EXTENSIONS = ['.pyi', '.py']


if os.altsep:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code?

SEPARATORS = frozenset([os.sep, os.altsep])
else:
SEPARATORS = frozenset([os.sep])


Graph = Dict[str, 'State']


def getmtime(name: str) -> int:
return int(os.path.getmtime(name))


def relpath(path: str) -> str:
if not path or path[0] not in SEPARATORS:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""Returns the path relative to the current working directory."""

# Relative path, or has drive letter prefix (C: etc.) on Windows.
assert not os.path.isabs(path), "%s is neither rel nor abs" % (path,)
return path
if os.altsep and os.altsep in path:
# So the rest of this code doesn't need to handle altsep on Windows.
path = path.replace(os.altsep, os.sep)
cwd = os.getcwd()
for i in range(10):
if path.startswith(cwd + os.sep):
# The .lstrip(os.sep) call strips duplicate leading slashes.
return '../' * i + path[len(cwd) + 1:].lstrip(os.sep)
cwd = os.path.dirname(cwd)
if all(c == os.sep for c in cwd):
break
return path


# TODO: Get rid of BuildResult. We might as well return a BuildManager.
class BuildResult:
"""The result of a successful build.
Expand Down Expand Up @@ -228,7 +253,12 @@ def _build(sources: List[BuildSource],
# to the lib_path
# TODO: Don't do this in some cases; for motivation see see
# https://github.com/python/mypy/issues/4195#issuecomment-341915031
lib_path.insert(0, os.getcwd())
if options.bazel:
dir = '.'
else:
dir = os.getcwd()
if dir not in lib_path:
lib_path.insert(0, dir)

# Prepend a config-defined mypy path.
lib_path[:0] = options.mypy_path
Expand Down Expand Up @@ -931,14 +961,17 @@ def get_cache_names(id: str, path: str, manager: BuildManager) -> Tuple[str, str

Args:
id: module ID
path: module path (used to recognize packages)
path: module path
cache_dir: cache directory
pyversion: Python version (major, minor)

Returns:
A tuple with the file names to be used for the meta JSON and the
data JSON, respectively.
"""
pair = manager.options.cache_map.get(path)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels naïvely like it'd be cleaner to always use the cache map in the presence of a cache map at all; I don't know that the hybrid mode of looking in a cache dir and a cache map file would be very meaningful, and it certainly sounds hard to debug.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might well be the case but I haven't solved what to do about typeshed yet, so without keeping cache_dir as a fallback, typeshed will always read the source.

if pair is not None:
return pair
cache_dir = manager.options.cache_dir
pyversion = manager.options.python_version
prefix = os.path.join(cache_dir, '%d.%d' % pyversion, *id.split('.'))
Expand Down Expand Up @@ -1055,15 +1088,16 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
manager.log('Metadata abandoned for {}: errors were previously ignored'.format(id))
return None

bazel = manager.options.bazel
assert path is not None, "Internal error: meta was provided without a path"
# Check data_json; assume if its mtime matches it's good.
# TODO: stat() errors
data_mtime = getmtime(meta.data_json)
data_mtime = 0 if bazel else getmtime(meta.data_json)
if data_mtime != meta.data_mtime:
manager.log('Metadata abandoned for {}: data cache is modified'.format(id))
return None

path = os.path.abspath(path)
path = relpath(path) if bazel else os.path.abspath(path)
try:
st = manager.get_stat(path)
except OSError:
Expand All @@ -1083,11 +1117,11 @@ def validate_meta(meta: Optional[CacheMeta], id: str, path: Optional[str],
return meta

size = st.st_size
if size != meta.size:
if not bazel and size != meta.size:
manager.log('Metadata abandoned for {}: file {} has different size'.format(id, path))
return None

mtime = int(st.st_mtime)
mtime = 0 if bazel else int(st.st_mtime)
if mtime != meta.mtime or path != meta.path:
try:
source_hash = manager.fscache.md5(path)
Expand Down Expand Up @@ -1171,8 +1205,18 @@ def write_cache(id: str, path: str, tree: MypyFile,
corresponding to the metadata that was written (the latter may
be None if the cache could not be written).
"""
# For Bazel we use relative paths and zero mtimes.
bazel = manager.options.bazel

# Obtain file paths
path = os.path.abspath(path)
if not bazel:
# Normally, make all paths absolute.
path = os.path.abspath(path)
else:
# For Bazel, make all paths relative (else Bazel caching is thwarted).
# And also patch tree.path, which might have been made absolute earlier.
tree.path = path = relpath(path)

meta_json, data_json = get_cache_names(id, path, manager)
manager.log('Writing {} {} {} {}'.format(id, path, meta_json, data_json))

Expand All @@ -1192,7 +1236,8 @@ def write_cache(id: str, path: str, tree: MypyFile,

# Obtain and set up metadata
try:
os.makedirs(parent, exist_ok=True)
if parent:
os.makedirs(parent, exist_ok=True)
st = manager.get_stat(path)
except OSError as err:
manager.log("Cannot get stat for {}: {}".format(path, err))
Expand All @@ -1207,10 +1252,11 @@ def write_cache(id: str, path: str, tree: MypyFile,
return interface_hash, None

# Write data cache file, if applicable
# Note that for Bazel we don't record the data file's mtime.
if old_interface_hash == interface_hash:
# If the interface is unchanged, the cached data is guaranteed
# to be equivalent, and we only need to update the metadata.
data_mtime = getmtime(data_json)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

data_mtime = 0 if bazel else getmtime(data_json)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put "0 if bazel else getmtime(path)" into a one-line function? (it appears in a lot of places and causes a bit of cognitive load each time).

manager.trace("Interface for {} is unchanged".format(id))
else:
manager.trace("Interface for {} has changed".format(id))
Expand All @@ -1227,9 +1273,9 @@ def write_cache(id: str, path: str, tree: MypyFile,
# Both have the effect of slowing down the next run a
# little bit due to an out-of-date cache file.
return interface_hash, None
data_mtime = getmtime(data_json)
data_mtime = 0 if bazel else getmtime(data_json)

mtime = int(st.st_mtime)
mtime = 0 if bazel else int(st.st_mtime)
size = st.st_size
options = manager.options.clone_for_module(id)
assert source_hash is not None
Expand Down Expand Up @@ -1271,7 +1317,7 @@ def delete_cache(id: str, path: str, manager: BuildManager) -> None:
This avoids inconsistent states with cache files from different mypy runs,
see #4043 for an example.
"""
path = os.path.abspath(path)
path = relpath(path) if manager.options.bazel else os.path.abspath(path)
meta_json, data_json = get_cache_names(id, path, manager)
manager.log('Deleting {} {} {} {}'.format(id, path, meta_json, data_json))

Expand Down
22 changes: 22 additions & 0 deletions mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ def add_invertible_flag(flag: str,
parser.add_argument('--shadow-file', nargs=2, metavar=('SOURCE_FILE', 'SHADOW_FILE'),
dest='shadow_file',
help='Typecheck SHADOW_FILE in place of SOURCE_FILE.')

# hidden options
# --debug-cache will disable any cache-related compressions/optimizations,
# which will make the cache writing process output pretty-printed JSON (which
Expand All @@ -381,6 +382,15 @@ def add_invertible_flag(flag: str,
# --local-partial-types disallows partial types spanning module top level and a function
# (implicitly defined in fine-grained incremental mode)
parser.add_argument('--local-partial-types', action='store_true', help=argparse.SUPPRESS)
# --bazel changes some behaviors for use with Bazel (https://bazel.build).
parser.add_argument('--bazel', action='store_true', help=argparse.SUPPRESS)
# --cache-map FILE ... gives a mapping from source files to cache files.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am interested in what happened to the idea of dropping cache files next to source files. I remember choosing the same option you did here, but I forget why, because now-me had the idea that cache files next to source would be cleaner for some reason.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to write this up in my overly long status report in #3912 -- Dropbox's build environment puts the output files in a different directory than the source files. It seems outputs (and inputs) must be File objects, the only way to create those is ctx.actions.declare_file(), which always puts it it in the output directory. (The File objects for the sources come from ctx.files.srcs, but I don't think I can add new ones via that approach.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is correct -- you cannot add anything via ctx.files.srcs. You should assume that the source tree is read-only.

If you declare a file, you must create it within the action (Bazel checks this).

# Each triple of arguments is a source file, a cache meta file, and a cache data file.
# Modules not mentioned in the file will go through cache_dir.
# Must be followed by another flag or by '--' (and then only file args may follow).
parser.add_argument('--cache-map', nargs='+', dest='special-opts:cache_map',
help=argparse.SUPPRESS)

# deprecated options
parser.add_argument('--disallow-any', dest='special-opts:disallow_any',
help=argparse.SUPPRESS)
Expand Down Expand Up @@ -520,6 +530,18 @@ def add_invertible_flag(flag: str,
report_dir = val
options.report_dirs[report_type] = report_dir

# Parse cache map. Uses assertions for checking.
if special_opts.cache_map:
n = len(special_opts.cache_map)
assert n % 3 == 0, "--cache-map requires one or more triples (see source)"
for i in range(0, n, 3):
source, meta_file, data_file = special_opts.cache_map[i:i + 3]
assert source not in options.cache_map
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In final code probably wants to be something other than an assert

assert source.endswith('.py') or source.endswith('.pyi'), source
assert meta_file.endswith('.meta.json'), meta_file
assert data_file.endswith('.data.json'), data_file
options.cache_map[source] = (meta_file, data_file)

# Let quick_and_dirty imply incremental.
if options.quick_and_dirty:
options.incremental = True
Expand Down
5 changes: 4 additions & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Options:
}

OPTIONS_AFFECTING_CACHE = ((PER_MODULE_OPTIONS |
{"quick_and_dirty", "platform", "cache_fine_grained"})
{"quick_and_dirty", "platform", "cache_fine_grained", "bazel"})
- {"debug_cache"})

def __init__(self) -> None:
Expand Down Expand Up @@ -176,6 +176,9 @@ def __init__(self) -> None:
self.dump_deps = False
# If True, partial types can't span a module top level and a function
self.local_partial_types = False
# Some behaviors are changed when using Bazel (https://bazel.build).
self.bazel = False
self.cache_map = {} # type: Dict[str, Tuple[str, str]]

def __eq__(self, other: object) -> bool:
return self.__class__ == other.__class__ and self.__dict__ == other.__dict__
Expand Down