From 5eeeef875a4b20043dd9baec3abb6e580e7cf36d Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 12 Jul 2025 14:23:28 +0400 Subject: [PATCH 01/13] Support "Removal" PRs for stubsbot --- scripts/stubsabot.py | 158 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 154 insertions(+), 4 deletions(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index 1ce147b0be65..f3d10dc697f8 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -10,6 +10,7 @@ import io import os import re +import shutil import subprocess import sys import tarfile @@ -25,11 +26,13 @@ import aiohttp import packaging.version +import tomli import tomlkit from packaging.specifiers import Specifier from termcolor import colored +from tomlkit.items import String -from ts_utils.metadata import StubMetadata, read_metadata, update_metadata +from ts_utils.metadata import NoSuchStubError, StubMetadata, metadata_path, read_metadata, update_metadata from ts_utils.paths import STUBS_PATH, distribution_path TYPESHED_OWNER = "python" @@ -37,6 +40,8 @@ STUBSABOT_LABEL = "bot: stubsabot" +PYRIGHT_CONFIG = Path("pyrightconfig.stricter.json") + class ActionLevel(enum.IntEnum): def __new__(cls, value: int, doc: str) -> Self: @@ -149,6 +154,16 @@ def __str__(self) -> str: return f"Marking {self.distribution} as obsolete since {self.obsolete_since_version!r}" +@dataclass +class Remove: + distribution: str + reason: str + links: dict[str, str] + + def __str__(self) -> str: + return f"Removing {self.distribution} as {self.reason}" + + @dataclass class NoUpdate: distribution: str @@ -470,11 +485,102 @@ async def analyze_diff( return DiffAnalysis(py_files=py_files, py_files_stubbed_in_typeshed=py_files_stubbed_in_typeshed) -async def determine_action(distribution: str, session: aiohttp.ClientSession) -> Update | NoUpdate | Obsolete: +def obsolete_more_than_6_months(distribution: str) -> bool: + try: + with metadata_path(distribution).open("rb") as file: + data: dict[str, object] = tomli.load(file) + except FileNotFoundError: + raise NoSuchStubError(f"Typeshed has no stubs for {distribution!r}!") from None + + obsolete_since = data.get("obsolete_since") + if not obsolete_since: + return False + + assert type(obsolete_since) is String + comment: str | None = obsolete_since.trivia.comment + if not comment: + return False + + release_date_string = comment.removeprefix("# Released on ") + release_date = datetime.date.fromisoformat(release_date_string) + today = datetime.datetime.now(tz=datetime.timezone.utc).date() + + return release_date >= today + + +def parse_no_longer_updated_from_archive(source: zipfile.ZipFile | tarfile.TarFile) -> bool: + if isinstance(source, zipfile.ZipFile): + try: + with source.open("METADATA.toml", "r") as f: + toml_data: dict[str, object] = tomli.load(f) + except KeyError: + return False + else: + try: + tarinfo = source.getmember("METADATA.toml") + file = source.extractfile(tarinfo) + if file is None: + return False + with file as f: + toml_data: dict[str, object] = tomli.load(f) + except KeyError: + return False + + no_longer_updated = toml_data.get("no_longer_updated", False) + assert type(no_longer_updated) is bool + return bool(no_longer_updated) + + +async def has_no_longer_updated_release(release_to_download: PypiReleaseDownload, session: aiohttp.ClientSession) -> bool: + """ + Return `True` if the `no_longer_updated` field exists and the value is + `True` in the `METADATA.toml` file of latest `types-{distribution}` pypi release. + """ + async with session.get(release_to_download.url) as response: + body = io.BytesIO(await response.read()) + + packagetype = release_to_download.packagetype + if packagetype == "bdist_wheel": + assert release_to_download.filename.endswith(".whl") + with zipfile.ZipFile(body) as zf: + return parse_no_longer_updated_from_archive(zf) + elif packagetype == "sdist": + # sdist defaults to `.tar.gz` on Lunix and to `.zip` on Windows: + # https://docs.python.org/3.11/distutils/sourcedist.html + if release_to_download.filename.endswith(".tar.gz"): + with tarfile.open(fileobj=body, mode="r:gz") as zf: + return parse_no_longer_updated_from_archive(zf) + elif release_to_download.filename.endswith(".zip"): + with zipfile.ZipFile(body) as zf: + return parse_no_longer_updated_from_archive(zf) + else: + raise AssertionError(f"Package file {release_to_download.filename!r} does not end with '.tar.gz' or '.zip'") + else: + raise AssertionError(f"Unknown package type for {release_to_download.distribution}: {packagetype!r}") + + +async def determine_action(distribution: str, session: aiohttp.ClientSession) -> Update | NoUpdate | Obsolete | Remove: stub_info = read_metadata(distribution) if stub_info.is_obsolete: + if obsolete_more_than_6_months(stub_info.distribution): + pypi_info = await fetch_pypi_info(f"types-{stub_info.distribution}", session) + latest_release = pypi_info.get_latest_release() + links = { + "Typeshed release": f"{pypi_info.pypi_root}", + "Typeshed stubs": f"https://github.com/{TYPESHED_OWNER}/typeshed/tree/main/stubs/{stub_info.distribution}", + } + return Remove(stub_info.distribution, reason="older than 6 months", links=links) return NoUpdate(stub_info.distribution, "obsolete") if stub_info.no_longer_updated: + pypi_info = await fetch_pypi_info(f"types-{stub_info.distribution}", session) + latest_release = pypi_info.get_latest_release() + + if await has_no_longer_updated_release(latest_release, session): + links = { + "Typeshed release": f"{pypi_info.pypi_root}", + "Typeshed stubs": f"https://github.com/{TYPESHED_OWNER}/typeshed/tree/main/stubs/{stub_info.distribution}", + } + return Remove(stub_info.distribution, reason="no longer updated", links=links) return NoUpdate(stub_info.distribution, "no longer updated") pypi_info = await fetch_pypi_info(stub_info.distribution, session) @@ -683,6 +789,25 @@ def get_update_pr_body(update: Update, metadata: Mapping[str, Any]) -> str: return body +def remove_stubs(distribution: str) -> None: + stub_path = distribution_path(distribution) + target_path_prefix = f'"stubs/{distribution}' + + if stub_path.exists() and stub_path.is_dir(): + shutil.rmtree(stub_path) + + if not PYRIGHT_CONFIG.exists(): + return + + with PYRIGHT_CONFIG.open("r", encoding="UTF-8") as f: + lines = f.readlines() + + lines = [line for line in lines if not line.lstrip().startswith(target_path_prefix)] + + with PYRIGHT_CONFIG.open("w", encoding="UTF-8") as f: + f.writelines(lines) + + async def suggest_typeshed_update(update: Update, session: aiohttp.ClientSession, action_level: ActionLevel) -> None: if action_level <= ActionLevel.nothing: return @@ -729,6 +854,28 @@ async def suggest_typeshed_obsolete(obsolete: Obsolete, session: aiohttp.ClientS await create_or_update_pull_request(title=title, body=body, branch_name=branch_name, session=session) +async def suggest_typeshed_remove(remove: Remove, session: aiohttp.ClientSession, action_level: ActionLevel) -> None: + if action_level <= ActionLevel.nothing: + return + title = f"[stubsabot] Remove {remove.distribution} as {remove.reason}" + async with _repo_lock: + branch_name = f"{BRANCH_PREFIX}/{normalize(remove.distribution)}" + subprocess.check_call(["git", "checkout", "-B", branch_name, "origin/main"]) + remove_stubs(remove.distribution) + body = "\n".join(f"{k}: {v}" for k, v in remove.links.items()) + subprocess.check_call(["git", "commit", "--all", "-m", f"{title}\n\n{body}"]) + if action_level <= ActionLevel.local: + return + if not latest_commit_is_different_to_last_commit_on_origin(branch_name): + print(f"No pushing to origin required: origin/{branch_name} exists and requires no changes!") + return + somewhat_safe_force_push(branch_name) + if action_level <= ActionLevel.fork: + return + + await create_or_update_pull_request(title=title, body=body, branch_name=branch_name, session=session) + + async def main() -> None: parser = argparse.ArgumentParser() parser.add_argument( @@ -803,10 +950,13 @@ async def main() -> None: if isinstance(update, Update): await suggest_typeshed_update(update, session, action_level=args.action_level) continue - # Redundant, but keeping for extra runtime validation - if isinstance(update, Obsolete): # pyright: ignore[reportUnnecessaryIsInstance] + if isinstance(update, Obsolete): await suggest_typeshed_obsolete(update, session, action_level=args.action_level) continue + # Redundant, but keeping for extra runtime validation + if isinstance(update, Remove): # pyright: ignore[reportUnnecessaryIsInstance] + await suggest_typeshed_remove(update, session, action_level=args.action_level) + continue except RemoteConflictError as e: print(colored(f"... but ran into {type(e).__qualname__}: {e}", "red")) continue From b6abcd73e6474966421fd10c2296b091c7ae30b4 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 12 Jul 2025 14:52:42 +0400 Subject: [PATCH 02/13] Fix few issues --- scripts/stubsabot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index f3d10dc697f8..21eb65b1d890 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -488,7 +488,7 @@ async def analyze_diff( def obsolete_more_than_6_months(distribution: str) -> bool: try: with metadata_path(distribution).open("rb") as file: - data: dict[str, object] = tomli.load(file) + data = tomlkit.load(file) except FileNotFoundError: raise NoSuchStubError(f"Typeshed has no stubs for {distribution!r}!") from None @@ -511,8 +511,7 @@ def obsolete_more_than_6_months(distribution: str) -> bool: def parse_no_longer_updated_from_archive(source: zipfile.ZipFile | tarfile.TarFile) -> bool: if isinstance(source, zipfile.ZipFile): try: - with source.open("METADATA.toml", "r") as f: - toml_data: dict[str, object] = tomli.load(f) + file = source.open("METADATA.toml", "r") except KeyError: return False else: @@ -521,11 +520,12 @@ def parse_no_longer_updated_from_archive(source: zipfile.ZipFile | tarfile.TarFi file = source.extractfile(tarinfo) if file is None: return False - with file as f: - toml_data: dict[str, object] = tomli.load(f) except KeyError: return False + with file as f: + toml_data: dict[str, object] = tomli.load(f) + no_longer_updated = toml_data.get("no_longer_updated", False) assert type(no_longer_updated) is bool return bool(no_longer_updated) From f0595ba1cb29617bcc60ca12c86026a1ac4e82ad Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 12 Jul 2025 14:53:45 +0400 Subject: [PATCH 03/13] Correct getting attr from TOMLDocument --- scripts/stubsabot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index 21eb65b1d890..150ed52488b1 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -492,7 +492,7 @@ def obsolete_more_than_6_months(distribution: str) -> bool: except FileNotFoundError: raise NoSuchStubError(f"Typeshed has no stubs for {distribution!r}!") from None - obsolete_since = data.get("obsolete_since") + obsolete_since = data["obsolete_since"] if not obsolete_since: return False From 82c9e43be9e53db6cf7fc5367788d4240598a116 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Sat, 12 Jul 2025 15:01:36 +0400 Subject: [PATCH 04/13] Add type ignore comment --- scripts/stubsabot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index 150ed52488b1..f5add66df1be 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -517,7 +517,7 @@ def parse_no_longer_updated_from_archive(source: zipfile.ZipFile | tarfile.TarFi else: try: tarinfo = source.getmember("METADATA.toml") - file = source.extractfile(tarinfo) + file = source.extractfile(tarinfo) # type: ignore[assignment] if file is None: return False except KeyError: From 23df219d8094452f7019f701b34c56c2ee3b4652 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 14 Jul 2025 11:48:05 +0400 Subject: [PATCH 05/13] Correct `obsolete_more_than_6_months` check --- scripts/stubsabot.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index f5add66df1be..9ec5761e4202 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -3,6 +3,7 @@ import argparse import asyncio +import calendar import contextlib import datetime import enum @@ -41,6 +42,7 @@ STUBSABOT_LABEL = "bot: stubsabot" PYRIGHT_CONFIG = Path("pyrightconfig.stricter.json") +POLICY_MONTHS_DELTA = 6 class ActionLevel(enum.IntEnum): @@ -486,6 +488,13 @@ async def analyze_diff( def obsolete_more_than_6_months(distribution: str) -> bool: + def add_months(date: datetime.date, months: int) -> datetime.date: + month = date.month - 1 + months + year = date.year + month // 12 + month = month % 12 + 1 + day = min(date.day, calendar.monthrange(year, month)[1]) + return datetime.date(year, month, day) + try: with metadata_path(distribution).open("rb") as file: data = tomlkit.load(file) @@ -503,9 +512,10 @@ def obsolete_more_than_6_months(distribution: str) -> bool: release_date_string = comment.removeprefix("# Released on ") release_date = datetime.date.fromisoformat(release_date_string) + remove_date = add_months(release_date, POLICY_MONTHS_DELTA) today = datetime.datetime.now(tz=datetime.timezone.utc).date() - return release_date >= today + return remove_date >= today def parse_no_longer_updated_from_archive(source: zipfile.ZipFile | tarfile.TarFile) -> bool: From 8d2da4b286f9577f4fc3d62e427b60c5c7cb6a5d Mon Sep 17 00:00:00 2001 From: donBarbos Date: Mon, 14 Jul 2025 11:51:10 +0400 Subject: [PATCH 06/13] Move `_add_months` function --- scripts/stubsabot.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index 9ec5761e4202..3e735cf3afa8 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -487,14 +487,15 @@ async def analyze_diff( return DiffAnalysis(py_files=py_files, py_files_stubbed_in_typeshed=py_files_stubbed_in_typeshed) -def obsolete_more_than_6_months(distribution: str) -> bool: - def add_months(date: datetime.date, months: int) -> datetime.date: - month = date.month - 1 + months - year = date.year + month // 12 - month = month % 12 + 1 - day = min(date.day, calendar.monthrange(year, month)[1]) - return datetime.date(year, month, day) +def _add_months(date: datetime.date, months: int) -> datetime.date: + month = date.month - 1 + months + year = date.year + month // 12 + month = month % 12 + 1 + day = min(date.day, calendar.monthrange(year, month)[1]) + return datetime.date(year, month, day) + +def obsolete_more_than_6_months(distribution: str) -> bool: try: with metadata_path(distribution).open("rb") as file: data = tomlkit.load(file) @@ -512,7 +513,7 @@ def add_months(date: datetime.date, months: int) -> datetime.date: release_date_string = comment.removeprefix("# Released on ") release_date = datetime.date.fromisoformat(release_date_string) - remove_date = add_months(release_date, POLICY_MONTHS_DELTA) + remove_date = _add_months(release_date, POLICY_MONTHS_DELTA) today = datetime.datetime.now(tz=datetime.timezone.utc).date() return remove_date >= today From 2f8d16707222ced86fbe3c9376250db41d11d426 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 16 Jul 2025 13:23:48 +0400 Subject: [PATCH 07/13] Bring out common logic of archive unpacking --- scripts/stubsabot.py | 84 ++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index 3e735cf3afa8..5b7e7f0fcae9 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -18,11 +18,11 @@ import textwrap import urllib.parse import zipfile -from collections.abc import Iterator, Mapping, Sequence +from collections.abc import Callable, Iterator, Mapping, Sequence from dataclasses import dataclass, field from http import HTTPStatus from pathlib import Path -from typing import Annotated, Any, ClassVar, NamedTuple +from typing import Annotated, Any, ClassVar, NamedTuple, TypeVar from typing_extensions import Self, TypeAlias import aiohttp @@ -175,6 +175,38 @@ def __str__(self) -> str: return f"Skipping {self.distribution}: {self.reason}" +_T = TypeVar("_T") + + +async def with_extracted_archive( + release_to_download: PypiReleaseDownload, + *, + session: aiohttp.ClientSession, + handler: Callable[[zipfile.ZipFile | tarfile.TarFile], _T], +) -> _T: + async with session.get(release_to_download.url) as response: + body = io.BytesIO(await response.read()) + + packagetype = release_to_download.packagetype + if packagetype == "bdist_wheel": + assert release_to_download.filename.endswith(".whl") + with zipfile.ZipFile(body) as zf: + return handler(zf) + elif packagetype == "sdist": + # sdist defaults to `.tar.gz` on Lunix and to `.zip` on Windows: + # https://docs.python.org/3.11/distutils/sourcedist.html + if release_to_download.filename.endswith(".tar.gz"): + with tarfile.open(fileobj=body, mode="r:gz") as zf: + return handler(zf) + elif release_to_download.filename.endswith(".zip"): + with zipfile.ZipFile(body) as zf: + return handler(zf) + else: + raise AssertionError(f"Package file {release_to_download.filename!r} does not end with '.tar.gz' or '.zip'") + else: + raise AssertionError(f"Unknown package type for {release_to_download.distribution}: {packagetype!r}") + + def all_py_files_in_source_are_in_py_typed_dirs(source: zipfile.ZipFile | tarfile.TarFile) -> bool: py_typed_dirs: list[Path] = [] all_python_files: list[Path] = [] @@ -224,27 +256,7 @@ def all_py_files_in_source_are_in_py_typed_dirs(source: zipfile.ZipFile | tarfil async def release_contains_py_typed(release_to_download: PypiReleaseDownload, *, session: aiohttp.ClientSession) -> bool: - async with session.get(release_to_download.url) as response: - body = io.BytesIO(await response.read()) - - packagetype = release_to_download.packagetype - if packagetype == "bdist_wheel": - assert release_to_download.filename.endswith(".whl") - with zipfile.ZipFile(body) as zf: - return all_py_files_in_source_are_in_py_typed_dirs(zf) - elif packagetype == "sdist": - # sdist defaults to `.tar.gz` on Lunix and to `.zip` on Windows: - # https://docs.python.org/3.11/distutils/sourcedist.html - if release_to_download.filename.endswith(".tar.gz"): - with tarfile.open(fileobj=body, mode="r:gz") as zf: - return all_py_files_in_source_are_in_py_typed_dirs(zf) - elif release_to_download.filename.endswith(".zip"): - with zipfile.ZipFile(body) as zf: - return all_py_files_in_source_are_in_py_typed_dirs(zf) - else: - raise AssertionError(f"Package file {release_to_download.filename!r} does not end with '.tar.gz' or '.zip'") - else: - raise AssertionError(f"Unknown package type for {release_to_download.distribution}: {packagetype!r}") + return await with_extracted_archive(release_to_download, session=session, handler=all_py_files_in_source_are_in_py_typed_dirs) async def find_first_release_with_py_typed(pypi_info: PypiInfo, *, session: aiohttp.ClientSession) -> PypiReleaseDownload | None: @@ -542,32 +554,12 @@ def parse_no_longer_updated_from_archive(source: zipfile.ZipFile | tarfile.TarFi return bool(no_longer_updated) -async def has_no_longer_updated_release(release_to_download: PypiReleaseDownload, session: aiohttp.ClientSession) -> bool: +async def has_no_longer_updated_release(release_to_download: PypiReleaseDownload, *, session: aiohttp.ClientSession) -> bool: """ Return `True` if the `no_longer_updated` field exists and the value is `True` in the `METADATA.toml` file of latest `types-{distribution}` pypi release. """ - async with session.get(release_to_download.url) as response: - body = io.BytesIO(await response.read()) - - packagetype = release_to_download.packagetype - if packagetype == "bdist_wheel": - assert release_to_download.filename.endswith(".whl") - with zipfile.ZipFile(body) as zf: - return parse_no_longer_updated_from_archive(zf) - elif packagetype == "sdist": - # sdist defaults to `.tar.gz` on Lunix and to `.zip` on Windows: - # https://docs.python.org/3.11/distutils/sourcedist.html - if release_to_download.filename.endswith(".tar.gz"): - with tarfile.open(fileobj=body, mode="r:gz") as zf: - return parse_no_longer_updated_from_archive(zf) - elif release_to_download.filename.endswith(".zip"): - with zipfile.ZipFile(body) as zf: - return parse_no_longer_updated_from_archive(zf) - else: - raise AssertionError(f"Package file {release_to_download.filename!r} does not end with '.tar.gz' or '.zip'") - else: - raise AssertionError(f"Unknown package type for {release_to_download.distribution}: {packagetype!r}") + return await with_extracted_archive(release_to_download, session=session, handler=parse_no_longer_updated_from_archive) async def determine_action(distribution: str, session: aiohttp.ClientSession) -> Update | NoUpdate | Obsolete | Remove: @@ -586,7 +578,7 @@ async def determine_action(distribution: str, session: aiohttp.ClientSession) -> pypi_info = await fetch_pypi_info(f"types-{stub_info.distribution}", session) latest_release = pypi_info.get_latest_release() - if await has_no_longer_updated_release(latest_release, session): + if await has_no_longer_updated_release(latest_release, session=session): links = { "Typeshed release": f"{pypi_info.pypi_root}", "Typeshed stubs": f"https://github.com/{TYPESHED_OWNER}/typeshed/tree/main/stubs/{stub_info.distribution}", From 75ec979b9edf2fb30020380751a0e076b9ba0321 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 16 Jul 2025 14:58:45 +0400 Subject: [PATCH 08/13] Accept suggestions --- lib/ts_utils/paths.py | 1 + scripts/stubsabot.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/ts_utils/paths.py b/lib/ts_utils/paths.py index 568a091a5ae6..3a9a65e32181 100644 --- a/lib/ts_utils/paths.py +++ b/lib/ts_utils/paths.py @@ -12,6 +12,7 @@ PYPROJECT_PATH: Final = TS_BASE_PATH / "pyproject.toml" REQUIREMENTS_PATH: Final = TS_BASE_PATH / "requirements-tests.txt" GITIGNORE_PATH: Final = TS_BASE_PATH / ".gitignore" +PYRIGHT_CONFIG: Final = TS_BASE_PATH / "pyrightconfig.stricter.json" TESTS_DIR: Final = "@tests" TEST_CASES_DIR: Final = "test_cases" diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index 5b7e7f0fcae9..7b9e6ac3c591 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -34,14 +34,13 @@ from tomlkit.items import String from ts_utils.metadata import NoSuchStubError, StubMetadata, metadata_path, read_metadata, update_metadata -from ts_utils.paths import STUBS_PATH, distribution_path +from ts_utils.paths import PYRIGHT_CONFIG, STUBS_PATH, distribution_path TYPESHED_OWNER = "python" TYPESHED_API_URL = f"https://api.github.com/repos/{TYPESHED_OWNER}/typeshed" STUBSABOT_LABEL = "bot: stubsabot" -PYRIGHT_CONFIG = Path("pyrightconfig.stricter.json") POLICY_MONTHS_DELTA = 6 @@ -526,7 +525,7 @@ def obsolete_more_than_6_months(distribution: str) -> bool: release_date_string = comment.removeprefix("# Released on ") release_date = datetime.date.fromisoformat(release_date_string) remove_date = _add_months(release_date, POLICY_MONTHS_DELTA) - today = datetime.datetime.now(tz=datetime.timezone.utc).date() + today = datetime.datetime.today() # noqa: DTZ002 return remove_date >= today @@ -573,7 +572,8 @@ async def determine_action(distribution: str, session: aiohttp.ClientSession) -> "Typeshed stubs": f"https://github.com/{TYPESHED_OWNER}/typeshed/tree/main/stubs/{stub_info.distribution}", } return Remove(stub_info.distribution, reason="older than 6 months", links=links) - return NoUpdate(stub_info.distribution, "obsolete") + else: + return NoUpdate(stub_info.distribution, "obsolete") if stub_info.no_longer_updated: pypi_info = await fetch_pypi_info(f"types-{stub_info.distribution}", session) latest_release = pypi_info.get_latest_release() @@ -584,7 +584,8 @@ async def determine_action(distribution: str, session: aiohttp.ClientSession) -> "Typeshed stubs": f"https://github.com/{TYPESHED_OWNER}/typeshed/tree/main/stubs/{stub_info.distribution}", } return Remove(stub_info.distribution, reason="no longer updated", links=links) - return NoUpdate(stub_info.distribution, "no longer updated") + else: + return NoUpdate(stub_info.distribution, "no longer updated") pypi_info = await fetch_pypi_info(stub_info.distribution, session) latest_release = pypi_info.get_latest_release() @@ -799,9 +800,6 @@ def remove_stubs(distribution: str) -> None: if stub_path.exists() and stub_path.is_dir(): shutil.rmtree(stub_path) - if not PYRIGHT_CONFIG.exists(): - return - with PYRIGHT_CONFIG.open("r", encoding="UTF-8") as f: lines = f.readlines() From f61c60dce493dffeb371c89c954477750a199162 Mon Sep 17 00:00:00 2001 From: Semyon Moroz Date: Wed, 16 Jul 2025 11:03:47 +0000 Subject: [PATCH 09/13] Update stubsabot.py Co-authored-by: Sebastian Rittau --- scripts/stubsabot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index 7b9e6ac3c591..026d6e013c2f 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -525,7 +525,7 @@ def obsolete_more_than_6_months(distribution: str) -> bool: release_date_string = comment.removeprefix("# Released on ") release_date = datetime.date.fromisoformat(release_date_string) remove_date = _add_months(release_date, POLICY_MONTHS_DELTA) - today = datetime.datetime.today() # noqa: DTZ002 + today = datetime.date.today() # noqa: DTZ002 return remove_date >= today From edf7aa4ff78092baf15bfcf863a31d9316664121 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:05:46 +0000 Subject: [PATCH 10/13] [pre-commit.ci] auto fixes from pre-commit.com hooks --- scripts/stubsabot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index 026d6e013c2f..ec670db7ce2f 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -525,7 +525,7 @@ def obsolete_more_than_6_months(distribution: str) -> bool: release_date_string = comment.removeprefix("# Released on ") release_date = datetime.date.fromisoformat(release_date_string) remove_date = _add_months(release_date, POLICY_MONTHS_DELTA) - today = datetime.date.today() # noqa: DTZ002 + today = datetime.date.today() return remove_date >= today From 0416e666b73d747c919e909c93c538ae5de3d969 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 16 Jul 2025 15:33:32 +0400 Subject: [PATCH 11/13] Use `datetime.datetime.now` --- scripts/stubsabot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index ec670db7ce2f..db74f6e71e4b 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -525,7 +525,7 @@ def obsolete_more_than_6_months(distribution: str) -> bool: release_date_string = comment.removeprefix("# Released on ") release_date = datetime.date.fromisoformat(release_date_string) remove_date = _add_months(release_date, POLICY_MONTHS_DELTA) - today = datetime.date.today() + today = datetime.datetime.now(tz=datetime.UTC).date() return remove_date >= today From 6cf25e0f90be130eb08c8ba80f51625b74336848 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 16 Jul 2025 15:35:30 +0400 Subject: [PATCH 12/13] Use new const --- scripts/create_baseline_stubs.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/create_baseline_stubs.py b/scripts/create_baseline_stubs.py index db4fd9f3bf08..2928bb943b6e 100755 --- a/scripts/create_baseline_stubs.py +++ b/scripts/create_baseline_stubs.py @@ -23,9 +23,7 @@ import aiohttp import termcolor -from ts_utils.paths import STDLIB_PATH, STUBS_PATH - -PYRIGHT_CONFIG = Path("pyrightconfig.stricter.json") +from ts_utils.paths import PYRIGHT_CONFIG, STDLIB_PATH, STUBS_PATH def search_pip_freeze_output(project: str, output: str) -> tuple[str, str] | None: From 6542f43c0ea6bd30347c7261a112512d481b971c Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 16 Jul 2025 15:38:03 +0400 Subject: [PATCH 13/13] Use old UTC call style --- scripts/stubsabot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/stubsabot.py b/scripts/stubsabot.py index db74f6e71e4b..49d038b7ab3c 100755 --- a/scripts/stubsabot.py +++ b/scripts/stubsabot.py @@ -525,7 +525,7 @@ def obsolete_more_than_6_months(distribution: str) -> bool: release_date_string = comment.removeprefix("# Released on ") release_date = datetime.date.fromisoformat(release_date_string) remove_date = _add_months(release_date, POLICY_MONTHS_DELTA) - today = datetime.datetime.now(tz=datetime.UTC).date() + today = datetime.datetime.now(tz=datetime.timezone.utc).date() return remove_date >= today