-
Notifications
You must be signed in to change notification settings - Fork 30
/
wheeler.py
129 lines (105 loc) · 3.95 KB
/
wheeler.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# coding=utf-8
"""
Functionality for building wheels
"""
import glob
import os.path as path
import shutil
import subprocess
import tempfile
from packaging import tags
from pkg_resources import Distribution, Requirement
from wheel_filename import InvalidFilenameError, parse_wheel_filename
from wheel_inspect import inspect_wheel
from wheel_inspect.classes import WheelFile
class BuildError(Exception):
def __init__(self, package, version, root_exception=None):
super(BuildError, self).__init__('Failed to create wheel for {} {}:\n{}\nOutput:\n{}'.format(
package,
version,
root_exception,
root_exception.output if hasattr(root_exception, 'output') and root_exception.output else ''
))
class Builder(object):
"""
Provides a context in which wheels can be generated. If the context goes out of scope
all created files will be removed.
"""
def __enter__(self):
self.scratch_dir = tempfile.mkdtemp()
self.wheelhouse = path.join(self.scratch_dir, 'wheels')
return lambda *args: self.build(*args)
def __exit__(self, exc_type, exc_val, exc_tb):
shutil.rmtree(self.scratch_dir)
def _matches_requirement(self, requirement, wheels):
"""
List wheels matching a requirement.
:param requirement:str : The requirement to satisfy
:param wheels: List of wheels to search.
"""
req = Requirement.parse(requirement)
matching = []
for wheel in wheels:
w = wheel.parsed_filename
dist = Distribution(project_name=w.project, version=w.version)
if dist in req:
matching.append(wheel.path)
return matching
def _find_wheel(self, name, version):
"""
Find a wheel with the given name and version
"""
candidates = [WheelFile(filename) for filename in glob.iglob(path.join(self.wheelhouse, '*.whl'))]
matches = self._matches_requirement('{}=={}'.format(name, version), candidates)
if len(matches) > 0:
return str(matches[0])
else:
raise BuildError(name, version, 'Failed to find the build wheel for {} {}'.format(name, version))
def build(self, package, version):
"""
Build a wheel for the given version of the given project.
:param package: The name of the project
:param version: The version to generate the wheel for
:return: The path of the build wheel. Valid until the context is exited.
"""
try:
subprocess.check_output([
'pip', 'wheel',
'--wheel-dir=' + self.wheelhouse,
'{}=={}'.format(package, version)
], stderr=subprocess.STDOUT)
return self._find_wheel(package, version)
except subprocess.CalledProcessError as e:
raise BuildError(package, version, e)
def is_pure(wheel):
"""
Check whether wheel given by the passed path is pure.
Pure wheels operate independent of the specific Python version and platform.
:param wheel: The path to the wheel to inspect
:return: True if the wheel is pure
"""
return (
inspect_wheel(wheel)
.get("dist_info", {})
.get("wheel", {})
.get("root_is_purelib")
)
def is_compatible(package):
"""
Check whether the given python package is a wheel compatible with the
current platform and python interpreter.
Compatibility is based on https://www.python.org/dev/peps/pep-0425/
"""
try:
w = parse_wheel_filename(package)
for systag in tags.sys_tags():
for tag in w.tag_triples():
if systag in tags.parse_tag(tag):
return True
except InvalidFilenameError:
return False
def has_compatible_wheel(packages):
"""
Check for a compatible wheel in the given list of python packages
"""
return any(is_compatible(pkg) for pkg in packages)