7272
7373TYPE_CHECKING = False
7474if TYPE_CHECKING :
75- from collections .abc import Iterator , Sequence , Set
75+ from collections .abc import Collection , Iterator , Sequence , Set
7676 from typing import Literal
7777
7878try :
@@ -101,7 +101,7 @@ def __reversed__(self) -> Iterator[Version]:
101101 return reversed (self ._seq )
102102
103103 @classmethod
104- def from_json (cls , data ) -> Versions :
104+ def from_json (cls , data : dict ) -> Versions :
105105 versions = sorted (
106106 [Version .from_json (name , release ) for name , release in data .items ()],
107107 key = Version .as_tuple ,
@@ -158,7 +158,9 @@ class Version:
158158 "prerelease" : "pre-release" ,
159159 }
160160
161- def __init__ (self , name , * , status , branch_or_tag = None ):
161+ def __init__ (
162+ self , name : str , * , status : str , branch_or_tag : str | None = None
163+ ) -> None :
162164 status = self .SYNONYMS .get (status , status )
163165 if status not in self .STATUSES :
164166 raise ValueError (
@@ -169,22 +171,22 @@ def __init__(self, name, *, status, branch_or_tag=None):
169171 self .branch_or_tag = branch_or_tag
170172 self .status = status
171173
172- def __repr__ (self ):
174+ def __repr__ (self ) -> str :
173175 return f"Version({ self .name } )"
174176
175- def __eq__ (self , other ) :
177+ def __eq__ (self , other : Version ) -> bool :
176178 return self .name == other .name
177179
178- def __gt__ (self , other ) :
180+ def __gt__ (self , other : Version ) -> bool :
179181 return self .as_tuple () > other .as_tuple ()
180182
181183 @classmethod
182- def from_json (cls , name , values ) :
184+ def from_json (cls , name : str , values : dict ) -> Version :
183185 """Loads a version from devguide's json representation."""
184186 return cls (name , status = values ["status" ], branch_or_tag = values ["branch" ])
185187
186188 @property
187- def requirements (self ):
189+ def requirements (self ) -> list [ str ] :
188190 """Generate the right requirements for this version.
189191
190192 Since CPython 3.8 a Doc/requirements.txt file can be used.
@@ -213,9 +215,10 @@ def requirements(self):
213215 return reqs + ["sphinx==2.3.1" ]
214216 if self .name == "3.5" :
215217 return reqs + ["sphinx==1.8.4" ]
218+ raise ValueError ("unreachable" )
216219
217220 @property
218- def changefreq (self ):
221+ def changefreq (self ) -> str :
219222 """Estimate this version change frequency, for the sitemap."""
220223 return {"EOL" : "never" , "security-fixes" : "yearly" }.get (self .status , "daily" )
221224
@@ -224,17 +227,17 @@ def as_tuple(self) -> tuple[int, ...]:
224227 return version_to_tuple (self .name )
225228
226229 @property
227- def url (self ):
230+ def url (self ) -> str :
228231 """The doc URL of this version in production."""
229232 return f"https://docs.python.org/{ self .name } /"
230233
231234 @property
232- def title (self ):
235+ def title (self ) -> str :
233236 """The title of this version's doc, for the sidebar."""
234237 return f"Python { self .name } ({ self .status } )"
235238
236239 @property
237- def picker_label (self ):
240+ def picker_label (self ) -> str :
238241 """Forge the label of a version picker."""
239242 if self .status == "in development" :
240243 return f"dev ({ self .name } )"
@@ -254,7 +257,7 @@ def __reversed__(self) -> Iterator[Language]:
254257 return reversed (self ._seq )
255258
256259 @classmethod
257- def from_json (cls , defaults , languages ) -> Languages :
260+ def from_json (cls , defaults : dict , languages : dict ) -> Languages :
258261 default_translated_name = defaults .get ("translated_name" , "" )
259262 default_in_prod = defaults .get ("in_prod" , True )
260263 default_sphinxopts = defaults .get ("sphinxopts" , [])
@@ -290,17 +293,19 @@ class Language:
290293 html_only : bool = False
291294
292295 @property
293- def tag (self ):
296+ def tag (self ) -> str :
294297 return self .iso639_tag .replace ("_" , "-" ).lower ()
295298
296299 @property
297- def switcher_label (self ):
300+ def switcher_label (self ) -> str :
298301 if self .translated_name :
299302 return f"{ self .name } | { self .translated_name } "
300303 return self .name
301304
302305
303- def run (cmd , cwd = None ) -> subprocess .CompletedProcess :
306+ def run (
307+ cmd : Sequence [str | Path ], cwd : Path | None = None
308+ ) -> subprocess .CompletedProcess :
304309 """Like subprocess.run, with logging before and after the command execution."""
305310 cmd = list (map (str , cmd ))
306311 cmdstring = shlex .join (cmd )
@@ -326,7 +331,7 @@ def run(cmd, cwd=None) -> subprocess.CompletedProcess:
326331 return result
327332
328333
329- def run_with_logging (cmd , cwd = None ):
334+ def run_with_logging (cmd : Sequence [ str | Path ] , cwd : Path | None = None ) -> None :
330335 """Like subprocess.check_call, with logging before the command execution."""
331336 cmd = list (map (str , cmd ))
332337 logging .debug ("Run: '%s'" , shlex .join (cmd ))
@@ -348,13 +353,13 @@ def run_with_logging(cmd, cwd=None):
348353 raise subprocess .CalledProcessError (return_code , cmd [0 ])
349354
350355
351- def changed_files (left , right ) :
356+ def changed_files (left : Path , right : Path ) -> list [ str ] :
352357 """Compute a list of different files between left and right, recursively.
353358 Resulting paths are relative to left.
354359 """
355360 changed = []
356361
357- def traverse (dircmp_result ) :
362+ def traverse (dircmp_result : filecmp . dircmp ) -> None :
358363 base = Path (dircmp_result .left ).relative_to (left )
359364 for file in dircmp_result .diff_files :
360365 changed .append (str (base / file ))
@@ -374,11 +379,11 @@ class Repository:
374379 remote : str
375380 directory : Path
376381
377- def run (self , * args ) :
382+ def run (self , * args : str ) -> subprocess . CompletedProcess :
378383 """Run git command in the clone repository."""
379384 return run (("git" , "-C" , self .directory ) + args )
380385
381- def get_ref (self , pattern ) :
386+ def get_ref (self , pattern : str ) -> str :
382387 """Return the reference of a given tag or branch."""
383388 try :
384389 # Maybe it's a branch
@@ -387,7 +392,7 @@ def get_ref(self, pattern):
387392 # Maybe it's a tag
388393 return self .run ("show-ref" , "-s" , "tags/" + pattern ).stdout .strip ()
389394
390- def fetch (self ):
395+ def fetch (self ) -> subprocess . CompletedProcess :
391396 """Try (and retry) to run git fetch."""
392397 try :
393398 return self .run ("fetch" )
@@ -396,12 +401,12 @@ def fetch(self):
396401 sleep (5 )
397402 return self .run ("fetch" )
398403
399- def switch (self , branch_or_tag ) :
404+ def switch (self , branch_or_tag : str ) -> None :
400405 """Reset and cleans the repository to the given branch or tag."""
401406 self .run ("reset" , "--hard" , self .get_ref (branch_or_tag ), "--" )
402407 self .run ("clean" , "-dfqx" )
403408
404- def clone (self ):
409+ def clone (self ) -> bool :
405410 """Maybe clone the repository, if not already cloned."""
406411 if (self .directory / ".git" ).is_dir ():
407412 return False # Already cloned
@@ -410,21 +415,23 @@ def clone(self):
410415 run (["git" , "clone" , self .remote , self .directory ])
411416 return True
412417
413- def update (self ):
418+ def update (self ) -> None :
414419 self .clone () or self .fetch ()
415420
416421
417- def version_to_tuple (version ) -> tuple [int , ...]:
422+ def version_to_tuple (version : str ) -> tuple [int , ...]:
418423 """Transform a version string to a tuple, for easy comparisons."""
419424 return tuple (int (part ) for part in version .split ("." ))
420425
421426
422- def tuple_to_version (version_tuple ) :
427+ def tuple_to_version (version_tuple : tuple [ int , ...]) -> str :
423428 """Reverse version_to_tuple."""
424429 return "." .join (str (part ) for part in version_tuple )
425430
426431
427- def locate_nearest_version (available_versions , target_version ):
432+ def locate_nearest_version (
433+ available_versions : Collection [str ], target_version : str
434+ ) -> str :
428435 """Look for the nearest version of target_version in available_versions.
429436 Versions are to be given as tuples, like (3, 7) for 3.7.
430437
@@ -468,7 +475,7 @@ def edit(file: Path):
468475 temporary .rename (file )
469476
470477
471- def setup_switchers (versions : Versions , languages : Languages , html_root : Path ):
478+ def setup_switchers (versions : Versions , languages : Languages , html_root : Path ) -> None :
472479 """Setup cross-links between CPython versions:
473480 - Cross-link various languages in a language switcher
474481 - Cross-link various versions in a version switcher
@@ -499,12 +506,12 @@ def setup_switchers(versions: Versions, languages: Languages, html_root: Path):
499506 ofile .write (line )
500507
501508
502- def head (text , lines = 10 ):
509+ def head (text : str , lines : int = 10 ) -> str :
503510 """Return the first *lines* lines from the given text."""
504511 return "\n " .join (text .split ("\n " )[:lines ])
505512
506513
507- def version_info ():
514+ def version_info () -> None :
508515 """Handler for --version."""
509516 try :
510517 platex_version = head (
@@ -554,15 +561,15 @@ class DocBuilder:
554561 theme : Path
555562
556563 @property
557- def html_only (self ):
564+ def html_only (self ) -> bool :
558565 return (
559566 self .select_output in {"only-html" , "only-html-en" }
560567 or self .quick
561568 or self .language .html_only
562569 )
563570
564571 @property
565- def includes_html (self ):
572+ def includes_html (self ) -> bool :
566573 """Does the build we are running include HTML output?"""
567574 return self .select_output != "no-html"
568575
@@ -601,12 +608,12 @@ def checkout(self) -> Path:
601608 """Path to CPython git clone."""
602609 return self .build_root / _checkout_name (self .select_output )
603610
604- def clone_translation (self ):
611+ def clone_translation (self ) -> None :
605612 self .translation_repo .update ()
606613 self .translation_repo .switch (self .translation_branch )
607614
608615 @property
609- def translation_repo (self ):
616+ def translation_repo (self ) -> Repository :
610617 """See PEP 545 for translations repository naming convention."""
611618
612619 locale_repo = f"https://github.com/python/python-docs-{ self .language .tag } .git"
@@ -620,7 +627,7 @@ def translation_repo(self):
620627 return Repository (locale_repo , locale_clone_dir )
621628
622629 @property
623- def translation_branch (self ):
630+ def translation_branch (self ) -> str :
624631 """Some CPython versions may be untranslated, being either too old or
625632 too new.
626633
@@ -633,7 +640,7 @@ def translation_branch(self):
633640 branches = re .findall (r"/([0-9]+\.[0-9]+)$" , remote_branches , re .M )
634641 return locate_nearest_version (branches , self .version .name )
635642
636- def build (self ):
643+ def build (self ) -> None :
637644 """Build this version/language doc."""
638645 logging .info ("Build start." )
639646 start_time = perf_counter ()
@@ -702,7 +709,7 @@ def build(self):
702709 )
703710 logging .info ("Build done (%s)." , format_seconds (perf_counter () - start_time ))
704711
705- def build_venv (self ):
712+ def build_venv (self ) -> None :
706713 """Build a venv for the specific Python version.
707714
708715 So we can reuse them from builds to builds, while they contain
@@ -819,7 +826,7 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None:
819826 "Publishing done (%s)." , format_seconds (perf_counter () - start_time )
820827 )
821828
822- def should_rebuild (self , force : bool ):
829+ def should_rebuild (self , force : bool ) -> str | Literal [ False ] :
823830 state = self .load_state ()
824831 if not state :
825832 logging .info ("Should rebuild: no previous state found." )
@@ -865,7 +872,9 @@ def load_state(self) -> dict:
865872 except (KeyError , FileNotFoundError ):
866873 return {}
867874
868- def save_state (self , build_start : dt .datetime , build_duration : float , trigger : str ):
875+ def save_state (
876+ self , build_start : dt .datetime , build_duration : float , trigger : str
877+ ) -> None :
869878 """Save current CPython sha1 and current translation sha1.
870879
871880 Using this we can deduce if a rebuild is needed or not.
@@ -911,14 +920,16 @@ def format_seconds(seconds: float) -> str:
911920 case h , m , s :
912921 return f"{ h } h { m } m { s } s"
913922
923+ raise ValueError ("unreachable" )
924+
914925
915926def _checkout_name (select_output : str | None ) -> str :
916927 if select_output is not None :
917928 return f"cpython-{ select_output } "
918929 return "cpython"
919930
920931
921- def main ():
932+ def main () -> None :
922933 """Script entry point."""
923934 args = parse_args ()
924935 setup_logging (args .log_directory , args .select_output )
@@ -934,7 +945,7 @@ def main():
934945 build_docs_with_lock (args , "build_docs_html_en.lock" )
935946
936947
937- def parse_args ():
948+ def parse_args () -> argparse . Namespace :
938949 """Parse command-line arguments."""
939950
940951 parser = argparse .ArgumentParser (
@@ -1028,7 +1039,7 @@ def parse_args():
10281039 return args
10291040
10301041
1031- def setup_logging (log_directory : Path , select_output : str | None ):
1042+ def setup_logging (log_directory : Path , select_output : str | None ) -> None :
10321043 """Setup logging to stderr if run by a human, or to a file if run from a cron."""
10331044 log_format = "%(asctime)s %(levelname)s: %(message)s"
10341045 if sys .stderr .isatty ():
@@ -1174,7 +1185,9 @@ def parse_languages_from_config() -> Languages:
11741185 return Languages .from_json (config ["defaults" ], config ["languages" ])
11751186
11761187
1177- def build_sitemap (versions : Versions , languages : Languages , www_root : Path , group ):
1188+ def build_sitemap (
1189+ versions : Versions , languages : Languages , www_root : Path , group : str
1190+ ) -> None :
11781191 """Build a sitemap with all live versions and translations."""
11791192 if not www_root .exists ():
11801193 logging .info ("Skipping sitemap generation (www root does not even exist)." )
@@ -1189,7 +1202,7 @@ def build_sitemap(versions: Versions, languages: Languages, www_root: Path, grou
11891202 run (["chgrp" , group , sitemap_path ])
11901203
11911204
1192- def build_404 (www_root : Path , group ) :
1205+ def build_404 (www_root : Path , group : str ) -> None :
11931206 """Build a nice 404 error page to display in case PDFs are not built yet."""
11941207 if not www_root .exists ():
11951208 logging .info ("Skipping 404 page generation (www root does not even exist)." )
@@ -1203,8 +1216,8 @@ def build_404(www_root: Path, group):
12031216
12041217def copy_robots_txt (
12051218 www_root : Path ,
1206- group ,
1207- skip_cache_invalidation ,
1219+ group : str ,
1220+ skip_cache_invalidation : bool ,
12081221 http : urllib3 .PoolManager ,
12091222) -> None :
12101223 """Copy robots.txt to www_root."""
@@ -1322,7 +1335,7 @@ def proofread_canonicals(
13221335)
13231336
13241337
1325- def _check_canonical_rel (file : Path , www_root : Path ):
1338+ def _check_canonical_rel (file : Path , www_root : Path ) -> Path | None :
13261339 # Check for a canonical relation link in the HTML.
13271340 # If one exists, ensure that the target exists
13281341 # or otherwise remove the canonical link element.
0 commit comments