mirror of
https://github.com/davidhalter/typeshed.git
synced 2025-12-07 12:44:28 +08:00
- Add several type hints to untyped functions. - While we're at it, upgrade several annotations to use modern syntax by using `from __future__ import annotations`. This means that the tests can't be run on Python 3.6, but 3.6 is EOL, and it is already the case that some scripts in this directory can only be run on more recent Python versions. E.g. `check_new_syntax.py` uses `ast.unparse`, which is only available in Python 3.9+. - Fix a few pieces of code that didn't type check.
173 lines
6.1 KiB
Python
Executable File
173 lines
6.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Test runner for typeshed.
|
|
|
|
Depends on pytype being installed.
|
|
|
|
If pytype is installed:
|
|
1. For every pyi, do nothing if it is in pytype_exclude_list.txt or is
|
|
Python 2-only.
|
|
2. Otherwise, call 'pytype.io.parse_pyi'.
|
|
Option two will load the file and all the builtins, typeshed dependencies. This
|
|
will also discover incorrect usage of imported modules.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import traceback
|
|
from collections.abc import Sequence
|
|
|
|
from pytype import config as pytype_config, load_pytd
|
|
from pytype.pytd import typeshed
|
|
|
|
TYPESHED_SUBDIRS = ["stdlib", "stubs"]
|
|
TYPESHED_HOME = "TYPESHED_HOME"
|
|
_LOADERS = {}
|
|
|
|
|
|
def main() -> None:
|
|
args = create_parser().parse_args()
|
|
typeshed_location = args.typeshed_location or os.getcwd()
|
|
subdir_paths = [os.path.join(typeshed_location, d) for d in TYPESHED_SUBDIRS]
|
|
check_subdirs_discoverable(subdir_paths)
|
|
old_typeshed_home = os.environ.get(TYPESHED_HOME)
|
|
os.environ[TYPESHED_HOME] = typeshed_location
|
|
files_to_test = determine_files_to_test(typeshed_location=typeshed_location, paths=args.files or subdir_paths)
|
|
run_all_tests(
|
|
files_to_test=files_to_test, typeshed_location=typeshed_location, print_stderr=args.print_stderr, dry_run=args.dry_run
|
|
)
|
|
if old_typeshed_home is None:
|
|
del os.environ[TYPESHED_HOME]
|
|
else:
|
|
os.environ[TYPESHED_HOME] = old_typeshed_home
|
|
|
|
|
|
def create_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(description="Pytype/typeshed tests.")
|
|
parser.add_argument("-n", "--dry-run", action="store_true", default=False, help="Don't actually run tests")
|
|
# Default to '' so that symlinking typeshed subdirs in cwd will work.
|
|
parser.add_argument("--typeshed-location", type=str, default="", help="Path to typeshed installation.")
|
|
# Set to true to print a stack trace every time an exception is thrown.
|
|
parser.add_argument(
|
|
"--print-stderr", action="store_true", default=False, help="Print stderr every time an error is encountered."
|
|
)
|
|
parser.add_argument(
|
|
"files", metavar="FILE", type=str, nargs="*", help="Files or directories to check. (Default: Check all files.)"
|
|
)
|
|
return parser
|
|
|
|
|
|
def run_pytype(*, filename: str, python_version: str, typeshed_location: str) -> str | None:
|
|
"""Runs pytype, returning the stderr if any."""
|
|
if python_version not in _LOADERS:
|
|
options = pytype_config.Options.create("", parse_pyi=True, python_version=python_version)
|
|
loader = load_pytd.create_loader(options)
|
|
_LOADERS[python_version] = (options, loader)
|
|
options, loader = _LOADERS[python_version]
|
|
stderr: str | None
|
|
try:
|
|
with pytype_config.verbosity_from(options):
|
|
ast = loader.load_file(_get_module_name(filename), filename)
|
|
loader.finish_and_verify_ast(ast)
|
|
except Exception:
|
|
stderr = traceback.format_exc()
|
|
else:
|
|
stderr = None
|
|
return stderr
|
|
|
|
|
|
def _get_relative(filename: str) -> str:
|
|
top = 0
|
|
for d in TYPESHED_SUBDIRS:
|
|
try:
|
|
top = filename.index(d + os.path.sep)
|
|
except ValueError:
|
|
continue
|
|
else:
|
|
break
|
|
return filename[top:]
|
|
|
|
|
|
def _get_module_name(filename: str) -> str:
|
|
"""Converts a filename {subdir}/m.n/module/foo to module.foo."""
|
|
parts = _get_relative(filename).split(os.path.sep)
|
|
if "@python2" in parts:
|
|
module_parts = parts[parts.index("@python2") + 1 :]
|
|
elif parts[0] == "stdlib":
|
|
module_parts = parts[1:]
|
|
else:
|
|
assert parts[0] == "stubs"
|
|
module_parts = parts[2:]
|
|
return ".".join(module_parts).replace(".pyi", "").replace(".__init__", "")
|
|
|
|
|
|
def _is_version(path: str, version: str) -> bool:
|
|
return any("{}{}{}".format(d, os.path.sep, version) in path for d in TYPESHED_SUBDIRS)
|
|
|
|
|
|
def check_subdirs_discoverable(subdir_paths: list[str]) -> None:
|
|
for p in subdir_paths:
|
|
if not os.path.isdir(p):
|
|
raise SystemExit("Cannot find typeshed subdir at {} (specify parent dir via --typeshed-location)".format(p))
|
|
|
|
|
|
def determine_files_to_test(*, typeshed_location: str, paths: Sequence[str]) -> list[str]:
|
|
"""Determine all files to test, checking if it's in the exclude list and which Python versions to use.
|
|
|
|
Returns a list of pairs of the file path and Python version as an int."""
|
|
filenames = find_stubs_in_paths(paths)
|
|
ts = typeshed.Typeshed()
|
|
skipped = set(ts.read_blacklist())
|
|
files = []
|
|
for f in sorted(filenames):
|
|
rel = _get_relative(f)
|
|
if rel in skipped or "@python2" in f:
|
|
continue
|
|
files.append(f)
|
|
return files
|
|
|
|
|
|
def find_stubs_in_paths(paths: Sequence[str]) -> list[str]:
|
|
filenames: list[str] = []
|
|
for path in paths:
|
|
if os.path.isdir(path):
|
|
for root, _, fns in os.walk(path):
|
|
filenames.extend(os.path.join(root, fn) for fn in fns if fn.endswith(".pyi"))
|
|
else:
|
|
filenames.append(path)
|
|
return filenames
|
|
|
|
|
|
def run_all_tests(*, files_to_test: Sequence[str], typeshed_location: str, print_stderr: bool, dry_run: bool) -> None:
|
|
bad = []
|
|
errors = 0
|
|
total_tests = len(files_to_test)
|
|
print("Testing files with pytype...")
|
|
for i, f in enumerate(files_to_test):
|
|
python_version = "{0.major}.{0.minor}".format(sys.version_info)
|
|
stderr = (
|
|
run_pytype(filename=f, python_version=python_version, typeshed_location=typeshed_location) if not dry_run else None
|
|
)
|
|
if stderr:
|
|
if print_stderr:
|
|
print(stderr)
|
|
errors += 1
|
|
stacktrace_final_line = stderr.rstrip().rsplit("\n", 1)[-1]
|
|
bad.append((_get_relative(f), python_version, stacktrace_final_line))
|
|
|
|
runs = i + 1
|
|
if runs % 25 == 0:
|
|
print(" {:3d}/{:d} with {:3d} errors".format(runs, total_tests, errors))
|
|
|
|
print("Ran pytype with {:d} pyis, got {:d} errors.".format(total_tests, errors))
|
|
for f, v, err in bad:
|
|
print("{} ({}): {}".format(f, v, err))
|
|
if errors:
|
|
raise SystemExit("\nRun again with --print-stderr to get the full stacktrace.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|