mirror of
https://github.com/davidhalter/typeshed.git
synced 2026-06-23 09:18:44 +08:00
Enable Ruff flake8-use-pathlib (PTH) (#13795)
Port existing code to pathlib
This commit is contained in:
@@ -11,6 +11,7 @@ STUBS_PATH: Final = TS_BASE_PATH / "stubs"
|
||||
|
||||
PYPROJECT_PATH: Final = TS_BASE_PATH / "pyproject.toml"
|
||||
REQUIREMENTS_PATH: Final = TS_BASE_PATH / "requirements-tests.txt"
|
||||
GITIGNORE_PATH: Final = TS_BASE_PATH / ".gitignore"
|
||||
|
||||
TESTS_DIR: Final = "@tests"
|
||||
TEST_CASES_DIR: Final = "test_cases"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -16,7 +15,7 @@ from typing_extensions import TypeAlias
|
||||
import pathspec
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
from .paths import REQUIREMENTS_PATH, STDLIB_PATH, STUBS_PATH, TEST_CASES_DIR, allowlists_path, test_cases_path
|
||||
from .paths import GITIGNORE_PATH, REQUIREMENTS_PATH, STDLIB_PATH, STUBS_PATH, TEST_CASES_DIR, allowlists_path, test_cases_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import OpenTextMode
|
||||
@@ -215,7 +214,7 @@ else:
|
||||
def NamedTemporaryFile(mode: OpenTextMode) -> TemporaryFileWrapper[str]: # noqa: N802
|
||||
def close(self: TemporaryFileWrapper[str]) -> None:
|
||||
TemporaryFileWrapper.close(self) # pyright: ignore[reportUnknownMemberType]
|
||||
os.remove(self.name)
|
||||
Path(self.name).unlink()
|
||||
|
||||
temp = tempfile.NamedTemporaryFile(mode, delete=False) # noqa: SIM115, TID251
|
||||
temp.close = MethodType(close, temp) # type: ignore[method-assign]
|
||||
@@ -229,7 +228,7 @@ else:
|
||||
|
||||
@functools.cache
|
||||
def get_gitignore_spec() -> pathspec.PathSpec:
|
||||
with open(".gitignore", encoding="UTF-8") as f:
|
||||
with GITIGNORE_PATH.open(encoding="UTF-8") as f:
|
||||
return pathspec.GitIgnoreSpec.from_lines(f)
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -54,6 +54,7 @@ select = [
|
||||
"PGH", # pygrep-hooks
|
||||
"PIE", # flake8-pie
|
||||
"PL", # Pylint
|
||||
"PTH", # flake8-use-pathlib
|
||||
"RSE", # flake8-raise
|
||||
"RUF", # Ruff-specific and unused-noqa
|
||||
"SLOT", # flake8-slots
|
||||
@@ -80,9 +81,6 @@ select = [
|
||||
"FURB187", # Use of assignment of `reversed` on list `{name}`
|
||||
# Used for lint.flake8-import-conventions.aliases
|
||||
"ICN001", # `{name}` should be imported as `{asname}`
|
||||
# Autofixable flake8-use-pathlib only
|
||||
"PTH201", # Do not pass the current directory explicitly to `Path`
|
||||
"PTH210", # Invalid suffix passed to `.with_suffix()`
|
||||
# PYI: only enable rules that have autofixes and that we always want to fix (even manually),
|
||||
# avoids duplicate # noqa with flake8-pyi
|
||||
"PYI009", # Empty body should contain `...`, not pass
|
||||
@@ -167,6 +165,8 @@ ignore = [
|
||||
"PLR2004", # Magic value used in comparison, consider replacing `{value}` with a constant variable
|
||||
# Keep codeflow path separation explicit
|
||||
"PLR5501", # Use `elif` instead of `else` then `if`, to reduce indentation
|
||||
# Often just leads to redundant more verbose code when needing an actual str
|
||||
"PTH208", # Use `pathlib.Path.iterdir()` instead.
|
||||
# Allow FIXME
|
||||
"TD001", # Invalid TODO tag: `{tag}`
|
||||
# Git blame is sufficient
|
||||
|
||||
@@ -12,19 +12,20 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import glob
|
||||
import os.path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
from http import HTTPStatus
|
||||
from importlib.metadata import distribution
|
||||
from pathlib import Path
|
||||
|
||||
import aiohttp
|
||||
import termcolor
|
||||
|
||||
PYRIGHT_CONFIG = "pyrightconfig.stricter.json"
|
||||
from ts_utils.paths import STDLIB_PATH, STUBS_PATH
|
||||
|
||||
PYRIGHT_CONFIG = Path("pyrightconfig.stricter.json")
|
||||
|
||||
|
||||
def search_pip_freeze_output(project: str, output: str) -> tuple[str, str] | None:
|
||||
@@ -52,22 +53,22 @@ def get_installed_package_info(project: str) -> tuple[str, str] | None:
|
||||
return search_pip_freeze_output(project, r.stdout)
|
||||
|
||||
|
||||
def run_stubgen(package: str, output: str) -> None:
|
||||
def run_stubgen(package: str, output: Path) -> None:
|
||||
print(f"Running stubgen: stubgen -o {output} -p {package}")
|
||||
subprocess.run(["stubgen", "-o", output, "-p", package, "--export-less"], check=True)
|
||||
|
||||
|
||||
def run_stubdefaulter(stub_dir: str) -> None:
|
||||
def run_stubdefaulter(stub_dir: Path) -> None:
|
||||
print(f"Running stubdefaulter: stubdefaulter --packages {stub_dir}")
|
||||
subprocess.run(["stubdefaulter", "--packages", stub_dir], check=False)
|
||||
|
||||
|
||||
def run_black(stub_dir: str) -> None:
|
||||
def run_black(stub_dir: Path) -> None:
|
||||
print(f"Running Black: black {stub_dir}")
|
||||
subprocess.run(["pre-commit", "run", "black", "--files", *glob.iglob(f"{stub_dir}/**/*.pyi")], check=False)
|
||||
subprocess.run(["pre-commit", "run", "black", "--files", *stub_dir.rglob("*.pyi")], check=False)
|
||||
|
||||
|
||||
def run_ruff(stub_dir: str) -> None:
|
||||
def run_ruff(stub_dir: Path) -> None:
|
||||
print(f"Running Ruff: ruff check {stub_dir} --fix-only")
|
||||
subprocess.run([sys.executable, "-m", "ruff", "check", stub_dir, "--fix-only"], check=False)
|
||||
|
||||
@@ -115,14 +116,14 @@ async def get_upstream_repo_url(project: str) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def create_metadata(project: str, stub_dir: str, version: str) -> None:
|
||||
def create_metadata(project: str, stub_dir: Path, version: str) -> None:
|
||||
"""Create a METADATA.toml file."""
|
||||
match = re.match(r"[0-9]+.[0-9]+", version)
|
||||
if match is None:
|
||||
sys.exit(f"Error: Cannot parse version number: {version}")
|
||||
filename = os.path.join(stub_dir, "METADATA.toml")
|
||||
filename = stub_dir / "METADATA.toml"
|
||||
version = match.group(0)
|
||||
if os.path.exists(filename):
|
||||
if filename.exists():
|
||||
return
|
||||
metadata = f'version = "{version}.*"\n'
|
||||
upstream_repo_url = asyncio.run(get_upstream_repo_url(project))
|
||||
@@ -135,13 +136,12 @@ def create_metadata(project: str, stub_dir: str, version: str) -> None:
|
||||
else:
|
||||
metadata += f'upstream_repository = "{upstream_repo_url}"\n'
|
||||
print(f"Writing {filename}")
|
||||
with open(filename, "w", encoding="UTF-8") as file:
|
||||
file.write(metadata)
|
||||
filename.write_text(metadata, encoding="UTF-8")
|
||||
|
||||
|
||||
def add_pyright_exclusion(stub_dir: str) -> None:
|
||||
def add_pyright_exclusion(stub_dir: Path) -> None:
|
||||
"""Exclude stub_dir from strict pyright checks."""
|
||||
with open(PYRIGHT_CONFIG, encoding="UTF-8") as f:
|
||||
with PYRIGHT_CONFIG.open(encoding="UTF-8") as f:
|
||||
lines = f.readlines()
|
||||
i = 0
|
||||
while i < len(lines) and not lines[i].strip().startswith('"exclude": ['):
|
||||
@@ -167,7 +167,7 @@ def add_pyright_exclusion(stub_dir: str) -> None:
|
||||
third_party_excludes[-1] = last_line + "\n"
|
||||
|
||||
# Must use forward slash in the .json file
|
||||
line_to_add = f' "{stub_dir}",\n'.replace("\\", "/")
|
||||
line_to_add = f' "{stub_dir.as_posix()}",\n'
|
||||
|
||||
if line_to_add in third_party_excludes:
|
||||
print(f"{PYRIGHT_CONFIG} already up-to-date")
|
||||
@@ -177,7 +177,7 @@ def add_pyright_exclusion(stub_dir: str) -> None:
|
||||
third_party_excludes.sort(key=str.lower)
|
||||
|
||||
print(f"Updating {PYRIGHT_CONFIG}")
|
||||
with open(PYRIGHT_CONFIG, "w", encoding="UTF-8") as f:
|
||||
with PYRIGHT_CONFIG.open("w", encoding="UTF-8") as f:
|
||||
f.writelines(before_third_party_excludes)
|
||||
f.writelines(third_party_excludes)
|
||||
f.writelines(after_third_party_excludes)
|
||||
@@ -194,7 +194,7 @@ def main() -> None:
|
||||
parser.add_argument("--package", help="generate stubs for this Python package (default is autodetected)")
|
||||
args = parser.parse_args()
|
||||
project = args.project
|
||||
package = args.package
|
||||
package: str = args.package
|
||||
|
||||
if not re.match(r"[a-zA-Z0-9-_.]+$", project):
|
||||
sys.exit(f"Invalid character in project name: {project!r}")
|
||||
@@ -214,7 +214,7 @@ def main() -> None:
|
||||
print(f'Using detected package "{package}" for project "{project}"', file=sys.stderr)
|
||||
print("Suggestion: Try again with --package argument if that's not what you wanted", file=sys.stderr)
|
||||
|
||||
if not os.path.isdir("stubs") or not os.path.isdir("stdlib"):
|
||||
if not STUBS_PATH.is_dir() or not STDLIB_PATH.is_dir():
|
||||
sys.exit("Error: Current working directory must be the root of typeshed repository")
|
||||
|
||||
# Get normalized project name and version of installed package.
|
||||
@@ -226,9 +226,9 @@ def main() -> None:
|
||||
sys.exit(1)
|
||||
project, version = info
|
||||
|
||||
stub_dir = os.path.join("stubs", project)
|
||||
package_dir = os.path.join(stub_dir, package)
|
||||
if os.path.exists(package_dir):
|
||||
stub_dir = STUBS_PATH / project
|
||||
package_dir = stub_dir / package
|
||||
if package_dir.exists():
|
||||
sys.exit(f"Error: {package_dir} already exists (delete it first)")
|
||||
|
||||
run_stubgen(package, stub_dir)
|
||||
|
||||
@@ -4,6 +4,7 @@ import subprocess
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from http.client import HTTPResponse
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.request import urlopen
|
||||
from zipfile import ZipFile
|
||||
@@ -18,11 +19,11 @@ if TYPE_CHECKING:
|
||||
MYPY_PROTOBUF_VERSION = mypy_protobuf__version__
|
||||
|
||||
|
||||
def download_file(url: str, destination: StrPath) -> None:
|
||||
def download_file(url: str, destination: Path) -> None:
|
||||
print(f"Downloading '{url}' to '{destination}'")
|
||||
resp: HTTPResponse
|
||||
with urlopen(url) as resp, open(destination, "wb") as file:
|
||||
file.write(resp.read())
|
||||
with urlopen(url) as resp:
|
||||
destination.write_bytes(resp.read())
|
||||
|
||||
|
||||
def extract_archive(archive_path: StrPath, destination: StrPath) -> None:
|
||||
|
||||
@@ -33,7 +33,7 @@ PROTO_FILE_PATTERN = re.compile(r'"//:(.*)_proto"')
|
||||
|
||||
def extract_python_version(file_path: Path) -> str:
|
||||
"""Extract the Python version from https://github.com/protocolbuffers/protobuf/blob/main/version.json ."""
|
||||
with open(file_path) as file:
|
||||
with file_path.open() as file:
|
||||
data: dict[str, Any] = json.load(file)
|
||||
# The root key will be the protobuf source code version
|
||||
version = next(iter(data.values()))["languages"]["python"]
|
||||
@@ -47,7 +47,7 @@ def extract_proto_file_paths(temp_dir: Path) -> list[str]:
|
||||
as described in py_proto_library calls in
|
||||
https://github.com/protocolbuffers/protobuf/blob/main/python/dist/BUILD.bazel .
|
||||
"""
|
||||
with open(temp_dir / EXTRACTED_PACKAGE_DIR / "python" / "dist" / "BUILD.bazel") as file:
|
||||
with (temp_dir / EXTRACTED_PACKAGE_DIR / "python" / "dist" / "BUILD.bazel").open() as file:
|
||||
matched_lines = filter(None, (re.search(PROTO_FILE_PATTERN, line) for line in file))
|
||||
proto_files = [
|
||||
EXTRACTED_PACKAGE_DIR + "/src/google/protobuf/" + match.group(1).replace("compiler_", "compiler/") + ".proto"
|
||||
|
||||
@@ -6,7 +6,6 @@ Generally, new minor versions are a good time to update the stubs.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -72,21 +71,19 @@ def post_creation() -> None:
|
||||
|
||||
for path in STUBS_FOLDER.rglob("*_pb2.pyi"):
|
||||
print(f"Fixing imports in '{path}'")
|
||||
with open(path, encoding="utf-8") as file:
|
||||
filedata = file.read()
|
||||
filedata = path.read_text(encoding="utf-8")
|
||||
|
||||
# Replace the target string
|
||||
filedata = re.sub(TSL_IMPORT_PATTERN, "\\1tensorflow.tsl.", filedata)
|
||||
filedata = re.sub(XLA_IMPORT_PATTERN, "\\1tensorflow.compiler.xla.", filedata)
|
||||
|
||||
# Write the file out again
|
||||
with open(path, "w", encoding="utf-8") as file:
|
||||
file.write(filedata)
|
||||
path.write_text(filedata, encoding="utf-8")
|
||||
|
||||
print()
|
||||
for to_remove in PROTOS_TO_REMOVE:
|
||||
file_path = STUBS_FOLDER / "tensorflow" / to_remove
|
||||
os.remove(file_path)
|
||||
file_path.unlink()
|
||||
print(f"Removed '{file_path}'")
|
||||
|
||||
|
||||
|
||||
@@ -113,11 +113,10 @@ def check_test_cases() -> None:
|
||||
|
||||
def check_no_symlinks() -> None:
|
||||
"""Check that there are no symlinks in the typeshed repository."""
|
||||
files = [os.path.join(root, file) for root, _, files in os.walk(".") for file in files]
|
||||
files = [Path(root, file) for root, _, files in os.walk(".") for file in files]
|
||||
no_symlink = "You cannot use symlinks in typeshed, please copy {} to its link."
|
||||
for file in files:
|
||||
_, ext = os.path.splitext(file)
|
||||
if ext == ".pyi" and os.path.islink(file):
|
||||
if file.suffix == ".pyi" and file.is_symlink():
|
||||
raise ValueError(no_symlink.format(file))
|
||||
|
||||
|
||||
@@ -141,18 +140,18 @@ def _find_stdlib_modules() -> set[str]:
|
||||
modules = set[str]()
|
||||
for path, _, files in os.walk(STDLIB_PATH):
|
||||
for filename in files:
|
||||
base_module = ".".join(os.path.normpath(path).split(os.sep)[1:])
|
||||
base_module = ".".join(Path(path).parts[1:])
|
||||
if filename == "__init__.pyi":
|
||||
modules.add(base_module)
|
||||
elif filename.endswith(".pyi"):
|
||||
mod, _ = os.path.splitext(filename)
|
||||
mod = filename[:-4]
|
||||
modules.add(f"{base_module}.{mod}" if base_module else mod)
|
||||
return modules
|
||||
|
||||
|
||||
def check_metadata() -> None:
|
||||
"""Check that all METADATA.toml files are valid."""
|
||||
for distribution in os.listdir("stubs"):
|
||||
for distribution in os.listdir(STUBS_PATH):
|
||||
# This function does various sanity checks for METADATA.toml files
|
||||
read_metadata(distribution)
|
||||
|
||||
|
||||
+7
-8
@@ -150,6 +150,8 @@ def match(path: Path, args: TestConfig) -> bool:
|
||||
|
||||
def add_files(files: list[Path], module: Path, args: TestConfig) -> None:
|
||||
"""Add all files in package or module represented by 'name' located in 'root'."""
|
||||
if module.name.startswith("."):
|
||||
return
|
||||
if module.is_file() and module.suffix == ".pyi":
|
||||
if match(module, args):
|
||||
files.append(module)
|
||||
@@ -244,10 +246,8 @@ def add_third_party_files(distribution: str, files: list[Path], args: TestConfig
|
||||
seen_dists.add(distribution)
|
||||
seen_dists.update(r.name for r in typeshed_reqs)
|
||||
root = distribution_path(distribution)
|
||||
for name in os.listdir(root):
|
||||
if name.startswith("."):
|
||||
continue
|
||||
add_files(files, (root / name), args)
|
||||
for path in root.iterdir():
|
||||
add_files(files, path, args)
|
||||
|
||||
|
||||
class TestResult(NamedTuple):
|
||||
@@ -295,7 +295,7 @@ def test_third_party_distribution(
|
||||
def test_stdlib(args: TestConfig) -> TestResult:
|
||||
files: list[Path] = []
|
||||
for file in STDLIB_PATH.iterdir():
|
||||
if file.name in ("VERSIONS", TESTS_DIR) or file.name.startswith("."):
|
||||
if file.name in ("VERSIONS", TESTS_DIR):
|
||||
continue
|
||||
add_files(files, file, args)
|
||||
|
||||
@@ -525,15 +525,14 @@ def test_third_party_stubs(args: TestConfig, tempdir: Path) -> TestSummary:
|
||||
|
||||
def test_typeshed(args: TestConfig, tempdir: Path) -> TestSummary:
|
||||
print(f"*** Testing Python {args.version} on {args.platform}")
|
||||
stdlib_dir, stubs_dir = Path("stdlib"), Path("stubs")
|
||||
summary = TestSummary()
|
||||
|
||||
if stdlib_dir in args.filter or any(stdlib_dir in path.parents for path in args.filter):
|
||||
if STDLIB_PATH in args.filter or any(STDLIB_PATH in path.parents for path in args.filter):
|
||||
mypy_result, files_checked = test_stdlib(args)
|
||||
summary.register_result(mypy_result, files_checked)
|
||||
print()
|
||||
|
||||
if stubs_dir in args.filter or any(stubs_dir in path.parents for path in args.filter):
|
||||
if STUBS_PATH in args.filter or any(STUBS_PATH in path.parents for path in args.filter):
|
||||
tp_results = test_third_party_stubs(args, tempdir)
|
||||
summary.merge(tp_results)
|
||||
print()
|
||||
|
||||
+40
-45
@@ -24,7 +24,6 @@ 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
|
||||
@@ -38,21 +37,22 @@ 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", "stubs"]
|
||||
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 = args.typeshed_location or os.getcwd()
|
||||
subdir_paths = [os.path.join(typeshed_location, d) for d in TYPESHED_SUBDIRS]
|
||||
check_subdirs_discoverable(subdir_paths)
|
||||
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] = typeshed_location
|
||||
files_to_test = determine_files_to_test(paths=args.files or subdir_paths)
|
||||
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]
|
||||
@@ -75,12 +75,12 @@ def create_parser() -> argparse.ArgumentParser:
|
||||
return parser
|
||||
|
||||
|
||||
def run_pytype(*, filename: str, python_version: str, missing_modules: Iterable[str]) -> str | None:
|
||||
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(os.path.join("stdlib", m) for m in missing_modules)
|
||||
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]
|
||||
@@ -100,13 +100,13 @@ def _get_relative(filename: StrPath) -> Path:
|
||||
filepath = Path(filename)
|
||||
for d in TYPESHED_SUBDIRS:
|
||||
try:
|
||||
return filepath.absolute().relative_to(Path(d).absolute().parent)
|
||||
return filepath.absolute().relative_to(d.parent)
|
||||
except ValueError:
|
||||
continue
|
||||
raise ValueError(f"{filepath} not relative to {TYPESHED_SUBDIRS}")
|
||||
|
||||
|
||||
def _get_module_name(filename: str) -> str:
|
||||
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":
|
||||
@@ -117,13 +117,13 @@ def _get_module_name(filename: str) -> str:
|
||||
return ".".join(module_parts).replace(".pyi", "").replace(".__init__", "")
|
||||
|
||||
|
||||
def check_subdirs_discoverable(subdir_paths: list[str]) -> None:
|
||||
def check_subdirs_discoverable(subdir_paths: Iterable[Path]) -> None:
|
||||
for p in subdir_paths:
|
||||
if not os.path.isdir(p):
|
||||
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: Sequence[str]) -> list[str]:
|
||||
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.
|
||||
@@ -132,28 +132,25 @@ def determine_files_to_test(*, paths: Sequence[str]) -> list[str]:
|
||||
ts = typeshed.Typeshed()
|
||||
exclude_list = set(ts.read_blacklist())
|
||||
stdlib_module_versions = parse_stdlib_versions_file()
|
||||
files = []
|
||||
for f in sorted(filenames):
|
||||
if _get_relative(f).as_posix() in exclude_list:
|
||||
continue
|
||||
if not _is_supported_stdlib_version(stdlib_module_versions, f):
|
||||
continue
|
||||
files.append(f)
|
||||
return files
|
||||
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: Sequence[str]) -> list[str]:
|
||||
filenames: list[str] = []
|
||||
def find_stubs_in_paths(paths: Iterable[Path]) -> list[Path]:
|
||||
filenames: list[Path] = []
|
||||
for path in paths:
|
||||
if os.path.isdir(path):
|
||||
if path.is_dir():
|
||||
for root, _, fns in os.walk(path):
|
||||
filenames.extend(os.path.join(root, fn) for fn in fns if fn.endswith(".pyi"))
|
||||
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: str) -> bool:
|
||||
def _is_supported_stdlib_version(module_versions: SupportedVersionsDict, filename: StrPath) -> bool:
|
||||
parts = _get_relative(filename).parts
|
||||
if parts[0] != "stdlib":
|
||||
return True
|
||||
@@ -181,7 +178,7 @@ def _get_pkgs_associated_with_requirement(req_name: str) -> list[str]:
|
||||
return sorted({package.removesuffix("-stubs") for package in packages})
|
||||
|
||||
|
||||
def get_missing_modules(files_to_test: Sequence[str]) -> Iterable[str]:
|
||||
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
|
||||
@@ -191,37 +188,35 @@ def get_missing_modules(files_to_test: Sequence[str]) -> Iterable[str]:
|
||||
Similarly, pytype cannot parse files on its exclude list, so we also treat
|
||||
those as missing.
|
||||
"""
|
||||
stub_distributions = set()
|
||||
stub_distributions = set[str]()
|
||||
for fi in files_to_test:
|
||||
parts = fi.split(os.sep)
|
||||
parts = fi.parts
|
||||
try:
|
||||
idx = parts.index("stubs")
|
||||
except ValueError:
|
||||
continue
|
||||
stub_distributions.add(parts[idx + 1])
|
||||
|
||||
missing_modules = set()
|
||||
for distribution in stub_distributions:
|
||||
for external_req in read_dependencies(distribution).external_pkgs:
|
||||
associated_packages = _get_pkgs_associated_with_requirement(external_req.name)
|
||||
missing_modules.update(associated_packages)
|
||||
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)
|
||||
}
|
||||
|
||||
test_dir = os.path.dirname(__file__)
|
||||
exclude_list = os.path.join(test_dir, "pytype_exclude_list.txt")
|
||||
with open(exclude_list) as f:
|
||||
excluded_files = f.readlines()
|
||||
for fi in excluded_files:
|
||||
if not fi.startswith("stubs/"):
|
||||
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
|
||||
unused_stubs_prefix, unused_pkg, mod_path = fi.split("/", 2) # pyright: ignore[reportUnusedVariable]
|
||||
missing_modules.add(os.path.splitext(mod_path)[0])
|
||||
_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[str], print_stderr: bool, dry_run: bool) -> None:
|
||||
bad = []
|
||||
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)
|
||||
|
||||
+11
-14
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -13,8 +12,8 @@ from pathlib import Path
|
||||
from ts_utils.paths import TEST_CASES_DIR, test_cases_path
|
||||
from ts_utils.utils import colored
|
||||
|
||||
_STRICTER_CONFIG_FILE = "pyrightconfig.stricter.json"
|
||||
_TESTCASES_CONFIG_FILE = "pyrightconfig.testcases.json"
|
||||
_STRICTER_CONFIG_FILE = Path("pyrightconfig.stricter.json")
|
||||
_TESTCASES_CONFIG_FILE = Path("pyrightconfig.testcases.json")
|
||||
_NPX_ERROR_PATTERN = r"error (runn|find)ing npx"
|
||||
_NPX_ERROR_MESSAGE = colored("\nSkipping Pyright tests: npx is not installed or can't be run!", "yellow")
|
||||
_SUCCESS = colored("Success", "green")
|
||||
@@ -33,10 +32,9 @@ def _parse_jsonc(json_text: str) -> str:
|
||||
return valid_json
|
||||
|
||||
|
||||
def _get_strict_params(stub_path: str) -> list[str]:
|
||||
with open(_STRICTER_CONFIG_FILE, encoding="UTF-8") as file:
|
||||
data = json.loads(_parse_jsonc(file.read()))
|
||||
lower_stub_path = stub_path.lower()
|
||||
def _get_strict_params(stub_path: Path) -> list[str | Path]:
|
||||
data = json.loads(_parse_jsonc(_STRICTER_CONFIG_FILE.read_text(encoding="UTF-8")))
|
||||
lower_stub_path = stub_path.as_posix().lower()
|
||||
if any(lower_stub_path == stub.lower() for stub in data["exclude"]):
|
||||
return []
|
||||
return ["-p", _STRICTER_CONFIG_FILE]
|
||||
@@ -60,23 +58,22 @@ def main() -> None:
|
||||
)
|
||||
parser.add_argument("path", help="Path of the stub to test in format <folder>/<stub>, from the root of the project.")
|
||||
args = parser.parse_args()
|
||||
path: str = args.path
|
||||
path = Path(args.path)
|
||||
run_stubtest: bool = args.run_stubtest
|
||||
python_version: str = args.python_version
|
||||
|
||||
path_tokens = Path(path).parts
|
||||
if len(path_tokens) != 2:
|
||||
if len(path.parts) != 2:
|
||||
parser.error("'path' argument should be in format <folder>/<stub>.")
|
||||
folder, stub = path_tokens
|
||||
folder, stub = path.parts
|
||||
if folder not in {"stdlib", "stubs"}:
|
||||
parser.error("Only the 'stdlib' and 'stubs' folders are supported.")
|
||||
if not os.path.exists(path):
|
||||
if not path.exists():
|
||||
parser.error(f"{path=} does not exist.")
|
||||
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(path).rglob("*")], check=False)
|
||||
pre_commit_result = subprocess.run(["pre-commit", "run", "--files", *path.rglob("*")], check=False)
|
||||
|
||||
print("\nRunning check_typeshed_structure.py...")
|
||||
check_structure_result = subprocess.run([sys.executable, "tests/check_typeshed_structure.py"], check=False)
|
||||
@@ -141,7 +138,7 @@ def main() -> None:
|
||||
regr_test_returncode = 0
|
||||
else:
|
||||
print(f"\nRunning Pyright regression tests for Python {python_version}...")
|
||||
command = [
|
||||
command: list[str | Path] = [
|
||||
sys.executable,
|
||||
"tests/pyright_test.py",
|
||||
str(cases_path),
|
||||
|
||||
Reference in New Issue
Block a user