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 not defined, and no value was provided ." )
42
+ logger .error (f"' { arg_name } ' not provided and the environment variable { env_var_name } is not set ." )
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__" :
0 commit comments