66import contextlib
77import logging
88import os
9+ import re
910import shutil
1011import subprocess
1112import sys
1213from pathlib import Path
1314from typing import Optional
1415
16+ from sphinx_intl .transifex import create_txconfig ,update_txconfig_resources
17+
1518ROOTDIR = Path (__file__ ).resolve ().parent .parent
16- COMMANDS = ["build" ]
19+ COMMANDS = ["build" , 'generate_templates' ]
1720
1821logging .basicConfig (level = logging .INFO ,format = "%(asctime)s - %(levelname)s - %(message)s" )
1922logger = logging .getLogger (__name__ )
@@ -25,81 +28,151 @@ def configure_parser() -> argparse.ArgumentParser:
2528parser .add_argument ("command" ,choices = COMMANDS ,help = "The command to execute" )
2629parser .add_argument ("-l" ,"--language" ,help = "Language for the translated documentation" )
2730parser .add_argument ("-v" ,"--python-version" ,help = "Python version to be used" )
28- parser .add_argument ("-L" ,"--logs-dir" ,default = ROOTDIR / "logs" ,type = Path ,help = "Directory for logs" )
29- parser .add_argument ("-c" ,"--cpython-path" ,default = ROOTDIR / "cpython" ,type = Path ,help = "Path to the CPython repository" )
31+ parser .add_argument ("-L" ,"--logs-dir" ,default = ROOTDIR / "logs" ,type = Path ,help = "Directory for logs (default: 'logs' in root directory" )
32+ parser .add_argument ("-c" ,"--cpython-path" ,default = ROOTDIR / "cpython" ,type = Path ,help = "Path to the CPython repository (default: 'cpython' in root directory" )
33+ parser .add_argument ("-p" ,"--po-dir" ,type = Path ,help = "Path to the language team repository containing PO files (default: CPYTHON_PATH/Doc/locales/LANGUAGE/LC_MESSAGES" )
34+ parser .add_argument ('-t' ,'--tx-project' ,help = "Name of the Transifex project under python-doc Transifex organization" )
3035return parser
3136
3237
33- def get_value (env_var_name : str , arg_value : Optional [ str ] )-> str :
38+ def get_value (arg_value : Optional [ str ], arg_name : str , env_var_name : str )-> str :
3439"""Return a CLI argument or environment variable value."""
3540value = arg_value or os .getenv (env_var_name )
3641if not value :
37- logger .error (f"The environment variable{ env_var_name } is notdefined, and no value was provided ." )
42+ logger .error (f"' { arg_name } ' not provided and the environment variable{ env_var_name } is notset ." )
3843sys .exit (1 )
3944return value
4045
4146
42- def get_minor_version (version :str )-> int :
43- """Return the minor version number from a version string (e.g., '3.13')."""
44- try :
45- return int (version .split ("." )[1 ])
46- except (IndexError ,ValueError )as e :
47- logger .error (f"Invalid version format '{ version } ':{ e } " )
47+ def validate_cpython_path (cpython_path :Path )-> None :
48+ if not (cpython_path / "Doc" / "conf.py" ).exists ():
49+ logger .error (f"Missing conf.py in{ cpython_path } . Invalid CPython directory." )
4850sys .exit (1 )
4951
5052
51- def build_docs (language :str ,version :str ,logs_dir :Path ,cpython_path :Path )-> None :
52- """Build the documentation using Sphinx."""
53- minor_version = get_minor_version (version )
54- warning_log = logs_dir / "sphinxwarnings.txt"
53+ def validate_po_dir (po_dir :Path )-> None :
54+ if not po_dir .exists ()or not list (po_dir .glob ("*.po" )):
55+ logger .error (f"Invalid locale directory '{ po_dir } '. No PO files found." )
56+ sys .exit (1 )
57+
5558
56- sphinx_opts = f"-E -D language={ language } --keep-going -w{ warning_log } "
57- if minor_version < 12 :
58- sphinx_opts += "-D gettext_compact=False"
59+ def validate_tx_config (tx_config :str )-> None :
60+ if not re .match (r"python-(newest|\d+)" ,tx_config ):
61+ logger .error (f"Invalid Transifex project name:{ tx_config } " )
62+ sys .exit (1 )
63+
64+
65+ # contextlib implemented chdir since Python 3.11
66+ @contextlib .contextmanager
67+ def chdir (path :Path ):
68+ """Temporarily change the working directory."""
69+ original_dir = Path .cwd ()
70+ logger .info (path )
71+ os .chdir (path )
72+ try :
73+ yield
74+ finally :
75+ os .chdir (original_dir )
76+
77+
78+ def build_docs (language :str ,version :str ,po_dir :Path ,logs_dir :Path ,cpython_path :Path )-> None :
79+ """Build the documentation using Sphinx."""
80+ warning_log = logs_dir / "sphinx_warnings_build_docs.txt"
81+ sphinx_opts = ["-E" ,"-Dgettext_compact=0" ,f"-Dlanguage={ language } " ,"--keep-going" ,"-w" ,f"{ warning_log } " ]
82+ locale_dirs = cpython_path / "Doc/locales"
83+ target_locale_dir = cpython_path / "Doc/locales" / language / "LC_MESSAGES"
84+
85+ # TODO Fix symlinking when po_dir is not equal to target_locale_dir
86+ #if not po_dir.relative_to(locale_dirs) and
87+ # not target_locale_dir.readlink() == po_dir:
88+ # if target_locale_dir.is_symlink():
89+ # target_locale_dir.unlink() # remove only if it is a symlink
90+ # if not target_locale_dir.exists() and not target_locale_dir.is_symlink():
91+ # (locale_dirs / language).mkdir(parents=True, exist_ok=True)
92+ # os.symlink(po_dir, target_locale_dir)
5993
6094try :
6195logger .info (f"Building documentation for{ language } , Python{ version } ." )
6296subprocess .run ([
63- "make" ,"-C" ,str (cpython_path / "Doc" ),"html" ,f"SPHINXOPTS={ sphinx_opts } "
97+ "make" ,"-C" ,str (cpython_path / "Doc" ),"html" ,f"SPHINXOPTS={ ' ' . join ( sphinx_opts ) } "
6498 ],check = True )
6599
66100if warning_log .exists ()and not warning_log .stat ().st_size :
67101warning_log .unlink ()
68- logger .info ("Empty warning log file removed ." )
102+ logger .info ("Removed empty warning log file." )
69103
70104except subprocess .CalledProcessError as e :
71105logger .error (f"Make command failed:{ e } " )
72106sys .exit (1 )
73107
74108
75- def validate_paths (cpython_path :Path )-> None :
76- """Validate necessary paths for handling documentation."""
77- if not (cpython_path / "Doc" / "conf.py" ).exists ():
78- logger .error (f"Missing conf.py in{ cpython_path } . Invalid CPython directory." )
109+ def generate_templates (logs_dir :Path ,cpython_path :Path ,tx_project :str )-> None :
110+ """Generate translation template files (a.k.a. POT files) with Sphinx"""
111+ warning_log = logs_dir / "sphinx_warnings_generate_templates.txt"
112+ all_sphinx_opts = [
113+ "-E" ,"-b" ,"gettext" ,"-Dgettext_compact=0" ,"--keep-going" ,
114+ "-w" ,f"{ warning_log } " ,"-d" ,"build/.doctrees-gettext" ,"." ,"build/gettext"
115+ ]
116+
117+ try :
118+ logger .info ("Generating template files for Python docs." )
119+ subprocess .run ([
120+ "make" ,"-C" ,str (cpython_path / "Doc" ),"build" ,f"ALLSPHINXOPTS={ ' ' .join (all_sphinx_opts )} "
121+ ],check = True )
122+
123+ if warning_log .exists ()and not warning_log .stat ().st_size :
124+ warning_log .unlink ()
125+ logger .info ("Removed empty warning log file." )
126+
127+ except subprocess .CalledProcessError as e :
128+ logger .error (f"Make command failed:{ e } " )
79129sys .exit (1 )
80130
131+ with chdir (cpython_path / "Doc/locales" ):
132+ logger .info ("Updating Transifex's resources configuration file" )
133+ Path (".tx/config" ).unlink (missing_ok = True )
134+ create_txconfig ()
135+ update_txconfig_resources (
136+ transifex_organization_name = 'python-doc' ,
137+ transifex_project_name = tx_project ,
138+ locale_dir = Path ("." ),
139+ pot_dir = Path ("../build/gettext" )
140+ )
141+
81142
82143def main ()-> None :
83144parser = configure_parser ()
84145args = parser .parse_args ()
85146
86- language = get_value ("PYDOC_LANGUAGE" ,args .language )
87- version = get_value ("PYDOC_VERSION" ,args .python_version )
88- logs_dir = Path (get_value ("PYDOC_LOGS" ,str (args .logs_dir )))
147+ # Set and require variable depending on the command issued by the user
89148cpython_path = args .cpython_path
149+ logs_dir = Path (get_value (str (args .logs_dir ),"--logs-dir" ,"PYDOC_LOGS" ))
90150
91- validate_paths (cpython_path )
151+ if args .command == "generate_templates" :
152+ tx_project = get_value (args .tx_project ,"--tx-project" ,"PYDOC_TX_PROJECT" )
92153
93154if args .command == "build" :
155+ language = get_value (args .language ,"--language" ,"PYDOC_LANGUAGE" )
156+ version = get_value (args .python_version ,"--python-version" ,"PYDOC_VERSION" )
157+ po_dir = args .po_dir .absolute ()or cpython_path / f"Doc/locales/{ language } /LC_MESSAGES"
158+
159+ if args .command in ["build" ,"generate_templates" ]:
94160if not shutil .which ("make" ):
95161logger .error ("'make' not found. Please install it." )
96162sys .exit (1 )
97163
98164logs_dir .mkdir (exist_ok = True )
99165logger .info (f"Logs will be stored in:{ logs_dir } " )
100166
101- build_docs (language ,version ,logs_dir ,cpython_path )
102- logger .info ("Documentation build completed successfully." )
167+ if args .command == "build" :
168+ validate_cpython_path (cpython_path )
169+ validate_po_dir (po_dir )
170+ build_docs (language ,version ,po_dir ,logs_dir ,cpython_path )
171+ logger .info ("Documentation build completed successfully." )
172+ elif args .command == "generate_templates" :
173+ validate_cpython_path (cpython_path )
174+ validate_tx_config (tx_project )
175+ generate_templates (logs_dir ,cpython_path ,tx_project )
103176
104177
105178if __name__ == "__main__" :