| """A simple function to find the METADATA file and parse it""" |
| |
| _NAME = "Name: " |
| _PROVIDES_EXTRA = "Provides-Extra: " |
| _REQUIRES_DIST = "Requires-Dist: " |
| _VERSION = "Version: " |
| |
| def whl_metadata(*, install_dir, read_fn, logger): |
| """Find and parse the METADATA file in the extracted whl contents dir. |
| |
| Args: |
| install_dir: {type}`path` location where the wheel has been extracted. |
| read_fn: the function used to read files. |
| logger: the function used to log failures. |
| |
| Returns: |
| A struct with parsed values: |
| * `name`: {type}`str` the name of the wheel. |
| * `version`: {type}`str` the version of the wheel. |
| * `requires_dist`: {type}`list[str]` the list of requirements. |
| * `provides_extra`: {type}`list[str]` the list of extras that this package |
| provides. |
| """ |
| metadata_file = find_whl_metadata(install_dir = install_dir, logger = logger) |
| contents = read_fn(metadata_file) |
| |
| result = parse_whl_metadata(contents) |
| |
| if not (result.name and result.version): |
| logger.fail("Failed to parse the wheel METADATA file:\n{}\n{}\n{}".format( |
| 80 * "=", |
| contents.rstrip("\n"), |
| 80 * "=", |
| )) |
| return None |
| |
| return result |
| |
| def parse_whl_metadata(contents): |
| """Parse .whl METADATA file |
| |
| Args: |
| contents: {type}`str` the contents of the file. |
| |
| Returns: |
| A struct with parsed values: |
| * `name`: {type}`str` the name of the wheel. |
| * `version`: {type}`str` the version of the wheel. |
| * `requires_dist`: {type}`list[str]` the list of requirements. |
| * `provides_extra`: {type}`list[str]` the list of extras that this package |
| provides. |
| """ |
| parsed = { |
| "name": "", |
| "provides_extra": [], |
| "requires_dist": [], |
| "version": "", |
| } |
| for line in contents.strip().split("\n"): |
| if not line: |
| # Stop parsing on first empty line, which marks the end of the |
| # headers containing the metadata. |
| break |
| |
| if line.startswith(_NAME): |
| _, _, value = line.partition(_NAME) |
| parsed["name"] = value.strip() |
| elif line.startswith(_VERSION): |
| _, _, value = line.partition(_VERSION) |
| parsed["version"] = value.strip() |
| elif line.startswith(_REQUIRES_DIST): |
| _, _, value = line.partition(_REQUIRES_DIST) |
| parsed["requires_dist"].append(value.strip(" ")) |
| elif line.startswith(_PROVIDES_EXTRA): |
| _, _, value = line.partition(_PROVIDES_EXTRA) |
| parsed["provides_extra"].append(value.strip(" ")) |
| |
| return struct( |
| name = parsed["name"], |
| provides_extra = parsed["provides_extra"], |
| requires_dist = parsed["requires_dist"], |
| version = parsed["version"], |
| ) |
| |
| def find_whl_metadata(*, install_dir, logger): |
| """Find the whl METADATA file in the install_dir. |
| |
| Args: |
| install_dir: {type}`path` location where the wheel has been extracted. |
| logger: the function used to log failures. |
| |
| Returns: |
| {type}`path` The path to the METADATA file. |
| """ |
| dist_info = None |
| for maybe_dist_info in install_dir.readdir(): |
| # first find the ".dist-info" folder |
| if not (maybe_dist_info.is_dir and maybe_dist_info.basename.endswith(".dist-info")): |
| continue |
| |
| dist_info = maybe_dist_info |
| metadata_file = dist_info.get_child("METADATA") |
| |
| if metadata_file.exists: |
| return metadata_file |
| |
| break |
| |
| if dist_info: |
| logger.fail("The METADATA file for the wheel could not be found in '{}/{}'".format(install_dir.basename, dist_info.basename)) |
| else: |
| logger.fail("The '*.dist-info' directory could not be found in '{}'".format(install_dir.basename)) |
| return None |
| |
| def parse_entry_points(contents): |
| """Parses entry_points.txt contents and returns console_scripts and gui_scripts entries. |
| |
| Args: |
| contents: {type}`str` The contents of the entry_points.txt file. |
| |
| Returns: |
| {type}`dict[str, dict]` A dict keyed by the original entry point name. |
| """ |
| entries = {} |
| seen_lower_names = {} |
| current_group = None |
| current_group_lower = None |
| for line in contents.splitlines(): |
| line = line.strip() |
| if not line or line.startswith("#"): |
| continue |
| if line.startswith("[") and line.endswith("]"): |
| current_group = line[1:-1].strip() |
| current_group_lower = current_group.lower() |
| continue |
| |
| if current_group_lower in ("console_scripts", "gui_scripts"): |
| name, _, ref = line.partition("=") |
| name = name.strip() |
| |
| # Names are case-insensitive. |
| # See https://packaging.python.org/en/latest/specifications/entry-points/#data-model |
| # Entry points must be unique for a given name because they turn |
| # into files and may be on a case-insensitive file system. |
| lower_name = name.lower() |
| if lower_name in seen_lower_names: |
| continue |
| seen_lower_names[lower_name] = True |
| |
| # remove inline comments |
| ref, _, _ = ref.partition("#") |
| ref = ref.strip() |
| |
| extras = "" |
| if "[" in ref and ref.endswith("]"): |
| ref, _, extras_part = ref.partition("[") |
| extras = extras_part[:-1].strip() |
| ref = ref.strip() |
| |
| module, _, attribute = ref.partition(":") |
| entries[name] = { |
| "attribute": attribute.strip(), |
| "extras": extras, |
| "group": current_group, |
| "module": module.strip(), |
| "name": name, |
| } |
| return entries |