@@ -294,21 +294,82 @@ class Language:
294
294
def tag (self ) -> str :
295
295
return self .iso639_tag .replace ("_" , "-" ).lower ()
296
296
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
-
305
297
@property
306
298
def switcher_label (self ) -> str :
307
299
if self .translated_name :
308
300
return f"{ self .name } | { self .translated_name } "
309
301
return self .name
310
302
311
303
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
+
312
373
def run (
313
374
cmd : Sequence [str | Path ], cwd : Path | None = None
314
375
) -> subprocess .CompletedProcess :
@@ -534,8 +595,7 @@ def version_info() -> None:
534
595
class DocBuilder :
535
596
"""Builder for a CPython version and a language."""
536
597
537
- version : Version
538
- language : Language
598
+ build_meta : BuildMetadata
539
599
cpython_repo : Repository
540
600
docs_by_version_content : bytes
541
601
switchers_content : bytes
@@ -553,7 +613,7 @@ def html_only(self) -> bool:
553
613
return (
554
614
self .select_output in {"only-html" , "only-html-en" }
555
615
or self .quick
556
- or self .language .html_only
616
+ or self .build_meta .html_only
557
617
)
558
618
559
619
@property
@@ -567,11 +627,11 @@ def run(self, http: urllib3.PoolManager, force_build: bool) -> bool | None:
567
627
start_timestamp = dt .datetime .now (tz = dt .UTC ).replace (microsecond = 0 )
568
628
logging .info ("Running." )
569
629
try :
570
- if self .language .html_only and not self .includes_html :
630
+ if self .build_meta .html_only and not self .includes_html :
571
631
logging .info ("Skipping non-HTML build (language is HTML-only)." )
572
632
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 :
575
635
self .clone_translation ()
576
636
if trigger_reason := self .should_rebuild (force_build ):
577
637
self .build_venv ()
@@ -593,7 +653,7 @@ def run(self, http: urllib3.PoolManager, force_build: bool) -> bool | None:
593
653
594
654
@property
595
655
def locale_dir (self ) -> Path :
596
- return self .build_root / self .version . name / "locale"
656
+ return self .build_root / self .build_meta . version / "locale"
597
657
598
658
@property
599
659
def checkout (self ) -> Path :
@@ -608,8 +668,8 @@ def clone_translation(self) -> None:
608
668
def translation_repo (self ) -> Repository :
609
669
"""See PEP 545 for translations repository naming convention."""
610
670
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 )
613
673
614
674
@property
615
675
def translation_branch (self ) -> str :
@@ -623,25 +683,25 @@ def translation_branch(self) -> str:
623
683
"""
624
684
remote_branches = self .translation_repo .run ("branch" , "-r" ).stdout
625
685
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 )
627
687
628
688
def build (self ) -> None :
629
689
"""Build this version/language doc."""
630
690
logging .info ("Build start." )
631
691
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 :
634
694
sphinxopts .extend ((
635
695
f"-D locale_dirs={ self .locale_dir } " ,
636
- f"-D language={ self .language .iso639_tag } " ,
696
+ f"-D language={ self .build_meta .iso639_tag } " ,
637
697
"-D gettext_compact=0" ,
638
698
"-D translation_progress_classes=1" ,
639
699
))
640
700
641
- if self .version . status == "EOL" :
701
+ if self .build_meta . is_eol :
642
702
sphinxopts .append ("-D html_context.outdated=1" )
643
703
644
- if self .version .status in ("in development" , "pre-release" ):
704
+ if self .build_meta .status in ("in development" , "pre-release" ):
645
705
maketarget = "autobuild-dev"
646
706
else :
647
707
maketarget = "autobuild-stable"
@@ -653,17 +713,15 @@ def build(self) -> None:
653
713
blurb = self .venv / "bin" / "blurb"
654
714
655
715
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
659
717
# Define a tag to enable opengraph socialcards previews
660
718
# (used in Doc/conf.py and requires matplotlib)
661
719
sphinxopts += (
662
720
"-t create-social-cards" ,
663
721
f"-D ogp_site_url={ site_url } " ,
664
722
)
665
723
666
- if self .version . as_tuple () < (3 , 8 ):
724
+ if self .build_meta . version_tuple < (3 , 8 ):
667
725
# Disable CPython switchers, we handle them now:
668
726
text = (self .checkout / "Doc" / "Makefile" ).read_text (encoding = "utf-8" )
669
727
text = text .replace (" -A switchers=1" , "" )
@@ -696,12 +754,12 @@ def build_venv(self) -> None:
696
754
So we can reuse them from builds to builds, while they contain
697
755
different Sphinx versions.
698
756
"""
699
- requirements = list (self .version . requirements )
757
+ requirements = list (self .build_meta . dependencies )
700
758
if self .includes_html :
701
759
# opengraph previews
702
760
requirements .append ("matplotlib>=3" )
703
761
704
- venv_path = self .build_root / f"venv- { self .version . name } "
762
+ venv_path = self .build_root / self .build_meta . venv_name
705
763
venv .create (venv_path , symlinks = os .name != "nt" , with_pip = True )
706
764
run (
707
765
(
@@ -726,7 +784,7 @@ def setup_indexsidebar(self) -> None:
726
784
dbv_path = tmpl_dst / "_docs_by_version.html"
727
785
728
786
shutil .copy (tmpl_src / "indexsidebar.html" , tmpl_dst / "indexsidebar.html" )
729
- if self .version . status != "EOL" :
787
+ if not self .build_meta . is_eol :
730
788
dbv_path .write_bytes (self .docs_by_version_content )
731
789
else :
732
790
shutil .copy (tmpl_src / "_docs_by_version.html" , dbv_path )
@@ -736,14 +794,14 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None:
736
794
logging .info ("Publishing start." )
737
795
start_time = perf_counter ()
738
796
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
741
799
else :
742
- language_dir = self .www_root / self .language . tag
800
+ language_dir = self .www_root / self .build_meta . language
743
801
language_dir .mkdir (parents = True , exist_ok = True )
744
802
chgrp (language_dir , group = self .group , recursive = True )
745
803
language_dir .chmod (0o775 )
746
- target = language_dir / self .version . name
804
+ target = language_dir / self .build_meta . version
747
805
748
806
target .mkdir (parents = True , exist_ok = True )
749
807
try :
@@ -792,8 +850,7 @@ def copy_build_to_webroot(self, http: urllib3.PoolManager) -> None:
792
850
793
851
logging .info ("%s files changed" , changed )
794
852
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 )
797
854
logging .info (
798
855
"Publishing done (%s)." , format_seconds (perf_counter () - start_time )
799
856
)
@@ -804,7 +861,7 @@ def should_rebuild(self, force: bool) -> str | Literal[False]:
804
861
logging .info ("Should rebuild: no previous state found." )
805
862
return "no previous state"
806
863
cpython_sha = self .cpython_repo .run ("rev-parse" , "HEAD" ).stdout .strip ()
807
- if self .language .is_translation :
864
+ if self .build_meta .is_translation :
808
865
translation_sha = self .translation_repo .run (
809
866
"rev-parse" , "HEAD"
810
867
).stdout .strip ()
@@ -839,7 +896,7 @@ def load_state(self) -> dict:
839
896
state_file = self .build_root / "state.toml"
840
897
try :
841
898
return tomlkit .loads (state_file .read_text (encoding = "UTF-8" ))[
842
- f"/{ self .language . tag } / { self . version . name } /"
899
+ f"/{ self .build_meta . slug } /"
843
900
]
844
901
except (KeyError , FileNotFoundError ):
845
902
return {}
@@ -860,14 +917,14 @@ def save_state(
860
917
except FileNotFoundError :
861
918
states = tomlkit .document ()
862
919
863
- key = f"/{ self .language . tag } / { self . version . name } /"
920
+ key = f"/{ self .build_meta . slug } /"
864
921
state = {
865
922
"last_build_start" : build_start ,
866
923
"last_build_duration" : round (build_duration , 0 ),
867
924
"triggered_by" : trigger ,
868
925
"cpython_sha" : self .cpython_repo .run ("rev-parse" , "HEAD" ).stdout .strip (),
869
926
}
870
- if self .language .is_translation :
927
+ if self .build_meta .is_translation :
871
928
state ["translation_sha" ] = self .translation_repo .run (
872
929
"rev-parse" , "HEAD"
873
930
).stdout .strip ()
@@ -1122,9 +1179,9 @@ def build_docs(args: argparse.Namespace) -> int:
1122
1179
# pairs from the end of the list, effectively reversing it.
1123
1180
# This runs languages in config.toml order and versions newest first.
1124
1181
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 ))
1128
1185
]
1129
1186
del args .branches
1130
1187
del args .languages
@@ -1141,28 +1198,25 @@ def build_docs(args: argparse.Namespace) -> int:
1141
1198
args .build_root / _checkout_name (args .select_output ),
1142
1199
)
1143
1200
while todo :
1144
- version , language = todo .pop ()
1201
+ b = todo .pop ()
1145
1202
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" )
1149
1204
)
1150
1205
if sentry_sdk :
1151
1206
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 )
1154
1209
cpython_repo .update ()
1155
1210
builder = DocBuilder (
1156
- version ,
1157
- language ,
1211
+ b ,
1158
1212
cpython_repo ,
1159
1213
docs_by_version_content ,
1160
1214
switchers_content ,
1161
1215
** vars (args ),
1162
1216
)
1163
1217
built_successfully = builder .run (http , force_build = force_build )
1164
1218
if built_successfully :
1165
- build_succeeded .add (( version . name , language . tag ) )
1219
+ build_succeeded .add (b . slug )
1166
1220
elif built_successfully is not None :
1167
1221
any_build_failed = True
1168
1222
@@ -1285,7 +1339,7 @@ def make_symlinks(
1285
1339
group : str ,
1286
1340
versions : Versions ,
1287
1341
languages : Languages ,
1288
- successful_builds : Set [tuple [ str , str ] ],
1342
+ successful_builds : Set [str ],
1289
1343
skip_cache_invalidation : bool ,
1290
1344
http : urllib3 .PoolManager ,
1291
1345
) -> None :
@@ -1305,7 +1359,7 @@ def make_symlinks(
1305
1359
("dev" , versions .current_dev .name ),
1306
1360
):
1307
1361
for language in languages :
1308
- if ( symlink_target , language .tag ) in successful_builds :
1362
+ if f" { language .tag } / { symlink_target } " in successful_builds :
1309
1363
symlink (
1310
1364
www_root ,
1311
1365
language .tag ,
0 commit comments