1717 from ddev .cli .application import Application
1818 from ddev .validation .tracker import ValidationTracker
1919
20+ # Python.org URLs
21+ PYTHON_FTP_URL = "https://www.python.org/ftp/python/"
22+ PYTHON_MACOS_PKG_URL_TEMPLATE = "https://www.python.org/ftp/python/{version}/python-{version}-macos11.pkg"
23+ PYTHON_SBOM_LINUX_URL_TEMPLATE = "https://www.python.org/ftp/python/{version}/Python-{version}.tgz.spdx.json"
24+ PYTHON_SBOM_WINDOWS_URL_TEMPLATE = "https://www.python.org/ftp/python/{version}/python-{version}-amd64.exe.spdx.json"
25+
26+ # Regex patterns for Dockerfile updates
27+ # Linux: ENV PYTHON3_VERSION=3.13.7 (no quotes, matches version at end of line)
28+ LINUX_VERSION_PATTERN = re .compile (r'(ENV PYTHON3_VERSION=)(\d+\.\d+\.\d+)$' , re .MULTILINE )
29+ # Windows: ENV PYTHON_VERSION="3.13.7" (with quotes)
30+ WINDOWS_VERSION_PATTERN = re .compile (r'(ENV PYTHON_VERSION=")(\d+\.\d+\.\d+)(")' , re .MULTILINE )
31+
32+ # SHA256 patterns must match the Python-specific ones:
33+ # Linux: SHA256 that comes after VERSION="${PYTHON3_VERSION}"
34+ LINUX_SHA_PATTERN = re .compile (r'VERSION="\$\{PYTHON3_VERSION\}"[^\n]*\n[^\n]*SHA256="([0-9a-f]+)"' , re .MULTILINE )
35+ # Windows: -Hash in the same RUN block with python-$Env:PYTHON_VERSION-amd64.exe
36+ WINDOWS_SHA_PATTERN = re .compile (
37+ r'python-\$Env:PYTHON_VERSION-amd64\.exe[^\n]*\n[^\n]*-Hash\s+\'([0-9a-f]+)\'' , re .MULTILINE
38+ )
39+
2040
2141@click .command ('update-python-version' , short_help = 'Upgrade the Python version used in the repository.' )
2242@click .pass_obj
@@ -125,19 +145,6 @@ def update_dockerfiles_python_version(
125145 tracker .error (('Dockerfiles' ,), message = f'Missing SHA256 hash entry: { error } ' )
126146 return
127147
128- # Linux: ENV PYTHON3_VERSION=3.13.7 (no quotes, matches version at end of line)
129- # Windows: ENV PYTHON_VERSION="3.13.7" (with quotes)
130- linux_version_pattern = re .compile (r'(ENV PYTHON3_VERSION=)(\d+\.\d+\.\d+)$' , re .MULTILINE )
131- windows_version_pattern = re .compile (r'(ENV PYTHON_VERSION=")(\d+\.\d+\.\d+)(")' , re .MULTILINE )
132-
133- # SHA256 patterns must match the Python-specific ones:
134- # Linux: SHA256 that comes after VERSION="${PYTHON3_VERSION}"
135- # Windows: -Hash in the same RUN block with python-$Env:PYTHON_VERSION-amd64.exe
136- linux_sha_pattern = re .compile (r'VERSION="\$\{PYTHON3_VERSION\}"[^\n]*\n[^\n]*SHA256="([0-9a-f]+)"' , re .MULTILINE )
137- windows_sha_pattern = re .compile (
138- r'python-\$Env:PYTHON_VERSION-amd64\.exe[^\n]*\n[^\n]*-Hash\s+\'([0-9a-f]+)\'' , re .MULTILINE
139- )
140-
141148 for dockerfile in dockerfiles :
142149 if not dockerfile .exists ():
143150 tracker .error ((dockerfile .name ,), message = f'File not found: { dockerfile } ' )
@@ -150,8 +157,8 @@ def update_dockerfiles_python_version(
150157 continue
151158
152159 is_windows = 'windows-x86_64' in dockerfile .parts
153- version_pattern = windows_version_pattern if is_windows else linux_version_pattern
154- sha_pattern = windows_sha_pattern if is_windows else linux_sha_pattern
160+ version_pattern = WINDOWS_VERSION_PATTERN if is_windows else LINUX_VERSION_PATTERN
161+ sha_pattern = WINDOWS_SHA_PATTERN if is_windows else LINUX_SHA_PATTERN
155162 target_sha = windows_sha if is_windows else linux_sha
156163
157164 def replace_version (match : re .Match [str ], _is_windows = is_windows ) -> str :
@@ -211,7 +218,7 @@ def update_macos_python_version(app: Application, new_version: str, tracker: Val
211218 tracker .error (('macOS workflow' ,), message = 'Could not find PYTHON3_DOWNLOAD_URL' )
212219 return
213220
214- new_url = f'https://www.python.org/ftp/python/ { new_version } /python- { new_version } -macos11.pkg'
221+ new_url = PYTHON_MACOS_PKG_URL_TEMPLATE . format ( version = new_version )
215222 indent = target_line [: target_line .index ('PYTHON3_DOWNLOAD_URL' )]
216223 new_line = f'{ indent } PYTHON3_DOWNLOAD_URL: "{ new_url } "'
217224
@@ -270,11 +277,9 @@ def get_latest_python_version(app: Application, major_minor: str) -> str | None:
270277 Returns:
271278 Latest version string (e.g., "3.13.1") or None if not found
272279 """
273- url = "https://www.python.org/ftp/python/"
274-
275280 try :
276281 # Explicitly verify SSL/TLS certificate
277- response = requests .get (url , timeout = 30 , verify = True )
282+ response = requests .get (PYTHON_FTP_URL , timeout = 30 , verify = True )
278283 response .raise_for_status ()
279284 except requests .RequestException as e :
280285 app .display_error (f"Error fetching Python versions: { e } " )
@@ -328,18 +333,13 @@ def get_python_sha256_hashes(app: Application, version: str) -> dict[str, str]:
328333 if not validate_version_string (version ):
329334 raise ValueError (f"Invalid version format: { version } " )
330335
331- # Steps:
332- # 1. Construct SBOM URLs for the files we need:
333- # - Linux source tarball:
334- # https://www.python.org/ftp/python/{version}/Python-{version}.tgz.spdx.json
335- # - Windows AMD64 installer:
336- # https://www.python.org/ftp/python/{version}/python-{version}-amd64.exe.spdx.json
337- SBOM_URLS = [
338- f"https://www.python.org/ftp/python/{ version } /Python-{ version } .tgz.spdx.json" ,
339- f"https://www.python.org/ftp/python/{ version } /python-{ version } -amd64.exe.spdx.json" ,
336+ # Construct SBOM URLs for the files we need
337+ sbom_urls = [
338+ PYTHON_SBOM_LINUX_URL_TEMPLATE .format (version = version ),
339+ PYTHON_SBOM_WINDOWS_URL_TEMPLATE .format (version = version ),
340340 ]
341341
342- # 2. Download and parse each SBOM JSON file
342+ # Download and parse each SBOM JSON file
343343 async def get_sbom_data (client , url ):
344344 try :
345345 # Explicitly verify SSL/TLS certificates
@@ -362,7 +362,7 @@ async def fetch_sbom_data(urls):
362362 async with httpx .AsyncClient (verify = True ) as client :
363363 return await asyncio .gather (* (get_sbom_data (client , url ) for url in urls ))
364364
365- sbom_packages = asyncio .run (fetch_sbom_data (SBOM_URLS ))
365+ sbom_packages = asyncio .run (fetch_sbom_data (sbom_urls ))
366366
367367 # Find the CPython package in the SBOM packages
368368 linux_cpython_package = next ((package for package in sbom_packages [0 ] if package .get ('name' ) == "CPython" ), None )
0 commit comments