mirror of
https://github.com/davidhalter/typeshed.git
synced 2025-12-06 20:24:30 +08:00
* Moves the pytype installation to requirements-tests-py3.txt, now
that pytype can run under Python 3.5+.
* Changes tests/pytype_test.py to not require a --python{version}-exe
argument when it can automatically find the Python interpreter, and
cleans up a few typos and out-of-date things.
* Updates the appropriate documentation.
* Updates .travis.yml.
289 lines
9.5 KiB
Python
Executable File
289 lines
9.5 KiB
Python
Executable File
#!/usr/bin/env python
|
|
r"""Test runner for typeshed.
|
|
|
|
Depends on pytype being installed.
|
|
|
|
If pytype is installed:
|
|
1. For every pyi, do nothing if it is in pytype_blacklist.txt.
|
|
2. If the blacklist line has a "# parse only" comment run
|
|
"pytd <foo.pyi>" in a separate process.
|
|
3. If the file is not in the blacklist run
|
|
"pytype --typeshed-location=typeshed_location --module-name=foo \
|
|
--parse-pyi <foo.pyi>.
|
|
Option two will parse the file, mostly syntactical correctness. Option three
|
|
will load the file and all the builtins, typeshed dependencies. This will
|
|
also discover incorrect usage of imported modules.
|
|
"""
|
|
|
|
import argparse
|
|
import collections
|
|
import itertools
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
parser = argparse.ArgumentParser(description='Pytype/typeshed tests.')
|
|
parser.add_argument('-n', '--dry-run', action='store_true',
|
|
help="Don't actually run tests")
|
|
parser.add_argument('--num-parallel', type=int, default=1,
|
|
help='Number of test processes to spawn')
|
|
# Default to '' so that symlinking typeshed subdirs in cwd will work.
|
|
parser.add_argument('--typeshed-location', type=str, default='',
|
|
help='Path to typeshed installation.')
|
|
# Default to '' so that finding pytype in path will work.
|
|
parser.add_argument('--pytype-bin-dir', type=str, default='',
|
|
help='Path to directory with pytype and pytd executables.')
|
|
# Set to true to print a stack trace every time an exception is thrown.
|
|
parser.add_argument('--print-stderr', type=bool, default=False,
|
|
help='Print stderr every time an error is encountered.')
|
|
# We need to invoke python2.7 and 3.6.
|
|
parser.add_argument('--python27-exe', type=str, default='python2.7',
|
|
help='Path to a python 2.7 interpreter.')
|
|
parser.add_argument('--python36-exe', type=str, default='python3.6',
|
|
help='Path to a python 3.6 interpreter.')
|
|
|
|
Dirs = collections.namedtuple('Dirs', ['pytype', 'typeshed'])
|
|
|
|
|
|
TYPESHED_SUBDIRS = ['stdlib', 'third_party']
|
|
|
|
|
|
def main():
|
|
args = parser.parse_args()
|
|
code, runs = pytype_test(args)
|
|
|
|
if code:
|
|
print('--- exit status %d ---' % code)
|
|
sys.exit(code)
|
|
if not runs:
|
|
print('--- nothing to do; exit 1 ---')
|
|
sys.exit(1)
|
|
|
|
|
|
def get_project_dirs(args):
|
|
"""Top-level project directories for pytype executables and typeshed."""
|
|
typeshed_location = args.typeshed_location or os.getcwd()
|
|
return Dirs(args.pytype_bin_dir, typeshed_location)
|
|
|
|
|
|
class PathMatcher(object):
|
|
def __init__(self, patterns):
|
|
if patterns:
|
|
self.matcher = re.compile('(%s)$' % '|'.join(patterns))
|
|
else:
|
|
self.matcher = None
|
|
|
|
def search(self, path):
|
|
if not self.matcher:
|
|
return False
|
|
return self.matcher.search(path)
|
|
|
|
|
|
def load_blacklist(dirs):
|
|
filename = os.path.join(dirs.typeshed, 'tests', 'pytype_blacklist.txt')
|
|
skip_re = re.compile(r'^\s*([^\s#]+)\s*(?:#.*)?$')
|
|
parse_only_re = re.compile(r'^\s*([^\s#]+)\s*#\s*parse only\s*')
|
|
skip = []
|
|
parse_only = []
|
|
|
|
with open(filename) as f:
|
|
for line in f:
|
|
parse_only_match = parse_only_re.match(line)
|
|
skip_match = skip_re.match(line)
|
|
if parse_only_match:
|
|
parse_only.append(parse_only_match.group(1))
|
|
elif skip_match:
|
|
skip.append(skip_match.group(1))
|
|
|
|
return skip, parse_only
|
|
|
|
|
|
class BinaryRun(object):
|
|
def __init__(self, args, dry_run=False, env=None):
|
|
self.args = args
|
|
self.results = None
|
|
|
|
if dry_run:
|
|
self.results = (0, '', '')
|
|
else:
|
|
if env is not None:
|
|
full_env = os.environ.copy()
|
|
full_env.update(env)
|
|
else:
|
|
full_env = None
|
|
self.proc = subprocess.Popen(
|
|
self.args,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env=full_env)
|
|
|
|
def communicate(self):
|
|
if self.results:
|
|
return self.results
|
|
|
|
stdout, stderr = self.proc.communicate()
|
|
self.results = self.proc.returncode, stdout, stderr
|
|
return self.results
|
|
|
|
|
|
def _get_relative(filename):
|
|
top = 0
|
|
for d in TYPESHED_SUBDIRS:
|
|
try:
|
|
top = filename.index(d)
|
|
except ValueError:
|
|
continue
|
|
else:
|
|
break
|
|
return filename[top:]
|
|
|
|
|
|
def _get_module_name(filename):
|
|
"""Converts a filename {subdir}/m.n/module/foo to module.foo."""
|
|
return '.'.join(_get_relative(filename).split(os.path.sep)[2:]).replace(
|
|
'.pyi', '').replace('.__init__', '')
|
|
|
|
|
|
def can_run(path, exe, *args):
|
|
exe = os.path.join(path, exe)
|
|
try:
|
|
BinaryRun([exe] + list(args)).communicate()
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _is_version(path, version):
|
|
return any('%s/%s' % (d, version) in path for d in TYPESHED_SUBDIRS)
|
|
|
|
|
|
def pytype_test(args):
|
|
dirs = get_project_dirs(args)
|
|
pytype_exe = os.path.join(dirs.pytype, 'pytype-single')
|
|
paths = [os.path.join(dirs.typeshed, d) for d in TYPESHED_SUBDIRS]
|
|
|
|
for p in paths:
|
|
if not os.path.isdir(p):
|
|
print('Cannot find typeshed subdir at %s '
|
|
'(specify parent dir via --typeshed-location)' % p)
|
|
return 0, 0
|
|
|
|
if can_run(dirs.pytype, 'pytd', '-h'):
|
|
pytd_exe = os.path.join(dirs.pytype, 'pytd')
|
|
elif can_run(dirs.pytype, 'pytd_tool', '-h'):
|
|
pytd_exe = os.path.join(dirs.pytype, 'pytd_tool')
|
|
else:
|
|
print('Cannot run pytd. Did you install pytype?')
|
|
return 0, 0
|
|
|
|
for python_version_str in ('27', '36'):
|
|
dest = 'python%s_exe' % python_version_str
|
|
version = '.'.join(list(python_version_str))
|
|
arg = '--python%s-exe' % python_version_str
|
|
if not can_run('', getattr(args, dest), '--version'):
|
|
print('Cannot run Python {version}. (point to a valid executable '
|
|
'via {arg})'.format(version=version, arg=arg))
|
|
return 0, 0
|
|
|
|
# TODO(rchen152): Keep expanding our third_party/ coverage so we can move
|
|
# to a small blacklist rather than an ever-growing whitelist.
|
|
wanted = [
|
|
'stdlib/',
|
|
'third_party/.*/mypy_extensions',
|
|
'third_party/.*/pkg_resources',
|
|
'third_party/.*/six/',
|
|
'third_party/.*/yaml/',
|
|
]
|
|
wanted_re = re.compile(r'(?:%s).*\.pyi$' % '|'.join(wanted))
|
|
skip, parse_only = load_blacklist(dirs)
|
|
skipped = PathMatcher(skip)
|
|
parse_only = PathMatcher(parse_only)
|
|
|
|
pytype_run = []
|
|
pytd_run = []
|
|
bad = []
|
|
|
|
def _make_test(filename, major_version):
|
|
if major_version == 3:
|
|
version = '3.6'
|
|
exe = args.python36_exe
|
|
else:
|
|
version = '2.7'
|
|
exe = args.python27_exe
|
|
run_cmd = [
|
|
pytype_exe,
|
|
'--module-name=%s' % _get_module_name(filename),
|
|
'--parse-pyi',
|
|
'-V %s' % version,
|
|
'--python_exe=%s' % exe,
|
|
]
|
|
return BinaryRun(run_cmd + [filename],
|
|
dry_run=args.dry_run,
|
|
env={"TYPESHED_HOME": dirs.typeshed})
|
|
|
|
for root, _, filenames in itertools.chain.from_iterable(
|
|
os.walk(p) for p in paths):
|
|
for f in sorted(filenames):
|
|
f = os.path.join(root, f)
|
|
rel = _get_relative(f)
|
|
if wanted_re.search(rel):
|
|
if parse_only.search(rel):
|
|
pytd_run.append(f)
|
|
elif not skipped.search(rel):
|
|
pytype_run.append(f)
|
|
|
|
running_tests = collections.deque()
|
|
max_code, runs, errors = 0, 0, 0
|
|
files = pytype_run + pytd_run
|
|
total_tests = len(files)
|
|
# Files in {subdir}/2and3 get tested twice
|
|
total_tests += sum(1 for f in pytype_run if _is_version(f, '2and3'))
|
|
print("Testing files with pytype...")
|
|
while 1:
|
|
while files and len(running_tests) < args.num_parallel:
|
|
f = files.pop()
|
|
if f in pytype_run:
|
|
if _is_version(f, '2and3'):
|
|
running_tests.append(_make_test(f, 2))
|
|
running_tests.append(_make_test(f, 3))
|
|
elif _is_version(f, '2'):
|
|
running_tests.append(_make_test(f, 2))
|
|
elif _is_version(f, '3'):
|
|
running_tests.append(_make_test(f, 3))
|
|
else:
|
|
print("Unrecognised path: %s" % f)
|
|
elif f in pytd_run:
|
|
test_run = BinaryRun([pytd_exe, f], dry_run=args.dry_run)
|
|
running_tests.append(test_run)
|
|
else:
|
|
raise ValueError('Unknown action for file: %s' % f)
|
|
|
|
if not running_tests:
|
|
break
|
|
|
|
test_run = running_tests.popleft()
|
|
code, _, stderr = test_run.communicate()
|
|
max_code = max(max_code, code)
|
|
runs += 1
|
|
|
|
if code:
|
|
if args.print_stderr:
|
|
print(stderr)
|
|
errors += 1
|
|
# We strip off the stack trace and just leave the last line with the
|
|
# actual error; to see the stack traces use --print_stderr.
|
|
bad.append((_get_relative(test_run.args[-1]),
|
|
stderr.rstrip().rsplit('\n', 1)[-1]))
|
|
|
|
if runs % 25 == 0:
|
|
print(" %3d/%d with %3d errors" % (runs, total_tests, errors))
|
|
|
|
print('Ran pytype with %d pyis, got %d errors.' % (runs, errors))
|
|
for f, err in bad:
|
|
print('%s: %s' % (f, err))
|
|
return max_code, runs
|
|
|
|
if __name__ == '__main__':
|
|
main()
|