Skip to content

Commit 0560264

Browse files
authored
Speed up mypy startup by avoiding imports (#6127)
Move some imports that aren't always needed to functions bodies. This speeds up mypy startup by up to 120ms on my laptop (when not compiled). Tests are a bit faster as well.
1 parent c1f48a0 commit 0560264

File tree

5 files changed

+39
-14
lines changed

5 files changed

+39
-14
lines changed

mypy/build.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
from mypy.indirection import TypeIndirectionVisitor
3939
from mypy.errors import Errors, CompileError, report_internal_error
4040
from mypy.util import DecodeError, decode_python_encoding, is_sub_path
41-
from mypy.report import Reports
41+
if MYPY:
42+
from mypy.report import Reports # Avoid unconditional slow import
4243
from mypy import moduleinfo
4344
from mypy.fixup import fixup_module
4445
from mypy.modulefinder import BuildSource, compute_search_paths, FindModuleCache, SearchPaths
@@ -50,7 +51,6 @@
5051
from mypy.version import __version__
5152
from mypy.plugin import Plugin, ChainedPlugin, plugin_types
5253
from mypy.plugins.default import DefaultPlugin
53-
from mypy.server.deps import get_dependencies
5454
from mypy.fscache import FileSystemCache
5555
from mypy.metastore import MetadataStore, FilesystemMetadataStore, SqliteMetadataStore
5656
from mypy.typestate import TypeState, reset_global_state
@@ -182,7 +182,12 @@ def _build(sources: List[BuildSource],
182182

183183
search_paths = compute_search_paths(sources, options, data_dir, alt_lib_path)
184184

185-
reports = Reports(data_dir, options.report_dirs)
185+
reports = None
186+
if options.report_dirs:
187+
# Import lazily to avoid slowing down startup.
188+
from mypy.report import Reports # noqa
189+
reports = Reports(data_dir, options.report_dirs)
190+
186191
source_set = BuildSourceSet(sources)
187192
errors = Errors(options.show_error_context, options.show_column_numbers)
188193
plugin, snapshot = load_plugins(options, errors)
@@ -214,8 +219,9 @@ def _build(sources: List[BuildSource],
214219
(time.time() - manager.start_time,
215220
len(manager.modules),
216221
manager.errors.num_messages()))
217-
# Finish the HTML or XML reports even if CompileError was raised.
218-
reports.finish()
222+
if reports is not None:
223+
# Finish the HTML or XML reports even if CompileError was raised.
224+
reports.finish()
219225

220226

221227
def default_data_dir() -> str:
@@ -473,7 +479,7 @@ def __init__(self, data_dir: str,
473479
search_paths: SearchPaths,
474480
ignore_prefix: str,
475481
source_set: BuildSourceSet,
476-
reports: Reports,
482+
reports: Optional['Reports'],
477483
options: Options,
478484
version_id: str,
479485
plugin: Plugin,
@@ -504,10 +510,11 @@ def __init__(self, data_dir: str,
504510
self.stale_modules = set() # type: Set[str]
505511
self.rechecked_modules = set() # type: Set[str]
506512
self.flush_errors = flush_errors
513+
has_reporters = reports is not None and reports.reporters
507514
self.cache_enabled = (options.incremental
508515
and (not options.fine_grained_incremental
509516
or options.use_fine_grained_cache)
510-
and not reports.reporters)
517+
and not has_reporters)
511518
self.fscache = fscache
512519
self.find_module_cache = FindModuleCache(self.search_paths, self.fscache, self.options)
513520
if options.sqlite_cache:
@@ -679,7 +686,7 @@ def report_file(self,
679686
file: MypyFile,
680687
type_map: Dict[Expression, Type],
681688
options: Options) -> None:
682-
if self.source_set.is_source(file):
689+
if self.reports is not None and self.source_set.is_source(file):
683690
self.reports.file(file, type_map, options)
684691

685692
def stats_summary(self) -> Mapping[str, object]:
@@ -1845,6 +1852,7 @@ def compute_fine_grained_deps(self) -> None:
18451852
# TODO: Not a reliable test, as we could have a package named typeshed.
18461853
# TODO: Consider relaxing this -- maybe allow some typeshed changes to be tracked.
18471854
return
1855+
from mypy.server.deps import get_dependencies # Lazy import to speed up startup
18481856
self.fine_grained_deps = get_dependencies(target=self.tree,
18491857
type_map=self.type_map(),
18501858
python_version=self.options.python_version,

mypy/defaults.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,17 @@
1010
SHARED_CONFIG_FILES = ('setup.cfg',) # type: Final
1111
USER_CONFIG_FILES = ('~/.mypy.ini',) # type: Final
1212
CONFIG_FILES = (CONFIG_FILE,) + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final
13+
14+
# This must include all reporters defined in mypy.report. This is defined here
15+
# to make reporter names available without importing mypy.report -- this speeds
16+
# up startup.
17+
REPORTER_NAMES = ['linecount',
18+
'any-exprs',
19+
'linecoverage',
20+
'memory-xml',
21+
'cobertura-xml',
22+
'xml',
23+
'xslt-html',
24+
'xslt-txt',
25+
'html',
26+
'txt'] # type: Final

mypy/main.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from mypy.fscache import FileSystemCache
2121
from mypy.errors import CompileError
2222
from mypy.options import Options, BuildType, PER_MODULE_OPTIONS
23-
from mypy.report import reporter_classes
2423

2524
from mypy.version import __version__
2625

@@ -606,7 +605,7 @@ def add_invertible_flag(flag: str,
606605
report_group = parser.add_argument_group(
607606
title='Report generation',
608607
description='Generate a report in the specified format.')
609-
for report_type in sorted(reporter_classes):
608+
for report_type in sorted(defaults.REPORTER_NAMES):
610609
report_group.add_argument('--%s-report' % report_type.replace('_', '-'),
611610
metavar='DIR',
612611
dest='special-opts:%s_report' % report_type)
@@ -968,7 +967,7 @@ def parse_section(prefix: str, template: Options,
968967
if dv is None:
969968
if key.endswith('_report'):
970969
report_type = key[:-7].replace('_', '-')
971-
if report_type in reporter_classes:
970+
if report_type in defaults.REPORTER_NAMES:
972971
report_dirs[report_type] = section[key]
973972
else:
974973
print("%s: Unrecognized report type: %s" % (prefix, key),

mypy/report.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from mypy.traverser import TraverserVisitor
2424
from mypy.types import Type, TypeOfAny
2525
from mypy.version import __version__
26+
from mypy.defaults import REPORTER_NAMES
2627

2728
MYPY = False
2829
if MYPY:
@@ -749,3 +750,7 @@ def on_finish(self) -> None:
749750

750751
alias_reporter('xslt-html', 'html')
751752
alias_reporter('xslt-txt', 'txt')
753+
754+
# Reporter class names are defined twice to speed up mypy startup, as this
755+
# module is slow to import. Ensure that the two definitions match.
756+
assert set(reporter_classes) == set(REPORTER_NAMES)

mypy/util.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
"""Utility functions with no non-trivial dependencies."""
2-
import inspect
32
import os
43
import pathlib
54
import re
65
import subprocess
76
import sys
8-
from xml.sax.saxutils import escape
97
from typing import TypeVar, List, Tuple, Optional, Dict, Sequence
108

119
MYPY = False
@@ -143,7 +141,7 @@ def try_find_python2_interpreter() -> Optional[str]:
143141

144142

145143
def write_junit_xml(dt: float, serious: bool, messages: List[str], path: str) -> None:
146-
"""XXX"""
144+
from xml.sax.saxutils import escape
147145
if not messages and not serious:
148146
xml = PASS_TEMPLATE.format(time=dt)
149147
elif not serious:
@@ -205,6 +203,7 @@ def correct_relative_import(cur_mod_id: str,
205203

206204

207205
def get_class_descriptors(cls: 'Type[object]') -> Sequence[str]:
206+
import inspect # Lazy import for minor startup speed win
208207
# Maintain a cache of type -> attributes defined by descriptors in the class
209208
# (that is, attributes from __slots__ and C extension classes)
210209
if cls not in fields_cache:

0 commit comments

Comments
 (0)