1010import urllib .error
1111from functools import partial
1212from pathlib import Path
13- from typing import Any , Dict , List
13+ from typing import Any
1414from urllib .request import urlopen
1515
1616from pipx import constants , paths
2222# Much of the code in this module is adapted with extreme gratitude from
2323# https://github.com/tusharsadhwani/yen/blob/main/src/yen/github.py
2424
25- MACHINE_SUFFIX : Dict [str , Dict [str , Any ]] = {
25+ MACHINE_SUFFIX : dict [str , dict [str , Any ]] = {
2626 "Darwin" : {
2727 "arm64" : ["aarch64-apple-darwin-install_only.tar.gz" ],
2828 "x86_64" : ["x86_64-apple-darwin-install_only.tar.gz" ],
@@ -71,7 +71,7 @@ def download_python_build_standalone(python_version: str, override: bool = False
7171 logger .warning (f"A previous attempt to install python { python_version } failed. Retrying." )
7272 shutil .rmtree (install_dir )
7373
74- full_version , download_link = resolve_python_version (python_version )
74+ full_version , ( download_link , digest ) = resolve_python_version (python_version )
7575
7676 with tempfile .TemporaryDirectory () as tempdir :
7777 archive = Path (tempdir ) / f"python-{ full_version } .tar.gz"
@@ -81,7 +81,7 @@ def download_python_build_standalone(python_version: str, override: bool = False
8181 _download (full_version , download_link , archive )
8282
8383 # unpack the python build
84- _unpack (full_version , download_link , archive , download_dir )
84+ _unpack (full_version , download_link , archive , download_dir , digest )
8585
8686 # the python installation we want is nested in the tarball
8787 # under a directory named 'python'. We move it to the install
@@ -104,15 +104,13 @@ def _download(full_version: str, download_link: str, archive: Path):
104104 raise PipxError (f"Unable to download python { full_version } build." ) from e
105105
106106
107- def _unpack (full_version , download_link , archive : Path , download_dir : Path ):
107+ def _unpack (full_version , download_link , archive : Path , download_dir : Path , expected_checksum : str ):
108108 with animate (f"Unpacking python { full_version } build" , True ):
109109 # Calculate checksum
110110 with open (archive , "rb" ) as python_zip :
111- checksum = hashlib .sha256 (python_zip .read ()).hexdigest ()
111+ checksum = "sha256:" + hashlib .sha256 (python_zip .read ()).hexdigest ()
112112
113113 # Validate checksum
114- checksum_link = download_link + ".sha256"
115- expected_checksum = urlopen (checksum_link ).read ().decode ().rstrip ("\n " )
116114 if checksum != expected_checksum :
117115 raise PipxError (
118116 f"Checksum mismatch for python { full_version } build. Expected { expected_checksum } , got { checksum } ."
@@ -142,7 +140,7 @@ def get_or_update_index(use_cache: bool = True):
142140 return index
143141
144142
145- def get_latest_python_releases () -> List [ str ]:
143+ def get_latest_python_releases () -> list [ tuple [ str , str | None ] ]:
146144 """Returns the list of python download links from the latest github release."""
147145 try :
148146 with urlopen (GITHUB_API_URL ) as response :
@@ -152,10 +150,10 @@ def get_latest_python_releases() -> List[str]:
152150 # raise
153151 raise PipxError (f"Unable to fetch python-build-standalone release data (from { GITHUB_API_URL } )." ) from e
154152
155- return [asset ["browser_download_url" ] for asset in release_data ["assets" ]]
153+ return [( asset ["browser_download_url" ], asset . get ( "digest" )) for asset in release_data ["assets" ]]
156154
157155
158- def list_pythons (use_cache : bool = True ) -> Dict [str , str ]:
156+ def list_pythons (use_cache : bool = True ) -> dict [str , tuple [ str , str | None ] ]:
159157 """Returns available python versions for your machine and their download links."""
160158 system , machine = platform .system (), platform .machine ()
161159 download_link_suffixes = MACHINE_SUFFIX [system ][machine ]
@@ -168,23 +166,23 @@ def list_pythons(use_cache: bool = True) -> Dict[str, str]:
168166 python_releases = get_or_update_index (use_cache )["releases" ]
169167
170168 available_python_links = [
171- link
169+ ( link , digest )
172170 # Suffixes are in order of preference.
173171 for download_link_suffix in download_link_suffixes
174- for link in python_releases
172+ for link , digest in python_releases
175173 if link .endswith (download_link_suffix )
176174 ]
177175
178- python_versions : dict [str , str ] = {}
179- for link in available_python_links :
176+ python_versions : dict [str , tuple [ str , str | None ] ] = {}
177+ for link , digest in available_python_links :
180178 match = PYTHON_VERSION_REGEX .search (link )
181179 assert match is not None
182180 python_version = match [1 ]
183181 # Don't override already found versions, they are in order of preference
184182 if python_version in python_versions :
185183 continue
186184
187- python_versions [python_version ] = link
185+ python_versions [python_version ] = link , digest
188186
189187 return {
190188 version : python_versions [version ]
0 commit comments