1+ #!/usr/bin/env python3
2+ """
3+ scripts/update_python_version.py
4+
5+ Sync this repo's .po files against a target CPython release tag.
6+
7+ Usage:
8+ python scripts/update_python_version.py v3.14.6
9+ python scripts/update_python_version.py v3.15.0 --keep-src
10+
11+ Run this from anywhere inside the repo (it locates the repo root from
12+ this file's location, assuming it lives in <repo>/scripts/).
13+ """
14+ import argparse
15+ import shutil
16+ import subprocess
17+ import sys
18+ from pathlib import Path
19+
20+ REPO_ROOT = Path (__file__ ).resolve ().parent .parent
21+ WORKDIR = REPO_ROOT / ".cpython-src" # scratch clone, deleted by default when done
22+
23+
24+ def run (cmd , cwd = None ):
25+ print (f"$ { ' ' .join (str (c ) for c in cmd )} " )
26+ subprocess .run (cmd , cwd = cwd , check = True )
27+
28+
29+ def fetch_cpython (tag : str ) -> None :
30+ if WORKDIR .exists ():
31+ shutil .rmtree (WORKDIR )
32+ run ([
33+ "git" , "clone" , "--depth" , "1" , "--branch" , tag ,
34+ "https://github.com/python/cpython.git" , str (WORKDIR ),
35+ ])
36+
37+
38+ def build_gettext (tag : str ) -> Path :
39+ """Build .pot templates from the CPython docs at `tag`, return their root dir."""
40+ doc_dir = WORKDIR / "Doc"
41+ venv_dir = doc_dir / "venv"
42+ run ([sys .executable , "-m" , "venv" , str (venv_dir )])
43+ pip = venv_dir / "bin" / "pip"
44+ sphinx_build = venv_dir / "bin" / "sphinx-build"
45+ run ([str (pip ), "install" , "-r" , "requirements.txt" ], cwd = doc_dir )
46+ run ([str (sphinx_build ), "-b" , "gettext" , "." , "build/gettext" ], cwd = doc_dir )
47+ return doc_dir / "build" / "gettext"
48+
49+
50+ def merge_all (pot_root : Path ) -> None :
51+ updated = new_po = missing_pot = 0
52+
53+ # existing .po files -> merge against matching .pot by relative path
54+ for po_path in sorted (REPO_ROOT .rglob ("*.po" )):
55+ if ".cpython-src" in po_path .parts or ".git" in po_path .parts :
56+ continue
57+ rel = po_path .relative_to (REPO_ROOT )
58+ pot_path = pot_root / rel .with_suffix (".pot" )
59+ if not pot_path .exists ():
60+ print (f" ! no matching .pot for { rel } "
61+ f"(page may have been removed/renamed upstream — review manually)" )
62+ missing_pot += 1
63+ continue
64+ run (["msgmerge" , "--update" , "--backup=off" , str (po_path ), str (pot_path )])
65+ updated += 1
66+
67+ # brand-new .pot files with no .po counterpart yet -> create empty .po via msginit
68+ for pot_path in sorted (pot_root .rglob ("*.pot" )):
69+ rel = pot_path .relative_to (pot_root )
70+ po_path = REPO_ROOT / rel .with_suffix (".po" )
71+ if not po_path .exists ():
72+ po_path .parent .mkdir (parents = True , exist_ok = True )
73+ run (["msginit" , "--no-translator" , "-l" , "fa" , "-i" , str (pot_path ), "-o" , str (po_path )])
74+ new_po += 1
75+
76+ print (f"\n Summary: { updated } .po files merged, { new_po } new .po files created, "
77+ f"{ missing_pot } .po files with no matching upstream source." )
78+
79+
80+ def check_po_files () -> None :
81+ bad = []
82+ for po_path in sorted (REPO_ROOT .rglob ("*.po" )):
83+ if ".cpython-src" in po_path .parts or ".git" in po_path .parts :
84+ continue
85+ result = subprocess .run (
86+ ["msgfmt" , "--check" , "-o" , "/dev/null" , str (po_path )],
87+ capture_output = True , text = True ,
88+ )
89+ if result .returncode != 0 :
90+ bad .append ((po_path , result .stderr .strip ()))
91+
92+ if bad :
93+ print ("\n Broken .po files (fix before committing):" )
94+ for path , err in bad :
95+ print (f" { path } :\n { err } " )
96+ sys .exit (1 )
97+ print ("\n All .po files pass `msgfmt --check`." )
98+
99+
100+ def main () -> None :
101+ parser = argparse .ArgumentParser (description = __doc__ )
102+ parser .add_argument ("tag" , help = "CPython git tag to sync against, e.g. v3.14.6" )
103+ parser .add_argument ("--keep-src" , action = "store_true" ,
104+ help = "keep the scratch CPython checkout instead of deleting it" )
105+ args = parser .parse_args ()
106+
107+ print (f"== Fetching CPython { args .tag } ==" )
108+ fetch_cpython (args .tag )
109+
110+ print ("\n == Building gettext templates ==" )
111+ pot_root = build_gettext (args .tag )
112+
113+ print ("\n == Merging into this repo's .po files ==" )
114+ merge_all (pot_root )
115+
116+ print ("\n == Validating .po files ==" )
117+ check_po_files ()
118+
119+ if not args .keep_src :
120+ shutil .rmtree (WORKDIR , ignore_errors = True )
121+
122+ print (f"\n Done. Review the diff, then commit as something like:\n "
123+ f' git commit -am "Sync translations with CPython { args .tag } "' )
124+
125+
126+ if __name__ == "__main__" :
127+ main ()
0 commit comments