66Additions by Barry Warsaw, Georg Brandl and Benjamin Peterson
77"""
88
9+ from __future__ import annotations
10+
911import datetime
1012import glob
1113import hashlib
1820import sys
1921import tempfile
2022from contextlib import contextmanager
23+ from typing import Any , Callable , Generator , Self
2124
2225COMMASPACE = ", "
2326SPACE = " "
2831
2932
3033class 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):
134142root = 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 )
0 commit comments