Skip to content

Allow multiple shadow files to be specified #5023

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 10 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
3 changes: 2 additions & 1 deletion docs/source/command_line.rst
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,8 @@ Here are some more useful flags:
make transformations to a file before type checking without having to change
the file in-place. (For example, tooling could use this to display the type
of an expression by wrapping it with a call to reveal_type in the shadow
file and then parsing the output.)
file and then parsing the output.) This argument may be specified multiple times
to make mypy substitute multiple different files with their shadow replacements.
Copy link
Member

Choose a reason for hiding this comment

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

This needs an example -- do you write --shadow-file X1 X2 Y1 Y2 or --shadow-file X1 X2 --shadow-file Y1 Y2? (I presume the latter, but some options use the former syntax, and argparse supports both.)

Also, this argument has caused many people to be confused. We should present an example explaining that --shadow-file X1 X2 means that whenever the checker is asked to check X1, it actually reads and checks the contents of X2, but diagnostics will refer to X1 (it may well be the latter bit that trips people up).


.. _no-implicit-optional:

Expand Down
29 changes: 26 additions & 3 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,13 +655,36 @@ def __init__(self, data_dir: str,
self.fscache = fscache
self.find_module_cache = FindModuleCache(self.fscache)

# a mapping from source files to their corresponding shadow files
# for efficient lookup
self.shadow_map = {} # type: Dict[str, str]
if self.options.shadow_file is not None:
self.shadow_map = {source_file: shadow_file
for (source_file, shadow_file)
in self.options.shadow_file}
# a mapping from each file being typechecked to its possible shadow file
self.shadow_equivalence_map = {} # type: Dict[str, Optional[str]]

def use_fine_grained_cache(self) -> bool:
return self.cache_enabled and self.options.use_fine_grained_cache

def maybe_swap_for_shadow_path(self, path: str) -> str:
if (self.options.shadow_file and
os.path.samefile(self.options.shadow_file[0], path)):
path = self.options.shadow_file[1]
if not self.options.shadow_file:
Copy link
Member

Choose a reason for hiding this comment

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

IMO it would be better if this checked for if not self.shadow_map. The Options object has been processed already in __init__() above.

return path

previously_checked = path in self.shadow_equivalence_map
if not previously_checked:
for k in self.shadow_map.keys():
Copy link
Member

Choose a reason for hiding this comment

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

I'd switch this to for k, v in self.shadow_map.items(), then you can use v instead of self.shadow_map.get(k) two lines below.

(Also perhaps use more imaginative names than k, v -- how about source, shadow?

if self.fscache.samefile(path, k):
self.shadow_equivalence_map[path] = self.shadow_map.get(k)
break
else:
self.shadow_equivalence_map[path] = None

shadow_file = self.shadow_equivalence_map.get(path)
Copy link
Member

Choose a reason for hiding this comment

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

This and the rest of the function could be shortened to

return self.shadow_equivalence_map.get(path, path)

if shadow_file:
path = shadow_file

return path

def get_stat(self, path: str) -> os.stat_result:
Expand Down
5 changes: 5 additions & 0 deletions mypy/fscache.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ def md5(self, path: str) -> str:
self.read(path)
return self.hash_cache[path]

def samefile(self, f1: str, f2: str) -> bool:
s1 = self.stat(f1)
s2 = self.stat(f2)
return os.path.samestat(s1, s2) # type: ignore
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious -- why do you need # type: ignore here?

Copy link
Member

Choose a reason for hiding this comment

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

We were missing this function in the stub. We just merged the PR adding it, so this ignore can be removed as soon as we sync typeshed.



def copy_os_error(e: OSError) -> OSError:
new = OSError(*e.args)
Expand Down
2 changes: 1 addition & 1 deletion mypy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ def add_invertible_flag(flag: str,
parser.add_argument('--strict', action='store_true', dest='special-opts:strict',
help=strict_help)
parser.add_argument('--shadow-file', nargs=2, metavar=('SOURCE_FILE', 'SHADOW_FILE'),
dest='shadow_file',
dest='shadow_file', action='append',
help='Typecheck SHADOW_FILE in place of SOURCE_FILE.')
# hidden options
# --debug-cache will disable any cache-related compressions/optimizations,
Expand Down
2 changes: 1 addition & 1 deletion mypy/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def __init__(self) -> None:
self.use_builtins_fixtures = False

# -- experimental options --
self.shadow_file = None # type: Optional[Tuple[str, str]]
self.shadow_file = None # type: Optional[List[Tuple[str, str]]]
self.show_column_numbers = False # type: bool
self.dump_graph = False
self.dump_deps = False
Expand Down
39 changes: 39 additions & 0 deletions test-data/unit/cmdline.test
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,45 @@ follow_imports_for_stubs = True
import math
math.frobnicate()

[case testShadowFile1]
# cmd: mypy --shadow-file source.py shadow.py source.py
[file source.py]
def foo() -> str:
return "bar"
[file shadow.py]
def bar() -> str:
return 14
[out]
source.py:2: error: Incompatible return value type (got "int", expected "str")

[case testShadowFile2]
# cmd: mypy --shadow-file s1.py shad1.py --shadow-file s2.py shad2.py --shadow-file s3.py shad3.py s1.py s2.py s3.py s4.py
[file s1.py]
def foo() -> str:
return "bar"
[file shad1.py]
def bar() -> str:
return 14
[file s2.py]
def baz() -> str:
return 14
[file shad2.py]
def baz() -> int:
return 14
[file s3.py]
def qux() -> str:
return "bar"
[file shad3.py]
def foo() -> int:
return [42]
[file s4.py]
def foo() -> str:
return 9
[out]
s4.py:2: error: Incompatible return value type (got "int", expected "str")
s3.py:2: error: Incompatible return value type (got "List[int]", expected "int")
s1.py:2: error: Incompatible return value type (got "int", expected "str")

[case testConfigWarnUnusedSection1]
# cmd: mypy foo.py quux.py spam/eggs.py
# flags: --follow-imports=skip
Expand Down