2
2
3
3
"""Build the Python docs for various branches and various languages.
4
4
5
- Without any arguments builds docs for all active versions configured in the
6
- global VERSIONS list and all languages configured in the LANGUAGES list .
5
+ Without any arguments builds docs for all active versions and
6
+ languages configured in the config.ini file .
7
7
8
8
-q selects "quick build", which means to build only HTML.
9
9
20
20
"""
21
21
22
22
from argparse import ArgumentParser
23
+ import configparser
23
24
from contextlib import suppress
24
25
from dataclasses import dataclass
25
26
import filecmp
41
42
from pathlib import Path
42
43
from string import Template
43
44
from textwrap import indent
45
+ from typing import Iterable
44
46
45
47
import zc .lockfile
46
48
import jinja2
@@ -154,14 +156,14 @@ def filter(versions, branch=None):
154
156
return [v for v in versions if v .status not in ("EOL" , "security-fixes" )]
155
157
156
158
@staticmethod
157
- def current_stable ():
159
+ def current_stable (versions ):
158
160
"""Find the current stable cPython version."""
159
- return max ([v for v in VERSIONS if v .status == "stable" ], key = Version .as_tuple )
161
+ return max ([v for v in versions if v .status == "stable" ], key = Version .as_tuple )
160
162
161
163
@staticmethod
162
- def current_dev ():
164
+ def current_dev (versions ):
163
165
"""Find the current de cPython version."""
164
- return max (VERSIONS , key = Version .as_tuple )
166
+ return max (versions , key = Version .as_tuple )
165
167
166
168
@property
167
169
def picker_label (self ):
@@ -172,7 +174,7 @@ def picker_label(self):
172
174
return f"pre ({ self .name } )"
173
175
return self .name
174
176
175
- def setup_indexsidebar (self , dest_path ):
177
+ def setup_indexsidebar (self , versions , dest_path ):
176
178
"""Build indexsidebar.html for Sphinx."""
177
179
with open (
178
180
HERE / "templates" / "indexsidebar.html" , encoding = "UTF-8"
@@ -183,7 +185,7 @@ def setup_indexsidebar(self, dest_path):
183
185
sidebar_template .render (
184
186
current_version = self ,
185
187
versions = sorted (
186
- VERSIONS , key = lambda v : version_to_tuple (v .name ), reverse = True
188
+ versions , key = lambda v : version_to_tuple (v .name ), reverse = True
187
189
),
188
190
)
189
191
)
@@ -195,7 +197,6 @@ def __gt__(self, other):
195
197
return self .as_tuple () > other .as_tuple ()
196
198
197
199
198
-
199
200
@dataclass (frozen = True , order = True )
200
201
class Language :
201
202
tag : str
@@ -206,23 +207,6 @@ class Language:
206
207
html_only : bool = False
207
208
208
209
209
- # EOL and security-fixes are not automatically built, no need to remove them
210
- # from the list, this way we can still rebuild them manually as needed.
211
- #
212
- # Please keep the list in reverse-order for ease of editing.
213
- VERSIONS = [
214
- Version ("3.13" , branch = "origin/main" , status = "in development" ),
215
- Version ("3.12" , branch = "origin/3.12" , status = "stable" ),
216
- Version ("3.11" , branch = "origin/3.11" , status = "stable" ),
217
- Version ("3.10" , branch = "origin/3.10" , status = "security-fixes" ),
218
- Version ("3.9" , branch = "origin/3.9" , status = "security-fixes" ),
219
- Version ("3.8" , branch = "origin/3.8" , status = "security-fixes" ),
220
- Version ("3.7" , tag = "3.7" , status = "EOL" ),
221
- Version ("3.6" , tag = "3.6" , status = "EOL" ),
222
- Version ("3.5" , tag = "3.5" , status = "EOL" ),
223
- Version ("2.7" , tag = "2.7" , status = "EOL" ),
224
- ]
225
-
226
210
XELATEX_DEFAULT = (
227
211
"-D latex_engine=xelatex" ,
228
212
"-D latex_elements.inputenc=" ,
@@ -285,22 +269,6 @@ class Language:
285
269
r"-D latex_elements.fontenc=\\usepackage{xeCJK}" ,
286
270
)
287
271
288
- LANGUAGES = {
289
- Language ("en" , "en" , "English" , True , XELATEX_DEFAULT ),
290
- Language ("es" , "es" , "Spanish" , True , XELATEX_WITH_FONTSPEC ),
291
- Language ("fr" , "fr" , "French" , True , XELATEX_WITH_FONTSPEC ),
292
- Language ("id" , "id" , "Indonesian" , False , XELATEX_DEFAULT ),
293
- Language ("it" , "it" , "Italian" , False , XELATEX_DEFAULT ),
294
- Language ("ja" , "ja" , "Japanese" , True , LUALATEX_FOR_JP ),
295
- Language ("ko" , "ko" , "Korean" , True , XELATEX_FOR_KOREAN ),
296
- Language ("pl" , "pl" , "Polish" , False , XELATEX_DEFAULT ),
297
- Language ("pt-br" , "pt_BR" , "Brazilian Portuguese" , True , XELATEX_DEFAULT ),
298
- Language ("tr" , "tr" , "Turkish" , True , XELATEX_DEFAULT ),
299
- Language ("uk" , "uk" , "Ukrainian" , False , XELATEX_DEFAULT , html_only = True ),
300
- Language ("zh-cn" , "zh_CN" , "Simplified Chinese" , True , XELATEX_WITH_CJK ),
301
- Language ("zh-tw" , "zh_TW" , "Traditional Chinese" , True , XELATEX_WITH_CJK ),
302
- }
303
-
304
272
305
273
def run (cmd , cwd = None ) -> subprocess .CompletedProcess :
306
274
"""Like subprocess.run, with logging before and after the command execution."""
@@ -446,7 +414,9 @@ def edit(file: Path):
446
414
temporary .rename (file )
447
415
448
416
449
- def setup_switchers (html_root : Path ):
417
+ def setup_switchers (
418
+ versions : Iterable [Version ], languages : Iterable [Language ], html_root : Path
419
+ ):
450
420
"""Setup cross-links between cpython versions:
451
421
- Cross-link various languages in a language switcher
452
422
- Cross-link various versions in a version switcher
@@ -464,7 +434,7 @@ def setup_switchers(html_root: Path):
464
434
sorted (
465
435
[
466
436
(language .tag , language .name )
467
- for language in LANGUAGES
437
+ for language in languages
468
438
if language .in_prod
469
439
]
470
440
)
@@ -475,7 +445,7 @@ def setup_switchers(html_root: Path):
475
445
[
476
446
(version .name , version .picker_label )
477
447
for version in sorted (
478
- VERSIONS ,
448
+ versions ,
479
449
key = lambda v : version_to_tuple (v .name ),
480
450
reverse = True ,
481
451
)
@@ -499,7 +469,13 @@ def setup_switchers(html_root: Path):
499
469
ofile .write (line )
500
470
501
471
502
- def build_robots_txt (www_root : Path , group , skip_cache_invalidation ):
472
+ def build_robots_txt (
473
+ versions : Iterable [Version ],
474
+ languages : Iterable [Language ],
475
+ www_root : Path ,
476
+ group ,
477
+ skip_cache_invalidation ,
478
+ ):
503
479
"""Disallow crawl of EOL versions in robots.txt."""
504
480
if not www_root .exists ():
505
481
logging .info ("Skipping robots.txt generation (www root does not even exists)." )
@@ -509,15 +485,17 @@ def build_robots_txt(www_root: Path, group, skip_cache_invalidation):
509
485
template = jinja2 .Template (template_file .read ())
510
486
with open (robots_file , "w" , encoding = "UTF-8" ) as robots_txt_file :
511
487
robots_txt_file .write (
512
- template .render (languages = LANGUAGES , versions = VERSIONS ) + "\n "
488
+ template .render (languages = languages , versions = versions ) + "\n "
513
489
)
514
490
robots_file .chmod (0o775 )
515
491
run (["chgrp" , group , robots_file ])
516
492
if not skip_cache_invalidation :
517
493
requests .request ("PURGE" , "https://docs.python.org/robots.txt" )
518
494
519
495
520
- def build_sitemap (www_root : Path , group ):
496
+ def build_sitemap (
497
+ versions : Iterable [Version ], languages : Iterable [Language ], www_root : Path , group
498
+ ):
521
499
"""Build a sitemap with all live versions and translations."""
522
500
if not www_root .exists ():
523
501
logging .info ("Skipping sitemap generation (www root does not even exists)." )
@@ -526,7 +504,7 @@ def build_sitemap(www_root: Path, group):
526
504
template = jinja2 .Template (template_file .read ())
527
505
sitemap_file = www_root / "sitemap.xml"
528
506
sitemap_file .write_text (
529
- template .render (languages = LANGUAGES , versions = VERSIONS ) + "\n " , encoding = "UTF-8"
507
+ template .render (languages = languages , versions = versions ) + "\n " , encoding = "UTF-8"
530
508
)
531
509
sitemap_file .chmod (0o664 )
532
510
run (["chgrp" , group , sitemap_file ])
@@ -631,8 +609,9 @@ def parse_args():
631
609
parser .add_argument (
632
610
"--languages" ,
633
611
nargs = "*" ,
634
- default = {language .tag for language in LANGUAGES },
635
- help = "Language translation, as a PEP 545 language tag like" " 'fr' or 'pt-br'." ,
612
+ default = "all" ,
613
+ help = "Language translation, as a PEP 545 language tag like" " 'fr' or 'pt-br'. "
614
+ "Use 'all' to build all of them (it's the default behavior)." ,
636
615
metavar = "fr" ,
637
616
)
638
617
parser .add_argument (
@@ -677,7 +656,9 @@ class DocBuilder:
677
656
"""Builder for a cpython version and a language."""
678
657
679
658
version : Version
659
+ versions : Iterable [Version ]
680
660
language : Language
661
+ languages : Iterable [Language ]
681
662
build_root : Path
682
663
www_root : Path
683
664
quick : bool
@@ -783,7 +764,7 @@ def build(self):
783
764
if self .version .status in ("in development" , "pre-release" )
784
765
else "stable"
785
766
)
786
- + ("" if self .full_build else "-html" )
767
+ + ("" if self .full_build else "-html" )
787
768
)
788
769
logging .info ("Running make %s" , maketarget )
789
770
python = self .venv / "bin" / "python"
@@ -799,7 +780,8 @@ def build(self):
799
780
]
800
781
)
801
782
self .version .setup_indexsidebar (
802
- self .checkout / "Doc" / "tools" / "templates" / "indexsidebar.html"
783
+ self .versions ,
784
+ self .checkout / "Doc" / "tools" / "templates" / "indexsidebar.html" ,
803
785
)
804
786
run (
805
787
[
@@ -817,7 +799,9 @@ def build(self):
817
799
)
818
800
run (["mkdir" , "-p" , self .log_directory ])
819
801
run (["chgrp" , "-R" , self .group , self .log_directory ])
820
- setup_switchers (self .checkout / "Doc" / "build" / "html" )
802
+ setup_switchers (
803
+ self .versions , self .languages , self .checkout / "Doc" / "build" / "html"
804
+ )
821
805
logging .info (
822
806
"Build done for version: %s, language: %s" ,
823
807
self .version .name ,
@@ -990,30 +974,32 @@ def symlink(www_root: Path, language: Language, directory: str, name: str, group
990
974
purge_path (www_root , link )
991
975
992
976
993
- def major_symlinks (www_root : Path , group ):
977
+ def major_symlinks (
978
+ www_root : Path , group , versions : Iterable [Version ], languages : Iterable [Language ]
979
+ ):
994
980
"""Maintains the /2/ and /3/ symlinks for each languages.
995
981
996
982
Like:
997
983
- /3/ → /3.9/
998
984
- /fr/3/ → /fr/3.9/
999
985
- /es/3/ → /es/3.9/
1000
986
"""
1001
- current_stable = Version .current_stable ().name
1002
- for language in LANGUAGES :
987
+ current_stable = Version .current_stable (versions ).name
988
+ for language in languages :
1003
989
symlink (www_root , language , current_stable , "3" , group )
1004
990
symlink (www_root , language , "2.7" , "2" , group )
1005
991
1006
992
1007
- def dev_symlink (www_root : Path , group ):
993
+ def dev_symlink (www_root : Path , group , versions , languages ):
1008
994
"""Maintains the /dev/ symlinks for each languages.
1009
995
1010
996
Like:
1011
997
- /dev/ → /3.11/
1012
998
- /fr/dev/ → /fr/3.11/
1013
999
- /es/dev/ → /es/3.11/
1014
1000
"""
1015
- current_dev = Version .current_dev ().name
1016
- for language in LANGUAGES :
1001
+ current_dev = Version .current_dev (versions ).name
1002
+ for language in languages :
1017
1003
symlink (www_root , language , current_dev , "dev" , group )
1018
1004
1019
1005
@@ -1051,14 +1037,44 @@ def purge_path(www_root: Path, path: Path):
1051
1037
run (["curl" , "-XPURGE" , f"https://docs.python.org/{{{ ',' .join (to_purge )} }}" ])
1052
1038
1053
1039
1040
+ def parse_config ():
1041
+ config = configparser .ConfigParser ()
1042
+ config .read (HERE / "config.ini" )
1043
+ versions , languages = [], []
1044
+ for name , section in config .items ():
1045
+ if section .get ("status" ): # It's a version
1046
+ versions .append (
1047
+ Version (
1048
+ name ,
1049
+ status = section ["status" ],
1050
+ branch = section .get ("branch" ),
1051
+ tag = section .get ("tag" ),
1052
+ )
1053
+ )
1054
+ if section .get ("name" ): # It's a language
1055
+ languages .append (
1056
+ Language (
1057
+ name ,
1058
+ section .get ("iso639_tag" , name ),
1059
+ section ["name" ],
1060
+ section .getboolean ("in_prod" , True ),
1061
+ sphinxopts = globals ()[section .get ("sphinxopts" , "XELATEX_DEFAULT" )],
1062
+ html_only = section .get ("html_only" , False ),
1063
+ )
1064
+ )
1065
+ return versions , languages
1066
+
1067
+
1054
1068
def build_docs (args ) -> bool :
1055
1069
"""Build all docs (each languages and each versions)."""
1056
- languages_dict = {language .tag : language for language in LANGUAGES }
1057
- versions = Version .filter (VERSIONS , args .branch )
1058
- languages = [languages_dict [tag ] for tag in args .languages ]
1059
- del args .languages
1060
- del args .branch
1061
- todo = list (product (versions , languages ))
1070
+ versions , languages = parse_config ()
1071
+ languages_dict = {language .tag : language for language in languages }
1072
+ todo = list (
1073
+ product (
1074
+ Version .filter (versions , args .branch ),
1075
+ [languages_dict [tag ] for tag in args .languages ],
1076
+ )
1077
+ )
1062
1078
all_built_successfully = True
1063
1079
while todo :
1064
1080
version , language = todo .pop ()
@@ -1068,11 +1084,13 @@ def build_docs(args) -> bool:
1068
1084
scope .set_tag ("language" , language .tag )
1069
1085
builder = DocBuilder (version , language , ** vars (args ))
1070
1086
all_built_successfully &= builder .run ()
1071
- build_sitemap (args .www_root , args .group )
1087
+ build_sitemap (versions , languages , args .www_root , args .group )
1072
1088
build_404 (args .www_root , args .group )
1073
- build_robots_txt (args .www_root , args .group , args .skip_cache_invalidation )
1074
- major_symlinks (args .www_root , args .group )
1075
- dev_symlink (args .www_root , args .group )
1089
+ build_robots_txt (
1090
+ versions , languages , args .www_root , args .group , args .skip_cache_invalidation
1091
+ )
1092
+ major_symlinks (args .www_root , args .group , versions , languages )
1093
+ dev_symlink (args .www_root , args .group , versions , languages )
1076
1094
proofread_canonicals (args .www_root , args .skip_cache_invalidation )
1077
1095
1078
1096
return all_built_successfully
0 commit comments