Skip to content
Draft
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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ supported:
This defaults to `types-<distribution>` and should only be set in special
cases.
* `upstream-repository` (recommended): The URL of the upstream repository.
* `obsolete-since` (optional): This field is part of our process for
* `obsolete-since` (optional): This table is part of our process for
[removing obsolete third-party libraries](#third-party-library-removal-policy).
It contains the first version of the corresponding library that ships
its own `py.typed` file.
its own `py.typed` file, and the date when that version was released.
* `no-longer-updated` (optional): This field is set to `true` before removing
stubs for other reasons than the upstream library shipping with type
information.
Expand Down
63 changes: 35 additions & 28 deletions lib/ts_utils/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated, Any, Final, NamedTuple, final
from typing import Annotated, Any, Final, NamedTuple, cast, final
from typing_extensions import TypeGuard

if sys.version_info >= (3, 11):
Expand All @@ -24,7 +24,6 @@
import tomlkit
from packaging.requirements import Requirement
from packaging.specifiers import Specifier
from tomlkit.items import String

from .paths import PYPROJECT_PATH, STUBS_PATH, distribution_path

Expand Down Expand Up @@ -239,27 +238,27 @@ def read_metadata(distribution: str) -> StubMetadata:
"""
try:
with metadata_path(distribution).open("rb") as f:
data = tomlkit.load(f)
data: dict[str, object] = tomllib.load(f)
except FileNotFoundError:
raise NoSuchStubError(f"Typeshed has no stubs for {distribution!r}!") from None

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

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

dependencies_s: object = data.get("dependencies", []) # pyright: ignore[reportUnknownMemberType]
dependencies_s = data.get("dependencies", [])
assert isinstance(dependencies_s, list)
dependencies = [parse_dependencies(distribution, dep) for dep in dependencies_s]

extra_description: object = data.get("extra-description") # pyright: ignore[reportUnknownMemberType]
extra_description = data.get("extra-description")
assert isinstance(extra_description, (str, type(None)))

if "stub-distribution" in data:
Expand All @@ -269,7 +268,7 @@ def read_metadata(distribution: str) -> StubMetadata:
else:
stub_distribution = f"types-{distribution}"

upstream_repository: object = data.get("upstream-repository") # pyright: ignore[reportUnknownMemberType]
upstream_repository = data.get("upstream-repository")
assert isinstance(upstream_repository, (str, type(None)))
if isinstance(upstream_repository, str):
parsed_url = urllib.parse.urlsplit(upstream_repository)
Expand All @@ -293,22 +292,25 @@ def read_metadata(distribution: str) -> StubMetadata:
)
assert num_url_path_parts == 2, bad_github_url_msg

obsolete_since: object = data.get("obsolete-since") # pyright: ignore[reportUnknownMemberType]
assert isinstance(obsolete_since, (String, type(None)))
if obsolete_since:
comment = obsolete_since.trivia.comment
since_date_string = comment.removeprefix("# Released on ")
since_date = datetime.date.fromisoformat(since_date_string)
obsolete = ObsoleteMetadata(since_version=obsolete_since, since_date=since_date)
obsolete_since = data.get("obsolete-since")
assert isinstance(obsolete_since, (dict, type(None)))
if obsolete_since is not None:
obsolete_table: dict[str, object] = obsolete_since
obsolete_since_version = obsolete_table.get("version")
obsolete_since_date = obsolete_table.get("date")
assert isinstance(obsolete_since_version, str)
assert isinstance(obsolete_since_date, str)
since_date = datetime.date.fromisoformat(obsolete_since_date)
obsolete = ObsoleteMetadata(since_version=obsolete_since_version, since_date=since_date)
else:
obsolete = None
no_longer_updated: object = data.get("no-longer-updated", False) # pyright: ignore[reportUnknownMemberType]
no_longer_updated = data.get("no-longer-updated", False)
assert type(no_longer_updated) is bool
uploaded_to_pypi: object = data.get("upload", True) # pyright: ignore[reportUnknownMemberType]
uploaded_to_pypi = data.get("upload", True)
assert type(uploaded_to_pypi) is bool
partial_stub: object = data.get("partial-stub", True) # pyright: ignore[reportUnknownMemberType]
partial_stub = data.get("partial-stub", True)
assert type(partial_stub) is bool
requires_python_str: object = data.get("requires-python") # pyright: ignore[reportUnknownMemberType]
requires_python_str = 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:
Expand All @@ -324,11 +326,11 @@ def read_metadata(distribution: str) -> StubMetadata:
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) # pyright: ignore[reportUnknownMemberType]
tools_settings = data.get("tool", empty_tools)
assert isinstance(tools_settings, dict)
assert tools_settings.keys() <= _KNOWN_METADATA_TOOL_FIELDS.keys(), f"Unrecognised tool for {distribution!r}"
for tool, tk in _KNOWN_METADATA_TOOL_FIELDS.items():
settings_for_tool: object = tools_settings.get(tool, {}) # pyright: ignore[reportUnknownMemberType]
settings_for_tool = cast(dict[str, object], tools_settings).get(tool, {})
assert isinstance(settings_for_tool, dict)
for key in settings_for_tool:
assert key in tk, f"Unrecognised {tool} key {key!r} for {distribution!r}"
Expand All @@ -349,23 +351,28 @@ def read_metadata(distribution: str) -> StubMetadata:
)


def update_metadata(distribution: str, **new_values: object) -> tomlkit.TOMLDocument:
def update_metadata(distribution: str, **new_values: object) -> dict[str, object]:
"""Update a distribution's METADATA.toml.

Return the updated TOML dictionary for use without having to open the file separately.
"""
path = metadata_path(distribution)
try:
with path.open("rb") as file:
data = tomlkit.load(file)
with path.open("rb") as f:
# This cast is necessary for pyright to understand that the
# variable is a dict with object values. Just using
# `data: dict[str, object] = tomlkit.load(f)` doesn't work because
# pyright still infers TOMLDocument which derives from
# dict[Unknown, Unknown].
data = cast(dict[str, object], tomlkit.load(f))
except FileNotFoundError:
raise NoSuchStubError(f"Typeshed has no stubs for {distribution!r}!") from None
data.update(new_values) # pyright: ignore[reportUnknownMemberType] # tomlkit.TOMLDocument.update is partially typed
data.update(new_values)
for key in list(data.keys()):
new_key = key.replace("_", "-") # pyright: ignore[reportUnknownMemberType] # tomlkit.TOMLDocument.keys is partially typed
data[new_key] = data.pop(key) # pyright: ignore[reportUnknownMemberType] # tomlkit.TOMLDocument.pop is partially typed
with path.open("w", encoding="UTF-8") as file:
tomlkit.dump(data, file) # pyright: ignore[reportUnknownMemberType] # tomlkit.dump has partially unknown Mapping type
new_key = key.replace("_", "-")
data[new_key] = data.pop(key)
with path.open("w", encoding="UTF-8") as f:
tomlkit.dump(data, f) # pyright: ignore[reportUnknownMemberType] # tomlkit.dump has partially unknown Mapping type
return data


Expand Down
4 changes: 2 additions & 2 deletions lib/ts_utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,12 @@ def close(self: TemporaryFileWrapper[str]) -> None:


@functools.cache
def get_gitignore_spec() -> pathspec.PathSpec:
def get_gitignore_spec() -> pathspec.GitIgnoreSpec:
with GITIGNORE_PATH.open(encoding="UTF-8") as f:
return pathspec.GitIgnoreSpec.from_lines(f)


def spec_matches_path(spec: pathspec.PathSpec, path: Path) -> bool:
def spec_matches_path(spec: pathspec.PathSpec[Any], path: Path) -> bool:
normalized_path = path.as_posix()
if path.is_dir():
normalized_path += "/"
Expand Down
2 changes: 1 addition & 1 deletion requirements-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ aiohttp==3.13.5
grpcio-tools>=1.76.0 # For grpc_tools.protoc
mypy-protobuf==5.0.0
packaging==26.0
pathspec>=1.0.3
pathspec>=1.1.1
pre-commit
# Required by create_baseline_stubs.py. Must match .pre-commit-config.yaml.
ruff==0.15.8
Expand Down
11 changes: 6 additions & 5 deletions scripts/stubsabot.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@
from dataclasses import dataclass, field
from http import HTTPStatus
from pathlib import Path
from typing import Annotated, Any, ClassVar, Literal, NamedTuple, TypedDict, TypeVar
from typing import Annotated, Any, ClassVar, Literal, NamedTuple, TypedDict, TypeVar, cast
from typing_extensions import Self, TypeAlias

import tomlkit

if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib

import aiohttp
import packaging.version
import tomlkit
from packaging.specifiers import Specifier
from termcolor import colored

Expand Down Expand Up @@ -906,9 +907,9 @@ async def suggest_typeshed_obsolete(obsolete: Obsolete, session: aiohttp.ClientS
async with _repo_lock:
branch_name = f"{BRANCH_PREFIX}/{normalize(obsolete.distribution)}"
subprocess.check_call(["git", "checkout", "-B", branch_name, "origin/main"])
obs_string = tomlkit.string(obsolete.obsolete_since_version)
obs_string.comment(f"Released on {obsolete.obsolete_since_date.date().isoformat()}")
update_metadata(obsolete.distribution, obsolete_since=obs_string)
obsolete_t = cast(dict[str, object], tomlkit.inline_table())
obsolete_t.update({"version": obsolete.obsolete_since_version, "date": obsolete.obsolete_since_date.date().isoformat()})
update_metadata(obsolete.distribution, obsolete_since=obsolete_t)
body = "\n".join(f"{k}: {v}" for k, v in obsolete.links.items())
subprocess.check_call(["git", "commit", "--all", "-m", f"{title}\n\n{body}"])
if action_level <= ActionLevel.local:
Expand Down
2 changes: 1 addition & 1 deletion stubs/binaryornot/METADATA.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version = "0.4.*"
upstream-repository = "https://github.com/binaryornot/binaryornot"
obsolete-since = "0.5.0" # Released on 2026-03-07
obsolete-since = { version = "0.5.0", date = "2026-03-07" }
2 changes: 1 addition & 1 deletion stubs/fpdf2/METADATA.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version = "2.8.4"
upstream-repository = "https://github.com/py-pdf/fpdf2"
dependencies = ["Pillow>=10.3.0"]
obsolete-since = "2.8.6" # Released on 2026-02-19
obsolete-since = { version = "2.8.6", date = "2026-02-19" }

[tool.stubtest]
stubtest-dependencies = ["cryptography"]
2 changes: 1 addition & 1 deletion stubs/icalendar/METADATA.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version = "6.3.2"
upstream-repository = "https://github.com/collective/icalendar"
dependencies = ["types-python-dateutil", "types-pytz"]
obsolete-since = "7.0.0" # Released on 2026-02-11
obsolete-since = { version = "7.0.0", date = "2026-02-11" }

[tool.stubtest]
stubtest-dependencies = ["pytz"]
Loading