From 34c83d078753d84e285887c393ca7c8aeeb5eb59 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 16 Jul 2024 12:09:40 +0200 Subject: [PATCH 1/5] Extract VERSIONS parsing to _utils.py --- tests/_utils.py | 32 ++++++++++++++++++++++++-- tests/check_typeshed_structure.py | 26 +++++---------------- tests/mypy_test.py | 38 +++---------------------------- 3 files changed, 39 insertions(+), 57 deletions(-) diff --git a/tests/_utils.py b/tests/_utils.py index 45b5a5d987a7..722560a6ff7a 100644 --- a/tests/_utils.py +++ b/tests/_utils.py @@ -7,7 +7,8 @@ from collections.abc import Iterable, Mapping from functools import lru_cache from pathlib import Path -from typing import Any, Final, NamedTuple +from typing import Any, Final, NamedTuple, Tuple +from typing_extensions import TypeAlias import pathspec from packaging.requirements import Requirement @@ -106,8 +107,35 @@ def get_mypy_req() -> str: # Parsing the stdlib/VERSIONS file # ==================================================================== +VersionTuple: TypeAlias = Tuple[int, int] -VERSIONS_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_.]*): ([23]\.\d{1,2})-([23]\.\d{1,2})?$") + +VERSIONS_PATH = STDLIB_PATH / "VERSIONS" +VERSION_LINE_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_.]*): ([23]\.\d{1,2})-([23]\.\d{1,2})?$") +VERSION_RE = re.compile(r"^([23])\.(\d+)$") + + +def parse_stdlib_versions_file() -> dict[str, tuple[VersionTuple, VersionTuple]]: + result: dict[str, tuple[VersionTuple, VersionTuple]] = {} + with VERSIONS_PATH.open(encoding="UTF-8") as f: + for line in f: + line = strip_comments(line) + if line == "": + continue + m = VERSION_LINE_RE.match(line) + assert m, f"invalid VERSIONS line: {line}" + mod: str = m.group(1) + assert mod not in result, f"Duplicate module {mod} in VERSIONS" + min_version = _parse_version(m.group(2)) + max_version = _parse_version(m.group(3)) if m.group(3) else (99, 99) + result[mod] = min_version, max_version + return result + + +def _parse_version(v_str: str) -> tuple[int, int]: + m = VERSION_RE.match(v_str) + assert m, f"invalid version: {v_str}" + return int(m.group(1)), int(m.group(2)) # ==================================================================== diff --git a/tests/check_typeshed_structure.py b/tests/check_typeshed_structure.py index 8e56bcc21136..eea636680a0d 100755 --- a/tests/check_typeshed_structure.py +++ b/tests/check_typeshed_structure.py @@ -18,12 +18,11 @@ STDLIB_PATH, TEST_CASES_DIR, TESTS_DIR, - VERSIONS_RE, get_all_testcase_directories, get_gitignore_spec, parse_requirements, + parse_stdlib_versions_file, spec_matches_path, - strip_comments, tests_path, ) @@ -127,30 +126,17 @@ def check_no_symlinks() -> None: def check_versions_file() -> None: """Check that the stdlib/VERSIONS file has the correct format.""" - versions = list[str]() - with open("stdlib/VERSIONS", encoding="UTF-8") as f: - data = f.read().splitlines() - for line in data: - line = strip_comments(line) - if line == "": - continue - m = VERSIONS_RE.match(line) - if not m: - raise AssertionError(f"Bad line in VERSIONS: {line}") - module = m.group(1) - assert module not in versions, f"Duplicate module {module} in VERSIONS" - versions.append(module) - - deduped_versions = set(versions) - assert len(versions) == len(deduped_versions) + version_map = parse_stdlib_versions_file() + versions = list(version_map.keys()) + sorted_versions = sorted(versions) assert versions == sorted_versions, f"{versions=}\n\n{sorted_versions=}" modules = _find_stdlib_modules() # Sub-modules don't need to be listed in VERSIONS. - extra = {m.split(".")[0] for m in modules} - deduped_versions + extra = {m.split(".")[0] for m in modules} - version_map.keys() assert not extra, f"Modules not in versions: {extra}" - extra = deduped_versions - modules + extra = version_map.keys() - modules assert not extra, f"Versions not in modules: {extra}" diff --git a/tests/mypy_test.py b/tests/mypy_test.py index cafaca811926..d6120a577172 100755 --- a/tests/mypy_test.py +++ b/tests/mypy_test.py @@ -6,7 +6,6 @@ import argparse import concurrent.futures import os -import re import subprocess import sys import tempfile @@ -17,11 +16,7 @@ from itertools import product from pathlib import Path from threading import Lock -from typing import TYPE_CHECKING, Any, NamedTuple, Tuple - -if TYPE_CHECKING: - from _typeshed import StrPath - +from typing import Any, NamedTuple from typing_extensions import Annotated, TypeAlias import tomli @@ -30,14 +25,13 @@ from _utils import ( PYTHON_VERSION, TESTS_DIR, - VERSIONS_RE as VERSION_LINE_RE, colored, get_gitignore_spec, get_mypy_req, + parse_stdlib_versions_file, print_error, print_success_msg, spec_matches_path, - strip_comments, venv_python, ) @@ -53,7 +47,6 @@ DIRECTORIES_TO_TEST = [Path("stdlib"), Path("stubs")] VersionString: TypeAlias = Annotated[str, "Must be one of the entries in SUPPORTED_VERSIONS"] -VersionTuple: TypeAlias = Tuple[int, int] Platform: TypeAlias = Annotated[str, "Must be one of the entries in SUPPORTED_PLATFORMS"] @@ -150,31 +143,6 @@ def match(path: Path, args: TestConfig) -> bool: return False -def parse_versions(fname: StrPath) -> dict[str, tuple[VersionTuple, VersionTuple]]: - result: dict[str, tuple[VersionTuple, VersionTuple]] = {} - with open(fname, encoding="UTF-8") as f: - for line in f: - line = strip_comments(line) - if line == "": - continue - m = VERSION_LINE_RE.match(line) - assert m, f"invalid VERSIONS line: {line}" - mod: str = m.group(1) - min_version = parse_version(m.group(2)) - max_version = parse_version(m.group(3)) if m.group(3) else (99, 99) - result[mod] = min_version, max_version - return result - - -_VERSION_RE = re.compile(r"^([23])\.(\d+)$") - - -def parse_version(v_str: str) -> tuple[int, int]: - m = _VERSION_RE.match(v_str) - assert m, f"invalid version: {v_str}" - return int(m.group(1)), int(m.group(2)) - - 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.is_file() and module.suffix == ".pyi": @@ -365,7 +333,7 @@ def test_third_party_distribution( def test_stdlib(args: TestConfig) -> TestResult: files: list[Path] = [] stdlib = Path("stdlib") - supported_versions = parse_versions(stdlib / "VERSIONS") + supported_versions = parse_stdlib_versions_file() for name in os.listdir(stdlib): if name in ("VERSIONS", TESTS_DIR) or name.startswith("."): continue From ce270a9419ae950d1db169d9d183aedb00eb9658 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 16 Jul 2024 12:42:03 +0200 Subject: [PATCH 2/5] mypy_test: Exclude sub-modules not in current Py version --- tests/_utils.py | 4 ++-- tests/mypy_test.py | 47 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/tests/_utils.py b/tests/_utils.py index 722560a6ff7a..7fa2fab99ed8 100644 --- a/tests/_utils.py +++ b/tests/_utils.py @@ -108,14 +108,14 @@ def get_mypy_req() -> str: # ==================================================================== VersionTuple: TypeAlias = Tuple[int, int] - +SupportedVersionsDict: TypeAlias = dict[str, tuple[VersionTuple, VersionTuple]] VERSIONS_PATH = STDLIB_PATH / "VERSIONS" VERSION_LINE_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_.]*): ([23]\.\d{1,2})-([23]\.\d{1,2})?$") VERSION_RE = re.compile(r"^([23])\.(\d+)$") -def parse_stdlib_versions_file() -> dict[str, tuple[VersionTuple, VersionTuple]]: +def parse_stdlib_versions_file() -> SupportedVersionsDict: result: dict[str, tuple[VersionTuple, VersionTuple]] = {} with VERSIONS_PATH.open(encoding="UTF-8") as f: for line in f: diff --git a/tests/mypy_test.py b/tests/mypy_test.py index d6120a577172..a379da8953b8 100755 --- a/tests/mypy_test.py +++ b/tests/mypy_test.py @@ -24,7 +24,10 @@ from _metadata import PackageDependencies, get_recursive_requirements, read_metadata from _utils import ( PYTHON_VERSION, + STDLIB_PATH, TESTS_DIR, + SupportedVersionsDict, + VersionTuple, colored, get_gitignore_spec, get_mypy_req, @@ -332,15 +335,12 @@ def test_third_party_distribution( def test_stdlib(args: TestConfig) -> TestResult: files: list[Path] = [] - stdlib = Path("stdlib") - supported_versions = parse_stdlib_versions_file() - for name in os.listdir(stdlib): - if name in ("VERSIONS", TESTS_DIR) or name.startswith("."): + for file in STDLIB_PATH.iterdir(): + if file.name in ("VERSIONS", TESTS_DIR) or file.name.startswith("."): continue - module = Path(name).stem - module_min_version, module_max_version = supported_versions[module] - if module_min_version <= tuple(map(int, args.version.split("."))) <= module_max_version: - add_files(files, (stdlib / name), args) + add_files(files, file, args) + + files = remove_modules_not_in_python_version(files, args.version) if not files: return TestResult(MypyResult.SUCCESS, 0) @@ -351,6 +351,37 @@ def test_stdlib(args: TestConfig) -> TestResult: return TestResult(result, len(files)) +def remove_modules_not_in_python_version(paths: list[Path], py_version: VersionString) -> list[Path]: + py_version_tuple = tuple(map(int, py_version.split("."))) + module_versions = parse_stdlib_versions_file() + new_paths = [] + for path in paths: + if path.parts[0] != "stdlib" or path.suffix != ".pyi": + continue + module_name = stdlib_module_name_from_path(path) + min_version, max_version = supported_versions_for_module(module_versions, module_name) + if min_version <= py_version_tuple <= max_version: + new_paths.append(path) + return new_paths + + +def stdlib_module_name_from_path(path: Path) -> str: + assert path.parts[0] == "stdlib" + assert path.suffix == ".pyi" + parts = list(path.parts[1:-1]) + if path.parts[-1] != "__init__.pyi": + parts.append(path.parts[-1].removesuffix(".pyi")) + return ".".join(parts) + + +def supported_versions_for_module(module_versions: SupportedVersionsDict, module_name: str) -> tuple[VersionTuple, VersionTuple]: + while "." in module_name: + if module_name in module_versions: + return module_versions[module_name] + module_name = ".".join(module_name.split(".")[:-1]) + return module_versions[module_name] + + @dataclass class TestSummary: mypy_result: MypyResult = MypyResult.SUCCESS From 6529a9a29863b5a556d584a15503a2ee126ce5ac Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 16 Jul 2024 12:44:43 +0200 Subject: [PATCH 3/5] Py 3.8 compat --- tests/_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/_utils.py b/tests/_utils.py index 7fa2fab99ed8..fab6793f7ffe 100644 --- a/tests/_utils.py +++ b/tests/_utils.py @@ -7,7 +7,7 @@ from collections.abc import Iterable, Mapping from functools import lru_cache from pathlib import Path -from typing import Any, Final, NamedTuple, Tuple +from typing import Any, Dict, Final, NamedTuple, Tuple from typing_extensions import TypeAlias import pathspec @@ -108,7 +108,7 @@ def get_mypy_req() -> str: # ==================================================================== VersionTuple: TypeAlias = Tuple[int, int] -SupportedVersionsDict: TypeAlias = dict[str, tuple[VersionTuple, VersionTuple]] +SupportedVersionsDict: TypeAlias = Dict[str, Tuple[VersionTuple, VersionTuple]] VERSIONS_PATH = STDLIB_PATH / "VERSIONS" VERSION_LINE_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_.]*): ([23]\.\d{1,2})-([23]\.\d{1,2})?$") From 2efd9d0a3645bccaaf621259fdefb7286aeac001 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 16 Jul 2024 12:50:06 +0200 Subject: [PATCH 4/5] Python 3.8 compatibility --- tests/mypy_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/mypy_test.py b/tests/mypy_test.py index a379da8953b8..d963b95bda42 100755 --- a/tests/mypy_test.py +++ b/tests/mypy_test.py @@ -370,7 +370,8 @@ def stdlib_module_name_from_path(path: Path) -> str: assert path.suffix == ".pyi" parts = list(path.parts[1:-1]) if path.parts[-1] != "__init__.pyi": - parts.append(path.parts[-1].removesuffix(".pyi")) + # TODO: Python 3.9+: Use removesuffix. + parts.append(path.parts[-1][:-4]) return ".".join(parts) From 31efd8dbeb9951aa7abbd6641963aa29de15618f Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Tue, 16 Jul 2024 12:57:28 +0200 Subject: [PATCH 5/5] Add type annotation --- tests/mypy_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mypy_test.py b/tests/mypy_test.py index d963b95bda42..35a7fdab1153 100755 --- a/tests/mypy_test.py +++ b/tests/mypy_test.py @@ -354,7 +354,7 @@ def test_stdlib(args: TestConfig) -> TestResult: def remove_modules_not_in_python_version(paths: list[Path], py_version: VersionString) -> list[Path]: py_version_tuple = tuple(map(int, py_version.split("."))) module_versions = parse_stdlib_versions_file() - new_paths = [] + new_paths: list[Path] = [] for path in paths: if path.parts[0] != "stdlib" or path.suffix != ".pyi": continue