• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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