6
6
import contextlib
7
7
import logging
8
8
import os
9
+ import re
9
10
import shutil
10
11
import subprocess
11
12
import sys
12
13
from pathlib import Path
13
14
from typing import Optional
14
15
16
+ from sphinx_intl .transifex import create_txconfig ,update_txconfig_resources
17
+
15
18
ROOTDIR = Path (__file__ ).resolve ().parent .parent
16
- COMMANDS = ["build" ]
19
+ COMMANDS = ["build" , 'generate_templates' ]
17
20
18
21
logging .basicConfig (level = logging .INFO ,format = "%(asctime)s - %(levelname)s - %(message)s" )
19
22
logger = logging .getLogger (__name__ )
@@ -25,81 +28,151 @@ def configure_parser() -> argparse.ArgumentParser:
25
28
parser .add_argument ("command" ,choices = COMMANDS ,help = "The command to execute" )
26
29
parser .add_argument ("-l" ,"--language" ,help = "Language for the translated documentation" )
27
30
parser .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" )
30
35
return parser
31
36
32
37
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 :
34
39
"""Return a CLI argument or environment variable value."""
35
40
value = arg_value or os .getenv (env_var_name )
36
41
if 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 ." )
38
43
sys .exit (1 )
39
44
return value
40
45
41
46
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." )
48
50
sys .exit (1 )
49
51
50
52
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
+
55
58
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)
59
93
60
94
try :
61
95
logger .info (f"Building documentation for{ language } , Python{ version } ." )
62
96
subprocess .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 ) } "
64
98
],check = True )
65
99
66
100
if warning_log .exists ()and not warning_log .stat ().st_size :
67
101
warning_log .unlink ()
68
- logger .info ("Empty warning log file removed ." )
102
+ logger .info ("Removed empty warning log file." )
69
103
70
104
except subprocess .CalledProcessError as e :
71
105
logger .error (f"Make command failed:{ e } " )
72
106
sys .exit (1 )
73
107
74
108
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 } " )
79
129
sys .exit (1 )
80
130
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
+
81
142
82
143
def main ()-> None :
83
144
parser = configure_parser ()
84
145
args = parser .parse_args ()
85
146
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
89
148
cpython_path = args .cpython_path
149
+ logs_dir = Path (get_value (str (args .logs_dir ),"--logs-dir" ,"PYDOC_LOGS" ))
90
150
91
- validate_paths (cpython_path )
151
+ if args .command == "generate_templates" :
152
+ tx_project = get_value (args .tx_project ,"--tx-project" ,"PYDOC_TX_PROJECT" )
92
153
93
154
if 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" ]:
94
160
if not shutil .which ("make" ):
95
161
logger .error ("'make' not found. Please install it." )
96
162
sys .exit (1 )
97
163
98
164
logs_dir .mkdir (exist_ok = True )
99
165
logger .info (f"Logs will be stored in:{ logs_dir } " )
100
166
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 )
103
176
104
177
105
178
if __name__ == "__main__" :