Typecheck typeshed's code with pyright (#9793)

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
Avasam
2023-02-22 13:53:07 -05:00
committed by GitHub
parent b0dc6a3405
commit 75f47d0cb1
12 changed files with 125 additions and 32 deletions

View File

@@ -10,6 +10,7 @@ import re
import sys
import urllib.parse
from pathlib import Path
from typing import TypedDict
import yaml
from packaging.requirements import Requirement
@@ -93,7 +94,7 @@ def check_no_symlinks() -> None:
def check_versions() -> None:
versions = set()
versions = set[str]()
with open("stdlib/VERSIONS", encoding="UTF-8") as f:
data = f.read().splitlines()
for line in data:
@@ -115,7 +116,7 @@ def check_versions() -> None:
def _find_stdlib_modules() -> set[str]:
modules = set()
modules = set[str]()
for path, _, files in os.walk("stdlib"):
for filename in files:
base_module = ".".join(os.path.normpath(path).split(os.sep)[1:])
@@ -140,11 +141,21 @@ def get_txt_requirements() -> dict[str, SpecifierSet]:
return {requirement.name: requirement.specifier for requirement in requirements}
class PreCommitConfigRepos(TypedDict):
hooks: list[dict[str, str]]
repo: str
rev: str
class PreCommitConfig(TypedDict):
repos: list[PreCommitConfigRepos]
def get_precommit_requirements() -> dict[str, SpecifierSet]:
with open(".pre-commit-config.yaml", encoding="UTF-8") as precommit_file:
precommit = precommit_file.read()
yam = yaml.load(precommit, Loader=yaml.Loader)
precommit_requirements = {}
yam: PreCommitConfig = yaml.load(precommit, Loader=yaml.Loader)
precommit_requirements: dict[str, SpecifierSet] = {}
for repo in yam["repos"]:
if not repo.get("python_requirement", True):
continue

View File

@@ -9,7 +9,7 @@ from pathlib import Path
def check_new_syntax(tree: ast.AST, path: Path, stub: str) -> list[str]:
errors = []
errors: list[str] = []
class IfFinder(ast.NodeVisitor):
def visit_If(self, node: ast.If) -> None:
@@ -31,7 +31,7 @@ def check_new_syntax(tree: ast.AST, path: Path, stub: str) -> list[str]:
def main() -> None:
errors = []
errors: list[str] = []
for path in chain(Path("stdlib").rglob("*.pyi"), Path("stubs").rglob("*.pyi")):
with open(path, encoding="UTF-8") as f:
stub = f.read()

View File

@@ -42,7 +42,7 @@ from utils import (
# Fail early if mypy isn't installed
try:
import mypy # noqa: F401
import mypy # pyright: ignore[reportUnusedImport] # noqa: F401
except ImportError:
print_error("Cannot import mypy. Did you install it?")
sys.exit(1)
@@ -57,7 +57,8 @@ VersionTuple: TypeAlias = Tuple[int, int]
Platform: TypeAlias = Annotated[str, "Must be one of the entries in SUPPORTED_PLATFORMS"]
class CommandLineArgs(argparse.Namespace):
@dataclass(init=False)
class CommandLineArgs:
verbose: int
filter: list[Path]
exclude: list[Path] | None
@@ -158,7 +159,7 @@ def match(path: Path, args: TestConfig) -> bool:
def parse_versions(fname: StrPath) -> dict[str, tuple[VersionTuple, VersionTuple]]:
result = {}
result: dict[str, tuple[VersionTuple, VersionTuple]] = {}
with open(fname, encoding="UTF-8") as f:
for line in f:
line = strip_comments(line)
@@ -209,7 +210,8 @@ def add_configuration(configurations: list[MypyDistConf], distribution: str) ->
with Path("stubs", distribution, "METADATA.toml").open("rb") as f:
data = tomli.load(f)
mypy_tests_conf = data.get("mypy-tests")
# TODO: This could be added to parse_metadata.py, but is currently unused
mypy_tests_conf: dict[str, dict[str, Any]] = data.get("mypy-tests", {})
if not mypy_tests_conf:
return
@@ -221,8 +223,8 @@ def add_configuration(configurations: list[MypyDistConf], distribution: str) ->
assert module_name is not None, f"{section_name} should have a module_name key"
assert isinstance(module_name, str), f"{section_name} should be a key-value pair"
values = mypy_section.get("values")
assert values is not None, f"{section_name} should have a values section"
assert "values" in mypy_section, f"{section_name} should have a values section"
values: dict[str, dict[str, Any]] = mypy_section["values"]
assert isinstance(values, dict), "values should be a section"
configurations.append(MypyDistConf(module_name, values.copy()))

View File

@@ -1,3 +1,6 @@
# This module is made specifically to abstract away those type errors
# pyright: reportUnknownVariableType=false, reportUnknownArgumentType=false, reportUnknownMemberType=false
"""Tools to help parse and validate information stored in METADATA.toml files."""
from __future__ import annotations
@@ -188,7 +191,8 @@ def read_metadata(distribution: str) -> StubMetadata:
uploaded_to_pypi = data.get("upload", True)
assert type(uploaded_to_pypi) is bool
tools_settings = data.get("tool", {})
empty_tools: dict[str, dict[str, object]] = {}
tools_settings = data.get("tool", empty_tools)
assert isinstance(tools_settings, dict)
assert tools_settings.keys() <= _KNOWN_METADATA_TOOL_FIELDS.keys(), f"Unrecognised tool for {distribution!r}"
for tool, tk in _KNOWN_METADATA_TOOL_FIELDS.items():
@@ -234,7 +238,8 @@ def read_dependencies(distribution: str) -> PackageDependencies:
If a typeshed stub is removed, this function will consider it to be an external dependency.
"""
pypi_name_to_typeshed_name_mapping = get_pypi_name_to_typeshed_name_mapping()
typeshed, external = [], []
typeshed: list[str] = []
external: list[str] = []
for dependency in read_metadata(distribution).requires:
maybe_typeshed_dependency = Requirement(dependency).name
if maybe_typeshed_dependency in pypi_name_to_typeshed_name_mapping:

View File

@@ -1,4 +1,6 @@
#!/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.
@@ -19,11 +21,14 @@ import traceback
from collections.abc import Iterable, Sequence
import pkg_resources
from pytype import config as pytype_config, load_pytd # type: ignore[import]
from pytype.imports import typeshed # type: ignore[import]
from parse_metadata import read_dependencies
assert sys.platform != "win32"
# pytype is not py.typed https://github.com/google/pytype/issues/1325
from pytype import config as pytype_config, load_pytd # type: ignore[import] # noqa: E402
from pytype.imports import typeshed # type: ignore[import] # noqa: E402
TYPESHED_SUBDIRS = ["stdlib", "stubs"]
TYPESHED_HOME = "TYPESHED_HOME"
_LOADERS = {}
@@ -155,7 +160,11 @@ def get_missing_modules(files_to_test: Sequence[str]) -> Iterable[str]:
for distribution in stub_distributions:
for pkg in read_dependencies(distribution).external_pkgs:
# See https://stackoverflow.com/a/54853084
top_level_file = os.path.join(pkg_resources.get_distribution(pkg).egg_info, "top_level.txt") # type: ignore[attr-defined]
top_level_file = os.path.join(
# Fixed in #9747
pkg_resources.get_distribution(pkg).egg_info, # type: ignore[attr-defined] # pyright: ignore[reportGeneralTypeIssues]
"top_level.txt",
)
with open(top_level_file) as f:
missing_modules.update(f.read().splitlines())
return missing_modules

View File

@@ -15,11 +15,12 @@ ReturnCode: TypeAlias = int
SUPPORTED_PLATFORMS = ("linux", "darwin", "win32")
SUPPORTED_VERSIONS = ("3.11", "3.10", "3.9")
DIRECTORIES_TO_TEST = ("scripts", "tests")
EMPTY: list[str] = []
parser = argparse.ArgumentParser(description="Run mypy on typeshed's own code in the `scripts` and `tests` directories.")
parser.add_argument(
"dir",
choices=DIRECTORIES_TO_TEST + ([],),
choices=DIRECTORIES_TO_TEST + (EMPTY,),
nargs="*",
action="extend",
help=f"Test only these top-level typeshed directories (defaults to {DIRECTORIES_TO_TEST!r})",

View File

@@ -135,4 +135,6 @@ def spec_matches_path(spec: pathspec.PathSpec, path: Path) -> bool:
normalized_path = path.as_posix()
if path.is_dir():
normalized_path += "/"
return spec.match_file(normalized_path)
# pathspec.PathSpec.match_file has partially Unknown file parameter
# https://github.com/cpburnz/python-pathspec/pull/75
return spec.match_file(normalized_path) # pyright: ignore[reportUnknownMemberType]