diff --git a/.travis.yml b/.travis.yml index 1dd1e896e6b4..90d53657cde0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,12 +11,13 @@ matrix: env: TEST_CMD="./tests/mypy_test.py --no-implicit-optional" - python: "2.7" env: TEST_CMD="./tests/pytype_test.py --num-parallel=4" + sudo: true install: # pytype needs py-2.7, mypy needs py-3.3+. Additional logic in runtests.py - if [[ $TRAVIS_PYTHON_VERSION == '3.6-dev' ]]; then pip install -U flake8==3.3.0 flake8-bugbear>=17.3.0 flake8-pyi>=17.1.0; fi - if [[ $TRAVIS_PYTHON_VERSION == '3.5' ]]; then pip install -U git+git://github.com/python/mypy git+git://github.com/python/typed_ast; fi - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -U git+git://github.com/google/pytype; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install -U git+git://github.com/google/pytype; wget https://s3.amazonaws.com/travis-python-archives/binaries/ubuntu/14.04/x86_64/python-3.6.tar.bz2; sudo tar xjf python-3.6.tar.bz2 --directory /; fi script: - $TEST_CMD diff --git a/README.md b/README.md index e43c1524a98a..c9ebd87f9b44 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ invoking: (Note that flake8 only works with Python 3.6 or higher.) To run the pytype tests, you need a separate virtual environment with -Python 2.7. Run: +Python 2.7, and a Python 3.6 interpreter somewhere you can point to. Run: ``` $ virtualenv --python=python2.7 .venv2 $ source .venv2/bin/activate @@ -136,7 +136,7 @@ $ source .venv2/bin/activate This will install pytype from its GitHub repo. You can then run pytype tests by running: ``` -(.venv2)$ python tests/pytype_test.py +(.venv2)$ python tests/pytype_test.py --python36-exe=/path/to/python3.6 ``` For mypy, if you are in the typeshed repo that is submodule of the diff --git a/tests/pytype_blacklist.txt b/tests/pytype_blacklist.txt index e31f91d0f80b..36bcafdb8e5b 100644 --- a/tests/pytype_blacklist.txt +++ b/tests/pytype_blacklist.txt @@ -79,3 +79,13 @@ stdlib/3/tokenize.pyi # parse only stdlib/3/types.pyi # parse only stdlib/3/urllib/error.pyi # parse only stdlib/3/urllib/request.pyi # parse only +stdlib/3/collections/abc.pyi # parse only +stdlib/3/signal.pyi # parse only +stdlib/3/shutil.pyi # parse only +stdlib/3/re.pyi # parse only +stdlib/3/posix.pyi # parse only +stdlib/3/platform.pyi # parse only +stdlib/3/fcntl.pyi # parse only +stdlib/3/configparser.pyi # parse only +stdlib/3/compileall.pyi # parse only +stdlib/3.4/pathlib.pyi # parse only diff --git a/tests/pytype_test.py b/tests/pytype_test.py index 55690fbc93ae..5d7ba17a70a0 100755 --- a/tests/pytype_test.py +++ b/tests/pytype_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""Test runner for typeshed. +r"""Test runner for typeshed. Depends on mypy and pytype being installed. @@ -15,17 +15,33 @@ also discover incorrect usage of imported modules. """ +import argparse +import collections import os import re -import sys -import argparse import subprocess -import collections +import sys -parser = argparse.ArgumentParser(description="Pytype tests.") -parser.add_argument('-n', '--dry-run', action='store_true', help="Don't actually run tests") +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") + help='Number of test processes to spawn') +# Default to '' so that symlinking typeshed/stdlib 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 python3.6. The default here works with our travis tests. +parser.add_argument('--python36-exe', type=str, + default='/opt/python/3.6/bin/python3.6', + help='Path to a python 3.6 interpreter.') + +Dirs = collections.namedtuple('Dirs', ['pytype', 'typeshed']) def main(): @@ -40,8 +56,27 @@ def main(): sys.exit(1) -def load_blacklist(): - filename = os.path.join(os.path.dirname(__file__), "pytype_blacklist.txt") +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 = [] @@ -87,34 +122,60 @@ def communicate(self): return self.results +def _get_relative(filename): + top = filename.find('stdlib/') + return filename[top:] + + def _get_module_name(filename): - """Converts a filename stdblib/m.n/module/foo to module.foo.""" - return '.'.join(filename.split(os.path.sep)[2:]).replace( + """Converts a filename stdlib/m.n/module/foo to module.foo.""" + return '.'.join(_get_relative(filename).split(os.path.sep)[2:]).replace( '.pyi', '').replace('.__init__', '') -def pytype_test(args): +def can_run(path, exe, *args): + exe = os.path.join(path, exe) try: - BinaryRun(['pytd', '-h']).communicate() + BinaryRun([exe] + list(args)).communicate() + return True except OSError: + return False + +def pytype_test(args): + dirs = get_project_dirs(args) + pytype_exe = os.path.join(dirs.pytype, 'pytype') + stdlib_path = os.path.join(dirs.typeshed, 'stdlib') + + if not os.path.isdir(stdlib_path): + print('Cannot find typeshed stdlib at %s ' + '(specify parent dir via --typeshed_location)' % stdlib_path) + 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 - skip, parse_only = load_blacklist() wanted = re.compile(r'stdlib/.*\.pyi$') - skipped = re.compile('(%s)$' % '|'.join(skip)) - parse_only = re.compile('(%s)$' % '|'.join(parse_only)) + skip, parse_only = load_blacklist(dirs) + skipped = PathMatcher(skip) + parse_only = PathMatcher(parse_only) pytype_run = [] pytd_run = [] + bad = [] - for root, _, filenames in os.walk('stdlib'): + for root, _, filenames in os.walk(stdlib_path): for f in sorted(filenames): f = os.path.join(root, f) - if wanted.search(f): - if parse_only.search(f): + rel = _get_relative(f) + if wanted.search(rel): + if parse_only.search(rel): pytd_run.append(f) - elif not skipped.search(f): + elif not skipped.search(rel): pytype_run.append(f) running_tests = collections.deque() @@ -124,15 +185,22 @@ def pytype_test(args): while files and len(running_tests) < args.num_parallel: f = files.pop() if f in pytype_run: + run_cmd = [ + pytype_exe, + '--module-name=%s' % _get_module_name(f), + '--parse-pyi' + ] + if 'stdlib/3' in f: + run_cmd += [ + '-V 3.6', + '--python_exe=%s' % args.python36_exe + ] test_run = BinaryRun( - ['pytype', - '--module-name=%s' % _get_module_name(f), - '--parse-pyi', - f], + run_cmd + [f], dry_run=args.dry_run, - env={"TYPESHED_HOME": os.getcwd()}) + env={"TYPESHED_HOME": dirs.typeshed}) elif f in pytd_run: - test_run = BinaryRun(['pytd', f], dry_run=args.dry_run) + test_run = BinaryRun([pytd_exe, f], dry_run=args.dry_run) else: raise ValueError('Unknown action for file: %s' % f) running_tests.append(test_run) @@ -141,15 +209,22 @@ def pytype_test(args): break test_run = running_tests.popleft() - code, stdout, stderr = test_run.communicate() + code, _, stderr = test_run.communicate() max_code = max(max_code, code) runs += 1 if code: - print(stderr) + 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])) 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__':