Skip to content

Commit 2a0c4e5

Browse files
garethtgvanrossum
authored andcommitted
Allow multiple shadow files to be specified (#5023)
1 parent a389280 commit 2a0c4e5

File tree

6 files changed

+86
-13
lines changed

6 files changed

+86
-13
lines changed

docs/source/command_line.rst

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -463,12 +463,20 @@ Here are some more useful flags:
463463

464464
.. _shadow-file:
465465

466-
- ``--shadow-file SOURCE_FILE SHADOW_FILE`` makes mypy typecheck SHADOW_FILE in
467-
place of SOURCE_FILE. Primarily intended for tooling. Allows tooling to
468-
make transformations to a file before type checking without having to change
469-
the file in-place. (For example, tooling could use this to display the type
470-
of an expression by wrapping it with a call to reveal_type in the shadow
471-
file and then parsing the output.)
466+
- ``--shadow-file SOURCE_FILE SHADOW_FILE``: when mypy is asked to typecheck
467+
``SOURCE_FILE``, this makes it read from and typecheck the contents of
468+
``SHADOW_FILE`` instead. However, diagnostics will continue to refer to
469+
``SOURCE_FILE``. Specifying this argument multiple times
470+
(``--shadow-file X1 Y1 --shadow-file X2 Y2``)
471+
will allow mypy to perform multiple substitutions.
472+
473+
This allows tooling to create temporary files with helpful modifications
474+
without having to change the source file in place. For example, suppose we
475+
have a pipeline that adds ``reveal_type`` for certain variables.
476+
This pipeline is run on ``original.py`` to produce ``temp.py``.
477+
Running ``mypy --shadow-file original.py temp.py original.py`` will then
478+
cause mypy to typecheck the contents of ``temp.py`` instead of ``original.py``,
479+
but error messages will still reference ``original.py``.
472480

473481
.. _no-implicit-optional:
474482

mypy/build.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -655,14 +655,34 @@ def __init__(self, data_dir: str,
655655
self.fscache = fscache
656656
self.find_module_cache = FindModuleCache(self.fscache)
657657

658+
# a mapping from source files to their corresponding shadow files
659+
# for efficient lookup
660+
self.shadow_map = {} # type: Dict[str, str]
661+
if self.options.shadow_file is not None:
662+
self.shadow_map = {source_file: shadow_file
663+
for (source_file, shadow_file)
664+
in self.options.shadow_file}
665+
# a mapping from each file being typechecked to its possible shadow file
666+
self.shadow_equivalence_map = {} # type: Dict[str, Optional[str]]
667+
658668
def use_fine_grained_cache(self) -> bool:
659669
return self.cache_enabled and self.options.use_fine_grained_cache
660670

661671
def maybe_swap_for_shadow_path(self, path: str) -> str:
662-
if (self.options.shadow_file and
663-
os.path.samefile(self.options.shadow_file[0], path)):
664-
path = self.options.shadow_file[1]
665-
return path
672+
if not self.shadow_map:
673+
return path
674+
675+
previously_checked = path in self.shadow_equivalence_map
676+
if not previously_checked:
677+
for source, shadow in self.shadow_map.items():
678+
if self.fscache.samefile(path, source):
679+
self.shadow_equivalence_map[path] = shadow
680+
break
681+
else:
682+
self.shadow_equivalence_map[path] = None
683+
684+
shadow_file = self.shadow_equivalence_map.get(path)
685+
return shadow_file if shadow_file else path
666686

667687
def get_stat(self, path: str) -> os.stat_result:
668688
return self.fscache.stat(self.maybe_swap_for_shadow_path(path))

mypy/fscache.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ def md5(self, path: str) -> str:
149149
self.read(path)
150150
return self.hash_cache[path]
151151

152+
def samefile(self, f1: str, f2: str) -> bool:
153+
s1 = self.stat(f1)
154+
s2 = self.stat(f2)
155+
return os.path.samestat(s1, s2) # type: ignore
156+
152157

153158
def copy_os_error(e: OSError) -> OSError:
154159
new = OSError(*e.args)

mypy/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,9 @@ def add_invertible_flag(flag: str,
463463
parser.add_argument('--strict', action='store_true', dest='special-opts:strict',
464464
help=strict_help)
465465
parser.add_argument('--shadow-file', nargs=2, metavar=('SOURCE_FILE', 'SHADOW_FILE'),
466-
dest='shadow_file',
467-
help='Typecheck SHADOW_FILE in place of SOURCE_FILE.')
466+
dest='shadow_file', action='append',
467+
help="When encountering SOURCE_FILE, read and typecheck "
468+
"the contents of SHADOW_FILE instead.")
468469
# hidden options
469470
# --debug-cache will disable any cache-related compressions/optimizations,
470471
# which will make the cache writing process output pretty-printed JSON (which

mypy/options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def __init__(self) -> None:
185185
self.use_builtins_fixtures = False
186186

187187
# -- experimental options --
188-
self.shadow_file = None # type: Optional[Tuple[str, str]]
188+
self.shadow_file = None # type: Optional[List[Tuple[str, str]]]
189189
self.show_column_numbers = False # type: bool
190190
self.dump_graph = False
191191
self.dump_deps = False

test-data/unit/cmdline.test

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,45 @@ follow_imports_for_stubs = True
11261126
import math
11271127
math.frobnicate()
11281128

1129+
[case testShadowFile1]
1130+
# cmd: mypy --shadow-file source.py shadow.py source.py
1131+
[file source.py]
1132+
def foo() -> str:
1133+
return "bar"
1134+
[file shadow.py]
1135+
def bar() -> str:
1136+
return 14
1137+
[out]
1138+
source.py:2: error: Incompatible return value type (got "int", expected "str")
1139+
1140+
[case testShadowFile2]
1141+
# 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
1142+
[file s1.py]
1143+
def foo() -> str:
1144+
return "bar"
1145+
[file shad1.py]
1146+
def bar() -> str:
1147+
return 14
1148+
[file s2.py]
1149+
def baz() -> str:
1150+
return 14
1151+
[file shad2.py]
1152+
def baz() -> int:
1153+
return 14
1154+
[file s3.py]
1155+
def qux() -> str:
1156+
return "bar"
1157+
[file shad3.py]
1158+
def foo() -> int:
1159+
return [42]
1160+
[file s4.py]
1161+
def foo() -> str:
1162+
return 9
1163+
[out]
1164+
s4.py:2: error: Incompatible return value type (got "int", expected "str")
1165+
s3.py:2: error: Incompatible return value type (got "List[int]", expected "int")
1166+
s1.py:2: error: Incompatible return value type (got "int", expected "str")
1167+
11291168
[case testConfigWarnUnusedSection1]
11301169
# cmd: mypy foo.py quux.py spam/eggs.py
11311170
# flags: --follow-imports=skip

0 commit comments

Comments
 (0)