Skip to content

Commit b20b97d

Browse files
authored
Add tests and type hints for add_to_pydotorg
1 parent 05e89bb commit b20b97d

File tree

7 files changed

+130
-47
lines changed

7 files changed

+130
-47
lines changed

.coveragerc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
# Regexes for lines to exclude from consideration
55
exclude_also =
66
# Don't complain if non-runnable code isn't run:
7-
if __name__ == .__main__.:
7+
if __name__ == .__main__.
88
def main

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#
22
.idea/
3-
venv/
43
# Byte-compiled / optimized / DLL files
54
__pycache__/
65
*.py[cod]

add-to-pydotorg.py renamed to add_to_pydotorg.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
1616
* Put an AUTH_INFO variable containing "username:api_key" in your environment.
1717
18-
* Call this script as "python add-to-pydotorg.py RELEASE".
18+
* Call this script as "python add_to_pydotorg.py RELEASE".
1919
2020
Each call will remove all previous file objects, so you can call the script
2121
multiple times.
@@ -30,20 +30,23 @@
3030
import subprocess
3131
import sys
3232
from os import path
33+
from typing import Any, Generator
3334

3435
import requests
3536

3637

3738
# Copied from release.py
38-
def error(*msgs):
39+
def error(*msgs: Any) -> None:
3940
print("**ERROR**", file=sys.stderr)
4041
for msg in msgs:
4142
print(msg, file=sys.stderr)
4243
sys.exit(1)
4344

4445

4546
# Copied from release.py
46-
def run_cmd(cmd, silent=False, shell=True, **kwargs):
47+
def run_cmd(
48+
cmd: list[str] | str, silent: bool = False, shell: bool = False, **kwargs: Any
49+
) -> None:
4750
if shell:
4851
cmd = " ".join(cmd)
4952
if not silent:
@@ -89,7 +92,9 @@ def run_cmd(cmd, silent=False, shell=True, **kwargs):
8992
}
9093

9194

92-
def get_file_descriptions(release):
95+
def get_file_descriptions(
96+
release: str,
97+
) -> list[tuple[re.Pattern[str], tuple[str, int, bool, str]]]:
9398
v = minor_version_tuple(release)
9499
rx = re.compile
95100
# value is (file "name", OS id, download button, file "description").
@@ -157,54 +162,59 @@ def get_file_descriptions(release):
157162
]
158163

159164

160-
def changelog_for(release):
161-
new_url = f"http://docs.python.org/release/{release}/whatsnew/changelog.html"
162-
if requests.head(new_url).status_code != 200:
163-
return f"http://hg.python.org/cpython/file/v{release}/Misc/NEWS"
164-
165-
166-
def slug_for(release):
165+
def slug_for(release: str) -> str:
167166
return base_version(release).replace(".", "") + (
168167
"-" + release[len(base_version(release)) :]
169168
if release[len(base_version(release)) :]
170169
else ""
171170
)
172171

173172

174-
def sigfile_for(release, rfile):
173+
def sigfile_for(release: str, rfile: str) -> str:
175174
return download_root + f"{release}/{rfile}.asc"
176175

177176

178-
def md5sum_for(release, rfile):
177+
def md5sum_for(release: str, rfile: str) -> str:
179178
return hashlib.md5(
180179
open(ftp_root + base_version(release) + "/" + rfile, "rb").read()
181180
).hexdigest()
182181

183182

184-
def filesize_for(release, rfile):
183+
def filesize_for(release: str, rfile: str) -> int:
185184
return path.getsize(ftp_root + base_version(release) + "/" + rfile)
186185

187186

188-
def make_slug(text):
187+
def make_slug(text: str) -> str:
189188
return re.sub("[^a-zA-Z0-9_-]", "", text.replace(" ", "-"))
190189

191190

192-
def base_version(release):
191+
def base_version(release: str) -> str:
193192
m = tag_cre.match(release)
193+
assert m is not None, f"Invalid release: {release}"
194194
return ".".join(m.groups()[:3])
195195

196196

197-
def minor_version(release):
197+
def minor_version(release: str) -> str:
198198
m = tag_cre.match(release)
199+
assert m is not None, f"Invalid release: {release}"
199200
return ".".join(m.groups()[:2])
200201

201202

202-
def minor_version_tuple(release):
203+
def minor_version_tuple(release: str) -> tuple[int, int]:
203204
m = tag_cre.match(release)
204-
return (int(m.groups()[0]), int(m.groups()[1]))
205-
206-
207-
def build_file_dict(release, rfile, rel_pk, file_desc, os_pk, add_download, add_desc):
205+
assert m is not None, f"Invalid release: {release}"
206+
return int(m.groups()[0]), int(m.groups()[1])
207+
208+
209+
def build_file_dict(
210+
release: str,
211+
rfile: str,
212+
rel_pk: int,
213+
file_desc: str,
214+
os_pk: int,
215+
add_download: bool,
216+
add_desc: str,
217+
) -> dict[str, Any]:
208218
"""Return a dictionary with all needed fields for a ReleaseFile object."""
209219
d = {
210220
"name": file_desc,
@@ -243,7 +253,7 @@ def build_file_dict(release, rfile, rel_pk, file_desc, os_pk, add_download, add_
243253
return d
244254

245255

246-
def list_files(release):
256+
def list_files(release: str) -> Generator[tuple[str, str, int, bool, str], None, None]:
247257
"""List all of the release's download files."""
248258
reldir = base_version(release)
249259
for rfile in os.listdir(path.join(ftp_root, reldir)):
@@ -279,7 +289,7 @@ def list_files(release):
279289
continue
280290

281291

282-
def query_object(objtype, **params):
292+
def query_object(objtype: str, **params: Any) -> int:
283293
"""Find an API object by query parameters."""
284294
uri = base_url + f"downloads/{objtype}/"
285295
uri += "?" + "&".join(f"{k}={v}" for k, v in params.items())
@@ -290,7 +300,7 @@ def query_object(objtype, **params):
290300
return int(obj["resource_uri"].strip("/").split("/")[-1])
291301

292302

293-
def post_object(objtype, datadict):
303+
def post_object(objtype: str, datadict: dict[str, Any]) -> int:
294304
"""Create a new API object."""
295305
resp = requests.post(
296306
base_url + "downloads/" + objtype + "/",
@@ -311,13 +321,15 @@ def post_object(objtype, datadict):
311321
return pk
312322

313323

314-
def sign_release_files_with_sigstore(release, release_files):
324+
def sign_release_files_with_sigstore(
325+
release: str, release_files: list[tuple[str, str, int, bool, str]]
326+
) -> None:
315327
filenames = [
316328
ftp_root + f"{base_version(release)}/{rfile}"
317329
for rfile, file_desc, os_pk, add_download, add_desc in release_files
318330
]
319331

320-
def has_sigstore_signature(filename):
332+
def has_sigstore_signature(filename: str) -> bool:
321333
return os.path.exists(filename + ".sigstore") or (
322334
os.path.exists(filename + ".sig") and os.path.exists(filename + ".crt")
323335
)
@@ -378,7 +390,7 @@ def has_sigstore_signature(filename):
378390
)
379391

380392

381-
def main():
393+
def main() -> None:
382394
rel = sys.argv[1]
383395
print("Querying python.org for release", rel)
384396
rel_pk = query_object("release", name="Python+" + rel)
@@ -412,5 +424,5 @@ def main():
412424
print(f"Done - {n} files added")
413425

414426

415-
if not sys.flags.interactive:
427+
if __name__ == "__main__" and not sys.flags.interactive:
416428
main()

mypy-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ mypy==1.10
33
pytest
44
pytest-mock
55
sigstore==1.1.2
6+
types-requests

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ extra_checks = true
99
warn_unreachable = true
1010

1111
exclude = [
12-
"^add-to-pydotorg.py$",
1312
"^buildbotapi.py$",
1413
"^run_release.py$",
1514
"^sbom.py$",

run_release.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import argparse
1010
import asyncio
11-
import builtins
1211
import contextlib
1312
import functools
1413
import getpass
@@ -24,7 +23,7 @@
2423
import urllib.request
2524
from dataclasses import dataclass
2625
from shelve import DbfilenameShelf
27-
from typing import Any, Callable, Generator, Iterator
26+
from typing import Any, Callable, Iterator
2827

2928
import aiohttp
3029
import gnupg
@@ -282,14 +281,6 @@ def cd(path: str) -> Iterator[None]:
282281
os.chdir(current_path)
283282

284283

285-
@contextlib.contextmanager
286-
def supress_print() -> Generator[None, None, None]:
287-
print_func = builtins.print
288-
builtins.print = lambda *args, **kwargs: None
289-
yield
290-
builtins.print = print_func
291-
292-
293284
def check_tool(db: DbfilenameShelf, tool: str) -> None:
294285
if shutil.which(tool) is None:
295286
raise ReleaseException(f"{tool} is not available")
@@ -533,8 +524,8 @@ def sign_source_artifacts(db: DbfilenameShelf) -> None:
533524
uid = input("Please enter key ID to use for signing: ")
534525

535526
tarballs_path = pathlib.Path(db["git_repo"] / str(db["release"]) / "src")
536-
tgz = str(tarballs_path / (f"Python-{db['release']}.tgz"))
537-
xz = str(tarballs_path / (f"Python-{db['release']}.tar.xz"))
527+
tgz = str(tarballs_path / f"Python-{db['release']}.tgz")
528+
xz = str(tarballs_path / f"Python-{db['release']}.tar.xz")
538529

539530
subprocess.check_call(["gpg", "-bas", "-u", uid, tgz])
540531
subprocess.check_call(["gpg", "-bas", "-u", uid, xz])
@@ -867,8 +858,8 @@ def run_add_to_python_dot_org(db: DbfilenameShelf) -> None:
867858
client.connect(DOWNLOADS_SERVER, port=22, username=db["ssh_user"])
868859

869860
# Ensure the file is there
870-
source = pathlib.Path(__file__).parent / "add-to-pydotorg.py"
871-
destination = pathlib.Path(f"/home/psf-users/{db['ssh_user']}/add-to-pydotorg.py")
861+
source = pathlib.Path(__file__).parent / "add_to_pydotorg.py"
862+
destination = pathlib.Path(f"/home/psf-users/{db['ssh_user']}/add_to_pydotorg.py")
872863
ftp_client = MySFTPClient.from_transport(client.get_transport())
873864
ftp_client.put(str(source), str(destination))
874865
ftp_client.close()
@@ -881,7 +872,7 @@ def run_add_to_python_dot_org(db: DbfilenameShelf) -> None:
881872
identity_token = issuer.identity_token()
882873

883874
stdin, stdout, stderr = client.exec_command(
884-
f"AUTH_INFO={auth_info} SIGSTORE_IDENTITY_TOKEN={identity_token} python3 add-to-pydotorg.py {db['release']}"
875+
f"AUTH_INFO={auth_info} SIGSTORE_IDENTITY_TOKEN={identity_token} python3 add_to_pydotorg.py {db['release']}"
885876
)
886877
stderr_text = stderr.read().decode()
887878
if stderr_text:

tests/test_add_to_pydotorg.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import os
2+
3+
import pytest
4+
5+
os.environ["AUTH_INFO"] = "test_username:test_api_key"
6+
7+
import add_to_pydotorg # noqa: E402
8+
9+
RELEASE = "3.14.0a0"
10+
11+
12+
@pytest.mark.parametrize(
13+
["release", "expected"],
14+
[
15+
("3.9.0a0", "390-a0"),
16+
("3.10.0b3", "3100-b3"),
17+
("3.11.0rc2", "3110-rc2"),
18+
("3.12.15", "31215"),
19+
],
20+
)
21+
def test_slug_for(release: str, expected: str) -> None:
22+
assert add_to_pydotorg.slug_for(release) == expected
23+
24+
25+
def test_sigfile_for() -> None:
26+
assert (
27+
add_to_pydotorg.sigfile_for("3.14.0", "Python-3.13.0.tgz")
28+
== "https://www.python.org/ftp/python/3.14.0/Python-3.13.0.tgz.asc"
29+
)
30+
31+
32+
@pytest.mark.parametrize(
33+
["release", "expected"],
34+
[
35+
("3.9.0a0", "390a0"),
36+
("3.10.0b3", "3100b3"),
37+
("3.11.0rc2", "3110rc2"),
38+
("3.12.15", "31215"),
39+
],
40+
)
41+
def test_make_slug(release: str, expected: str) -> None:
42+
assert add_to_pydotorg.make_slug(release) == expected
43+
44+
45+
@pytest.mark.parametrize(
46+
["release", "expected"],
47+
[
48+
("3.9.0a0", "3.9.0"),
49+
("3.10.0b3", "3.10.0"),
50+
("3.11.0rc2", "3.11.0"),
51+
("3.12.15", "3.12.15"),
52+
],
53+
)
54+
def test_base_version(release: str, expected: str) -> None:
55+
assert add_to_pydotorg.base_version(release) == expected
56+
57+
58+
@pytest.mark.parametrize(
59+
["release", "expected"],
60+
[
61+
("3.9.0a0", "3.9"),
62+
("3.10.0b3", "3.10"),
63+
("3.11.0rc2", "3.11"),
64+
("3.12.15", "3.12"),
65+
],
66+
)
67+
def test_minor_version(release: str, expected: str) -> None:
68+
assert add_to_pydotorg.minor_version(release) == expected
69+
70+
71+
@pytest.mark.parametrize(
72+
["release", "expected"],
73+
[
74+
("3.9.0a0", (3, 9)),
75+
("3.10.0b3", (3, 10)),
76+
("3.11.0rc2", (3, 11)),
77+
("3.12.15", (3, 12)),
78+
],
79+
)
80+
def test_minor_version_tuple(release: str, expected: tuple[int, int]) -> None:
81+
assert add_to_pydotorg.minor_version_tuple(release) == expected

0 commit comments

Comments
 (0)