11#!/usr/bin/env python3
22
33
4- import copy
54import difflib
65import logging
76import operator
3433ArchStr = Literal ["32" , "64" , "ARM64" ]
3534
3635
37- class ConfigWinCP (TypedDict ):
36+ class Config (TypedDict ):
3837 identifier : str
3938 version : str
40- arch : str
4139
4240
43- class ConfigWinPP (TypedDict ):
44- identifier : str
45- version : str
46- arch : str
47- url : str
48-
49-
50- class ConfigWinGP (TypedDict ):
51- identifier : str
52- version : str
53- url : str
54-
55-
56- class ConfigApple (TypedDict ):
57- identifier : str
58- version : str
59- url : str
60-
61-
62- class ConfigAndroid (TypedDict ):
63- identifier : str
64- version : str
41+ class ConfigUrl (Config ):
6542 url : str
6643
6744
68- class ConfigPyodide (TypedDict ):
69- identifier : str
70- version : str
45+ class ConfigPyodide (Config ):
7146 default_pyodide_version : str
7247 node_version : str
7348
7449
75- AnyConfig = ConfigWinCP | ConfigWinPP | ConfigWinGP | ConfigApple | ConfigAndroid | ConfigPyodide
76-
77-
7850# The following set of "Versions" classes allow the initial call to the APIs to
7951# be cached and reused in the `update_version_*` methods.
8052
@@ -106,7 +78,7 @@ def __init__(self, arch_str: ArchStr, free_threaded: bool) -> None:
10678
10779 self .version_dict = {Version (v ): v for v in cp_info ["versions" ]}
10880
109- def update_version_windows (self , spec : Specifier ) -> ConfigWinCP | None :
81+ def update_version_windows (self , spec : Specifier ) -> Config | None :
11082 # Specifier.filter selects all non pre-releases that match the spec,
11183 # unless there are only pre-releases, then it selects pre-releases
11284 # instead (like pip)
@@ -121,10 +93,9 @@ def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
12193 flags = "t" if self .free_threaded else ""
12294 version = versions [0 ]
12395 identifier = f"cp{ version .major } { version .minor } { flags } -{ self .arch } "
124- return ConfigWinCP (
96+ return Config (
12597 identifier = identifier ,
12698 version = self .version_dict [version ],
127- arch = self .arch_str ,
12899 )
129100
130101
@@ -146,7 +117,7 @@ def __init__(self) -> None:
146117
147118 self .releases = [r for r in releases if "graalpy_version" in r and "python_version" in r ]
148119
149- def update_version (self , identifier : str , spec : Specifier ) -> AnyConfig :
120+ def update_version (self , identifier : str , spec : Specifier ) -> ConfigUrl :
150121 if "x86_64" in identifier or "amd64" in identifier :
151122 arch = "x86_64"
152123 elif "arm64" in identifier or "aarch64" in identifier :
@@ -172,11 +143,9 @@ def update_version(self, identifier: str, spec: Specifier) -> AnyConfig:
172143
173144 if "macosx" in identifier :
174145 arch = "x86_64" if "x86_64" in identifier else "arm64"
175- config = ConfigApple
176146 platform = "macos"
177147 elif "win" in identifier :
178148 arch = "aarch64" if "arm64" in identifier else "x86_64"
179- config = ConfigWinGP
180149 platform = "windows"
181150 else :
182151 msg = "GraalPy provides downloads for macOS and Windows and is included for manylinux"
@@ -191,7 +160,7 @@ def update_version(self, identifier: str, spec: Specifier) -> AnyConfig:
191160 and rf ["name" ].startswith (f"graalpy-{ gpversion .major } " )
192161 )
193162
194- return config (
163+ return ConfigUrl (
195164 identifier = identifier ,
196165 version = f"{ version .major } .{ version .minor } " ,
197166 url = url ,
@@ -223,7 +192,7 @@ def get_arch_file(self, release: Mapping[str, Any]) -> str:
223192 ]
224193 return urls [0 ] if urls else ""
225194
226- def update_version_windows (self , spec : Specifier ) -> ConfigWinCP :
195+ def update_version_windows (self , spec : Specifier ) -> ConfigUrl :
227196 releases = [r for r in self .releases if spec .contains (r ["python_version" ])]
228197 releases = sorted (releases , key = operator .itemgetter ("pypy_version" ))
229198 releases = [r for r in releases if self .get_arch_file (r )]
@@ -239,14 +208,13 @@ def update_version_windows(self, spec: Specifier) -> ConfigWinCP:
239208 identifier = f"pp{ version .major } { version .minor } -{ version_arch } "
240209 url = self .get_arch_file (release )
241210
242- return ConfigWinPP (
211+ return ConfigUrl (
243212 identifier = identifier ,
244213 version = f"{ version .major } .{ version .minor } " ,
245- arch = self .arch ,
246214 url = url ,
247215 )
248216
249- def update_version_macos (self , spec : Specifier ) -> ConfigApple :
217+ def update_version_macos (self , spec : Specifier ) -> ConfigUrl :
250218 if self .arch not in {"64" , "ARM64" }:
251219 msg = f"'{ self .arch } ' arch not supported yet on macOS"
252220 raise RuntimeError (msg )
@@ -270,7 +238,7 @@ def update_version_macos(self, spec: Specifier) -> ConfigApple:
270238 if "" in rf ["platform" ] == "darwin" and rf ["arch" ] == arch
271239 )
272240
273- return ConfigApple (
241+ return ConfigUrl (
274242 identifier = identifier ,
275243 version = f"{ version .major } .{ version .minor } " ,
276244 url = url ,
@@ -298,16 +266,11 @@ def __init__(self) -> None:
298266 uri = int (release ["resource_uri" ].rstrip ("/" ).split ("/" )[- 1 ])
299267 self .versions_dict [version ] = uri
300268
301- def update_version_macos (
302- self , identifier : str , version : Version , spec : Specifier
303- ) -> ConfigApple | None :
269+ def update_version (self , identifier : str , spec : Specifier , file_ident : str ) -> ConfigUrl | None :
304270 # see note above on Specifier.filter
305271 unsorted_versions = spec .filter (self .versions_dict )
306272 sorted_versions = sorted (unsorted_versions , reverse = True )
307273
308- macver = "x10.9" if version <= Version ("3.8.9999" ) else "11"
309- file_ident = f"macos{ macver } .pkg"
310-
311274 for new_version in sorted_versions :
312275 # Find the first patch version that contains the requested file
313276 uri = self .versions_dict [new_version ]
@@ -319,17 +282,25 @@ def update_version_macos(
319282
320283 urls = [rf ["url" ] for rf in file_info if file_ident in rf ["url" ]]
321284 if urls :
322- return ConfigApple (
285+ return ConfigUrl (
323286 identifier = identifier ,
324287 version = f"{ new_version .major } .{ new_version .minor } " ,
325288 url = urls [0 ],
326289 )
327290
328291 return None
329292
293+ def update_version_macos (
294+ self , identifier : str , version : Version , spec : Specifier
295+ ) -> ConfigUrl | None :
296+ macver = "x10.9" if version <= Version ("3.8.9999" ) else "11"
297+ return self .update_version (identifier , spec , f"macos{ macver } .pkg" )
298+
299+ def update_version_android (self , identifier : str , spec : Specifier ) -> ConfigUrl | None :
300+ return self .update_version (identifier , spec , android_triplet (identifier ))
330301
331- class AndroidVersions :
332- # This should be replaced with official python.org downloads once they're available.
302+
303+ class MavenVersions :
333304 MAVEN_URL = "https://repo.maven.apache.org/maven2/com/chaquo/python/python"
334305
335306 def __init__ (self ) -> None :
@@ -343,18 +314,16 @@ def __init__(self) -> None:
343314 assert isinstance (version_str , str ), version_str
344315 self .versions .append (Version (version_str ))
345316
346- def update_version_android (
347- self , identifier : str , version : Version , spec : Specifier
348- ) -> ConfigAndroid | None :
317+ def update_version_android (self , identifier : str , spec : Specifier ) -> ConfigUrl | None :
349318 sorted_versions = sorted (spec .filter (self .versions ), reverse = True )
350319
351320 # Return a config using the highest version for the given specifier.
352321 if sorted_versions :
353322 max_version = sorted_versions [0 ]
354323 triplet = android_triplet (identifier )
355- return ConfigAndroid (
324+ return ConfigUrl (
356325 identifier = identifier ,
357- version = str ( version ) ,
326+ version = f" { max_version . major } . { max_version . minor } " ,
358327 url = f"{ self .MAVEN_URL } /{ max_version } /python-{ max_version } -{ triplet } .tar.gz" ,
359328 )
360329 else :
@@ -390,11 +359,11 @@ def __init__(self) -> None:
390359 if filename .endswith ("-iOS-support" ):
391360 self .versions_dict [version ][int (build [1 :])] = asset ["browser_download_url" ]
392361
393- def update_version_ios (self , identifier : str , version : Version ) -> ConfigApple | None :
362+ def update_version_ios (self , identifier : str , version : Version ) -> ConfigUrl | None :
394363 # Return a config using the highest build number for the given version.
395364 urls = [url for _ , url in sorted (self .versions_dict .get (version , {}).items ())]
396365 if urls :
397- return ConfigApple (
366+ return ConfigUrl (
398367 identifier = identifier ,
399368 version = str (version ),
400369 url = urls [- 1 ],
@@ -450,11 +419,11 @@ def __init__(self) -> None:
450419 self .windows_t_arm64 = WindowsVersions ("ARM64" , True )
451420 self .windows_pypy_64 = PyPyVersions ("64" )
452421
453- self .macos_cpython = CPythonVersions ()
422+ self .cpython = CPythonVersions ()
454423 self .macos_pypy = PyPyVersions ("64" )
455424 self .macos_pypy_arm64 = PyPyVersions ("ARM64" )
456425
457- self .android = AndroidVersions ()
426+ self .maven = MavenVersions ()
458427 self .ios_cpython = CPythonIOSVersions ()
459428
460429 self .graalpy = GraalPyVersions ()
@@ -466,13 +435,12 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
466435 version = Version (config ["version" ])
467436 spec = Specifier (f"=={ version .major } .{ version .minor } .*" )
468437 log .info ("Reading in %r -> %s @ %s" , str (identifier ), spec , version )
469- orig_config = copy .copy (config )
470- config_update : AnyConfig | None = None
438+ config_update : Config | None = None
471439
472440 # We need to use ** in update due to MyPy (probably a bug)
473441 if "macosx" in identifier :
474442 if identifier .startswith ("cp" ):
475- config_update = self .macos_cpython .update_version_macos (identifier , version , spec )
443+ config_update = self .cpython .update_version_macos (identifier , version , spec )
476444 elif identifier .startswith ("pp" ):
477445 if "macosx_x86_64" in identifier :
478446 config_update = self .macos_pypy .update_version_macos (spec )
@@ -498,7 +466,10 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
498466 elif "win_arm64" in identifier and identifier .startswith ("cp" ):
499467 config_update = self .windows_arm64 .update_version_windows (spec )
500468 elif "android" in identifier :
501- config_update = self .android .update_version_android (identifier , version , spec )
469+ # Python 3.13 is released by Chaquopy on Maven Central.
470+ # Python 3.14 and newer have official releases on python.org.
471+ versions = self .maven if identifier .startswith ("cp313" ) else self .cpython
472+ config_update = versions .update_version_android (identifier , spec )
502473 elif "ios" in identifier :
503474 config_update = self .ios_cpython .update_version_ios (identifier , version )
504475 elif "pyodide" in identifier :
@@ -507,10 +478,10 @@ def update_config(self, config: MutableMapping[str, str]) -> None:
507478 )
508479
509480 assert config_update is not None , f"{ identifier } not found!"
510- config . update ( ** config_update )
511-
512- if config != orig_config :
513- log . info ( " Updated %s to %s" , orig_config , config )
481+ if config_update != config :
482+ log . info ( " Updated %s to %s" , config , config_update )
483+ config . clear ()
484+ config . update ( ** config_update )
514485
515486
516487@click .command ()
0 commit comments