Skip to content

Commit df09499

Browse files
committed
Merge branch 'main' into alex/more-linters
2 parents a782bb5 + ad46d41 commit df09499

19 files changed

Lines changed: 166 additions & 132 deletions

File tree

‎.pre-commit-config.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ repos:
4848
rev: 451b56af716f9f0d0c2b816503a3fd0cf8b036fa # frozen: v1.29.0
4949
hooks:
5050
- id: zizmor
51+
args: [--no-progress, --fix]
5152

5253
ci:
5354
autofix_commit_msg: "[pre-commit.ci] auto fixes from pre-commit.com hooks"

‎scripts/stubsabot.py‎

Lines changed: 92 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,14 @@
3636
from packaging.specifiers import Specifier
3737
from termcolor import colored
3838

39-
from ts_utils.metadata import ObsoleteMetadata, StubMetadata, read_metadata, update_metadata
39+
from ts_utils.metadata import (
40+
ObsoleteMetadata,
41+
StubMetadata,
42+
StubtestSettings,
43+
read_metadata,
44+
read_stubtest_settings,
45+
update_metadata,
46+
)
4047
from ts_utils.paths import PYRIGHT_CONFIG, STUBS_PATH, distribution_path
4148
from ts_utils.stubs import third_party_stubs
4249

@@ -772,36 +779,41 @@ async def update_pull_request_label(*, pr_number: int, session: aiohttp.ClientSe
772779
response.raise_for_status()
773780

774781

782+
def remote_branch_exists(branch: str) -> bool:
783+
return (
784+
subprocess.run(["git", "show-ref", "--verify", "--quiet", f"refs/remotes/origin/{branch}"], check=False).returncode == 0
785+
)
786+
787+
775788
def has_non_stubsabot_commits(branch: str) -> bool:
776789
assert not branch.startswith("origin/")
777-
try:
778-
# commits on origin/branch that are not on branch or are
779-
# patch equivalent to a commit on branch
780-
output = subprocess.check_output(
781-
["git", "log", "--right-only", "--pretty=%an", "--cherry-pick", f"{branch}...origin/{branch}"],
782-
stderr=subprocess.DEVNULL,
783-
)
784-
return bool(set(output.splitlines()) - {b"stubsabot"})
785-
except subprocess.CalledProcessError:
786-
# origin/branch does not exist
790+
791+
if not remote_branch_exists(branch):
787792
return False
788793

794+
# commits on origin/branch that are not on branch or are
795+
# patch equivalent to a commit on branch
796+
output = subprocess.check_output(
797+
["git", "log", "--right-only", "--pretty=%an", "--cherry-pick", f"{branch}...origin/{branch}"], stderr=subprocess.DEVNULL
798+
)
799+
return bool(set(output.splitlines()) - {b"stubsabot"})
800+
789801

790802
def latest_commit_is_different_to_last_commit_on_origin(branch: str) -> bool:
791803
assert not branch.startswith("origin/")
792-
try:
793-
# https://www.git-scm.com/docs/git-range-diff
794-
# If the number of lines is >1,
795-
# it indicates that something about our commit is different to the last commit
796-
# (Could be the commit "content", or the commit message).
797-
commit_comparison = subprocess.run(
798-
["git", "range-diff", f"origin/{branch}~1..origin/{branch}", "HEAD~1..HEAD"], check=True, capture_output=True
799-
)
800-
return len(commit_comparison.stdout.splitlines()) > 1
801-
except subprocess.CalledProcessError:
802-
# origin/branch does not exist
804+
805+
if not remote_branch_exists(branch):
803806
return True
804807

808+
# https://www.git-scm.com/docs/git-range-diff
809+
# If the number of lines is >1,
810+
# it indicates that something about our commit is different to the last commit
811+
# (Could be the commit "content", or the commit message).
812+
commit_comparison = subprocess.run(
813+
["git", "range-diff", f"origin/{branch}~1..origin/{branch}", "HEAD~1..HEAD"], check=True, capture_output=True
814+
)
815+
return len(commit_comparison.stdout.splitlines()) > 1
816+
805817

806818
class RemoteConflictError(Exception):
807819
pass
@@ -810,7 +822,12 @@ class RemoteConflictError(Exception):
810822
def somewhat_safe_force_push(branch: str) -> None:
811823
if has_non_stubsabot_commits(branch):
812824
raise RemoteConflictError(f"origin/{branch} has non-stubsabot changes that are not on {branch}!")
813-
subprocess.check_call(["git", "push", "origin", branch, "--force"])
825+
result = subprocess.run(["git", "push", "--quiet", "origin", branch, "--force"], text=True, capture_output=True, check=False)
826+
if result.returncode:
827+
# Capture both streams to suppress stderr on success without losing it on failure
828+
sys.stdout.write(result.stdout)
829+
sys.stderr.write(result.stderr)
830+
result.check_returncode()
814831

815832

816833
def normalize(name: str) -> str:
@@ -824,15 +841,13 @@ def normalize(name: str) -> str:
824841
BRANCH_PREFIX = "stubsabot"
825842

826843

827-
def get_update_pr_body(update: Update, metadata: Mapping[str, Any]) -> str:
844+
def get_update_pr_body(update: Update, settings: StubtestSettings) -> str:
828845
body = "\n".join(f"{k}: {v}" for k, v in update.links.items())
829846

830847
if update.diff_analysis is not None:
831848
body += f"\n\n{update.diff_analysis}"
832849

833-
stubtest_settings: dict[str, Any] = metadata.get("tool", {}).get("stubtest", {})
834-
stubtest_will_run = not stubtest_settings.get("skip", False)
835-
if stubtest_will_run:
850+
if not settings.skip:
836851
body += textwrap.dedent("""
837852
838853
If stubtest fails for this PR:
@@ -866,66 +881,55 @@ def remove_stubs(distribution: str) -> None:
866881
f.writelines(lines)
867882

868883

869-
async def suggest_typeshed_update(update: Update, session: aiohttp.ClientSession, action_level: ActionLevel) -> None:
884+
def get_commit_message(action: Update | Obsolete | Remove) -> tuple[str, str]:
885+
"""Return git commit message title and body."""
886+
match action:
887+
case Update():
888+
stubtest_settings = read_stubtest_settings(action.distribution)
889+
return (
890+
f"[stubsabot] Bump {action.distribution} to {action.new_version}",
891+
get_update_pr_body(action, stubtest_settings),
892+
)
893+
case Obsolete():
894+
return (
895+
f"[stubsabot] Mark {action.distribution} as obsolete since {action.obsolete_since_version}",
896+
"\n".join(f"{k}: {v}" for k, v in action.links.items()),
897+
)
898+
case Remove():
899+
return (
900+
f"[stubsabot] Remove {action.distribution} as {action.reason}",
901+
"\n".join(f"{k}: {v}" for k, v in action.links.items()),
902+
)
903+
904+
905+
def run_action(action: Update | Obsolete | Remove) -> None:
906+
match action:
907+
case Update():
908+
update_metadata(action.distribution, version=action.new_version)
909+
case Obsolete():
910+
obsolete_t = cast(dict[str, object], tomlkit.inline_table())
911+
obsolete_t.update({"version": action.obsolete_since_version, "date": action.obsolete_since_date.date().isoformat()})
912+
update_metadata(action.distribution, obsolete_since=obsolete_t)
913+
case Remove():
914+
remove_stubs(action.distribution)
915+
916+
917+
async def process_typeshed_change(
918+
action: Update | Obsolete | Remove, session: aiohttp.ClientSession, action_level: ActionLevel
919+
) -> None:
870920
if action_level <= ActionLevel.nothing:
871921
return
872-
title = f"[stubsabot] Bump {update.distribution} to {update.new_version}"
873-
async with _repo_lock:
874-
branch_name = f"{BRANCH_PREFIX}/{normalize(update.distribution)}"
875-
subprocess.check_call(["git", "checkout", "-B", branch_name, "origin/main"])
876-
meta = update_metadata(update.distribution, version=update.new_version)
877-
body = get_update_pr_body(update, meta)
878-
subprocess.check_call(["git", "commit", "--all", "-m", f"{title}\n\n{body}"])
879-
if action_level <= ActionLevel.local:
880-
return
881-
if not latest_commit_is_different_to_last_commit_on_origin(branch_name):
882-
print(f"No pushing to origin required: origin/{branch_name} exists and requires no changes!")
883-
return
884-
somewhat_safe_force_push(branch_name)
885-
if action_level <= ActionLevel.fork:
886-
return
887922

888-
await create_or_update_pull_request(title=title, body=body, branch_name=branch_name, session=session)
889-
890-
891-
async def suggest_typeshed_obsolete(obsolete: Obsolete, session: aiohttp.ClientSession, action_level: ActionLevel) -> None:
892-
if action_level <= ActionLevel.nothing:
893-
return
894-
title = f"[stubsabot] Mark {obsolete.distribution} as obsolete since {obsolete.obsolete_since_version}"
895-
async with _repo_lock:
896-
branch_name = f"{BRANCH_PREFIX}/{normalize(obsolete.distribution)}"
897-
subprocess.check_call(["git", "checkout", "-B", branch_name, "origin/main"])
898-
obsolete_t = cast(dict[str, object], tomlkit.inline_table())
899-
obsolete_t.update({"version": obsolete.obsolete_since_version, "date": obsolete.obsolete_since_date.date().isoformat()})
900-
update_metadata(obsolete.distribution, obsolete_since=obsolete_t)
901-
body = "\n".join(f"{k}: {v}" for k, v in obsolete.links.items())
902-
subprocess.check_call(["git", "commit", "--all", "-m", f"{title}\n\n{body}"])
903-
if action_level <= ActionLevel.local:
904-
return
905-
if not latest_commit_is_different_to_last_commit_on_origin(branch_name):
906-
print(f"No PR required: origin/{branch_name} exists and requires no changes!")
907-
return
908-
somewhat_safe_force_push(branch_name)
909-
if action_level <= ActionLevel.fork:
910-
return
911-
912-
await create_or_update_pull_request(title=title, body=body, branch_name=branch_name, session=session)
913-
914-
915-
async def suggest_typeshed_remove(remove: Remove, session: aiohttp.ClientSession, action_level: ActionLevel) -> None:
916-
if action_level <= ActionLevel.nothing:
917-
return
918-
title = f"[stubsabot] Remove {remove.distribution} as {remove.reason}"
919923
async with _repo_lock:
920-
branch_name = f"{BRANCH_PREFIX}/{normalize(remove.distribution)}"
921-
subprocess.check_call(["git", "checkout", "-B", branch_name, "origin/main"])
922-
remove_stubs(remove.distribution)
923-
body = "\n".join(f"{k}: {v}" for k, v in remove.links.items())
924-
subprocess.check_call(["git", "commit", "--all", "-m", f"{title}\n\n{body}"])
924+
branch_name = f"{BRANCH_PREFIX}/{normalize(action.distribution)}"
925+
subprocess.check_call(["git", "checkout", "--quiet", "-B", branch_name, "origin/main"])
926+
run_action(action)
927+
title, body = get_commit_message(action)
928+
subprocess.check_call(["git", "commit", "--quiet", "--all", "-m", f"{title}\n\n{body}"])
925929
if action_level <= ActionLevel.local:
926930
return
927931
if not latest_commit_is_different_to_last_commit_on_origin(branch_name):
928-
print(f"No pushing to origin required: origin/{branch_name} exists and requires no changes!")
932+
print(f"No pushing to origin required: 'origin/{branch_name}' exists and requires no changes!")
929933
return
930934
somewhat_safe_force_push(branch_name)
931935
if action_level <= ActionLevel.fork:
@@ -995,13 +999,13 @@ async def main() -> int:
995999

9961000
action_count = 0
9971001
for task in asyncio.as_completed(tasks):
998-
update = await task
999-
print(f"{update.distribution}... ", end="")
1000-
print(update)
1002+
action = await task
1003+
print(f"{action.distribution}... ", end="")
1004+
print(action)
10011005

1002-
if isinstance(update, NoUpdate):
1006+
if isinstance(action, NoUpdate):
10031007
continue
1004-
if isinstance(update, Error):
1008+
if isinstance(action, Error):
10051009
error = True
10061010
continue
10071011

@@ -1011,16 +1015,8 @@ async def main() -> int:
10111015
action_count += 1
10121016

10131017
try:
1014-
if isinstance(update, Update):
1015-
await suggest_typeshed_update(update, session, action_level=args.action_level)
1016-
continue
1017-
if isinstance(update, Obsolete):
1018-
await suggest_typeshed_obsolete(update, session, action_level=args.action_level)
1019-
continue
1020-
# Redundant, but keeping for extra runtime validation
1021-
if isinstance(update, Remove): # pyright: ignore[reportUnnecessaryIsInstance]
1022-
await suggest_typeshed_remove(update, session, action_level=args.action_level)
1023-
continue
1018+
await process_typeshed_change(action, session, action_level=args.action_level)
1019+
continue
10241020
except RemoteConflictError as e:
10251021
print(colored(f"... but ran into {type(e).__qualname__}: {e}", "red"))
10261022
continue
@@ -1029,7 +1025,7 @@ async def main() -> int:
10291025
# if you need to cleanup, try:
10301026
# git branch -D $(git branch --list 'stubsabot/*')
10311027
if args.action_level >= ActionLevel.local and original_branch:
1032-
subprocess.check_call(["git", "checkout", original_branch])
1028+
subprocess.check_call(["git", "checkout", "--quiet", original_branch])
10331029

10341030
return 1 if error else 0
10351031

‎stdlib/@tests/stubtest_allowlists/py315.txt‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
# ============================================
44

55
_frozen_importlib_external.PathFinder.discover
6-
_frozen_importlib_external.SourceFileLoader.source_to_code
7-
_frozen_importlib_external.SourceLoader.source_to_code
86
dataclasses.MISSING
97
dataclasses._MISSING_TYPE
108
dataclasses.field
11-
mailbox._ProxyFile.__class_getitem__
129

1310
# =============================================================
1411
# Allowlist entries that cannot or should not be fixed; >= 3.15

‎stdlib/_frozen_importlib_external.pyi‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,24 @@ class SourceLoader(_LoaderBasics):
101101
def set_data(self, path: str, data: bytes) -> None: ...
102102
def get_source(self, fullname: str) -> str | None: ...
103103
def path_stats(self, path: str) -> Mapping[str, Any]: ...
104-
def source_to_code(
105-
self, data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive, path: bytes | StrPath
106-
) -> types.CodeType: ...
104+
if sys.version_info >= (3, 15):
105+
def source_to_code(
106+
self,
107+
data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive,
108+
path: bytes | StrPath,
109+
fullname: str | None = None,
110+
*,
111+
_optimize: int = -1,
112+
) -> types.CodeType: ...
113+
else:
114+
def source_to_code(
115+
self,
116+
data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive,
117+
path: bytes | StrPath,
118+
*,
119+
_optimize: int = -1,
120+
) -> types.CodeType: ...
121+
107122
def get_code(self, fullname: str) -> types.CodeType | None: ...
108123

109124
class FileLoader:
@@ -120,13 +135,6 @@ class FileLoader:
120135
class SourceFileLoader(importlib.abc.FileLoader, FileLoader, importlib.abc.SourceLoader, SourceLoader): # type: ignore[misc] # incompatible method arguments in base classes
121136
def set_data(self, path: str, data: ReadableBuffer, *, _mode: int = 0o666) -> None: ...
122137
def path_stats(self, path: str) -> Mapping[str, Any]: ...
123-
def source_to_code( # type: ignore[override] # incompatible with InspectLoader.source_to_code
124-
self,
125-
data: ReadableBuffer | str | _ast.Module | _ast.Expression | _ast.Interactive,
126-
path: bytes | StrPath,
127-
*,
128-
_optimize: int = -1,
129-
) -> types.CodeType: ...
130138

131139
class SourcelessFileLoader(importlib.abc.FileLoader, FileLoader, _LoaderBasics):
132140
def get_code(self, fullname: str) -> types.CodeType | None: ...

‎stdlib/argparse.pyi‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
223223
title: str = "subcommands",
224224
description: str | None = None,
225225
prog: str | None = None,
226-
action: type[Action] = ...,
226+
action: str | type[Action] = ...,
227227
option_string: str = ...,
228228
dest: str | None = None,
229229
required: bool = False,
@@ -238,7 +238,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
238238
description: str | None = None,
239239
prog: str | None = None,
240240
parser_class: type[_ArgumentParserT],
241-
action: type[Action] = ...,
241+
action: str | type[Action] = ...,
242242
option_string: str = ...,
243243
dest: str | None = None,
244244
required: bool = False,

‎stdlib/configparser.pyi‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ class RawConfigParser(_Parser):
334334
def items(self, section: _SectionName, raw: bool = False, vars: _Section | None = None) -> list[tuple[str, str]]: ...
335335

336336
def values(self) -> ValuesView[SectionProxy]: ...
337+
def popitem(self) -> tuple[str, SectionProxy]: ...
337338
def set(self, section: _SectionName, option: str, value: str | None = None) -> None: ...
338339
def write(self, fp: SupportsWrite[str], space_around_delimiters: bool = True) -> None: ...
339340
def remove_option(self, section: _SectionName, option: str) -> bool: ...

‎stdlib/mailbox.pyi‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,8 @@ class _ProxyFile:
294294
def flush(self) -> None: ...
295295
@property
296296
def closed(self) -> bool: ...
297-
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
297+
if sys.version_info < (3, 15):
298+
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
298299

299300
class _PartialFile(_ProxyFile):
300301
def __init__(self, f: _GetFileReturn, start: int | None = None, stop: int | None = None) -> None: ...

‎stubs/Authlib/@tests/stubtest_allowlist.txt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,5 @@ authlib.integrations.flask_client.*
4949
authlib.integrations.flask_oauth1.*
5050
authlib.integrations.flask_oauth2.*
5151
authlib.integrations.httpx_client.*
52-
authlib.integrations.requests_client.*
5352
authlib.integrations.sqla_oauth2.*
5453
authlib.integrations.starlette_client.*

‎stubs/Authlib/METADATA.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
version = "1.7.2"
22
upstream-repository = "https://github.com/authlib/authlib"
33
dependencies = ["cryptography"]
4+
optional-dependencies = ["requests"]

‎stubs/jwcrypto/@tests/stubtest_allowlist.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# test code does not need type hints
22
jwcrypto.tests
33
jwcrypto.tests-cookbook
4+
jwcrypto.tests_mldsa
45

56
# even if the deprecated decorator is applied, the attribute is not present
67
jwcrypto.jwt.JWTMissingKeyID.__deprecated__

0 commit comments

Comments
 (0)