Skip to content

Commit 0fd28c1

Browse files
committed
Add --install-types option for installing missing stub packages
It can used in a normal mypy run (missing stub packages will be installed at the end). We also record the missing stub packages in a file so that another "mypy --install-types" run (with no other arguments) can be used to just install stub packages, without type checking anything.
1 parent 02d63ed commit 0fd28c1

File tree

4 files changed

+77
-2
lines changed

4 files changed

+77
-2
lines changed

mypy/build.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ def _build(sources: List[BuildSource],
268268
reports.finish()
269269
if not cache_dir_existed and os.path.isdir(options.cache_dir):
270270
add_catch_all_gitignore(options.cache_dir)
271+
if os.path.isdir(options.cache_dir):
272+
record_missing_stub_packages(options.cache_dir, manager.missing_stub_packages)
271273

272274

273275
def default_data_dir() -> str:
@@ -640,6 +642,8 @@ def __init__(self, data_dir: str,
640642
# the semantic analyzer, used only for testing. Currently used only by the new
641643
# semantic analyzer.
642644
self.processed_targets = [] # type: List[str]
645+
# Missing stub packages encountered.
646+
self.missing_stub_packages = set() # type: Set[str]
643647

644648
def dump_stats(self) -> None:
645649
if self.options.dump_build_stats:
@@ -2498,6 +2502,8 @@ def module_not_found(manager: BuildManager, line: int, caller_state: State,
24982502
if '{}' in note:
24992503
note = note.format(legacy_bundled_packages[target])
25002504
errors.report(line, 0, note, severity='note', only_once=True, code=codes.IMPORT)
2505+
if reason is ModuleNotFoundReason.STUBS_NOT_INSTALLED:
2506+
manager.missing_stub_packages.add(legacy_bundled_packages[target])
25012507
errors.set_import_context(save_import_context)
25022508

25032509

@@ -3223,3 +3229,23 @@ def topsort(data: Dict[AbstractSet[str],
32233229
for item, dep in data.items()
32243230
if item not in ready}
32253231
assert not data, "A cyclic dependency exists amongst %r" % data
3232+
3233+
3234+
def missing_stubs_file(cache_dir: str) -> str:
3235+
return os.path.join(cache_dir, 'missing_stubs')
3236+
3237+
3238+
def record_missing_stub_packages(cache_dir: str, missing_stub_packages: Set[str]) -> None:
3239+
"""Write a file containing missing stub packages.
3240+
3241+
This allows a subsequent "mypy --install-types" run (without other arguments)
3242+
to install missing stub packages.
3243+
"""
3244+
fnam = missing_stubs_file(cache_dir)
3245+
if missing_stub_packages:
3246+
with open(fnam, 'w') as f:
3247+
for pkg in sorted(missing_stub_packages):
3248+
f.write('%s\n' % pkg)
3249+
else:
3250+
if os.path.isfile(fnam):
3251+
os.remove(fnam)

mypy/main.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ def main(script_path: Optional[str],
6666
messages = []
6767
formatter = util.FancyFormatter(stdout, stderr, options.show_error_codes)
6868

69+
if options.install_types and (stdout is not sys.stdout or stderr is not sys.stderr):
70+
# Since --install-types performs user input, we want regular stdout and stderr.
71+
fail("--install-types not supported in this mode of running mypy", stderr, options)
72+
73+
if options.install_types and not sources:
74+
install_types(options.cache_dir, formatter)
75+
return
76+
6977
def flush_errors(new_messages: List[str], serious: bool) -> None:
7078
if options.pretty:
7179
new_messages = formatter.fit_in_terminal(new_messages)
@@ -116,6 +124,11 @@ def flush_errors(new_messages: List[str], serious: bool) -> None:
116124
stdout.write(formatter.format_success(len(sources),
117125
options.color_output) + '\n')
118126
stdout.flush()
127+
128+
if options.install_types:
129+
install_types(options.cache_dir, formatter, after_run=True)
130+
return
131+
119132
if options.fast_exit:
120133
# Exit without freeing objects -- it's faster.
121134
#
@@ -722,6 +735,10 @@ def add_invertible_flag(flag: str,
722735
'--scripts-are-modules', action='store_true',
723736
help="Script x becomes module x instead of __main__")
724737

738+
add_invertible_flag('--install-types', default=False, strict_flag=False,
739+
help="Install detected missing library stub packages using pip",
740+
group=other_group)
741+
725742
if server_options:
726743
# TODO: This flag is superfluous; remove after a short transition (2018-03-16)
727744
other_group.add_argument(
@@ -849,7 +866,7 @@ def set_strict_flags() -> None:
849866
code_methods = sum(bool(c) for c in [special_opts.modules + special_opts.packages,
850867
special_opts.command,
851868
special_opts.files])
852-
if code_methods == 0:
869+
if code_methods == 0 and not options.install_types:
853870
parser.error("Missing target module, package, files, or command.")
854871
elif code_methods > 1:
855872
parser.error("May only specify one of: module/package, files, or command.")
@@ -996,3 +1013,31 @@ def fail(msg: str, stderr: TextIO, options: Options) -> None:
9961013
stderr.write('%s\n' % msg)
9971014
maybe_write_junit_xml(0.0, serious=True, messages=[msg], options=options)
9981015
sys.exit(2)
1016+
1017+
1018+
def install_types(cache_dir: str,
1019+
formatter: util.FancyFormatter,
1020+
after_run: bool = False) -> None:
1021+
"""Install stub packages using pip if some missing stubs were detected."""
1022+
if not os.path.isdir(cache_dir):
1023+
sys.stderr.write(
1024+
"Error: no mypy cache directory (you must enable incremental mode)\n")
1025+
sys.exit(2)
1026+
fnam = build.missing_stubs_file(cache_dir)
1027+
if not os.path.isfile(fnam):
1028+
# If there are no missing stubs, generate no output.
1029+
return
1030+
with open(fnam) as f:
1031+
packages = [line.strip() for line in f.readlines()]
1032+
if after_run:
1033+
print()
1034+
print('Installing missing stub packages:')
1035+
cmd = ['python3', '-m', 'pip', 'install'] + packages
1036+
print(formatter.style(' '.join(cmd), 'none', bold=True))
1037+
print()
1038+
x = input('Install? [yN] ')
1039+
if not x.strip() or not x.lower().startswith('y'):
1040+
print(formatter.style('mypy: Skipping installation', 'red', bold=True))
1041+
sys.exit(2)
1042+
print()
1043+
subprocess.run(cmd)

mypy/modulefinder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ def error_message_templates(self) -> Tuple[str, List[str]]:
6262
notes = [doc_link]
6363
elif self is ModuleNotFoundReason.STUBS_NOT_INSTALLED:
6464
msg = 'Library stubs not installed for "{}"'
65-
notes = ['Hint: "python3 -m pip install {}"', doc_link]
65+
notes = ['Hint: "python3 -m pip install {}"',
66+
'(or run "mypy --install-types" to install all missing stub packages)',
67+
doc_link]
6668
else:
6769
assert False
6870
return msg, notes

mypy/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ def __init__(self) -> None:
268268
self.transform_source = None # type: Optional[Callable[[Any], Any]]
269269
# Print full path to each file in the report.
270270
self.show_absolute_path = False # type: bool
271+
# Install missing stub packages if True
272+
self.install_types = False
271273

272274
# To avoid breaking plugin compatibility, keep providing new_semantic_analyzer
273275
@property

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy