Skip to content

Commit 7cf443e

Browse files
authored
[stubsabot] Move obsolete data to ts_utils.metadata (#14422)
1 parent d57b2fb commit 7cf443e

File tree

2 files changed

+39
-39
lines changed

2 files changed

+39
-39
lines changed

lib/ts_utils/metadata.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from __future__ import annotations
77

8+
import datetime
89
import functools
910
import re
1011
import urllib.parse
@@ -18,6 +19,7 @@
1819
import tomlkit
1920
from packaging.requirements import Requirement
2021
from packaging.specifiers import Specifier
22+
from tomlkit.items import String
2123

2224
from .paths import PYPROJECT_PATH, STUBS_PATH, distribution_path
2325

@@ -140,6 +142,13 @@ def read_stubtest_settings(distribution: str) -> StubtestSettings:
140142
)
141143

142144

145+
@final
146+
@dataclass(frozen=True)
147+
class ObsoleteMetadata:
148+
since_version: Annotated[str, "A string representing a specific version"]
149+
since_date: Annotated[datetime.date, "A date when the package became obsolete"]
150+
151+
143152
@final
144153
@dataclass(frozen=True)
145154
class StubMetadata:
@@ -154,7 +163,7 @@ class StubMetadata:
154163
extra_description: str | None
155164
stub_distribution: Annotated[str, "The name under which the distribution is uploaded to PyPI"]
156165
upstream_repository: Annotated[str, "The URL of the upstream repository"] | None
157-
obsolete_since: Annotated[str, "A string representing a specific version"] | None
166+
obsolete: Annotated[ObsoleteMetadata, "Metadata indicating when the stubs package became obsolete"] | None
158167
no_longer_updated: bool
159168
uploaded_to_pypi: Annotated[bool, "Whether or not a distribution is uploaded to PyPI"]
160169
partial_stub: Annotated[bool, "Whether this is a partial type stub package as per PEP 561."]
@@ -163,7 +172,7 @@ class StubMetadata:
163172

164173
@property
165174
def is_obsolete(self) -> bool:
166-
return self.obsolete_since is not None
175+
return self.obsolete is not None
167176

168177

169178
_KNOWN_METADATA_FIELDS: Final = frozenset(
@@ -214,27 +223,27 @@ def read_metadata(distribution: str) -> StubMetadata:
214223
"""
215224
try:
216225
with metadata_path(distribution).open("rb") as f:
217-
data: dict[str, object] = tomli.load(f)
226+
data = tomlkit.load(f)
218227
except FileNotFoundError:
219228
raise NoSuchStubError(f"Typeshed has no stubs for {distribution!r}!") from None
220229

221230
unknown_metadata_fields = data.keys() - _KNOWN_METADATA_FIELDS
222231
assert not unknown_metadata_fields, f"Unexpected keys in METADATA.toml for {distribution!r}: {unknown_metadata_fields}"
223232

224233
assert "version" in data, f"Missing 'version' field in METADATA.toml for {distribution!r}"
225-
version = data["version"]
234+
version: object = data.get("version") # pyright: ignore[reportUnknownMemberType]
226235
assert isinstance(version, str) and len(version) > 0, f"Invalid 'version' field in METADATA.toml for {distribution!r}"
227236
# Check that the version spec parses
228237
if version[0].isdigit():
229238
version = f"=={version}"
230239
version_spec = Specifier(version)
231240
assert version_spec.operator in {"==", "~="}, f"Invalid 'version' field in METADATA.toml for {distribution!r}"
232241

233-
requires_s: object = data.get("requires", [])
242+
requires_s: object = data.get("requires", []) # pyright: ignore[reportUnknownMemberType]
234243
assert isinstance(requires_s, list)
235244
requires = [parse_requires(distribution, req) for req in requires_s]
236245

237-
extra_description: object = data.get("extra_description")
246+
extra_description: object = data.get("extra_description") # pyright: ignore[reportUnknownMemberType]
238247
assert isinstance(extra_description, (str, type(None)))
239248

240249
if "stub_distribution" in data:
@@ -244,7 +253,7 @@ def read_metadata(distribution: str) -> StubMetadata:
244253
else:
245254
stub_distribution = f"types-{distribution}"
246255

247-
upstream_repository: object = data.get("upstream_repository")
256+
upstream_repository: object = data.get("upstream_repository") # pyright: ignore[reportUnknownMemberType]
248257
assert isinstance(upstream_repository, (str, type(None)))
249258
if isinstance(upstream_repository, str):
250259
parsed_url = urllib.parse.urlsplit(upstream_repository)
@@ -268,21 +277,28 @@ def read_metadata(distribution: str) -> StubMetadata:
268277
)
269278
assert num_url_path_parts == 2, bad_github_url_msg
270279

271-
obsolete_since: object = data.get("obsolete_since")
272-
assert isinstance(obsolete_since, (str, type(None)))
273-
no_longer_updated: object = data.get("no_longer_updated", False)
280+
obsolete_since: object = data.get("obsolete_since") # pyright: ignore[reportUnknownMemberType]
281+
assert isinstance(obsolete_since, (String, type(None)))
282+
if obsolete_since:
283+
comment = obsolete_since.trivia.comment
284+
since_date_string = comment.removeprefix("# Released on ")
285+
since_date = datetime.date.fromisoformat(since_date_string)
286+
obsolete = ObsoleteMetadata(since_version=obsolete_since, since_date=since_date)
287+
else:
288+
obsolete = None
289+
no_longer_updated: object = data.get("no_longer_updated", False) # pyright: ignore[reportUnknownMemberType]
274290
assert type(no_longer_updated) is bool
275-
uploaded_to_pypi: object = data.get("upload", True)
291+
uploaded_to_pypi: object = data.get("upload", True) # pyright: ignore[reportUnknownMemberType]
276292
assert type(uploaded_to_pypi) is bool
277-
partial_stub: object = data.get("partial_stub", True)
293+
partial_stub: object = data.get("partial_stub", True) # pyright: ignore[reportUnknownMemberType]
278294
assert type(partial_stub) is bool
279-
requires_python_str: object = data.get("requires_python")
295+
requires_python_str: object = data.get("requires_python") # pyright: ignore[reportUnknownMemberType]
280296
oldest_supported_python = get_oldest_supported_python()
281297
oldest_supported_python_specifier = Specifier(f">={oldest_supported_python}")
282298
if requires_python_str is None:
283299
requires_python = oldest_supported_python_specifier
284300
else:
285-
assert type(requires_python_str) is str
301+
assert isinstance(requires_python_str, str)
286302
requires_python = Specifier(requires_python_str)
287303
assert requires_python != oldest_supported_python_specifier, f'requires_python="{requires_python}" is redundant'
288304
# Check minimum Python version is not less than the oldest version of Python supported by typeshed
@@ -292,7 +308,7 @@ def read_metadata(distribution: str) -> StubMetadata:
292308
assert requires_python.operator == ">=", "'requires_python' should be a minimum version specifier, use '>=3.x'"
293309

294310
empty_tools: dict[object, object] = {}
295-
tools_settings: object = data.get("tool", empty_tools)
311+
tools_settings: object = data.get("tool", empty_tools) # pyright: ignore[reportUnknownMemberType]
296312
assert isinstance(tools_settings, dict)
297313
assert tools_settings.keys() <= _KNOWN_METADATA_TOOL_FIELDS.keys(), f"Unrecognised tool for {distribution!r}"
298314
for tool, tk in _KNOWN_METADATA_TOOL_FIELDS.items():
@@ -308,7 +324,7 @@ def read_metadata(distribution: str) -> StubMetadata:
308324
extra_description=extra_description,
309325
stub_distribution=stub_distribution,
310326
upstream_repository=upstream_repository,
311-
obsolete_since=obsolete_since,
327+
obsolete=obsolete,
312328
no_longer_updated=no_longer_updated,
313329
uploaded_to_pypi=uploaded_to_pypi,
314330
partial_stub=partial_stub,

scripts/stubsabot.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@
3131
import tomlkit
3232
from packaging.specifiers import Specifier
3333
from termcolor import colored
34-
from tomlkit.items import String
3534

36-
from ts_utils.metadata import NoSuchStubError, StubMetadata, metadata_path, read_metadata, update_metadata
35+
from ts_utils.metadata import ObsoleteMetadata, StubMetadata, read_metadata, update_metadata
3736
from ts_utils.paths import PYRIGHT_CONFIG, STUBS_PATH, distribution_path
3837

3938
TYPESHED_OWNER = "python"
@@ -506,27 +505,9 @@ def _add_months(date: datetime.date, months: int) -> datetime.date:
506505
return datetime.date(year, month, day)
507506

508507

509-
def obsolete_more_than_6_months(distribution: str) -> bool:
510-
try:
511-
with metadata_path(distribution).open("rb") as file:
512-
data = tomlkit.load(file)
513-
except FileNotFoundError:
514-
raise NoSuchStubError(f"Typeshed has no stubs for {distribution!r}!") from None
515-
516-
obsolete_since = data["obsolete_since"]
517-
if not obsolete_since:
518-
return False
519-
520-
assert type(obsolete_since) is String
521-
comment: str | None = obsolete_since.trivia.comment
522-
if not comment:
523-
return False
524-
525-
release_date_string = comment.removeprefix("# Released on ")
526-
release_date = datetime.date.fromisoformat(release_date_string)
527-
remove_date = _add_months(release_date, POLICY_MONTHS_DELTA)
508+
def obsolete_more_than_n_months(since_date: datetime.date) -> bool:
509+
remove_date = _add_months(since_date, POLICY_MONTHS_DELTA)
528510
today = datetime.datetime.now(tz=datetime.timezone.utc).date()
529-
530511
return remove_date <= today
531512

532513

@@ -564,7 +545,10 @@ async def has_no_longer_updated_release(release_to_download: PypiReleaseDownload
564545
async def determine_action(distribution: str, session: aiohttp.ClientSession) -> Update | NoUpdate | Obsolete | Remove:
565546
stub_info = read_metadata(distribution)
566547
if stub_info.is_obsolete:
567-
if obsolete_more_than_6_months(stub_info.distribution):
548+
assert type(stub_info.obsolete) is ObsoleteMetadata
549+
since_date = stub_info.obsolete.since_date
550+
551+
if obsolete_more_than_n_months(since_date):
568552
pypi_info = await fetch_pypi_info(f"types-{stub_info.distribution}", session)
569553
latest_release = pypi_info.get_latest_release()
570554
links = {

0 commit comments

Comments
 (0)