mirror of
https://github.com/davidhalter/typeshed.git
synced 2025-12-08 04:54:47 +08:00
170 lines
6.0 KiB
Python
Executable File
170 lines
6.0 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.
|
|
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 # type: ignore[import]
|
|
from pytype.imports import typeshed # type: ignore[import]
|
|
|
|
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 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(f"{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(f"Cannot find typeshed subdir at {p} (specify parent dir via --typeshed-location)")
|
|
|
|
|
|
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:
|
|
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("\n{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(f" {runs:3d}/{total_tests:d} with {errors:3d} errors")
|
|
|
|
print(f"Ran pytype with {total_tests:d} pyis, got {errors:d} errors.")
|
|
for f, v, err in bad:
|
|
print(f"\n{f} ({v}): {err}")
|
|
if errors:
|
|
raise SystemExit("\nRun again with --print-stderr to get the full stacktrace.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|