-
Notifications
You must be signed in to change notification settings - Fork 31
/
Copy pathpoet.py
executable file
·260 lines (215 loc) · 8.94 KB
/
poet.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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
#!/usr/bin/env python
""" homebrew-pypi-poet
Invoked like "poet foo" for some package foo **which is presently
installed in sys.path**, determines which packages foo and its dependents
depend on, downloads them from pypi and computes their checksums, and
spits out Homebrew resource stanzas.
"""
from __future__ import print_function
import argparse
import codecs
from collections import OrderedDict
from contextlib import closing
from hashlib import sha256
import json
import logging
import os
import sys
import warnings
import pkg_resources
from .templates import FORMULA_TEMPLATE, RESOURCE_TEMPLATE
from .version import __version__
try:
# Python 2.x
from urllib2 import urlopen
except ImportError:
# Python 3.x
from urllib.request import urlopen
# Show warnings and greater by default
logging.basicConfig(level=int(os.environ.get("POET_DEBUG", 30)))
class PackageNotInstalledWarning(UserWarning):
pass
class PackageVersionNotFoundWarning(UserWarning):
pass
class ConflictingDependencyWarning(UserWarning):
pass
def recursive_dependencies(package):
if not isinstance(package, pkg_resources.Requirement):
raise TypeError("Expected a Requirement; got a %s" % type(package))
discovered = {package.project_name.lower()}
visited = set()
def walk(package):
if not isinstance(package, pkg_resources.Requirement):
raise TypeError("Expected a Requirement; got a %s" % type(package))
if package in visited:
return
visited.add(package)
extras = package.extras
if package == "requests":
extras += ("security",)
try:
reqs = pkg_resources.get_distribution(package).requires(extras)
except pkg_resources.DistributionNotFound:
return
discovered.update(req.project_name.lower() for req in reqs)
for req in reqs:
walk(req)
walk(package)
return sorted(discovered)
def research_package(name, version=None):
with closing(urlopen("https://pypi.io/pypi/{}/json".format(name))) as f:
reader = codecs.getreader("utf-8")
pkg_data = json.load(reader(f))
d = {}
d['name'] = pkg_data['info']['name']
d['homepage'] = pkg_data['info'].get('home_page', '')
artefact = None
if version:
for pypi_version in pkg_data['releases']:
if pkg_resources.safe_version(pypi_version) == version:
for version_artefact in pkg_data['releases'][pypi_version]:
if version_artefact['packagetype'] == 'sdist':
artefact = version_artefact
break
if artefact is None:
warnings.warn("Could not find an exact version match for "
"{} version {}; using newest instead".
format(name, version), PackageVersionNotFoundWarning)
if artefact is None: # no version given or exact match not found
for url in pkg_data['urls']:
if url['packagetype'] == 'sdist':
artefact = url
break
if artefact:
d['url'] = artefact['url']
if 'digests' in artefact and 'sha256' in artefact['digests']:
logging.debug("Using provided checksum for %s", name)
d['checksum'] = artefact['digests']['sha256']
else:
logging.debug("Fetching sdist to compute checksum for %s", name)
with closing(urlopen(artefact['url'])) as f:
d['checksum'] = sha256(f.read()).hexdigest()
logging.debug("Done fetching %s", name)
else: # no sdist found
d['url'] = ''
d['checksum'] = ''
warnings.warn("No sdist found for %s" % name)
d['checksum_type'] = 'sha256'
return d
def make_graph(pkg):
"""Returns a dictionary of information about pkg & its recursive deps.
Given a string, which can be parsed as a requirement specifier, return a
dictionary where each key is the name of pkg or one of its recursive
dependencies, and each value is a dictionary returned by research_package.
(No, it's not really a graph.)
"""
ignore = ['argparse', 'pip', 'setuptools', 'wsgiref']
pkg_deps = recursive_dependencies(pkg_resources.Requirement.parse(pkg))
dependencies = {key: {} for key in pkg_deps if key not in ignore}
installed_packages = pkg_resources.working_set
versions = {package.key: package.version for package in installed_packages}
for package in dependencies:
try:
dependencies[package]['version'] = versions[package]
except KeyError:
warnings.warn("{} is not installed so we cannot compute "
"resources for its dependencies.".format(package),
PackageNotInstalledWarning)
dependencies[package]['version'] = None
for package in dependencies:
package_data = research_package(package, dependencies[package]['version'])
dependencies[package].update(package_data)
return OrderedDict(
[(package, dependencies[package]) for package in sorted(dependencies.keys())]
)
def formula_for(package, also=None):
also = also or []
req = pkg_resources.Requirement.parse(package)
package_name = req.project_name
nodes = merge_graphs(make_graph(p) for p in [package] + also)
resources = [value for key, value in nodes.items()
if key.lower() != package_name.lower()]
if package_name in nodes:
root = nodes[package_name]
elif package_name.lower() in nodes:
root = nodes[package_name.lower()]
else:
raise Exception("Could not find package {} in nodes {}".format(package, nodes.keys()))
python = "python" if sys.version_info.major == 2 else "python3"
return FORMULA_TEMPLATE.render(package=root,
resources=resources,
python=python,
ResourceTemplate=RESOURCE_TEMPLATE)
def resources_for(packages):
nodes = merge_graphs(make_graph(p) for p in packages)
return '\n\n'.join([RESOURCE_TEMPLATE.render(resource=node)
for node in nodes.values()])
def merge_graphs(graphs):
result = {}
for g in graphs:
for key in g:
if key not in result:
result[key] = g[key]
elif result[key] == g[key]:
pass
else:
warnings.warn(
"Merge conflict: {l.name} {l.version} and "
"{r.name} {r.version}; using the former.".
format(l=result[key], r=g[key]),
ConflictingDependencyWarning)
return OrderedDict([k, result[k]] for k in sorted(result.keys()))
def main():
parser = argparse.ArgumentParser(
description='Generate Homebrew resource stanzas for pypi packages '
'and their dependencies.')
actions = parser.add_mutually_exclusive_group()
actions.add_argument(
'--single', '-s', metavar='package', nargs='+',
help='Generate a resource stanza for one or more packages, '
'without considering dependencies.')
actions.add_argument(
'--formula', '-f', metavar='package',
help='Generate a complete formula for a pypi package with its '
'recursive pypi dependencies as resources.')
actions.add_argument(
'--resources', '-r', metavar='package',
help='Generate resource stanzas for a package and its recursive '
'dependencies (default).')
parser.add_argument(
'--also', '-a', metavar='package', action='append', default=[],
help='Specify an additional package that should be added to the '
'resource list with its recursive dependencies. May not be used '
'with --single. May be specified more than once.')
parser.add_argument('package', help=argparse.SUPPRESS, nargs='?')
parser.add_argument(
'-V', '--version', action='version',
version='homebrew-pypi-poet {}'.format(__version__))
args = parser.parse_args()
if (args.formula or args.resources) and args.package:
print('--formula and --resources take a single argument.',
file=sys.stderr)
parser.print_usage(sys.stderr)
return 1
if args.also and args.single:
print("Can't use --also with --single",
file=sys.stderr)
parser.print_usage(sys.stderr)
return 1
if args.formula:
print(formula_for(args.formula, args.also))
elif args.single:
for i, package in enumerate(args.single):
data = research_package(package)
print(RESOURCE_TEMPLATE.render(resource=data))
if i != len(args.single)-1:
print()
else:
package = args.resources or args.package
if not package:
parser.print_usage(sys.stderr)
return 1
print(resources_for([package] + args.also))
return 0
if __name__ == '__main__':
sys.exit(main())