Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ select = [

[tool.typeshed]
pyright_version = "1.1.328"
oldest_supported_python = "3.7"
39 changes: 30 additions & 9 deletions tests/mypy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import tomli

from parse_metadata import PackageDependencies, get_recursive_requirements
from parse_metadata import PYTHON_VERSION, PackageDependencies, get_recursive_requirements, read_metadata
from utils import (
VERSIONS_RE as VERSION_LINE_RE,
VenvInfo,
Expand Down Expand Up @@ -307,6 +307,7 @@ def add_third_party_files(
class TestResults(NamedTuple):
exit_code: int
files_checked: int
packages_skipped: int = 0


def test_third_party_distribution(
Expand Down Expand Up @@ -471,6 +472,7 @@ def setup_virtual_environments(distributions: dict[str, PackageDependencies], ar
def test_third_party_stubs(code: int, args: TestConfig, tempdir: Path) -> TestResults:
print("Testing third-party packages...")
files_checked = 0
packages_skipped = 0
gitignore_spec = get_gitignore_spec()
distributions_to_check: dict[str, PackageDependencies] = {}

Expand All @@ -480,6 +482,18 @@ def test_third_party_stubs(code: int, args: TestConfig, tempdir: Path) -> TestRe
if spec_matches_path(gitignore_spec, distribution_path):
continue

metadata = read_metadata(distribution)
if not metadata.requires_python.contains(PYTHON_VERSION):
msg = f"skipping {distribution!r} on Python {PYTHON_VERSION} (requires Python {metadata.requires_python})"
print(colored(msg, "yellow"))
packages_skipped += 1
continue
if not metadata.requires_python.contains(args.version):
msg = f"skipping {distribution!r} for target Python {args.version} (requires Python {metadata.requires_python})"
print(colored(msg, "yellow"))
packages_skipped += 1
continue

if (
distribution_path in args.filter
or Path("stubs") in args.filter
Expand All @@ -497,30 +511,32 @@ def test_third_party_stubs(code: int, args: TestConfig, tempdir: Path) -> TestRe

for distribution, venv_info in _DISTRIBUTION_TO_VENV_MAPPING.items():
non_types_dependencies = venv_info.python_exe != sys.executable
this_code, checked = test_third_party_distribution(
this_code, checked, _ = test_third_party_distribution(
distribution, args, venv_info=venv_info, non_types_dependencies=non_types_dependencies
)
code = max(code, this_code)
files_checked += checked

return TestResults(code, files_checked)
return TestResults(code, files_checked, packages_skipped)


def test_typeshed(code: int, args: TestConfig, tempdir: Path) -> TestResults:
print(f"*** Testing Python {args.version} on {args.platform}")
files_checked_this_version = 0
packages_skipped_this_version = 0
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)
code, stdlib_files_checked, _ = test_stdlib(code, args)
files_checked_this_version += stdlib_files_checked
print()

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, tempdir)
code, third_party_files_checked, third_party_packages_skipped = test_third_party_stubs(code, args, tempdir)
files_checked_this_version += third_party_files_checked
packages_skipped_this_version = third_party_packages_skipped
print()

return TestResults(code, files_checked_this_version)
return TestResults(code, files_checked_this_version, packages_skipped_this_version)


def main() -> None:
Expand All @@ -531,19 +547,24 @@ def main() -> None:
exclude = args.exclude or []
code = 0
total_files_checked = 0
total_packages_skipped = 0
with tempfile.TemporaryDirectory() as td:
td_path = Path(td)
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, tempdir=td_path)
code, files_checked_this_version, packages_skipped_this_version = test_typeshed(code, args=config, tempdir=td_path)
total_files_checked += files_checked_this_version
total_packages_skipped += packages_skipped_this_version
if code:
print_error(f"--- exit status {code}, {total_files_checked} files checked ---")
sys.exit(code)
if not total_files_checked:
if total_packages_skipped:
print(colored(f"--- {total_packages_skipped} packages skipped ---", "yellow"))
elif not total_files_checked:
print_error("--- nothing to do; exit 1 ---")
sys.exit(1)
print(colored(f"--- success, {total_files_checked} files checked ---", "green"))
if total_files_checked:
print(colored(f"--- success, {total_files_checked} files checked ---", "green"))


if __name__ == "__main__":
Expand Down
29 changes: 29 additions & 0 deletions tests/parse_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import os
import re
import sys
import urllib.parse
from collections.abc import Mapping
from dataclasses import dataclass
Expand All @@ -15,6 +16,7 @@

import tomli
from packaging.requirements import Requirement
from packaging.specifiers import Specifier
from packaging.version import Version

from utils import cache
Expand All @@ -28,8 +30,10 @@
"read_dependencies",
"read_metadata",
"read_stubtest_settings",
"PYTHON_VERSION",
]

PYTHON_VERSION: Final = f"{sys.version_info.major}.{sys.version_info.minor}"

_STUBTEST_PLATFORM_MAPPING: Final = {"linux": "apt_dependencies", "darwin": "brew_dependencies", "win32": "choco_dependencies"}
# Some older websites have a bad pattern of using query params for navigation.
Expand All @@ -40,6 +44,14 @@ def _is_list_of_strings(obj: object) -> TypeGuard[list[str]]:
return isinstance(obj, list) and all(isinstance(item, str) for item in obj)


@cache
def _get_oldest_supported_python() -> str:
with open("pyproject.toml", "rb") as config:
val = tomli.load(config)["tool"]["typeshed"]["oldest_supported_python"]
assert type(val) is str
return val


@final
@dataclass(frozen=True)
class StubtestSettings:
Expand Down Expand Up @@ -130,6 +142,7 @@ class StubMetadata:
uploaded_to_pypi: Annotated[bool, "Whether or not a distribution is uploaded to PyPI"]
partial_stub: Annotated[bool, "Whether this is a partial type stub package as per PEP 561."]
stubtest_settings: StubtestSettings
requires_python: Annotated[Specifier, "Versions of Python supported by the stub package"]


_KNOWN_METADATA_FIELDS: Final = frozenset(
Expand All @@ -144,6 +157,7 @@ class StubMetadata:
"upload",
"tool",
"partial_stub",
"requires_python",
}
)
_KNOWN_METADATA_TOOL_FIELDS: Final = {
Expand Down Expand Up @@ -240,6 +254,20 @@ def read_metadata(distribution: str) -> StubMetadata:
assert type(uploaded_to_pypi) is bool
partial_stub: object = data.get("partial_stub", True)
assert type(partial_stub) is bool
requires_python_str: object = data.get("requires_python")
oldest_supported_python = _get_oldest_supported_python()
oldest_supported_python_specifier = Specifier(f">={oldest_supported_python}")
if requires_python_str is None:
requires_python = oldest_supported_python_specifier
else:
assert isinstance(requires_python_str, str)
requires_python = Specifier(requires_python_str)
assert requires_python != oldest_supported_python_specifier, f'requires_python="{requires_python}" is redundant'
# Check minimum Python version is not less than the oldest version of Python supported by typeshed
assert oldest_supported_python_specifier.contains(
requires_python.version
), f"'requires_python' contains versions lower than the oldest supported Python ({oldest_supported_python})"
assert requires_python.operator == ">=", "'requires_python' should be a minimum version specifier, use '>=3.x'"

empty_tools: dict[object, object] = {}
tools_settings: object = data.get("tool", empty_tools)
Expand All @@ -262,6 +290,7 @@ def read_metadata(distribution: str) -> StubMetadata:
uploaded_to_pypi=uploaded_to_pypi,
partial_stub=partial_stub,
stubtest_settings=read_stubtest_settings(distribution),
requires_python=requires_python,
)


Expand Down
18 changes: 16 additions & 2 deletions tests/regr_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from pathlib import Path
from typing_extensions import TypeAlias

from parse_metadata import get_recursive_requirements
from parse_metadata import PYTHON_VERSION, get_recursive_requirements, read_metadata
from utils import (
PackageInfo,
VenvInfo,
Expand Down Expand Up @@ -250,13 +250,27 @@ def main() -> ReturnCode:
platforms_to_test, versions_to_test = SUPPORTED_PLATFORMS, SUPPORTED_VERSIONS
else:
platforms_to_test = args.platforms_to_test or [sys.platform]
versions_to_test = args.versions_to_test or [f"3.{sys.version_info[1]}"]
versions_to_test = args.versions_to_test or [PYTHON_VERSION]

code = 0
for testcase_dir in testcase_directories:
assert isinstance(testcase_dir, PackageInfo)
metadata = None
if not testcase_dir.is_stdlib:
metadata = read_metadata(testcase_dir.name)
if not metadata.requires_python.contains(PYTHON_VERSION):
msg = f"skipping {testcase_dir.name!r} on Python {PYTHON_VERSION} (requires Python {metadata.requires_python})"
print(colored(msg, "yellow"))
continue
with tempfile.TemporaryDirectory() as td:
tempdir = Path(td)
for platform, version in product(platforms_to_test, versions_to_test):
if not testcase_dir.is_stdlib:
assert metadata is not None
if not metadata.requires_python.contains(version):
msg = f"skipping {testcase_dir.name!r} for target Python {version} (requires Python {metadata.requires_python})"
print(colored(msg, "yellow"))
continue
this_code = test_testcase_directory(testcase_dir, version, platform, verbosity=verbosity, tempdir=tempdir)
code = max(code, this_code)
if code:
Expand Down
6 changes: 5 additions & 1 deletion tests/stubtest_third_party.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from textwrap import dedent
from typing import NoReturn

from parse_metadata import NoSuchStubError, get_recursive_requirements, read_metadata
from parse_metadata import PYTHON_VERSION, NoSuchStubError, get_recursive_requirements, read_metadata
from utils import colored, get_mypy_req, make_venv, print_error, print_success_msg


Expand All @@ -37,6 +37,10 @@ def run_stubtest(
return True
print(colored(f"Note: {dist_name} is not currently tested on {sys.platform} in typeshed's CI.", "yellow"))

if not metadata.requires_python.contains(PYTHON_VERSION):
print(colored(f"skipping (requires Python {metadata.requires_python})", "yellow"))
return True

with tempfile.TemporaryDirectory() as tmp:
venv_dir = Path(tmp)
try:
Expand Down