-
Couldn't load subscription status.
- Fork 1
[Command] Addition of a simple command make_variant
#35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b93b2f5
3852ce6
159a22f
39ce5e5
327f451
50693bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,212 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import logging | ||
| import os | ||
| import pathlib | ||
| import re | ||
| import tempfile | ||
|
|
||
| import wheel.cli.pack as whl_pck | ||
| from wheel.cli.unpack import unpack as wheel_unpack | ||
|
|
||
| from variantlib.api import VariantDescription | ||
| from variantlib.api import VariantProperty | ||
| from variantlib.constants import METADATA_VARIANT_HASH_HEADER | ||
| from variantlib.constants import METADATA_VARIANT_PROPERTY_HEADER | ||
| from variantlib.constants import VARIANT_HASH_LEN | ||
| from variantlib.constants import WHEEL_NAME_VALIDATION_REGEX | ||
| from variantlib.errors import ValidationError | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
| logger.setLevel(logging.INFO) | ||
|
|
||
|
|
||
| def wheel_variant_pack( | ||
| directory: str | pathlib.Path, | ||
| dest_dir: str | pathlib.Path, | ||
| variant_hash: str, | ||
| build_number: str | None = None, | ||
| ) -> str: | ||
| """Repack a previously unpacked wheel directory into a new wheel file. | ||
| The .dist-info/WHEEL file must contain one or more tags so that the target | ||
| wheel file name can be determined. | ||
| This function is heavily taken from: | ||
| https://github.com/pypa/wheel/blob/main/src/wheel/_commands/pack.py#L14 | ||
| Minimal changes tried to be applied to make it work with the Variant Hash. | ||
| :param directory: The unpacked wheel directory | ||
| :param dest_dir: Destination directory (defaults to the current directory) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it? Or is it required? |
||
| :param variant_hash: The hash of the variant to be stored | ||
| """ | ||
|
|
||
| # Input Validation | ||
| variant_hash_pattern = rf"^[a-fA-F0-9]{{{VARIANT_HASH_LEN}}}$" | ||
| if not re.match(variant_hash_pattern, variant_hash): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
| raise ValidationError(f"Invalid Variant Hash Value `{variant_hash}` ...") | ||
|
|
||
| # Find the .dist-info directory | ||
| dist_info_dirs = [ | ||
| fn | ||
| for fn in os.listdir(directory) # noqa: PTH208 | ||
| if os.path.isdir(os.path.join(directory, fn)) and whl_pck.DIST_INFO_RE.match(fn) # noqa: PTH112, PTH118 | ||
| ] | ||
| if len(dist_info_dirs) > 1: | ||
| raise whl_pck.WheelError( | ||
| f"Multiple .dist-info directories found in {directory}" | ||
| ) | ||
| if not dist_info_dirs: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you're checking the length of a list, and you're explicitly calling |
||
| raise whl_pck.WheelError(f"No .dist-info directories found in {directory}") | ||
|
|
||
| # Determine the target wheel filename | ||
| dist_info_dir = dist_info_dirs[0] | ||
| name_version = whl_pck.DIST_INFO_RE.match(dist_info_dir).group("namever") | ||
|
|
||
| # Read the tags and the existing build number from .dist-info/WHEEL | ||
| wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL") # noqa: PTH118 | ||
| with open(wheel_file_path, "rb") as f: # noqa: PTH123 | ||
| info = whl_pck.BytesParser(policy=whl_pck.email.policy.compat32).parse(f) | ||
| tags: list[str] = info.get_all("Tag", []) | ||
| existing_build_number = info.get("Build") | ||
|
|
||
| if not tags: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another place where it's generally better to use I generally prefer to be as explicit about the truthiness test as possible. E.g. if you know you're testing the length of a sequence, use |
||
| raise whl_pck.WheelError( | ||
| f"No tags present in {dist_info_dir}/WHEEL; cannot determine target " | ||
| f"wheel filename" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm kind of surprised you didn't get a linter warning here, since the line 78 string doesn't need to be an f-string. |
||
| ) | ||
|
|
||
| # Set the wheel file name and add/replace/remove the Build tag in .dist-info/WHEEL | ||
| build_number = build_number if build_number is not None else existing_build_number | ||
| if build_number is not None: | ||
| del info["Build"] | ||
| if build_number: | ||
| info["Build"] = build_number | ||
| name_version += "-" + build_number | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one could be an f-string: |
||
|
|
||
| if build_number != existing_build_number: | ||
| with open(wheel_file_path, "wb") as f: # noqa: PTH123 | ||
| whl_pck.BytesGenerator(f, maxheaderlen=0).flatten(info) | ||
|
|
||
| # Reassemble the tags for the wheel file | ||
| tagline = whl_pck.compute_tagline(tags) | ||
|
|
||
| # Repack the wheel | ||
| wheel_path = os.path.join(dest_dir, f"{name_version}-{tagline}-{variant_hash}.whl") # noqa: PTH118 | ||
| with whl_pck.WheelFile(wheel_path, "w") as wf: | ||
| logging.info( | ||
| "Repacking wheel as `%(wheel_path)s` ...", {"wheel_path": wheel_path} | ||
| ) | ||
| wf.write_files(directory) | ||
|
|
||
| return wheel_path | ||
|
|
||
|
|
||
| def make_variant(args: list[str]) -> None: | ||
| parser = argparse.ArgumentParser( | ||
| prog="make_variant", | ||
| description="Transform a normal Wheel into a Wheel Variant.", | ||
| ) | ||
|
|
||
| parser.add_argument( | ||
| "-p", | ||
| "--property", | ||
| dest="properties", | ||
DEKHTIARJonathan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| type=VariantProperty.from_str, | ||
| required=True, | ||
| action="extend", | ||
| nargs="+", | ||
| help=( | ||
| "Variant Properties to add to the Wheel Variant, can be repeated as many " | ||
| "times as needed" | ||
| ), | ||
| ) | ||
|
|
||
| parser.add_argument( | ||
| "-f", | ||
| "--file", | ||
| dest="input_filepath", | ||
| type=pathlib.Path, | ||
| required=True, | ||
| help="Wheel file to process", | ||
| ) | ||
|
|
||
| parser.add_argument( | ||
| "-o", | ||
| "--output-directory", | ||
| type=pathlib.Path, | ||
| required=True, | ||
| help="Output Directory to use to store the Wheel Variant", | ||
| ) | ||
|
|
||
| parsed_args = parser.parse_args(args) | ||
|
|
||
| input_filepath: pathlib.Path = parsed_args.input_filepath | ||
| output_directory: pathlib.Path = parsed_args.output_directory | ||
|
|
||
| # Input Validation | ||
| if not input_filepath.is_file(): | ||
| raise FileNotFoundError(f"Input Wheel File `{input_filepath}` does not exists.") | ||
|
|
||
| if not output_directory.is_dir(): | ||
| raise FileNotFoundError( | ||
| f"Output Directory `{output_directory}` does not exists." | ||
| ) | ||
|
|
||
| # Input Validation - Wheel Filename is valid and non variant already. | ||
| wheel_info = WHEEL_NAME_VALIDATION_REGEX.match(input_filepath.name) | ||
| if not wheel_info: | ||
| raise ValueError(f"{input_filepath.name!r} is not a valid wheel filename.") | ||
|
|
||
| # Transform properties into a VariantDescription | ||
| vdesc = VariantDescription(properties=parsed_args.properties) | ||
|
|
||
| with tempfile.TemporaryDirectory() as _tmpdir: | ||
| tempdir = pathlib.Path(_tmpdir) | ||
| wheel_unpack(input_filepath, tempdir) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we really need to unpack it. Unfortunately, it looks like I.e. basically open both archives, iterate over members: if not metadata, copy between archives as-is; if metadata, read, alter and write. It will probably even be simpler than the current logic, which seems to make unnecessary changes to the archive (probably because it was adapted from the command altering tags, which we don't do here). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't disagree though it's a little complicated for a first iteration - feel free to experiment and optimize ;) |
||
|
|
||
| wheel_dir = next(tempdir.iterdir()) | ||
|
|
||
| for _dir in wheel_dir.iterdir(): | ||
| if _dir.is_dir() and _dir.name.endswith(".dist-info"): | ||
| distinfo_dir = _dir | ||
| break | ||
| else: | ||
| raise FileNotFoundError("Impossible to find the .dist-info directory.") | ||
|
|
||
| if not (metadata_f := distinfo_dir / "METADATA").exists(): | ||
| raise FileNotFoundError(metadata_f) | ||
|
|
||
| with metadata_f.open(mode="r+") as file: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think it would be better to write a new file and move it into place atomically? E.g. copy lines from You probably still need a try/except that deletes the |
||
| # Read all lines | ||
| lines = file.readlines() | ||
|
|
||
| # Remove trailing empty lines | ||
| while lines and lines[-1].strip() == "": | ||
| lines.pop() | ||
|
|
||
| # Move the file pointer to the beginning | ||
| file.seek(0) | ||
|
|
||
| # Write back the non-empty content | ||
| file.writelines(lines) | ||
|
|
||
| # Truncate the file to remove any remaining old content | ||
| file.truncate() | ||
|
|
||
| file.write(f"{METADATA_VARIANT_HASH_HEADER}: {vdesc.hexdigest}\n") | ||
|
|
||
| for vprop in vdesc.properties: | ||
| file.write(f"{METADATA_VARIANT_PROPERTY_HEADER}: {vprop.to_str()}\n") | ||
|
|
||
| dest_whl_path = wheel_variant_pack( | ||
| directory=wheel_dir, | ||
| dest_dir=output_directory, | ||
| variant_hash=vdesc.hexdigest, | ||
| ) | ||
|
|
||
| logger.info( | ||
| "Variant Wheel Created: `%s`", pathlib.Path(dest_whl_path).resolve() | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,21 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import re | ||
|
|
||
| VARIANT_HASH_LEN = 8 | ||
| CONFIG_FILENAME = "wheelvariant.toml" | ||
|
|
||
| VALIDATION_NAMESPACE_REGEX = r"^[A-Za-z0-9_]+$" | ||
| VALIDATION_FEATURE_REGEX = r"^[A-Za-z0-9_]+$" | ||
| VALIDATION_VALUE_REGEX = r"^[A-Za-z0-9_.]+$" | ||
|
|
||
| METADATA_VARIANT_HASH_HEADER = "Variant-hash" | ||
| METADATA_VARIANT_PROPERTY_HEADER = "Variant" | ||
|
|
||
| WHEEL_NAME_VALIDATION_REGEX = re.compile( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A multi-line regex string with comments would help with readability here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general, I agree here but the regex is copied (and slightly modified) from |
||
| rf"""^(?P<namever>(?P<name>[^\s-]+?)-(?P<ver>[^\s-]*?)) | ||
| ((-(?P<build>\d[^-]*?))?-(?P<pyver>[^\s-]+?)-(?P<abi>[^\s-]+?)-(?P<plat>[^\s-]+?) | ||
| (-(?P<variant_hash>[0-9a-f]{{{VARIANT_HASH_LEN}}}))? | ||
| \.whl|\.dist-info)$""", | ||
| re.VERBOSE, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about using
cli(orcommands) for the optional instead?