Source code for vsi.tools.patch_wheel

#!/usr/bin/env python

from pathlib import Path
import os
import argparse
import tempfile
import subprocess
import re
from wheel.wheelfile import WHEEL_INFO_RE
from wheel.cli.pack import DIST_INFO_RE
from wheel.cli import WheelError

[docs]def get_parser(): parser = argparse.ArgumentParser( description="Patch a wheel to change the package name or version in the " "wheel metatdata. Note: This will also change any filenames or " "versions in the rest of the file namess (e.g. dist-info " "directory name) but will not change hardcoded strings " "like __version__ in .py files") parser.add_argument('filename', help="Wheel (.whl) file") parser.add_argument('--name', help="The package name") group = parser.add_mutually_exclusive_group() group.add_argument('--version', help="Version number to change to") group.add_argument('--add-local', help="Add to the local version segment without having to " "specify the full version") return parser
[docs]def main(filename, name=None, version=None): filename = Path(filename) parsed_name = WHEEL_INFO_RE.match(filename.name) with tempfile.TemporaryDirectory() as temp_extract: # Extract Wheel (pid := subprocess.Popen(['wheel', 'unpack', '--dest', temp_extract, filename])).wait() if pid.returncode != 0: raise RuntimeError('Wheel unpack failed. Is the wheel bad?') # Find the .dist-info directory dist_info_dirs = [ fn for fn in Path(temp_extract).glob('*/*.dist-info/') if os.path.isdir(os.path.join(temp_extract, fn)) and DIST_INFO_RE.match(str(fn)) ] if len(dist_info_dirs) > 1: raise WheelError(f"Multiple .dist-info's found in {temp_extract}") elif not dist_info_dirs: raise WheelError(f"No .dist-info's found in {temp_extract}") metadata = (Path(dist_info_dirs[0]) / 'METADATA') with open(metadata, 'r') as fid: meta_lines = fid.readlines() original_name = next(line_split[1].strip() for line in meta_lines if (line_split := line.split(':',1))[0] == 'Name') original_version = next(line_split[1].strip() for line in meta_lines if (line_split := line.split(':',1))[0] == 'Version') meta_lines = (patch[key] if (key := line.split(':',1)[0]) in patch.keys() else line for line in meta_lines) # These values will be replaced patch = {} if name is not None: patch['Name'] = f'Name: {name}\n' else: name = original_name if version is not None: patch['Version'] = f'Version: {version}\n' else: version = original_version if name == original_name and version == original_version: raise WheelError('No change detected. Package name and version are ' 'identical') # Patch metadata with open(metadata, 'w') as fid: fid.writelines(meta_lines) # Rename files # https://peps.python.org/pep-0491/#escaping-and-unicode normalized_name = re.sub(r"[^\w\d.]+", "_", name).lower() normalized_version = re.sub(r"[^\w\d.+]+", "_", version).lower() normalized_original_name = re.sub(r"[^\w\d.]+", "_", original_name).lower() normalized_original_version = re.sub(r"[^\w\d.+]+", "_", original_version).lower() os.rename(Path(temp_extract) / f'{normalized_original_name}-{normalized_original_version}' / f'{normalized_original_name}-{normalized_original_version}.dist-info', Path(temp_extract) / f'{normalized_original_name}-{normalized_original_version}' / f'{normalized_name}-{normalized_version}.dist-info') os.rename(Path(temp_extract) / f'{normalized_original_name}-{normalized_original_version}', Path(temp_extract) / f'{normalized_name}-{normalized_version}') # Save new wheel with tempfile.TemporaryDirectory() as temp_wheel: subprocess.Popen(['wheel', 'pack', '--dest', temp_wheel, Path(temp_extract) / f'{normalized_name}-{normalized_version}']).wait() new_wheel = next(Path(temp_wheel).glob('*.whl')) # Replace wheel os.rename(new_wheel, filename.parent / new_wheel.name) # It would be possible for the package name/version to change but the # whl name to rename unchanged due to filename normalization (e.g.: -➡️_) if new_wheel.name != filename.name: os.remove(filename)
if __name__ == '__main__': parser = get_parser() args = parser.parse_args() version = args.version filename = Path(args.filename) if args.add_local is not None: version = WHEEL_INFO_RE.match(filename.name).group('ver') if '+' in version: # https://packaging.python.org/en/latest/specifications/version-specifiers/#local-version-identifiers # "each segment of the local version [is] divided by a ." version = f'{version}.{args.add_local}' else: version = f'{version}+{args.add_local}' main(filename, name=args.name, version=version)