Skip to content

Commit 56be8e8

Browse files
committed
Move whl.sh into a Python script that has unit testing.
1 parent 1aafd84 commit 56be8e8

File tree

6 files changed

+217
-85
lines changed

6 files changed

+217
-85
lines changed

WORKSPACE

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,33 @@ load("@io_bazel_skydoc//skylark:skylark.bzl", "skydoc_repositories")
3434

3535
skydoc_repositories()
3636

37+
# Test data for WHL tool testing.
38+
http_file(
39+
name = "grpc_whl",
40+
sha256 = "c232d6d168cb582e5eba8e1c0da8d64b54b041dd5ea194895a2fe76050916561",
41+
# From https://pypi.python.org/pypi/grpcio/1.6.0
42+
url = ("https://pypi.python.org/packages/c6/28/" +
43+
"67651b4eabe616b27472c5518f9b2aa3f63beab8f62100b26f05ac428639/" +
44+
"grpcio-1.6.0-cp27-cp27m-manylinux1_i686.whl"),
45+
)
46+
47+
http_file(
48+
name = "futures_whl",
49+
sha256 = "c4884a65654a7c45435063e14ae85280eb1f111d94e542396717ba9828c4337f",
50+
# From https://pypi.python.org/pypi/futures
51+
url = ("https://pypi.python.org/packages/a6/1c/" +
52+
"72a18c8c7502ee1b38a604a5c5243aa8c2a64f4bba4e6631b1b8972235dd/" +
53+
"futures-3.1.1-py2-none-any.whl"),
54+
)
55+
56+
http_file(
57+
name = "mock_whl",
58+
sha256 = "5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
59+
# From https://pypi.python.org/pypi/mock
60+
url = ("https://pypi.python.org/packages/e6/35/" +
61+
"f187bdf23be87092bd0f1200d43d23076cee4d0dec109f195173fd3ebc79/" +
62+
"mock-2.0.0-py2.py3-none-any.whl"),
63+
)
3764

3865
# Imports for examples
3966
load("//python:pip.bzl", "pip_import")

python/BUILD

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,21 @@ exports_files([
2222
"whl.bzl",
2323
"whl.sh",
2424
])
25+
26+
load(":python.bzl", "py_library", "py_test")
27+
28+
py_library(
29+
name = "whl",
30+
srcs = ["whl.py"],
31+
)
32+
33+
py_test(
34+
name = "whl_test",
35+
srcs = ["whl_test.py"],
36+
data = [
37+
"@futures_whl//file",
38+
"@grpc_whl//file",
39+
"@mock_whl//file",
40+
],
41+
deps = [":whl"],
42+
)

python/whl.bzl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ def _whl_impl(repository_ctx):
1717
"""Core implementation of whl_library."""
1818

1919
result = repository_ctx.execute([
20+
"python",
2021
repository_ctx.path(repository_ctx.attr._script),
21-
repository_ctx.path(repository_ctx.attr.whl),
22-
repository_ctx.attr.requirements,
22+
"--whl", repository_ctx.path(repository_ctx.attr.whl),
23+
"--requirements", repository_ctx.attr.requirements,
2324
])
2425
if result.return_code:
2526
fail("whl_library failed: %s (%s)" % (result.stdout, result.stderr))
@@ -34,7 +35,7 @@ whl_library = repository_rule(
3435
"requirements": attr.string(),
3536
"_script": attr.label(
3637
executable = True,
37-
default = Label("//python:whl.sh"),
38+
default = Label("//python:whl.py"),
3839
cfg = "host",
3940
),
4041
},

python/whl.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Copyright 2017 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+
"""The packages modules defines classes for interacting with Python packages."""
15+
16+
import argparse
17+
import json
18+
import os
19+
import zipfile
20+
21+
22+
class Wheel(object):
23+
24+
def __init__(self, path):
25+
self._path = path
26+
27+
def _basename(self):
28+
return os.path.basename(self._path)
29+
30+
def distribution(self):
31+
# See https://www.python.org/dev/peps/pep-0427/#file-name-convention
32+
parts = self._basename().split('-')
33+
return parts[0]
34+
35+
def version(self):
36+
# See https://www.python.org/dev/peps/pep-0427/#file-name-convention
37+
parts = self._basename().split('-')
38+
return parts[1]
39+
40+
def _dist_info(self):
41+
# Return the name of the dist-info directory within the .whl file.
42+
return '{}-{}.dist-info'.format(self.distribution(), self.version())
43+
44+
def metadata(self):
45+
# Extract the structured data from metadata.json in the WHL's dist-info
46+
# directory.
47+
with zipfile.ZipFile(self._path, 'r') as whl:
48+
with whl.open(os.path.join(self._dist_info(), 'metadata.json')) as f:
49+
return json.loads(f.read())
50+
51+
def name(self):
52+
return self.metadata().get('name')
53+
54+
def dependencies(self):
55+
# TODO(mattmoor): Is there a schema to follow for this?
56+
run_requires = self.metadata().get('run_requires', [])
57+
for requirement in run_requires:
58+
if 'extra' in requirement:
59+
# TODO(mattmoor): What's the best way to support "extras"?
60+
# https://packaging.python.org/tutorials/installing-packages/#installing-setuptools-extras
61+
continue
62+
requires = requirement.get('requires', [])
63+
for entry in requires:
64+
# Strip off any trailing versioning data.
65+
parts = entry.split(' ', 1)
66+
yield parts[0]
67+
68+
def expand(self, directory):
69+
with zipfile.ZipFile(self._path, 'r') as whl:
70+
whl.extractall(directory)
71+
72+
73+
parser = argparse.ArgumentParser(
74+
description='Unpack a WHL file as a py_library.')
75+
76+
parser.add_argument('--whl', action='store',
77+
help=('The .whl file we are expanding.'))
78+
79+
parser.add_argument('--requirements', action='store',
80+
help='The pip_import from which to draw dependencies.')
81+
82+
parser.add_argument('--directory', action='store', default='.',
83+
help='The directory into which to expand things.')
84+
85+
def main():
86+
args = parser.parse_args()
87+
whl = Wheel(args.whl)
88+
89+
# Extract the files into the current directory
90+
whl.expand(args.directory)
91+
92+
with open(os.path.join(args.directory, 'BUILD'), 'w') as f:
93+
f.write("""
94+
package(default_visibility = ["//visibility:public"])
95+
96+
load("{requirements}", "packages")
97+
98+
py_library(
99+
name = "pkg",
100+
srcs = glob(["**/*.py"]),
101+
data = glob(["**/*"], exclude=["**/*.py", "**/* *", "BUILD", "WORKSPACE"]),
102+
# This makes this directory a top-level in the python import
103+
# search path for anything that depends on this.
104+
imports = ["."],
105+
deps = [{dependencies}],
106+
)""".format(
107+
requirements=args.requirements,
108+
dependencies=','.join([
109+
'packages("%s")' % d
110+
for d in whl.dependencies()
111+
])))
112+
113+
if __name__ == '__main__':
114+
main()

python/whl.sh

Lines changed: 0 additions & 82 deletions
This file was deleted.

python/whl_test.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright 2017 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+
import os
16+
import unittest
17+
18+
from python import whl
19+
20+
21+
def TestData(name):
22+
return os.path.join(os.environ['TEST_SRCDIR'], name)
23+
24+
25+
class WheelTest(unittest.TestCase):
26+
27+
def test_grpc_whl(self):
28+
td = TestData('grpc_whl/file/grpcio-1.6.0-cp27-cp27m-manylinux1_i686.whl')
29+
wheel = whl.Wheel(td)
30+
self.assertEqual(wheel.name(), 'grpcio')
31+
self.assertEqual(wheel.distribution(), 'grpcio')
32+
self.assertEqual(wheel.version(), '1.6.0')
33+
self.assertEqual(set(wheel.dependencies()),
34+
set(['enum34', 'futures', 'protobuf', 'six']))
35+
36+
def test_futures_whl(self):
37+
td = TestData('futures_whl/file/futures-3.1.1-py2-none-any.whl')
38+
wheel = whl.Wheel(td)
39+
self.assertEqual(wheel.name(), 'futures')
40+
self.assertEqual(wheel.distribution(), 'futures')
41+
self.assertEqual(wheel.version(), '3.1.1')
42+
self.assertEqual(set(wheel.dependencies()), set())
43+
44+
def test_mock_whl(self):
45+
td = TestData('mock_whl/file/mock-2.0.0-py2.py3-none-any.whl')
46+
wheel = whl.Wheel(td)
47+
self.assertEqual(wheel.name(), 'mock')
48+
self.assertEqual(wheel.distribution(), 'mock')
49+
self.assertEqual(wheel.version(), '2.0.0')
50+
self.assertEqual(set(wheel.dependencies()),
51+
set(['pbr', 'six', 'funcsigs']))
52+
53+
if __name__ == '__main__':
54+
unittest.main()

0 commit comments

Comments
 (0)