1# Copyright 2023 The Bazel Authors. All rights reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Utility class to inspect an extracted wheel directory""" 16import email 17from typing import Dict, Optional, Set, Tuple 18 19import installer 20import pkg_resources 21from pip._vendor.packaging.utils import canonicalize_name 22 23 24class Wheel: 25 """Representation of the compressed .whl file""" 26 27 def __init__(self, path: str): 28 self._path = path 29 30 @property 31 def path(self) -> str: 32 return self._path 33 34 @property 35 def name(self) -> str: 36 # TODO Also available as installer.sources.WheelSource.distribution 37 name = str(self.metadata["Name"]) 38 return canonicalize_name(name) 39 40 @property 41 def metadata(self) -> email.message.Message: 42 with installer.sources.WheelFile.open(self.path) as wheel_source: 43 metadata_contents = wheel_source.read_dist_info("METADATA") 44 metadata = installer.utils.parse_metadata_file(metadata_contents) 45 return metadata 46 47 @property 48 def version(self) -> str: 49 # TODO Also available as installer.sources.WheelSource.version 50 return str(self.metadata["Version"]) 51 52 def entry_points(self) -> Dict[str, Tuple[str, str]]: 53 """Returns the entrypoints defined in the current wheel 54 55 See https://packaging.python.org/specifications/entry-points/ for more info 56 57 Returns: 58 Dict[str, Tuple[str, str]]: A mapping of the entry point's name to it's module and attribute 59 """ 60 with installer.sources.WheelFile.open(self.path) as wheel_source: 61 if "entry_points.txt" not in wheel_source.dist_info_filenames: 62 return dict() 63 64 entry_points_mapping = dict() 65 entry_points_contents = wheel_source.read_dist_info("entry_points.txt") 66 entry_points = installer.utils.parse_entrypoints(entry_points_contents) 67 for script, module, attribute, script_section in entry_points: 68 if script_section == "console": 69 entry_points_mapping[script] = (module, attribute) 70 71 return entry_points_mapping 72 73 def dependencies(self, extras_requested: Optional[Set[str]] = None) -> Set[str]: 74 dependency_set = set() 75 76 for wheel_req in self.metadata.get_all("Requires-Dist", []): 77 req = pkg_resources.Requirement(wheel_req) # type: ignore 78 79 if req.marker is None or any( 80 req.marker.evaluate({"extra": extra}) 81 for extra in extras_requested or [""] 82 ): 83 dependency_set.add(req.name) # type: ignore 84 85 return dependency_set 86 87 def unzip(self, directory: str) -> None: 88 installation_schemes = { 89 "purelib": "/site-packages", 90 "platlib": "/site-packages", 91 "headers": "/include", 92 "scripts": "/bin", 93 "data": "/data", 94 } 95 destination = installer.destinations.SchemeDictionaryDestination( 96 installation_schemes, 97 # TODO Should entry_point scripts also be handled by installer rather than custom code? 98 interpreter="/dev/null", 99 script_kind="posix", 100 destdir=directory, 101 bytecode_optimization_levels=[], 102 ) 103 104 with installer.sources.WheelFile.open(self.path) as wheel_source: 105 installer.install( 106 source=wheel_source, 107 destination=destination, 108 additional_metadata={ 109 "INSTALLER": b"https://github.com/bazelbuild/rules_python", 110 }, 111 ) 112