blob: 034302cc85d9e8ed798d697e08dd0cb429c3a91a [file] [log] [blame]
"""Routines for finding the sources that mypy will check"""
import os.path
from typing import List, Sequence, Set, Tuple, Optional, Dict
from mypy.modulefinder import BuildSource, PYTHON_EXTENSIONS
from mypy.fscache import FileSystemCache
from mypy.options import Options
MYPY = False
if MYPY:
from typing_extensions import Final
PY_EXTENSIONS = tuple(PYTHON_EXTENSIONS) # type: Final
class InvalidSourceList(Exception):
"""Exception indicating a problem in the list of sources given to mypy."""
def create_source_list(files: Sequence[str], options: Options,
fscache: Optional[FileSystemCache] = None,
allow_empty_dir: bool = False) -> List[BuildSource]:
"""From a list of source files/directories, makes a list of BuildSources.
Raises InvalidSourceList on errors.
"""
fscache = fscache or FileSystemCache()
finder = SourceFinder(fscache)
targets = []
for f in files:
if f.endswith(PY_EXTENSIONS):
# Can raise InvalidSourceList if a directory doesn't have a valid module name.
name, base_dir = finder.crawl_up(os.path.normpath(f))
targets.append(BuildSource(f, name, None, base_dir))
elif fscache.isdir(f):
sub_targets = finder.expand_dir(os.path.normpath(f))
if not sub_targets and not allow_empty_dir:
raise InvalidSourceList("There are no .py[i] files in directory '{}'"
.format(f))
targets.extend(sub_targets)
else:
mod = os.path.basename(f) if options.scripts_are_modules else None
targets.append(BuildSource(f, mod, None))
return targets
def keyfunc(name: str) -> Tuple[int, str]:
"""Determines sort order for directory listing.
The desirable property is foo < foo.pyi < foo.py.
"""
base, suffix = os.path.splitext(name)
for i, ext in enumerate(PY_EXTENSIONS):
if suffix == ext:
return (i, base)
return (-1, name)
class SourceFinder:
def __init__(self, fscache: FileSystemCache) -> None:
self.fscache = fscache
# A cache for package names, mapping from directory path to module id and base dir
self.package_cache = {} # type: Dict[str, Tuple[str, str]]
def expand_dir(self, arg: str, mod_prefix: str = '') -> List[BuildSource]:
"""Convert a directory name to a list of sources to build."""
f = self.get_init_file(arg)
if mod_prefix and not f:
return []
seen = set() # type: Set[str]
sources = []
top_mod, base_dir = self.crawl_up_dir(arg)
if f and not mod_prefix:
mod_prefix = top_mod + '.'
if mod_prefix:
sources.append(BuildSource(f, mod_prefix.rstrip('.'), None, base_dir))
names = self.fscache.listdir(arg)
names.sort(key=keyfunc)
for name in names:
# Skip certain names altogether
if (name == '__pycache__' or name == 'py.typed'
or name.startswith('.')
or name.endswith(('~', '.pyc', '.pyo'))):
continue
path = os.path.join(arg, name)
if self.fscache.isdir(path):
sub_sources = self.expand_dir(path, mod_prefix + name + '.')
if sub_sources:
seen.add(name)
sources.extend(sub_sources)
else:
base, suffix = os.path.splitext(name)
if base == '__init__':
continue
if base not in seen and '.' not in base and suffix in PY_EXTENSIONS:
seen.add(base)
src = BuildSource(path, mod_prefix + base, None, base_dir)
sources.append(src)
return sources
def crawl_up(self, arg: str) -> Tuple[str, str]:
"""Given a .py[i] filename, return module and base directory
We crawl up the path until we find a directory without
__init__.py[i], or until we run out of path components.
"""
dir, mod = os.path.split(arg)
mod = strip_py(mod) or mod
base, base_dir = self.crawl_up_dir(dir)
if mod == '__init__' or not mod:
mod = base
else:
mod = module_join(base, mod)
return mod, base_dir
def crawl_up_dir(self, dir: str) -> Tuple[str, str]:
"""Given a directory name, return the corresponding module name and base directory
Use package_cache to cache results.
"""
if dir in self.package_cache:
return self.package_cache[dir]
parent_dir, base = os.path.split(dir)
if not dir or not self.get_init_file(dir) or not base:
res = ''
base_dir = dir or '.'
else:
# Ensure that base is a valid python module name
if not base.isidentifier():
raise InvalidSourceList('{} is not a valid Python package name'.format(base))
parent, base_dir = self.crawl_up_dir(parent_dir)
res = module_join(parent, base)
self.package_cache[dir] = res, base_dir
return res, base_dir
def get_init_file(self, dir: str) -> Optional[str]:
"""Check whether a directory contains a file named __init__.py[i].
If so, return the file's name (with dir prefixed). If not, return
None.
This prefers .pyi over .py (because of the ordering of PY_EXTENSIONS).
"""
for ext in PY_EXTENSIONS:
f = os.path.join(dir, '__init__' + ext)
if self.fscache.isfile(f):
return f
if ext == '.py' and self.fscache.init_under_package_root(f):
return f
return None
def module_join(parent: str, child: str) -> str:
"""Join module ids, accounting for a possibly empty parent."""
if parent:
return parent + '.' + child
else:
return child
def strip_py(arg: str) -> Optional[str]:
"""Strip a trailing .py or .pyi suffix.
Return None if no such suffix is found.
"""
for ext in PY_EXTENSIONS:
if arg.endswith(ext):
return arg[:-len(ext)]
return None