@@ -299,21 +299,59 @@ def make_source(
299299 return source
300300
301301
302+ def parse_req_hashes (raw_lines : list [str ]) -> dict [str , set [str ]]:
303+ result : dict [str , set [str ]] = {}
304+ for line in raw_lines :
305+ hashes = set (re .findall (r"--hash=sha256:([a-f0-9]+)" , line ))
306+ if not hashes :
307+ continue
308+ name_part = re .split (r"[=\s;]" , line .split ("--hash" )[0 ].strip ())[0 ]
309+ if not name_part :
310+ continue
311+ normalized = normalize_name (name_part )
312+ result .setdefault (normalized , set ()).update (hashes )
313+ return result
314+
315+
302316def resolve_package_sources (
303317 name : str ,
304318 version : str ,
305319 candidates : list [str ],
306320 is_preferred : bool ,
321+ known_hashes : set [str ] | None = None ,
307322) -> tuple [list [dict ], list [str ]]:
323+ sources_out : list [dict ] = []
308324 pypi_files : list [dict ] | None = None
309325
326+ def fetch_pypi_files (name : str , version : str ) -> list [dict ]:
327+ url = f"https://pypi.org/pypi/{ name } /{ version } /json"
328+ print (f"Fetching PyPI metadata for { name } =={ version } " )
329+ with urllib .request .urlopen (url ) as response : # noqa: S310
330+ return json .loads (response .read ().decode ("utf-8" ))["urls" ]
331+
310332 def get_pypi_files () -> list [dict ]:
311333 nonlocal pypi_files
312334 if pypi_files is None :
313- url = f"https://pypi.org/pypi/{ name } /{ version } /json"
314- print (f"Fetching PyPI metadata for { name } =={ version } " )
315- with urllib .request .urlopen (url ) as response : # noqa: S310
316- pypi_files = json .loads (response .read ().decode ("utf-8" ))["urls" ]
335+ all_files = fetch_pypi_files (name , version )
336+ if known_hashes :
337+ pypi_hashes = {f ["digests" ]["sha256" ] for f in all_files }
338+ if known_hashes != pypi_hashes :
339+ print (
340+ f"\n WARNING: Requirements file does not include hashes for all "
341+ f"artifacts for { name } =={ version } . Resolution may be"
342+ "restricted.\n "
343+ )
344+ missing = known_hashes - {f ["digests" ]["sha256" ] for f in all_files }
345+ if missing :
346+ sys .exit (
347+ f"ERROR: Hash(es) { missing } for { name } =={ version } "
348+ "not found on PyPI. Aborting."
349+ )
350+ pypi_files = [
351+ f for f in all_files if f ["digests" ]["sha256" ] in known_hashes
352+ ]
353+ else :
354+ pypi_files = all_files
317355 return pypi_files
318356
319357 def is_universal (filename : str ) -> bool :
@@ -440,12 +478,17 @@ def collect(compat_fn) -> list[dict]:
440478 if any (is_platform_wheel (f ["filename" ]) for f in get_pypi_files ()):
441479 return [], [f"__PLATFORM_ONLY__:{ name } " ]
442480
443- return [], [f"{ name } : No suitable source found on PyPI" ]
481+ if known_hashes :
482+ return [], [
483+ f"{ name } : No suitable source found among artifacts matching the "
484+ "hashes in the requirements file."
485+ ]
486+ else :
487+ return [], [f"{ name } : No suitable source found on PyPI" ]
444488
445489 assert SUPPORTED_TAG_SET is not None
446490 native_py_ver = runtime_python_ver (SUPPORTED_TAG_SET )
447491
448- sources_out : list [dict ] = []
449492 errors : list [str ] = []
450493
451494 for arch in DEFAULT_WHEEL_ARCHES :
@@ -461,7 +504,15 @@ def collect(compat_fn) -> list[dict]:
461504 arch_candidates = arch_platform_candidates (arch , cast (int , py_ver ))
462505
463506 if not arch_candidates :
464- errors .append (f"{ name } : No platform wheel found for arch '{ arch } ' on PyPI" )
507+ if known_hashes :
508+ errors .append (
509+ f"{ name } : No compatible platform wheel for arch '{ arch } ' among artifacts matching "
510+ "the hashes in the requirements file."
511+ )
512+ else :
513+ errors .append (
514+ f"{ name } : No platform wheel found for arch '{ arch } ' on PyPI"
515+ )
465516 continue
466517
467518 wheel = max (
@@ -720,28 +771,37 @@ def to_ver(v: str) -> Version | None:
720771
721772
722773packages = []
774+ req_hashes_by_pkg : dict [str , set [str ]] = {}
775+
723776if opts .requirements_file :
724777 requirements_file_input = os .path .expanduser (opts .requirements_file )
725778 try :
726779 with open (requirements_file_input ) as in_req_file :
727- reqs = parse_continuation_lines (in_req_file )
780+ raw_lines = list (parse_continuation_lines (in_req_file ))
781+ req_hashes_by_pkg = parse_req_hashes (raw_lines )
728782 reqs_as_str = handle_req_env_markers (
729- "\n " .join ([ r .split ("--hash" )[0 ] for r in reqs ] )
783+ "\n " .join (r .split ("--hash" )[0 ] for r in raw_lines )
730784 )
731- reqs_list_raw = reqs_as_str .splitlines ()
732- py_version_regex = re .compile (
733- r";.*python_version .+$"
734- ) # Remove when pip-generator can handle python_version
735- reqs_list = [py_version_regex .sub ("" , p ) for p in reqs_list_raw ]
785+ py_version_regex = re .compile (r";.*python_version .+$" )
786+ reqs_list = [
787+ py_version_regex .sub ("" , line ) for line in reqs_as_str .splitlines ()
788+ ]
736789 if opts .ignore_pkg :
737- reqs_new = "\n " .join (i for i in reqs_list if i not in opts .ignore_pkg )
738- else :
739- reqs_new = reqs_as_str
740- packages = list (requirements .parse (reqs_new ))
790+ reqs_list = [
791+ line
792+ for line in reqs_list
793+ if line .strip ().split ("==" )[0 ].strip () not in opts .ignore_pkg
794+ ]
795+ raw_lines = [
796+ line
797+ for line in raw_lines
798+ if line .strip ().split ("==" )[0 ].strip () not in opts .ignore_pkg
799+ ]
800+ packages = list (requirements .parse ("\n " .join (reqs_list )))
741801 with tempfile .NamedTemporaryFile (
742802 "w" , delete = False , prefix = "requirements."
743803 ) as temp_req_file :
744- temp_req_file .write (reqs_new )
804+ temp_req_file .write (" \n " . join ( raw_lines ) )
745805 requirements_file_output = temp_req_file .name
746806 except FileNotFoundError as err :
747807 print (err )
@@ -954,8 +1014,9 @@ def to_ver(v: str) -> Version | None:
9541014 version = get_file_version (candidates [0 ])
9551015 is_preferred = name_cf in prefer_set
9561016
1017+ known_hashes = req_hashes_by_pkg .get (name_cf ) if use_hash else None
9571018 resolved , errors = resolve_package_sources (
958- name_cf , version , candidates , is_preferred
1019+ name_cf , version , candidates , is_preferred , known_hashes
9591020 )
9601021
9611022 for error in errors :
0 commit comments