Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions tests/check_consistent.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import Version
from utils import get_all_testcase_directories
from utils import VERSIONS_RE, get_all_testcase_directories, strip_comments

metadata_keys = {"version", "requires", "extra_description", "obsolete_since", "no_longer_updated", "tool"}
tool_keys = {"stubtest": {"skip", "apt_dependencies", "extras", "ignore_missing_stub"}}
Expand Down Expand Up @@ -85,13 +85,6 @@ def check_no_symlinks() -> None:
raise ValueError(no_symlink.format(file))


_VERSIONS_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_.]*): [23]\.\d{1,2}-(?:[23]\.\d{1,2})?$")


def strip_comments(text: str) -> str:
return text.split("#")[0].strip()


def check_versions() -> None:
versions = set()
with open("stdlib/VERSIONS") as f:
Expand All @@ -100,7 +93,7 @@ def check_versions() -> None:
line = strip_comments(line)
if line == "":
continue
m = _VERSIONS_RE.match(line)
m = VERSIONS_RE.match(line)
if not m:
raise AssertionError(f"Bad line in VERSIONS: {line}")
module = m.group(1)
Expand Down
151 changes: 71 additions & 80 deletions tests/mypy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,53 +21,55 @@
from typing_extensions import Annotated, TypeAlias

import tomli
from utils import colored, print_error, print_success_msg, read_dependencies
from utils import VERSIONS_RE as VERSION_LINE_RE, colored, print_error, print_success_msg, read_dependencies, strip_comments

SUPPORTED_VERSIONS = [(3, 11), (3, 10), (3, 9), (3, 8), (3, 7)]
SUPPORTED_VERSIONS = ["3.11", "3.10", "3.9", "3.8", "3.7"]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to centralize this in utils.py? It would be nice to minimize the number of places we have to change when 3.12 comes around or when we drop 3.7.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, definitely. I'd like to do something like that in a follow-up PR, though — I have a few more ideas about things I'd like to do in that vein :)

SUPPORTED_PLATFORMS = ("linux", "win32", "darwin")
TYPESHED_DIRECTORIES = frozenset({"stdlib", "stubs"})
DIRECTORIES_TO_TEST = [Path("stdlib"), Path("stubs")]

ReturnCode: TypeAlias = int
MajorVersion: TypeAlias = int
MinorVersion: TypeAlias = int
MinVersion: TypeAlias = tuple[MajorVersion, MinorVersion]
MaxVersion: TypeAlias = tuple[MajorVersion, MinorVersion]
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"]
Directory: TypeAlias = Annotated[str, "Must be one of the entries in TYPESHED_DIRECTORIES"]


def python_version(arg: str) -> tuple[MajorVersion, MinorVersion]:
version = tuple(map(int, arg.split("."))) # This will naturally raise TypeError if it's not in the form "{major}.{minor}"
if version not in SUPPORTED_VERSIONS:
raise ValueError
# mypy infers the return type as tuple[int, ...]
return version # type: ignore[return-value]


class CommandLineArgs(argparse.Namespace):
verbose: int
exclude: list[str] | None
python_version: list[tuple[MajorVersion, MinorVersion]] | None
dir: list[Directory] | None
filter: list[Path]
exclude: list[Path] | None
python_version: list[VersionString] | None
platform: list[Platform] | None
filter: list[str]


def valid_path(cmd_arg: str) -> Path:
"""Helper function for argument-parsing"""
path = Path(cmd_arg)
if not path.exists():
raise argparse.ArgumentTypeError(f'"{path}" does not exist in typeshed!')
if not (path in DIRECTORIES_TO_TEST or any(directory in path.parents for directory in DIRECTORIES_TO_TEST)):
raise argparse.ArgumentTypeError('mypy_test.py only tests the stubs found in the "stdlib" and "stubs" directories')
return path


parser = argparse.ArgumentParser(
description="Typecheck typeshed's stubs with mypy. Patterns are unanchored regexps on the full path."
)
parser.add_argument("-v", "--verbose", action="count", default=0, help="More output")
parser.add_argument("-x", "--exclude", type=str, nargs="*", help="Exclude pattern")
parser.add_argument(
"-p", "--python-version", type=python_version, nargs="*", action="extend", help="These versions only (major[.minor])"
"filter",
type=valid_path,
nargs="*",
help='Test these files and directories (defaults to all files in the "stdlib" and "stubs" directories)',
)
parser.add_argument("-x", "--exclude", type=valid_path, nargs="*", help="Exclude these files and directories")
parser.add_argument("-v", "--verbose", action="count", default=0, help="More output")
parser.add_argument(
"-d",
"--dir",
choices=TYPESHED_DIRECTORIES,
"-p",
"--python-version",
type=str,
choices=SUPPORTED_VERSIONS,
nargs="*",
action="extend",
help="Test only these top-level typeshed directories (defaults to all typeshed directories)",
help="These versions only (major[.minor])",
)
parser.add_argument(
"--platform",
Expand All @@ -76,20 +78,17 @@ class CommandLineArgs(argparse.Namespace):
action="extend",
help="Run mypy for certain OS platforms (defaults to sys.platform only)",
)
parser.add_argument("filter", type=str, nargs="*", help="Include pattern (default all)")


@dataclass
class TestConfig:
"""Configuration settings for a single run of the `test_typeshed` function."""

verbose: int
exclude: list[str] | None
major: MajorVersion
minor: MinorVersion
directories: frozenset[Directory]
filter: list[Path]
exclude: list[Path]
version: VersionString
platform: Platform
filter: list[str]


def log(args: TestConfig, *varargs: object) -> None:
Expand All @@ -98,39 +97,35 @@ def log(args: TestConfig, *varargs: object) -> None:


def match(path: Path, args: TestConfig) -> bool:
fn = str(path)
if not args.filter and not args.exclude:
log(args, fn, "accept by default")
return True
if args.exclude:
for f in args.exclude:
if re.search(f, fn):
log(args, fn, "excluded by pattern", f)
return False
if args.filter:
for f in args.filter:
if re.search(f, fn):
log(args, fn, "accepted by pattern", f)
return True
if args.filter:
log(args, fn, "rejected (no pattern matches)")
return False
log(args, fn, "accepted (no exclude pattern matches)")
return True


_VERSION_LINE_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_.]*): ([23]\.\d{1,2})-([23]\.\d{1,2})?$")


def parse_versions(fname: StrPath) -> dict[str, tuple[MinVersion, MaxVersion]]:
for excluded_path in args.exclude:
if path == excluded_path:
log(args, path, "explicitly excluded")
return False
if excluded_path in path.parents:
log(args, path, f'is in an explicitly excluded directory "{excluded_path}"')
return False
for included_path in args.filter:
if path == included_path:
log(args, path, "was explicitly included")
return True
if included_path in path.parents:
log(args, path, f'is in an explicitly included directory "{included_path}"')
return True
log_msg = (
f'is implicitly excluded: was not in any of the directories or paths specified on the command line: "{args.filter!r}"'
)
log(args, path, log_msg)
return False


def parse_versions(fname: StrPath) -> dict[str, tuple[VersionTuple, VersionTuple]]:
result = {}
with open(fname) as f:
for line in f:
# Allow having some comments or empty lines.
line = line.split("#")[0].strip()
line = strip_comments(line)
if line == "":
continue
m = _VERSION_LINE_RE.match(line)
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))
Expand All @@ -151,10 +146,11 @@ def parse_version(v_str: str) -> tuple[int, int]:
def add_files(files: list[Path], seen: set[str], 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":
files.append(module)
seen.add(module.stem)
if match(module, args):
files.append(module)
seen.add(module.stem)
else:
to_add = sorted(module.rglob("*.pyi"))
to_add = sorted(file for file in module.rglob("*.pyi") if match(file, args))
files.extend(to_add)
seen.update(path.stem for path in to_add)

Expand Down Expand Up @@ -239,7 +235,7 @@ def run_mypy(args: TestConfig, configurations: list[MypyDistConf], files: list[P
def get_mypy_flags(args: TestConfig, temp_name: str) -> list[str]:
return [
"--python-version",
f"{args.major}.{args.minor}",
args.version,
"--show-traceback",
"--warn-incomplete-stub",
"--show-error-codes",
Expand Down Expand Up @@ -323,7 +319,8 @@ def test_stdlib(code: int, args: TestConfig) -> TestResults:
if name == "VERSIONS" or name.startswith("."):
continue
module = Path(name).stem
if supported_versions[module][0] <= (args.major, args.minor) <= supported_versions[module][1]:
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, seen, (stdlib / name), args)

if files:
Expand Down Expand Up @@ -354,14 +351,15 @@ def test_third_party_stubs(code: int, args: TestConfig) -> TestResults:


def test_typeshed(code: int, args: TestConfig) -> TestResults:
print(f"*** Testing Python {args.major}.{args.minor} on {args.platform}")
print(f"*** Testing Python {args.version} on {args.platform}")
files_checked_this_version = 0
if "stdlib" in args.directories:
stdlib_dir, stubs_dir = Path("stdlib"), Path("stubs")
if stdlib_dir in args.filter or any(stdlib_dir in path.parents for path in args.filter):
code, stdlib_files_checked = test_stdlib(code, args)
files_checked_this_version += stdlib_files_checked
print()

if "stubs" in args.directories:
if stubs_dir in args.filter or any(stubs_dir in path.parents for path in args.filter):
code, third_party_files_checked = test_third_party_stubs(code, args)
files_checked_this_version += third_party_files_checked
print()
Expand All @@ -373,19 +371,12 @@ def main() -> None:
args = parser.parse_args(namespace=CommandLineArgs())
versions = args.python_version or SUPPORTED_VERSIONS
platforms = args.platform or [sys.platform]
tested_directories = frozenset(args.dir) if args.dir else TYPESHED_DIRECTORIES
filter = args.filter or DIRECTORIES_TO_TEST
exclude = args.exclude or []
code = 0
total_files_checked = 0
for (major, minor), platform in product(versions, platforms):
config = TestConfig(
verbose=args.verbose,
exclude=args.exclude,
major=major,
minor=minor,
directories=tested_directories,
platform=platform,
filter=args.filter,
)
for version, platform in product(versions, platforms):
config = TestConfig(args.verbose, filter, exclude, version, platform)
code, files_checked_this_version = test_typeshed(code, args=config)
total_files_checked += files_checked_this_version
if code:
Expand Down
14 changes: 14 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
"""Utilities that are imported by multiple scripts in the tests directory."""

import os
import re
from functools import cache
from pathlib import Path
from typing import NamedTuple

import tomli


def strip_comments(text: str) -> str:
return text.split("#")[0].strip()


try:
from termcolor import colored as colored
except ImportError:
Expand Down Expand Up @@ -46,6 +52,14 @@ def read_dependencies(distribution: str) -> tuple[str, ...]:
return tuple(dependencies)


# ====================================================================
# Parsing the stdlib/VERSIONS file
# ====================================================================


VERSIONS_RE = re.compile(r"^([a-zA-Z_][a-zA-Z0-9_.]*): ([23]\.\d{1,2})-([23]\.\d{1,2})?$")


# ====================================================================
# Getting test-case directories from package names
# ====================================================================
Expand Down