Skip to content

Commit db02926

Browse files
authored
Refactor import diagnostics, fix diagnostics in some fine-grained cases (#4840)
1 parent 572dd61 commit db02926

File tree

4 files changed

+298
-123
lines changed

4 files changed

+298
-123
lines changed

mypy/build.py

Lines changed: 181 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -717,28 +717,6 @@ def parse_file(self, id: str, path: str, source: str, ignore_errors: bool) -> My
717717
self.errors.set_file_ignored_lines(path, tree.ignored_lines, ignore_errors)
718718
return tree
719719

720-
def module_not_found(self, path: str, source: str, line: int, target: str) -> None:
721-
self.errors.set_file(path, source)
722-
stub_msg = "(Stub files are from https://github.com/python/typeshed)"
723-
if target == 'builtins':
724-
self.errors.report(line, 0, "Cannot find 'builtins' module. Typeshed appears broken!",
725-
blocker=True)
726-
self.errors.raise_error()
727-
elif ((self.options.python_version[0] == 2 and moduleinfo.is_py2_std_lib_module(target))
728-
or (self.options.python_version[0] >= 3
729-
and moduleinfo.is_py3_std_lib_module(target))):
730-
self.errors.report(
731-
line, 0, "No library stub file for standard library module '{}'".format(target))
732-
self.errors.report(line, 0, stub_msg, severity='note', only_once=True)
733-
elif moduleinfo.is_third_party_module(target):
734-
self.errors.report(line, 0, "No library stub file for module '{}'".format(target))
735-
self.errors.report(line, 0, stub_msg, severity='note', only_once=True)
736-
else:
737-
self.errors.report(line, 0, "Cannot find module named '{}'".format(target))
738-
self.errors.report(line, 0, '(Perhaps setting MYPYPATH '
739-
'or using the "--ignore-missing-imports" flag would help)',
740-
severity='note', only_once=True)
741-
742720
def report_file(self,
743721
file: MypyFile,
744722
type_map: Dict[Expression, Type],
@@ -1512,63 +1490,15 @@ def __init__(self,
15121490
self.fine_grained_deps = {}
15131491
if not path and source is None:
15141492
assert id is not None
1515-
file_id = id
1516-
if id == 'builtins' and self.options.python_version[0] == 2:
1517-
# The __builtin__ module is called internally by mypy
1518-
# 'builtins' in Python 2 mode (similar to Python 3),
1519-
# but the stub file is __builtin__.pyi. The reason is
1520-
# that a lot of code hard-codes 'builtins.x' and it's
1521-
# easier to work it around like this. It also means
1522-
# that the implementation can mostly ignore the
1523-
# difference and just assume 'builtins' everywhere,
1524-
# which simplifies code.
1525-
file_id = '__builtin__'
1526-
path = manager.find_module_cache.find_module(file_id, manager.lib_path)
1527-
if path:
1528-
# For non-stubs, look at options.follow_imports:
1529-
# - normal (default) -> fully analyze
1530-
# - silent -> analyze but silence errors
1531-
# - skip -> don't analyze, make the type Any
1532-
follow_imports = self.options.follow_imports
1533-
if (follow_imports != 'normal'
1534-
and not root_source # Honor top-level modules
1535-
and (path.endswith('.py') # Stubs are always normal
1536-
or self.options.follow_imports_for_stubs) # except when they aren't
1537-
and id != 'builtins'): # Builtins is always normal
1538-
if follow_imports == 'silent':
1539-
# Still import it, but silence non-blocker errors.
1540-
manager.log("Silencing %s (%s)" % (path, id))
1541-
self.ignore_all = True
1542-
else:
1543-
# In 'error' mode, produce special error messages.
1544-
if id not in manager.missing_modules:
1545-
manager.log("Skipping %s (%s)" % (path, id))
1546-
if follow_imports == 'error':
1547-
if ancestor_for:
1548-
self.skipping_ancestor(id, path, ancestor_for)
1549-
else:
1550-
self.skipping_module(id, path)
1551-
path = None
1552-
manager.missing_modules.add(id)
1553-
raise ModuleNotFound
1554-
else:
1555-
# Could not find a module. Typically the reason is a
1556-
# misspelled module name, missing stub, module not in
1557-
# search path or the module has not been installed.
1558-
if caller_state:
1559-
if not self.options.ignore_missing_imports:
1560-
save_import_context = manager.errors.import_context()
1561-
manager.errors.set_import_context(caller_state.import_context)
1562-
manager.module_not_found(caller_state.xpath, caller_state.id,
1563-
caller_line, id)
1564-
manager.errors.set_import_context(save_import_context)
1565-
manager.missing_modules.add(id)
1566-
raise ModuleNotFound
1567-
else:
1568-
# If we can't find a root source it's always fatal.
1569-
# TODO: This might hide non-fatal errors from
1570-
# root sources processed earlier.
1571-
raise CompileError(["mypy: can't find module '%s'" % id])
1493+
try:
1494+
path, follow_imports = find_module_and_diagnose(
1495+
manager, id, self.options, caller_state, caller_line,
1496+
ancestor_for, root_source)
1497+
except ModuleNotFound:
1498+
manager.missing_modules.add(id)
1499+
raise
1500+
if follow_imports == 'silent':
1501+
self.ignore_all = True
15721502
self.path = path
15731503
self.xpath = path or '<string>'
15741504
self.source = source
@@ -1604,35 +1534,6 @@ def __init__(self,
16041534
self.compute_dependencies()
16051535
self.child_modules = set()
16061536

1607-
def skipping_ancestor(self, id: str, path: str, ancestor_for: 'State') -> None:
1608-
# TODO: Read the path (the __init__.py file) and return
1609-
# immediately if it's empty or only contains comments.
1610-
# But beware, some package may be the ancestor of many modules,
1611-
# so we'd need to cache the decision.
1612-
manager = self.manager
1613-
manager.errors.set_import_context([])
1614-
manager.errors.set_file(ancestor_for.xpath, ancestor_for.id)
1615-
manager.errors.report(-1, -1, "Ancestor package '%s' ignored" % (id,),
1616-
severity='note', only_once=True)
1617-
manager.errors.report(-1, -1,
1618-
"(Using --follow-imports=error, submodule passed on command line)",
1619-
severity='note', only_once=True)
1620-
1621-
def skipping_module(self, id: str, path: str) -> None:
1622-
assert self.caller_state, (id, path)
1623-
manager = self.manager
1624-
save_import_context = manager.errors.import_context()
1625-
manager.errors.set_import_context(self.caller_state.import_context)
1626-
manager.errors.set_file(self.caller_state.xpath, self.caller_state.id)
1627-
line = self.caller_line
1628-
manager.errors.report(line, 0,
1629-
"Import of '%s' ignored" % (id,),
1630-
severity='note')
1631-
manager.errors.report(line, 0,
1632-
"(Using --follow-imports=error, module not passed on command line)",
1633-
severity='note', only_once=True)
1634-
manager.errors.set_import_context(save_import_context)
1635-
16361537
def add_ancestors(self) -> None:
16371538
if self.path is not None:
16381539
_, name = os.path.split(self.path)
@@ -2008,6 +1909,35 @@ def write_cache(self) -> None:
20081909
self.mark_interface_stale()
20091910
self.interface_hash = new_interface_hash
20101911

1912+
def verify_dependencies(self) -> None:
1913+
"""Report errors for import targets in modules that don't exist."""
1914+
# Strip out indirect dependencies. See comment in build.load_graph().
1915+
manager = self.manager
1916+
dependencies = [dep for dep in self.dependencies
1917+
if self.priorities.get(dep) != PRI_INDIRECT]
1918+
assert self.ancestors is not None
1919+
for dep in dependencies + self.suppressed + self.ancestors:
1920+
options = manager.options.clone_for_module(dep)
1921+
if dep not in manager.modules and not options.ignore_missing_imports:
1922+
line = self.dep_line_map.get(dep, 1)
1923+
try:
1924+
if dep in self.ancestors:
1925+
state, ancestor = None, self # type: (Optional[State], Optional[State])
1926+
else:
1927+
state, ancestor = self, None
1928+
# Called just for its side effects of producing diagnostics.
1929+
find_module_and_diagnose(
1930+
manager, dep, options,
1931+
caller_state=state, caller_line=line,
1932+
ancestor_for=ancestor)
1933+
except (ModuleNotFound, CompileError):
1934+
# Swallow up any ModuleNotFounds or CompilerErrors while generating
1935+
# a diagnostic. CompileErrors may get generated in
1936+
# fine-grained mode when an __init__.py is deleted, if a module
1937+
# that was in that package has targets reprocessed before
1938+
# it is renamed.
1939+
pass
1940+
20111941
def dependency_priorities(self) -> List[int]:
20121942
return [self.priorities.get(dep, PRI_HIGH) for dep in self.dependencies]
20131943

@@ -2019,6 +1949,149 @@ def generate_unused_ignore_notes(self) -> None:
20191949
self.manager.errors.generate_unused_ignore_notes(self.xpath)
20201950

20211951

1952+
# Module import and diagnostic glue
1953+
1954+
1955+
def find_module_and_diagnose(manager: BuildManager,
1956+
id: str,
1957+
options: Options,
1958+
caller_state: 'Optional[State]' = None,
1959+
caller_line: int = 0,
1960+
ancestor_for: 'Optional[State]' = None,
1961+
root_source: bool = False) -> Tuple[str, str]:
1962+
"""Find a module by name, respecting follow_imports and producing diagnostics.
1963+
1964+
Args:
1965+
id: module to find
1966+
options: the options for the module being loaded
1967+
caller_state: the state of the importing module, if applicable
1968+
caller_line: the line number of the import
1969+
ancestor_for: the child module this is an ancestor of, if applicable
1970+
root_source: whether this source was specified on the command line
1971+
1972+
The specified value of follow_imports for a module can be overridden
1973+
if the module is specified on the command line or if it is a stub,
1974+
so we compute and return the "effective" follow_imports of the module.
1975+
1976+
Returns a tuple containing (file path, target's effective follow_imports setting)
1977+
"""
1978+
file_id = id
1979+
if id == 'builtins' and options.python_version[0] == 2:
1980+
# The __builtin__ module is called internally by mypy
1981+
# 'builtins' in Python 2 mode (similar to Python 3),
1982+
# but the stub file is __builtin__.pyi. The reason is
1983+
# that a lot of code hard-codes 'builtins.x' and it's
1984+
# easier to work it around like this. It also means
1985+
# that the implementation can mostly ignore the
1986+
# difference and just assume 'builtins' everywhere,
1987+
# which simplifies code.
1988+
file_id = '__builtin__'
1989+
path = manager.find_module_cache.find_module(file_id, manager.lib_path)
1990+
if path:
1991+
# For non-stubs, look at options.follow_imports:
1992+
# - normal (default) -> fully analyze
1993+
# - silent -> analyze but silence errors
1994+
# - skip -> don't analyze, make the type Any
1995+
follow_imports = options.follow_imports
1996+
if (root_source # Honor top-level modules
1997+
or (not path.endswith('.py') # Stubs are always normal
1998+
and not options.follow_imports_for_stubs) # except when they aren't
1999+
or id == 'builtins'): # Builtins is always normal
2000+
follow_imports = 'normal'
2001+
2002+
if follow_imports == 'silent':
2003+
# Still import it, but silence non-blocker errors.
2004+
manager.log("Silencing %s (%s)" % (path, id))
2005+
elif follow_imports == 'skip' or follow_imports == 'error':
2006+
# In 'error' mode, produce special error messages.
2007+
if id not in manager.missing_modules:
2008+
manager.log("Skipping %s (%s)" % (path, id))
2009+
if follow_imports == 'error':
2010+
if ancestor_for:
2011+
skipping_ancestor(manager, id, path, ancestor_for)
2012+
else:
2013+
skipping_module(manager, caller_line, caller_state,
2014+
id, path)
2015+
raise ModuleNotFound
2016+
2017+
return (path, follow_imports)
2018+
else:
2019+
# Could not find a module. Typically the reason is a
2020+
# misspelled module name, missing stub, module not in
2021+
# search path or the module has not been installed.
2022+
if caller_state:
2023+
if not options.ignore_missing_imports:
2024+
module_not_found(manager, caller_line, caller_state, id)
2025+
raise ModuleNotFound
2026+
else:
2027+
# If we can't find a root source it's always fatal.
2028+
# TODO: This might hide non-fatal errors from
2029+
# root sources processed earlier.
2030+
raise CompileError(["mypy: can't find module '%s'" % id])
2031+
2032+
2033+
def module_not_found(manager: BuildManager, line: int, caller_state: State,
2034+
target: str) -> None:
2035+
errors = manager.errors
2036+
save_import_context = errors.import_context()
2037+
errors.set_import_context(caller_state.import_context)
2038+
errors.set_file(caller_state.xpath, caller_state.id)
2039+
stub_msg = "(Stub files are from https://github.com/python/typeshed)"
2040+
if target == 'builtins':
2041+
errors.report(line, 0, "Cannot find 'builtins' module. Typeshed appears broken!",
2042+
blocker=True)
2043+
errors.raise_error()
2044+
elif ((manager.options.python_version[0] == 2 and moduleinfo.is_py2_std_lib_module(target))
2045+
or (manager.options.python_version[0] >= 3
2046+
and moduleinfo.is_py3_std_lib_module(target))):
2047+
errors.report(
2048+
line, 0, "No library stub file for standard library module '{}'".format(target))
2049+
errors.report(line, 0, stub_msg, severity='note', only_once=True)
2050+
elif moduleinfo.is_third_party_module(target):
2051+
errors.report(line, 0, "No library stub file for module '{}'".format(target))
2052+
errors.report(line, 0, stub_msg, severity='note', only_once=True)
2053+
else:
2054+
errors.report(line, 0, "Cannot find module named '{}'".format(target))
2055+
errors.report(line, 0, '(Perhaps setting MYPYPATH '
2056+
'or using the "--ignore-missing-imports" flag would help)',
2057+
severity='note', only_once=True)
2058+
errors.set_import_context(save_import_context)
2059+
2060+
2061+
def skipping_module(manager: BuildManager, line: int, caller_state: Optional[State],
2062+
id: str, path: str) -> None:
2063+
"""Produce an error for an import ignored due to --follow_imports=error"""
2064+
assert caller_state, (id, path)
2065+
save_import_context = manager.errors.import_context()
2066+
manager.errors.set_import_context(caller_state.import_context)
2067+
manager.errors.set_file(caller_state.xpath, caller_state.id)
2068+
manager.errors.report(line, 0,
2069+
"Import of '%s' ignored" % (id,),
2070+
severity='note')
2071+
manager.errors.report(line, 0,
2072+
"(Using --follow-imports=error, module not passed on command line)",
2073+
severity='note', only_once=True)
2074+
manager.errors.set_import_context(save_import_context)
2075+
2076+
2077+
def skipping_ancestor(manager: BuildManager, id: str, path: str, ancestor_for: 'State') -> None:
2078+
"""Produce an error for an ancestor ignored due to --follow_imports=error"""
2079+
# TODO: Read the path (the __init__.py file) and return
2080+
# immediately if it's empty or only contains comments.
2081+
# But beware, some package may be the ancestor of many modules,
2082+
# so we'd need to cache the decision.
2083+
manager.errors.set_import_context([])
2084+
manager.errors.set_file(ancestor_for.xpath, ancestor_for.id)
2085+
manager.errors.report(-1, -1, "Ancestor package '%s' ignored" % (id,),
2086+
severity='note', only_once=True)
2087+
manager.errors.report(-1, -1,
2088+
"(Using --follow-imports=error, submodule passed on command line)",
2089+
severity='note', only_once=True)
2090+
2091+
2092+
# The driver
2093+
2094+
20222095
def dispatch(sources: List[BuildSource], manager: BuildManager) -> Graph:
20232096
manager.log()
20242097
manager.log("Mypy version %s" % __version__)

mypy/server/update.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
)
121121

122122
from mypy.build import (
123-
BuildManager, State, BuildSource, BuildResult, Graph, load_graph,
123+
BuildManager, State, BuildSource, BuildResult, Graph, load_graph, module_not_found,
124124
PRI_INDIRECT, DEBUG_FINE_GRAINED,
125125
)
126126
from mypy.checker import DeferredNode
@@ -592,18 +592,6 @@ def get_all_changed_modules(root_module: str,
592592
return changed_modules
593593

594594

595-
def verify_dependencies(state: State, manager: BuildManager) -> None:
596-
"""Report errors for import targets in module that don't exist."""
597-
# Strip out indirect dependencies. See comment in build.load_graph().
598-
dependencies = [dep for dep in state.dependencies if state.priorities.get(dep) != PRI_INDIRECT]
599-
for dep in dependencies + state.suppressed: # TODO: ancestors?
600-
if dep not in manager.modules and not state.options.ignore_missing_imports:
601-
assert state.tree
602-
line = state.dep_line_map.get(dep, 1)
603-
assert state.path
604-
manager.module_not_found(state.path, state.id, line, dep)
605-
606-
607595
def collect_dependencies(new_modules: Mapping[str, Optional[MypyFile]],
608596
deps: Dict[str, Set[str]],
609597
graph: Dict[str, State]) -> None:
@@ -877,7 +865,7 @@ def key(node: DeferredNode) -> int:
877865
update_deps(module_id, nodes, graph, deps, options)
878866

879867
# Report missing imports.
880-
verify_dependencies(graph[module_id], manager)
868+
graph[module_id].verify_dependencies()
881869

882870
return new_triggered
883871

mypy/test/testfinegrained.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def parse_sources(self, program_text: str,
224224
m = re.search('# cmd: mypy ([a-zA-Z0-9_./ ]+)$', program_text, flags=re.MULTILINE)
225225
regex = '# cmd{}: mypy ([a-zA-Z0-9_./ ]+)$'.format(incremental_step)
226226
alt_m = re.search(regex, program_text, flags=re.MULTILINE)
227-
if alt_m is not None and incremental_step > 1:
227+
if alt_m is not None:
228228
# Optionally return a different command if in a later step
229229
# of incremental mode, otherwise default to reusing the
230230
# original cmd.

0 commit comments

Comments
 (0)