diff --git a/.gitattributes b/.gitattributes index f2de17522..19c9bb2b0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,5 @@ # Set linguist-language to support comments syntax highlight **/stubtest_allowlist*.txt linguist-language=ini **/stubtest_allowlists/*.txt linguist-language=ini -tests/pytype_exclude_list.txt linguist-language=ini pyrightconfig*.json linguist-language=jsonc .vscode/*.json linguist-language=jsonc diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 5b0e2d1e7..0a0312fbc 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -24,15 +24,15 @@ { groupName: "most test/lint dependencies", matchManagers: ["pip_requirements", "pre-commit"], - matchPackageNames: ["!pytype", "!pyright"], + matchPackageNames: ["!pyright"], description: "Quarterly update of most test dependencies", schedule: ["every 3 months on the first day of the month"] }, { - "groupName": "pytype and pyright", + "groupName": "pyright", "matchManagers": ["pip_requirements"], - "matchPackageNames": ["pytype", "pyright"], - "description": "Daily update of pyright and pytype", + "matchPackageNames": ["pyright"], + "description": "Daily update of pyright", "schedule": ["before 4am"] } ] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 00dfea2e9..c736d18e1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,29 +34,6 @@ jobs: - run: uv pip install -r requirements-tests.txt --system - run: python ./tests/check_typeshed_structure.py - pytype: - name: "pytype: Check stubs" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - # Max supported Python version as of pytype 2024.10.11 - python-version: "3.12" - - uses: astral-sh/setup-uv@v6 - with: - version-file: "requirements-tests.txt" - - run: uv pip install -r requirements-tests.txt --system - - name: Install external dependencies for 3rd-party stubs - run: | - DEPENDENCIES=$( python tests/get_external_stub_requirements.py ) - if [ -n "$DEPENDENCIES" ]; then - printf "Installing packages:\n $(echo $DEPENDENCIES | sed 's/ /\n /g')\n" - uv pip install --system $DEPENDENCIES - fi - - run: uv pip freeze - - run: ./tests/pytype_test.py --print-stderr - mypy: name: "mypy: Check stubs" runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab6fc1723..f7061f52a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,13 +83,6 @@ Note that some tests require extra setup steps to install the required dependenc (.venv) > pip install -r requirements-tests.txt ``` - To be able to run pytype tests, you'll also need to install it manually -as it's currently excluded from the requirements file: - - ```powershell - (.venv) > pip install -U pytype - ``` - @@ -104,10 +97,6 @@ as it's currently excluded from the requirements file: uv pip install -r requirements-tests.txt ``` - ```shell - uv pip install -U pytype - ``` - diff --git a/README.md b/README.md index ee09529b9..1467aa20b 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ Typeshed supports Python versions 3.9 to 3.14. ## Using -If you're just using a type checker ([mypy](https://github.com/python/mypy/), -[pyright](https://github.com/microsoft/pyright), -[pytype](https://github.com/google/pytype/), PyCharm, ...), as opposed to +If you're just using a type checker (e.g. [mypy](https://github.com/python/mypy/), +[pyright](https://github.com/microsoft/pyright), or PyCharm's built-in type +checker), as opposed to developing it, you don't need to interact with the typeshed repo at all: a copy of standard library part of typeshed is bundled with type checkers. And type stubs for third party packages and modules you are using can diff --git a/pyproject.toml b/pyproject.toml index 322a383e8..1ef50ab4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ exclude = [ # cache directories, etc.: ".git", ".mypy_cache", - ".pytype", ] [tool.ruff.lint] diff --git a/requirements-tests.txt b/requirements-tests.txt index 5457f419e..d9dbeb1f8 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,8 +2,6 @@ # be pinned to a specific version to make failure reproducible. mypy==1.16.1 pyright==1.1.403 -# pytype can be installed on Windows, but requires building wheels, let's not do that on the CI -pytype==2024.10.11; platform_system != "Windows" and python_version >= "3.10" and python_version < "3.13" # Libraries used by our various scripts. # TODO (2025-05-09): Installing this on Python 3.14 on Windows fails at diff --git a/tests/README.md b/tests/README.md index e2fd81fb7..a8f2c5e9a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,8 +1,6 @@ This directory contains several tests: - `tests/mypy_test.py` tests the stubs with [mypy](https://github.com/python/mypy/) -- `tests/pytype_test.py` tests the stubs with -[pytype](https://github.com/google/pytype/). - `tests/pyright_test.py` tests the stubs with [pyright](https://github.com/microsoft/pyright). - `tests/regr_test.py` runs mypy against the test cases for typeshed's @@ -19,7 +17,7 @@ in the `tests` and `scripts` directories. To run the tests, follow the [setup instructions](../CONTRIBUTING.md#preparing-the-environment) in the `CONTRIBUTING.md` document. In particular, you have to run with Python 3.9+. -In order for `pytype_test` and `pyright_test` to work correctly, some third-party stubs +In order for `pyright_test` to work correctly, some third-party stubs may require extra dependencies external to typeshed to be installed in your virtual environment prior to running the test. You can list or install all of a stubs package's external dependencies using the following script: @@ -67,18 +65,6 @@ imported but doesn't check whether stubs match their implementation Run `python tests/mypy_test.py --help` for information on the various configuration options for this script. -## pytype\_test.py - -Note: This test cannot be run on Python version < 3.13 as pytype does not yet support -Python 3.13 and above. - -Run using: -```bash -(.venv3)$ python3 tests/pytype_test.py -``` - -This test works similarly to `mypy_test.py`, except it uses `pytype`. - ## pyright\_test.py This test requires [Node.js](https://nodejs.org) to be installed. Although diff --git a/tests/check_typeshed_structure.py b/tests/check_typeshed_structure.py index 191173b42..a0e905b99 100755 --- a/tests/check_typeshed_structure.py +++ b/tests/check_typeshed_structure.py @@ -25,7 +25,7 @@ extension_descriptions = {".pyi": "stub", ".py": ".py"} # These type checkers and linters must have exact versions in the requirements file to ensure # consistent CI runs. -linters = {"mypy", "pyright", "pytype", "ruff"} +linters = {"mypy", "pyright", "ruff"} ALLOWED_PY_FILES_IN_TESTS_DIR = { "django_settings.py" # This file contains Django settings used by the mypy_django_plugin during stubtest execution. diff --git a/tests/pytype_exclude_list.txt b/tests/pytype_exclude_list.txt deleted file mode 100644 index bad4109bf..000000000 --- a/tests/pytype_exclude_list.txt +++ /dev/null @@ -1,28 +0,0 @@ -# Pytype exclude list. Files will not be tested with pytype. - -# pytype has its own version of these files, and thus doesn't mind if it -# can't parse the typeshed version: -stdlib/builtins.pyi -stdlib/typing.pyi - -# can't subscript memoryview for some reason -stdlib/asyncio/sslproto.pyi -stdlib/asyncio/transports.pyi - -# errors about import statements -stubs/mysqlclient/MySQLdb/__init__.pyi -stubs/mysqlclient/MySQLdb/connections.pyi -stubs/mysqlclient/MySQLdb/cursors.pyi - -# twisted not installed during tests -stubs/pika/pika/adapters/twisted_connection.pyi - -# pytype.pytd.visitors.ContainerError: Conflicting values for TypeVar -stubs/openpyxl/openpyxl/descriptors/nested.pyi - -# matplotlib not installed during tests -stubs/shapely/shapely/plotting.pyi - -# matplotlib et al not installed during tests -stubs/geopandas/geopandas/plotting.pyi -stubs/geopandas/geopandas/explore.pyi diff --git a/tests/pytype_test.py b/tests/pytype_test.py deleted file mode 100755 index 1c5fce2c1..000000000 --- a/tests/pytype_test.py +++ /dev/null @@ -1,249 +0,0 @@ -#!/usr/bin/env python3 -# Lack of pytype typing -# pyright: reportUnknownVariableType=false, reportUnknownMemberType=false, reportUnknownArgumentType=false, reportMissingTypeStubs=false -"""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 sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - assert sys.platform != "win32", "pytype isn't yet installed in CI, but wheels can be built on Windows" - from _typeshed import StrPath -if sys.version_info >= (3, 13): - print("pytype does not support Python 3.13+ yet.", file=sys.stderr) - sys.exit(1) - -import argparse -import importlib.metadata -import inspect -import os -import traceback -from collections.abc import Iterable, Sequence -from pathlib import Path - -# pytype is not py.typed https://github.com/google/pytype/issues/1325 -from pytype import config as pytype_config, load_pytd # type: ignore[import] -from pytype.imports import typeshed # type: ignore[import] - -from ts_utils.metadata import read_dependencies -from ts_utils.paths import STDLIB_PATH, STUBS_PATH, TS_BASE_PATH -from ts_utils.utils import SupportedVersionsDict, parse_stdlib_versions_file, supported_versions_for_module - -TYPESHED_SUBDIRS = [STDLIB_PATH.absolute(), STUBS_PATH.absolute()] -TYPESHED_HOME = "TYPESHED_HOME" -EXCLUDE_LIST = TS_BASE_PATH / "tests" / "pytype_exclude_list.txt" -_LOADERS: dict[str, tuple[pytype_config.Options, load_pytd.Loader]] = {} - - -def main() -> None: - args = create_parser().parse_args() - typeshed_location = Path(args.typeshed_location) or Path.cwd() - check_subdirs_discoverable(TYPESHED_SUBDIRS) - old_typeshed_home = os.environ.get(TYPESHED_HOME) - os.environ[TYPESHED_HOME] = str(typeshed_location) - files_to_test = determine_files_to_test(paths=[Path(file) for file in args.files] or TYPESHED_SUBDIRS) - run_all_tests(files_to_test=files_to_test, 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: StrPath, python_version: str, missing_modules: Iterable[str]) -> str | None: - """Run pytype, returning the stderr if any.""" - if python_version not in _LOADERS: - options = pytype_config.Options.create("", parse_pyi=True, python_version=python_version) - # For simplicity, pretends missing modules are part of the stdlib. - missing_modules = tuple(str(STDLIB_PATH / m) for m in missing_modules) - loader = load_pytd.create_loader(options, missing_modules) - _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: StrPath) -> Path: - filepath = Path(filename) - for d in TYPESHED_SUBDIRS: - try: - return filepath.absolute().relative_to(d.parent) - except ValueError: - continue - raise ValueError(f"{filepath} not relative to {TYPESHED_SUBDIRS}") - - -def _get_module_name(filename: StrPath) -> str: - """Convert a filename {subdir}/m.n/module/foo to module.foo.""" - parts = _get_relative(filename).parts - 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 check_subdirs_discoverable(subdir_paths: Iterable[Path]) -> None: - for p in subdir_paths: - if not p.is_dir(): - raise SystemExit(f"Cannot find typeshed subdir at {p} (specify parent dir via --typeshed-location)") - - -def determine_files_to_test(*, paths: Iterable[Path]) -> list[Path]: - """Determine all files to test. - - Checks for files in the pytype exclude list and for the stdlib VERSIONS file. - """ - filenames = find_stubs_in_paths(paths) - ts = typeshed.Typeshed() - exclude_list = set(ts.read_blacklist()) - stdlib_module_versions = parse_stdlib_versions_file() - return [ - f - for f in sorted(filenames) - if _get_relative(f).as_posix() not in exclude_list and _is_supported_stdlib_version(stdlib_module_versions, f) - ] - - -def find_stubs_in_paths(paths: Iterable[Path]) -> list[Path]: - filenames: list[Path] = [] - for path in paths: - if path.is_dir(): - for root, _, fns in os.walk(path): - filenames.extend(Path(root, fn) for fn in fns if fn.endswith(".pyi")) - else: - filenames.append(path) - return filenames - - -def _is_supported_stdlib_version(module_versions: SupportedVersionsDict, filename: StrPath) -> bool: - parts = _get_relative(filename).parts - if parts[0] != "stdlib": - return True - module_name = _get_module_name(filename) - min_version, max_version = supported_versions_for_module(module_versions, module_name) - return min_version <= sys.version_info <= max_version - - -def _get_pkgs_associated_with_requirement(req_name: str) -> list[str]: - try: - dist = importlib.metadata.distribution(req_name) - except importlib.metadata.PackageNotFoundError: - # The package wasn't installed, probably because an environment - # marker excluded it. - return [] - toplevel_txt_contents = dist.read_text("top_level.txt") - if toplevel_txt_contents is None: - if dist.files is None: - raise RuntimeError(f"Can't read find the packages associated with requirement {req_name!r}") - maybe_modules = [f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f) for f in dist.files] - packages = [name for name in maybe_modules if name is not None and "." not in name] - else: - packages = toplevel_txt_contents.split() - # https://peps.python.org/pep-0561/#stub-only-packages - return sorted({package.removesuffix("-stubs") for package in packages}) - - -def get_missing_modules(files_to_test: Iterable[Path]) -> Iterable[str]: - """Get names of modules that should be treated as missing. - - Some typeshed stubs depend on dependencies outside of typeshed. Since pytype - isn't able to read such dependencies, we instead declare them as "missing" - modules, so that no errors are reported for them. - - Similarly, pytype cannot parse files on its exclude list, so we also treat - those as missing. - """ - stub_distributions = set[str]() - for fi in files_to_test: - parts = fi.parts - try: - idx = parts.index("stubs") - except ValueError: - continue - stub_distributions.add(parts[idx + 1]) - - missing_modules = { - associated_package - for distribution in stub_distributions - for external_req in read_dependencies(distribution).external_pkgs - for associated_package in _get_pkgs_associated_with_requirement(external_req.name) - } - - with EXCLUDE_LIST.open() as f: - for line in f: - if not line.startswith("stubs/"): - # Skips comments, empty lines, and stdlib files, which are in - # the exclude list because pytype has its own version. - continue - _ts_subdir, _distribution, module_path = line.strip().split("/", 2) - missing_modules.add(module_path.removesuffix(".pyi")) - return missing_modules - - -def run_all_tests(*, files_to_test: Sequence[Path], print_stderr: bool, dry_run: bool) -> None: - bad: list[tuple[StrPath, str, str]] = [] - errors = 0 - total_tests = len(files_to_test) - missing_modules = get_missing_modules(files_to_test) - python_version = f"{sys.version_info.major}.{sys.version_info.minor}" - print("Testing files with pytype...") - for i, file_to_test in enumerate(files_to_test): - if dry_run: - stderr = None - else: - stderr = run_pytype(filename=file_to_test, python_version=python_version, missing_modules=missing_modules) - if stderr: - if print_stderr: - print(f"\n{stderr}") - errors += 1 - stacktrace_final_line = stderr.rstrip().rsplit("\n", 1)[-1] - bad.append((_get_relative(file_to_test), 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() diff --git a/tests/runtests.py b/tests/runtests.py index efffc84df..296cb8ccd 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -6,7 +6,6 @@ import json import re import subprocess import sys -from importlib.util import find_spec from pathlib import Path from ts_utils.metadata import get_oldest_supported_python, read_metadata @@ -77,7 +76,6 @@ def main() -> None: python_version = get_oldest_supported_python() stubtest_result: subprocess.CompletedProcess[bytes] | None = None - pytype_result: subprocess.CompletedProcess[bytes] | None = None print("\nRunning pre-commit...") pre_commit_result = subprocess.run(["pre-commit", "run", "--files", *path.rglob("*")], check=False) @@ -125,17 +123,6 @@ def main() -> None: else: print(colored("\nSkipping stubtest since mypy failed.", "yellow")) - if find_spec("pytype"): - print("\nRunning pytype...") - pytype_result = subprocess.run([sys.executable, "tests/pytype_test.py", path], check=False) - else: - print( - colored( - f"\nSkipping pytype on Windows. You need to install it first: `{sys.executable} -m pip install pytype` .", - "yellow", - ) - ) - cases_path = test_cases_path(stub if folder == "stubs" else "stdlib") if not cases_path.exists(): # No test means they all ran successfully (0 out of 0). Not all 3rd-party stubs have regression tests. @@ -186,7 +173,6 @@ def main() -> None: pyright_returncode, mypy_result.returncode, getattr(stubtest_result, "returncode", 0), - getattr(pytype_result, "returncode", 0), pyright_testcases_returncode, regr_test_returncode, ] @@ -218,10 +204,6 @@ def main() -> None: print("stubtest:", _SKIPPED) else: print("stubtest:", _SUCCESS if stubtest_result.returncode == 0 else _FAILED) - if not pytype_result: - print("pytype:", _SKIPPED) - else: - print("pytype:", _SUCCESS if pytype_result.returncode == 0 else _FAILED) if pyright_testcases_skipped: print("Pyright regression tests:", _SKIPPED) else: