Skip to content

Commit 5fdc937

Browse files
committed
feat: adding tags CLI interface
usage: wheel tags [-h] [--remove] [--python-tag TAG] [--abi-tag TAG] [--platform-tag TAG] [--build NUMBER] [wheel ...] Make a new wheel with given tags. Any tags unspecified will remain the same. Separate multiple tags with a dot. Starting with a dot will append to the existing tags. The original file will remain unless --remove is given. The output file(s) will be displayed on stdout. positional arguments: wheel Existing wheel(s) to retag options: -h, --help Show this help message and exit --remove Remove the original files, keeping only the renamed ones --python-tag TAG Specify an interpreter tag(s) --abi-tag TAG Specify an ABI tag(s) --platform-tag TAG Specify a platform tag(s) --build NUMBER Specify a build number
1 parent 0acd203 commit 5fdc937

File tree

3 files changed

+351
-0
lines changed

3 files changed

+351
-0
lines changed

src/wheel/cli/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,33 @@ def convert_f(args):
3535
convert(args.files, args.dest_dir, args.verbose)
3636

3737

38+
def tags_f(args):
39+
from .tags import tags
40+
41+
for name in tags(
42+
args.wheel,
43+
args.python_tag and args.python_tag.split('.'),
44+
args.abi_tag and args.abi_tag.split('.'),
45+
args.platform_tag and args.platform_tag.split('.'),
46+
args.build,
47+
args.remove
48+
):
49+
print(name)
50+
51+
3852
def version_f(args):
3953
from .. import __version__
4054
print("wheel %s" % __version__)
4155

4256

57+
TAGS_HELP = '''\
58+
Make a new wheel with given tags. Any tags unspecified will remain the same.
59+
Separate multiple tags with a dot. Starting with a dot will append to the
60+
existing tags. The original file will remain unless --remove is given. The
61+
output file(s) will be displayed on stdout.
62+
'''
63+
64+
4365
def parser():
4466
p = argparse.ArgumentParser()
4567
s = p.add_subparsers(help="commands")
@@ -64,6 +86,17 @@ def parser():
6486
convert_parser.add_argument('--verbose', '-v', action='store_true')
6587
convert_parser.set_defaults(func=convert_f)
6688

89+
tags_parser = s.add_parser('tags', help='Add or replace the tags on a wheel',
90+
description=TAGS_HELP)
91+
tags_parser.add_argument('wheel', nargs='*', help='Existing wheel(s) to retag')
92+
tags_parser.add_argument('--remove', action='store_true',
93+
help='Remove the original files, keeping only the renamed ones')
94+
tags_parser.add_argument('--python-tag', metavar='TAG', help='Specify an interpreter tag(s)')
95+
tags_parser.add_argument('--abi-tag', metavar='TAG', help='Specify an ABI tag(s)')
96+
tags_parser.add_argument('--platform-tag', metavar='TAG', help='Specify a platform tag(s)')
97+
tags_parser.add_argument('--build', type=int, metavar='NUMBER', help='Specify a build number')
98+
tags_parser.set_defaults(func=tags_f)
99+
67100
version_parser = s.add_parser('version', help='Print version and exit')
68101
version_parser.set_defaults(func=version_f)
69102

src/wheel/cli/tags.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from __future__ import print_function
2+
3+
import os
4+
import sys
5+
import tempfile
6+
import itertools
7+
from contextlib import contextmanager
8+
import shutil
9+
10+
from ..wheelfile import WheelFile
11+
from .pack import pack
12+
from .unpack import unpack
13+
14+
try:
15+
from typing import List, Optional, Iterator
16+
except ImportError:
17+
pass
18+
19+
20+
@contextmanager
21+
def redirect_stdout(new_target):
22+
old_target, sys.stdout = sys.stdout, new_target
23+
try:
24+
yield new_target
25+
finally:
26+
sys.stdout = old_target
27+
28+
29+
@contextmanager
30+
def temporary_directory():
31+
try:
32+
dirname = tempfile.mkdtemp()
33+
yield dirname
34+
finally:
35+
shutil.rmtree(dirname)
36+
37+
38+
class InWheelCtx(object):
39+
@property
40+
def parsed_filename(self):
41+
return self.wheel.parsed_filename
42+
43+
@property
44+
def filename(self):
45+
return self.wheel.filename
46+
47+
def __init__(self, wheel, tmpdir):
48+
self.wheel = WheelFile(wheel)
49+
self.tmpdir = tmpdir
50+
self.build_number = None
51+
# If dirname is unset, don't pack a new wheel
52+
self.dirname = None
53+
54+
def __enter__(self):
55+
with redirect_stdout(sys.stderr):
56+
unpack(self.wheel.filename, self.tmpdir)
57+
return self
58+
59+
def __exit__(self, *args):
60+
if self.dirname:
61+
with redirect_stdout(sys.stderr):
62+
pack(
63+
os.path.join(
64+
self.tmpdir, self.wheel.parsed_filename.group("namever")
65+
),
66+
self.dirname,
67+
self.build_number,
68+
)
69+
70+
71+
def compute_tags(original_tags, new_tags):
72+
# type: (List[str], Optional[List[str]]) -> List[str]
73+
"""Add or replace tags."""
74+
75+
if not new_tags:
76+
return original_tags
77+
78+
if new_tags[0] == '':
79+
return original_tags + new_tags[1:]
80+
else:
81+
return new_tags
82+
83+
84+
def tags(
85+
wheels,
86+
python_tags=None,
87+
abi_tags=None,
88+
platform_tags=None,
89+
build_number=None,
90+
remove=False,
91+
):
92+
# type: (List[str], Optional[List[str]], Optional[List[str]], Optional[List[str]], Optional[int], bool) -> Iterator[str] # noqa: E501
93+
"""Change the tags on a wheel file.
94+
95+
The tags are left unchanged if they are not specified. To specify "none",
96+
use ["none"]. To append to the previous tags, use ["", ...].
97+
98+
:param wheels: The paths to the wheels.
99+
:param python_tags: The Python tags to set.
100+
:param abi_tags: The ABI tags to set.
101+
:param platform_tags: The platform tags to set.
102+
:param build_number: The build number to set.
103+
:param remove: Remove the original wheel.
104+
"""
105+
106+
for wheel in wheels:
107+
with temporary_directory() as tmpdir, InWheelCtx(wheel, tmpdir) as wfctx:
108+
namever = wfctx.parsed_filename.group('namever')
109+
build = wfctx.parsed_filename.group('build')
110+
original_python_tags = wfctx.parsed_filename.group('pyver').split('.')
111+
original_abi_tags = wfctx.parsed_filename.group('abi').split('.')
112+
orignial_plat_tags = wfctx.parsed_filename.group('plat').split('.')
113+
114+
if build_number is not None:
115+
build = str(build_number)
116+
117+
final_python_tags = compute_tags(original_python_tags, python_tags)
118+
final_abi_tags = compute_tags(original_abi_tags, abi_tags)
119+
final_plat_tags = compute_tags(orignial_plat_tags, platform_tags)
120+
121+
final_tags = [
122+
'.'.join(sorted(final_python_tags)),
123+
'.'.join(sorted(final_abi_tags)),
124+
'.'.join(sorted(final_plat_tags)),
125+
]
126+
127+
if build:
128+
final_tags.insert(0, build)
129+
final_tags.insert(0, namever)
130+
131+
original_wheel_name = os.path.basename(wfctx.filename)
132+
final_wheel_name = '-'.join(final_tags) + '.whl'
133+
134+
if original_wheel_name != final_wheel_name:
135+
136+
wheelinfo = os.path.join(
137+
tmpdir, namever, wfctx.wheel.dist_info_path, 'WHEEL'
138+
)
139+
with open(wheelinfo, 'rb+') as f:
140+
lines = [line for line in f if not line.startswith(b'Tag:')]
141+
for a, b, c in itertools.product(
142+
final_python_tags, final_abi_tags, final_plat_tags
143+
):
144+
lines.append(
145+
'Tag: {}-{}-{}\r\n'.format(a, b, c).encode('ascii')
146+
)
147+
f.seek(0)
148+
f.truncate()
149+
f.write(b''.join(lines))
150+
151+
wfctx.build_number = build
152+
wfctx.dirname = os.path.dirname(wheel)
153+
154+
if remove:
155+
os.remove(wheel)
156+
157+
yield final_wheel_name

tests/cli/test_tags.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from wheel.cli.tags import tags
2+
import os
3+
import py
4+
import pytest
5+
6+
from wheel.wheelfile import WheelFile
7+
from wheel.cli import parser
8+
9+
THISDIR = os.path.dirname(__file__)
10+
TESTWHEEL_NAME = 'test-1.0-py2.py3-none-any.whl'
11+
TESTWHEEL_PATH = os.path.join(THISDIR, '..', 'testdata', TESTWHEEL_NAME)
12+
13+
14+
@pytest.fixture
15+
def wheelpath(tmpdir):
16+
fn = tmpdir.mkdir("wheels").join(TESTWHEEL_NAME)
17+
py.path.local(TESTWHEEL_PATH).copy(fn)
18+
return fn
19+
20+
21+
def test_tags_no_args(wheelpath):
22+
(newname,) = tags([str(wheelpath)])
23+
assert TESTWHEEL_NAME == newname
24+
assert wheelpath.exists()
25+
26+
27+
def test_python_tags(wheelpath):
28+
(newname,) = tags([str(wheelpath)], python_tags=['py3'])
29+
assert TESTWHEEL_NAME.replace('py2.py3', 'py3') == newname
30+
output_file = wheelpath.dirpath(newname)
31+
with WheelFile(str(output_file)) as f:
32+
output = f.read(f.dist_info_path + '/WHEEL')
33+
assert (
34+
output
35+
== b'Wheel-Version: 1.0\r\nGenerator: bdist_wheel (0.30.0)'
36+
b'\r\nRoot-Is-Purelib: false\r\nTag: py3-none-any\r\n'
37+
)
38+
output_file.remove()
39+
40+
(newname,) = tags([str(wheelpath)], python_tags=['py2.py3'])
41+
assert TESTWHEEL_NAME == newname
42+
43+
(newname,) = tags([str(wheelpath)], python_tags=['', 'py4'], remove=True)
44+
assert not wheelpath.exists()
45+
assert TESTWHEEL_NAME.replace('py2.py3', 'py2.py3.py4') == newname
46+
output_file = wheelpath.dirpath(newname)
47+
output_file.remove()
48+
49+
50+
def test_abi_tags(wheelpath):
51+
(newname,) = tags([str(wheelpath)], abi_tags=['cp33m'])
52+
assert TESTWHEEL_NAME.replace('none', 'cp33m') == newname
53+
output_file = wheelpath.dirpath(newname)
54+
output_file.remove()
55+
56+
(newname,) = tags([str(wheelpath)], abi_tags=['abi3', 'cp33m'])
57+
assert TESTWHEEL_NAME.replace('none', 'abi3.cp33m') == newname
58+
output_file = wheelpath.dirpath(newname)
59+
output_file.remove()
60+
61+
(newname,) = tags([str(wheelpath)], abi_tags=['none'])
62+
assert TESTWHEEL_NAME == newname
63+
64+
(newname,) = tags([str(wheelpath)], abi_tags=['', 'abi3', 'cp33m'], remove=True)
65+
assert not wheelpath.exists()
66+
assert TESTWHEEL_NAME.replace('none', 'abi3.cp33m.none') == newname
67+
output_file = wheelpath.dirpath(newname)
68+
output_file.remove()
69+
70+
71+
def test_plat_tags(wheelpath):
72+
(newname,) = tags([str(wheelpath)], platform_tags=['linux_x86_64'])
73+
assert TESTWHEEL_NAME.replace('any', 'linux_x86_64') == newname
74+
output_file = wheelpath.dirpath(newname)
75+
assert output_file.exists()
76+
output_file.remove()
77+
78+
(newname,) = tags([str(wheelpath)], platform_tags=['linux_x86_64', 'win32'])
79+
assert TESTWHEEL_NAME.replace('any', 'linux_x86_64.win32') == newname
80+
output_file = wheelpath.dirpath(newname)
81+
assert output_file.exists()
82+
output_file.remove()
83+
84+
(newname,) = tags([str(wheelpath)], platform_tags=['', 'linux_x86_64', 'win32'])
85+
assert TESTWHEEL_NAME.replace('any', 'any.linux_x86_64.win32') == newname
86+
output_file = wheelpath.dirpath(newname)
87+
assert output_file.exists()
88+
output_file.remove()
89+
90+
(newname,) = tags([str(wheelpath)], platform_tags=['any'])
91+
assert TESTWHEEL_NAME == newname
92+
93+
94+
def test_build_number(wheelpath):
95+
(newname,) = tags([str(wheelpath)], build_number=1)
96+
assert TESTWHEEL_NAME.replace('-py2', '-1-py2') == newname
97+
output_file = wheelpath.dirpath(newname)
98+
assert output_file.exists()
99+
output_file.remove()
100+
101+
102+
def test_multi_tags(wheelpath):
103+
(newname,) = tags(
104+
[str(wheelpath)],
105+
platform_tags=['linux_x86_64'],
106+
python_tags=['', 'py4'],
107+
build_number=1,
108+
)
109+
assert 'test-1.0-1-py2.py3.py4-none-linux_x86_64.whl' == newname
110+
111+
output_file = wheelpath.dirpath(newname)
112+
assert output_file.exists()
113+
with WheelFile(str(output_file)) as f:
114+
output = f.read(f.dist_info_path + '/WHEEL')
115+
assert (
116+
output
117+
== b'Wheel-Version: 1.0\r\nGenerator: bdist_wheel (0.30.0)\r\nRoot-Is-Purelib:'
118+
b' false\r\nTag: py2-none-linux_x86_64\r\nTag: py3-none-linux_x86_64\r\nTag:'
119+
b' py4-none-linux_x86_64\r\nBuild: 1\r\n'
120+
)
121+
output_file.remove()
122+
123+
124+
def test_tags_command(capsys, wheelpath):
125+
args = [
126+
'tags',
127+
'--python-tag', 'py3',
128+
'--abi-tag', 'cp33m',
129+
'--platform-tag', 'linux_x86_64',
130+
'--build', '7',
131+
str(wheelpath),
132+
]
133+
p = parser()
134+
args = p.parse_args(args)
135+
args.func(args)
136+
assert wheelpath.exists()
137+
138+
newname = capsys.readouterr().out.strip()
139+
assert 'test-1.0-7-py3-cp33m-linux_x86_64.whl' == newname
140+
output_file = wheelpath.dirpath(newname)
141+
output_file.remove()
142+
143+
144+
def test_tags_command_del(capsys, wheelpath):
145+
args = [
146+
'tags',
147+
'--python-tag', '.py4',
148+
'--abi-tag', 'cp33m',
149+
'--platform-tag', 'linux_x86_64',
150+
'--remove',
151+
str(wheelpath),
152+
]
153+
p = parser()
154+
args = p.parse_args(args)
155+
args.func(args)
156+
assert not wheelpath.exists()
157+
158+
newname = capsys.readouterr().out.strip()
159+
assert 'test-1.0-py2.py3.py4-cp33m-linux_x86_64.whl' == newname
160+
output_file = wheelpath.dirpath(newname)
161+
output_file.remove()

0 commit comments

Comments
 (0)