Skip to content

Commit 772a28e

Browse files
committed
Add type hints to release.py
1 parent 2be22ff commit 772a28e

File tree

6 files changed

+131
-36
lines changed

6 files changed

+131
-36
lines changed

.github/workflows/lint.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ jobs:
1414

1515
steps:
1616
- uses: actions/checkout@v4
17-
- uses: actions/setup-python@v5.1.0
17+
- uses: actions/setup-python@v5
1818
with:
1919
python-version: "3.x"
2020
cache: pip
2121
- uses: pre-commit/[email protected]
2222

23+
- name: Install dependencies
24+
run: |
25+
python3 -m pip install -U pip
26+
python3 -m pip install -U tox
27+
28+
- name: Mypy
29+
run: tox -e mypy
30+
2331
- name: Run PSScriptAnalyzer on PowerShell scripts
2432
shell: pwsh
2533
run: |

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ jobs:
1717
cache: pip
1818
cache-dependency-path: dev-requirements.txt
1919
- run: |
20-
python -m pip install -r dev-requirements.txt
20+
python -m pip install tox
2121
- run: |
22-
pytest tests/ --cov . --cov tests --cov-report term --cov-report xml
22+
tox -e py
2323
2424
- name: Upload coverage
2525
uses: codecov/[email protected]

.pre-commit-config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ repos:
3535
hooks:
3636
- id: actionlint
3737

38+
- repo: https://github.com/tox-dev/pyproject-fmt
39+
rev: 2.1.3
40+
hooks:
41+
- id: pyproject-fmt
42+
43+
- repo: https://github.com/abravalheri/validate-pyproject
44+
rev: v0.18
45+
hooks:
46+
- id: validate-pyproject
47+
48+
- repo: https://github.com/tox-dev/tox-ini-fmt
49+
rev: 1.3.1
50+
hooks:
51+
- id: tox-ini-fmt
52+
3853
- repo: meta
3954
hooks:
4055
- id: check-hooks-apply

pyproject.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[tool.mypy]
2+
python_version = "3.12"
3+
pretty = true
4+
disallow_any_generics = true
5+
enable_error_code = "ignore-without-code"
6+
extra_checks = true
7+
follow_imports = "silent"
8+
strict = true
9+
warn_redundant_casts = true
10+
warn_unreachable = true
11+
warn_unused_ignores = true
12+
exclude = [
13+
"^add-to-pydotorg.py$",
14+
"^buildbotapi.py$",
15+
"^run_release.py$",
16+
"^sbom.py$",
17+
"^tests/test_release_tag.py$",
18+
"^tests/test_sbom.py$",
19+
"^windows-release/purge.py$",
20+
]

release.py

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
Additions by Barry Warsaw, Georg Brandl and Benjamin Peterson
77
"""
88

9+
from __future__ import annotations
10+
911
import datetime
1012
import glob
1113
import hashlib
@@ -18,6 +20,7 @@
1820
import sys
1921
import tempfile
2022
from contextlib import contextmanager
23+
from typing import Any, Callable, Generator, Self
2124

2225
COMMASPACE = ", "
2326
SPACE = " "
@@ -28,7 +31,7 @@
2831

2932

3033
class Tag:
31-
def __init__(self, tag_name):
34+
def __init__(self, tag_name: str) -> None:
3235
# if tag is ".", use current directory name as tag
3336
# e.g. if current directory name is "3.4.6",
3437
# "release.py --bump 3.4.6" and "release.py --bump ." are the same
@@ -37,6 +40,7 @@ def __init__(self, tag_name):
3740
result = tag_cre.match(tag_name)
3841
if result is None:
3942
error(f"tag {tag_name} is not valid")
43+
assert result is not None
4044
data = list(result.groups())
4145
if data[3] is None:
4246
# A final release.
@@ -56,47 +60,49 @@ def __init__(self, tag_name):
5660
# This has the effect of normalizing the version.
5761
self.text = self.normalized()
5862
if self.level != "f":
63+
assert self.level is not None
5964
self.text += self.level + str(self.serial)
6065
self.basic_version = f"{self.major}.{self.minor}"
6166

62-
def __str__(self):
67+
def __str__(self) -> str:
6368
return self.text
6469

65-
def normalized(self):
70+
def normalized(self) -> str:
6671
return f"{self.major}.{self.minor}.{self.patch}"
6772

6873
@property
69-
def branch(self):
74+
def branch(self) -> str:
7075
return "main" if self.is_alpha_release else f"{self.major}.{self.minor}"
7176

7277
@property
73-
def is_alpha_release(self):
78+
def is_alpha_release(self) -> bool:
7479
return self.level == "a"
7580

7681
@property
77-
def is_release_candidate(self):
82+
def is_release_candidate(self) -> bool:
7883
return self.level == "rc"
7984

8085
@property
81-
def is_feature_freeze_release(self):
86+
def is_feature_freeze_release(self) -> bool:
8287
return self.level == "b" and self.serial == 1
8388

8489
@property
85-
def nickname(self):
90+
def nickname(self) -> str:
8691
return self.text.replace(".", "")
8792

8893
@property
89-
def gitname(self):
94+
def gitname(self) -> str:
9095
return "v" + self.text
9196

92-
def next_minor_release(self):
97+
def next_minor_release(self) -> Self:
9398
return self.__class__(f"{self.major}.{int(self.minor)+1}.0a0")
9499

95-
def as_tuple(self):
96-
return (self.major, self.minor, self.patch, self.level, self.serial)
100+
def as_tuple(self) -> tuple[int, int, int, str, int]:
101+
assert isinstance(self.level, str)
102+
return self.major, self.minor, self.patch, self.level, self.serial
97103

98104
@property
99-
def committed_at(self):
105+
def committed_at(self) -> datetime.datetime:
100106
# Fetch the epoch of the tagged commit for build reproducibility.
101107
proc = subprocess.run(
102108
["git", "log", self.gitname, "-1", "--pretty=%ct"], stdout=subprocess.PIPE
@@ -108,14 +114,16 @@ def committed_at(self):
108114
)
109115

110116

111-
def error(*msgs):
117+
def error(*msgs: str) -> None:
112118
print("**ERROR**", file=sys.stderr)
113119
for msg in msgs:
114120
print(msg, file=sys.stderr)
115121
sys.exit(1)
116122

117123

118-
def run_cmd(cmd, silent=False, shell=False, **kwargs):
124+
def run_cmd(
125+
cmd: list[str] | str, silent: bool = False, shell: bool = False, **kwargs: Any
126+
) -> None:
119127
if shell:
120128
cmd = SPACE.join(cmd)
121129
if not silent:
@@ -134,7 +142,7 @@ def run_cmd(cmd, silent=False, shell=False, **kwargs):
134142
root = None
135143

136144

137-
def chdir_to_repo_root():
145+
def chdir_to_repo_root() -> str:
138146
global root
139147

140148
# find the root of the local CPython repo
@@ -152,7 +160,10 @@ def chdir_to_repo_root():
152160

153161
os.chdir(path)
154162

155-
def test_first_line(filename, test):
163+
def test_first_line(
164+
filename: str,
165+
test: Callable[[str], re.Match[str] | None] | Callable[[object], bool],
166+
) -> bool:
156167
if not os.path.exists(filename):
157168
return False
158169
with open(filename) as f:
@@ -180,18 +191,18 @@ def test_first_line(filename, test):
180191
return root
181192

182193

183-
def get_output(args):
194+
def get_output(args: list[str]) -> bytes:
184195
return subprocess.check_output(args)
185196

186197

187-
def check_env():
198+
def check_env() -> None:
188199
if "EDITOR" not in os.environ:
189200
error("editor not detected.", "Please set your EDITOR environment variable")
190201
if not os.path.exists(".git"):
191202
error("CWD is not a git clone")
192203

193204

194-
def get_arg_parser():
205+
def get_arg_parser() -> optparse.OptionParser:
195206
usage = "%prog [options] tagname"
196207
p = optparse.OptionParser(usage=usage)
197208
p.add_option(
@@ -244,7 +255,9 @@ def get_arg_parser():
244255
return p
245256

246257

247-
def constant_replace(fn, updated_constants, comment_start="/*", comment_end="*/"):
258+
def constant_replace(
259+
fn: str, updated_constants: str, comment_start: str = "/*", comment_end: str = "*/"
260+
) -> None:
248261
"""Inserts in between --start constant-- and --end constant-- in a file"""
249262
start_tag = comment_start + "--start constants--" + comment_end
250263
end_tag = comment_start + "--end constants--" + comment_end
@@ -271,7 +284,7 @@ def constant_replace(fn, updated_constants, comment_start="/*", comment_end="*/"
271284
os.rename(fn + ".new", fn)
272285

273286

274-
def tweak_patchlevel(tag, done=False):
287+
def tweak_patchlevel(tag: Tag, done: bool = False) -> None:
275288
print("Updating Include/patchlevel.h...", end=" ")
276289
template = '''
277290
#define PY_MAJOR_VERSION\t{tag.major}
@@ -282,6 +295,7 @@ def tweak_patchlevel(tag, done=False):
282295
283296
/* Version as a string */
284297
#define PY_VERSION \t\"{tag.text}{plus}"'''.strip()
298+
assert tag.level is str
285299
level_def = {
286300
"a": "PY_RELEASE_LEVEL_ALPHA",
287301
"b": "PY_RELEASE_LEVEL_BETA",
@@ -297,7 +311,7 @@ def tweak_patchlevel(tag, done=False):
297311
print("done")
298312

299313

300-
def bump(tag):
314+
def bump(tag: Tag) -> None:
301315
print(f"Bumping version to {tag}")
302316

303317
tweak_patchlevel(tag)
@@ -338,7 +352,7 @@ def manual_edit(fn: str) -> None:
338352

339353

340354
@contextmanager
341-
def pushd(new):
355+
def pushd(new: str) -> Generator[None, None, None]:
342356
print(f"chdir'ing to {new}")
343357
old = os.getcwd()
344358
os.chdir(new)
@@ -348,7 +362,7 @@ def pushd(new):
348362
os.chdir(old)
349363

350364

351-
def make_dist(name):
365+
def make_dist(name: str) -> None:
352366
try:
353367
os.mkdir(name)
354368
except OSError:
@@ -360,7 +374,7 @@ def make_dist(name):
360374
print(f"created dist directory {name}")
361375

362376

363-
def tarball(source, clamp_mtime):
377+
def tarball(source: str, clamp_mtime: str) -> None:
364378
"""Build tarballs for a directory."""
365379
print("Making .tgz")
366380
base = os.path.basename(source)
@@ -408,7 +422,7 @@ def tarball(source, clamp_mtime):
408422
print(" %s %8s %s" % (checksum_xz.hexdigest(), int(os.path.getsize(xz)), xz))
409423

410424

411-
def export(tag, silent=False, skip_docs=False):
425+
def export(tag: Tag, silent: bool = False, skip_docs: bool = False) -> None:
412426
make_dist(tag.text)
413427
print("Exporting tag:", tag.text)
414428
archivename = f"Python-{tag.text}"
@@ -531,7 +545,7 @@ def export(tag, silent=False, skip_docs=False):
531545
print("**You may also want to run make install and re-test**")
532546

533547

534-
def build_docs():
548+
def build_docs() -> str:
535549
"""Build and tarball the documentation"""
536550
print("Building docs")
537551
with tempfile.TemporaryDirectory() as venv:
@@ -548,11 +562,11 @@ def build_docs():
548562
return os.path.abspath("dist")
549563

550564

551-
def upload(tag, username):
565+
def upload(tag: Tag, username: str) -> None:
552566
"""scp everything to dinsdale"""
553567
address = f'"{username}@dinsdale.python.org:'
554568

555-
def scp(from_loc, to_loc):
569+
def scp(from_loc: str, to_loc: str) -> None:
556570
run_cmd(["scp", from_loc, address + to_loc])
557571

558572
with pushd(tag.text):
@@ -566,7 +580,7 @@ def scp(from_loc, to_loc):
566580
)
567581

568582

569-
def make_tag(tag):
583+
def make_tag(tag: Tag) -> bool:
570584
# make sure we've run blurb export
571585
good_files = glob.glob("Misc/NEWS.d/" + str(tag) + ".rst")
572586
bad_files = list(glob.glob("Misc/NEWS.d/next/*/0*.rst"))
@@ -604,11 +618,11 @@ def make_tag(tag):
604618
return True
605619

606620

607-
def done(tag):
621+
def done(tag: Tag) -> None:
608622
tweak_patchlevel(tag, done=True)
609623

610624

611-
def main(argv):
625+
def main(argv: Any) -> None:
612626
chdir_to_repo_root()
613627
parser = get_arg_parser()
614628
options, args = parser.parse_args(argv)

tox.ini

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[tox]
2+
requires =
3+
tox>=4.2
4+
env_list =
5+
py
6+
lint
7+
8+
[testenv]
9+
skip_install = true
10+
deps =
11+
-r dev-requirements.txt
12+
commands =
13+
{envpython} -m pytest \
14+
tests/ \
15+
--cov . \
16+
--cov tests \
17+
--cov-report html \
18+
--cov-report term \
19+
--cov-report xml \
20+
{posargs}
21+
22+
[testenv:lint]
23+
skip_install = true
24+
deps =
25+
pre-commit
26+
pass_env =
27+
PRE_COMMIT_COLOR
28+
commands =
29+
pre-commit run --all-files --show-diff-on-failure
30+
31+
[testenv:mypy]
32+
skip_install = true
33+
deps =
34+
mypy==1.10
35+
pytest
36+
pytest-mock
37+
commands =
38+
mypy . {posargs}

0 commit comments

Comments
 (0)