@@ -294,21 +294,82 @@ class Language:
294294 def tag (self ) -> str :
295295 return self .iso639_tag .replace ("_" , "-" ).lower ()
296296
297- @property
298- def is_translation (self ) -> bool :
299- return self .tag != "en"
300-
301- @property
302- def locale_repo_url (self ) -> str :
303- return f"https://github.com/python/python-docs-{ self .tag } .git"
304-
305297 @property
306298 def switcher_label (self ) -> str :
307299 if self .translated_name :
308300 return f"{ self .name } | { self .translated_name } "
309301 return self .name
310302
311303
304+ @dataclasses .dataclass (frozen = True , kw_only = True , slots = True )
305+ class BuildMetadata :
306+ _ver : Version
307+ _lang : Language
308+
309+ @property
310+ def sphinxopts (self ) -> Sequence [str ]:
311+ return self ._lang .sphinxopts
312+
313+ @property
314+ def iso639_tag (self ) -> str :
315+ return self ._lang .iso639_tag
316+
317+ @property
318+ def html_only (self ) -> bool :
319+ return self ._lang .html_only
320+
321+ @property
322+ def url (self ):
323+ """The URL of this version in production."""
324+ if self .is_translation :
325+ return f"https://docs.python.org/{ self .version } /{ self .language } /"
326+ return f"https://docs.python.org/{ self .version } /"
327+
328+ @property
329+ def branch_or_tag (self ) -> str :
330+ return self ._ver .branch_or_tag
331+
332+ @property
333+ def status (self ) -> str :
334+ return self ._ver .status
335+
336+ @property
337+ def is_eol (self ) -> bool :
338+ return self ._ver .status == "EOL"
339+
340+ @property
341+ def dependencies (self ) -> list [str ]:
342+ return self ._ver .requirements
343+
344+ @property
345+ def version (self ):
346+ return self ._ver .name
347+
348+ @property
349+ def version_tuple (self ):
350+ return self ._ver .as_tuple ()
351+
352+ @property
353+ def language (self ):
354+ return self ._lang .tag
355+
356+ @property
357+ def is_translation (self ):
358+ return self .language != "en"
359+
360+ @property
361+ def slug (self ) -> str :
362+ return f"{ self .language } /{ self .version } "
363+
364+ @property
365+ def venv_name (self ) -> str :
366+ return f"venv-{ self .version } "
367+
368+ @property
369+ def locale_repo_url (self ) -> str :
370+ return f"https://github.com/python/python-docs-{ self .language } .git"
371+
372+
312373def run (
313374 cmd : Sequence [str | Path ], cwd : Path | None = None
314375) -> subprocess .CompletedProcess :
@@ -534,8 +595,7 @@ def version_info() -> None:
534595class DocBuilder :
535596 """Builder for a CPython version and a language."""
536597
537- version : Version
538- language : Language
598+ build_meta : BuildMetadata
539599 cpython_repo : Repository
540600 docs_by_version_content : bytes
541601 switchers_content : bytes
@@ -553,7 +613,7 @@ def html_only(self) -> bool:
553613 return (
554614 self .select_output in {"only-html" , "only-html-en" }
555615 or self .quick
556- or self .language .html_only
616+ or self .build_meta .html_only
557617 )
558618
559619 @property
@@ -567,11 +627,11 @@ def run(self, http: urllib3.PoolManager, force_build: bool) -> bool | None:
567627 start_timestamp = dt .datetime .now (tz = dt .UTC ).replace (microsecond = 0 )
568628 logging .info ("Running." )
569629 try :
570- if self .language .html_only and not self .includes_html :
630+ if self .build_meta .html_only and not self .includes_html :
571631 logging .info ("Skipping non-HTML build (language is HTML-only)." )
572632 return None # skipped
573- self .cpython_repo .switch (self .version .branch_or_tag )
574- if self .language .is_translation :
633+ self .cpython_repo .switch (self .build_meta .branch_or_tag )
634+ if self .build_meta .is_translation :
575635 self .clone_translation ()
576636 if trigger_reason := self .should_rebuild (force_build ):
577637 self .build_venv ()
@@ -593,7 +653,7 @@ def run(self, http: urllib3.PoolManager, force_build: bool) -> bool | None:
593653
594654 @property
595655 def locale_dir (self ) -> Path :
596- return self .build_root / self .version . name / "locale"
656+ return self .build_root / self .build_meta . version / "locale"
597657
598658 @property
599659 def checkout (self ) -> Path :
@@ -608,8 +668,8 @@ def clone_translation(self) -> None:
608668 def translation_repo (self ) -> Repository :
609669 """See PEP 545 for translations repository naming convention."""
610670
611- locale_clone_dir = self .locale_dir / self .language .iso639_tag / "LC_MESSAGES"
612- return Repository (self .language .locale_repo_url , locale_clone_dir )
671+ locale_clone_dir = self .locale_dir / self .build_meta .iso639_tag / "LC_MESSAGES"
672+ return Repository (self .build_meta .locale_repo_url , locale_clone_dir )
613673
614674 @property
615675 def translation_branch (self ) -> str :
@@ -623,25 +683,25 @@ def translation_branch(self) -> str:
623683 """
624684 remote_branches = self .translation_repo .run ("branch" , "-r" ).stdout
625685 branches = re .findall (r"/([0-9]+\.[0-9]+)$" , remote_branches , re .M )
626- return locate_nearest_version (branches , self .version . name )
686+ return locate_nearest_version (branches , self .build_meta . version )
627687
628688 def build (self ) -> None :
629689 """Build this version/language doc."""
630690 logging .info ("Build start." )
631691 start_time = perf_counter ()
632- sphinxopts = list (self .language .sphinxopts )
633- if self .language .is_translation :
692+ sphinxopts = list (self .build_meta .sphinxopts )
693+ if self .build_meta .is_translation :
634694 sphinxopts .extend ((
635695 f"-D locale_dirs={ self .locale_dir } " ,
636- f"-D language={ self .language .iso639_tag } " ,
696+ f"-D language={ self .build_meta .iso639_tag } " ,
637697 "-D gettext_compact=0" ,
638698 "-D translation_progress_classes=1" ,
639699 ))
640700
641- if self .version . status == "EOL" :
701+ if self .build_meta . is_eol :
642702 sphinxopts .append ("-D html_context.outdated=1" )
643703
644- if self .version .status in ("in development" , "pre-release" ):
704+ if self .build_meta .status in ("in development" , "pre-release" ):
645705 maketarget = "autobuild-dev"
646706 else :
647707 maketarget = "autobuild-stable"
@@ -653,17 +713,15 @@ def build(self) -> None:
653713 blurb = self .venv / "bin" / "blurb"
654714
655715 if self .includes_html :
656- site_url = self .version .url
657- if self .language .is_translation :
658- site_url += f"{ self .language .tag } /"
716+ site_url = self .build_meta .url
659717 # Define a tag to enable opengraph socialcards previews
660718 # (used in Doc/conf.py and requires matplotlib)
661719 sphinxopts += (
662720 "-t create-social-cards" ,
663721 f"-D ogp_site_url={ site_url } " ,
664722 )
665723
666- if self .version . as_tuple () < (3 , 8 ):
724+ if self .build_meta . version_tuple < (3 , 8 ):
667725 # Disable CPython switchers, we handle them now:
668726 text = (self .checkout / "Doc" / "Makefile" ).read_text (encoding = "utf-8" )
669727 text = text .replace (" -A switchers=1" , "" )
@@ -696,12 +754,12 @@ def build_venv(self) -> None:
696754 So we can reuse them from builds to builds, while they contain
697755 different Sphinx versions.
698756 """
699- requirements = list (self .version . requirements )
757+ requirements = list (self .build_meta . dependencies )
700758 if self .includes_html :
701759 # opengraph previews
702760 requirements .append ("matplotlib>=3" )
703761
704- venv_path = self .build_root / f"venv- { self .version . name } "
762+ venv_path = self .build_root / self .build_meta . venv_name
705763 venv .create (venv_path , symlinks = os .name != "nt" , with_pip = True )
706764 run (
707765 (
@@ -726,7 +784,7 @@ def setup_indexsidebar(self) -> None:
726784 dbv_path = tmpl_dst / "_docs_by_version.html"
727785
728786 shutil .copy (tmpl_src / "indexsidebar.html" , tmpl_dst / "indexsidebar.html" )
729- if self .version . status != "EOL" :
787+ if not self .build_meta . is_eol :
730788 dbv_path .write_bytes (self .docs_by_version_content )
731789 else :
732790 shutil .copy (tmpl_src / "_docs_by_version.html" , dbv_path )
@@ -736,14 +794,14 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None:
736794 logging .info ("Publishing start." )
737795 start_time = perf_counter ()
738796 self .www_root .mkdir (parents = True , exist_ok = True )
739- if not self .language .is_translation :
740- target = self .www_root / self .version . name
797+ if not self .build_meta .is_translation :
798+ target = self .www_root / self .build_meta . version
741799 else :
742- language_dir = self .www_root / self .language . tag
800+ language_dir = self .www_root / self .build_meta . language
743801 language_dir .mkdir (parents = True , exist_ok = True )
744802 chgrp (language_dir , group = self .group , recursive = True )
745803 language_dir .chmod (0o775 )
746- target = language_dir / self .version . name
804+ target = language_dir / self .build_meta . version
747805
748806 target .mkdir (parents = True , exist_ok = True )
749807 try :
@@ -792,8 +850,7 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None:
792850
793851 logging .info ("%s files changed" , changed )
794852 if changed and not self .skip_cache_invalidation :
795- surrogate_key = f"{ self .language .tag } /{ self .version .name } "
796- purge_surrogate_key (http , surrogate_key )
853+ purge_surrogate_key (http , self .build_meta .slug )
797854 logging .info (
798855 "Publishing done (%s)." , format_seconds (perf_counter () - start_time )
799856 )
@@ -804,7 +861,7 @@ def should_rebuild(self, force: bool) -> str | Literal[False]:
804861 logging .info ("Should rebuild: no previous state found." )
805862 return "no previous state"
806863 cpython_sha = self .cpython_repo .run ("rev-parse" , "HEAD" ).stdout .strip ()
807- if self .language .is_translation :
864+ if self .build_meta .is_translation :
808865 translation_sha = self .translation_repo .run (
809866 "rev-parse" , "HEAD"
810867 ).stdout .strip ()
@@ -839,7 +896,7 @@ def load_state(self) -> dict:
839896 state_file = self .build_root / "state.toml"
840897 try :
841898 return tomlkit .loads (state_file .read_text (encoding = "UTF-8" ))[
842- f"/{ self .language . tag } / { self . version . name } /"
899+ f"/{ self .build_meta . slug } /"
843900 ]
844901 except (KeyError , FileNotFoundError ):
845902 return {}
@@ -860,14 +917,14 @@ def save_state(
860917 except FileNotFoundError :
861918 states = tomlkit .document ()
862919
863- key = f"/{ self .language . tag } / { self . version . name } /"
920+ key = f"/{ self .build_meta . slug } /"
864921 state = {
865922 "last_build_start" : build_start ,
866923 "last_build_duration" : round (build_duration , 0 ),
867924 "triggered_by" : trigger ,
868925 "cpython_sha" : self .cpython_repo .run ("rev-parse" , "HEAD" ).stdout .strip (),
869926 }
870- if self .language .is_translation :
927+ if self .build_meta .is_translation :
871928 state ["translation_sha" ] = self .translation_repo .run (
872929 "rev-parse" , "HEAD"
873930 ).stdout .strip ()
@@ -1122,9 +1179,9 @@ def build_docs(args: argparse.Namespace) -> int:
11221179 # pairs from the end of the list, effectively reversing it.
11231180 # This runs languages in config.toml order and versions newest first.
11241181 todo = [
1125- ( version , language )
1126- for version in versions .filter (args .branches )
1127- for language in reversed (languages .filter (args .languages ))
1182+ BuildMetadata ( _ver = ver , _lang = lang )
1183+ for ver in versions .filter (args .branches )
1184+ for lang in reversed (languages .filter (args .languages ))
11281185 ]
11291186 del args .branches
11301187 del args .languages
@@ -1141,28 +1198,25 @@ def build_docs(args: argparse.Namespace) -> int:
11411198 args .build_root / _checkout_name (args .select_output ),
11421199 )
11431200 while todo :
1144- version , language = todo .pop ()
1201+ b = todo .pop ()
11451202 logging .root .handlers [0 ].setFormatter (
1146- logging .Formatter (
1147- f"%(asctime)s %(levelname)s { language .tag } /{ version .name } : %(message)s"
1148- )
1203+ logging .Formatter (f"%(asctime)s %(levelname)s { b .slug } : %(message)s" )
11491204 )
11501205 if sentry_sdk :
11511206 scope = sentry_sdk .get_isolation_scope ()
1152- scope .set_tag ("version" , version . name )
1153- scope .set_tag ("language" , language . tag )
1207+ scope .set_tag ("version" , b . version )
1208+ scope .set_tag ("language" , b . language )
11541209 cpython_repo .update ()
11551210 builder = DocBuilder (
1156- version ,
1157- language ,
1211+ b ,
11581212 cpython_repo ,
11591213 docs_by_version_content ,
11601214 switchers_content ,
11611215 ** vars (args ),
11621216 )
11631217 built_successfully = builder .run (http , force_build = force_build )
11641218 if built_successfully :
1165- build_succeeded .add (( version . name , language . tag ) )
1219+ build_succeeded .add (b . slug )
11661220 elif built_successfully is not None :
11671221 any_build_failed = True
11681222
@@ -1285,7 +1339,7 @@ def make_symlinks(
12851339 group : str ,
12861340 versions : Versions ,
12871341 languages : Languages ,
1288- successful_builds : Set [tuple [ str , str ] ],
1342+ successful_builds : Set [str ],
12891343 skip_cache_invalidation : bool ,
12901344 http : urllib3 .PoolManager ,
12911345) -> None :
@@ -1305,7 +1359,7 @@ def make_symlinks(
13051359 ("dev" , versions .current_dev .name ),
13061360 ):
13071361 for language in languages :
1308- if ( symlink_target , language .tag ) in successful_builds :
1362+ if f" { language .tag } / { symlink_target } " in successful_builds :
13091363 symlink (
13101364 www_root ,
13111365 language .tag ,
0 commit comments