Skip to content

Commit 0310ba6

Browse files
committed
refactor: custom impl
1 parent 37a23e2 commit 0310ba6

File tree

3 files changed

+187
-76
lines changed

3 files changed

+187
-76
lines changed

src/wheel/cli/pack.py

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,8 @@ def pack(directory: str, dest_dir: str, build_number: str | None):
3737
# Read the tags and the existing build number from .dist-info/WHEEL
3838
existing_build_number = None
3939
wheel_file_path = os.path.join(directory, dist_info_dir, "WHEEL")
40-
with open(wheel_file_path) as f:
41-
tags = []
42-
for line in f:
43-
if line.startswith("Tag: "):
44-
tags.append(line.split(" ")[1].rstrip())
45-
elif line.startswith("Build: "):
46-
existing_build_number = line.split(" ")[1].rstrip()
40+
with open(wheel_file_path, "rb") as f:
41+
tags, existing_build_number = read_tags(f.read())
4742

4843
if not tags:
4944
raise WheelError(
@@ -58,28 +53,16 @@ def pack(directory: str, dest_dir: str, build_number: str | None):
5853
name_version += "-" + build_number
5954

6055
if build_number != existing_build_number:
61-
replacement = (
62-
("Build: %s\r\n" % build_number).encode("ascii")
63-
if build_number
64-
else b""
65-
)
6656
with open(wheel_file_path, "rb+") as f:
6757
wheel_file_content = f.read()
68-
wheel_file_content, num_replaced = BUILD_NUM_RE.subn(
69-
replacement, wheel_file_content
70-
)
71-
if not num_replaced:
72-
wheel_file_content += replacement
58+
wheel_file_content = set_build_number(wheel_file_content, build_number)
7359

7460
f.seek(0)
7561
f.truncate()
7662
f.write(wheel_file_content)
7763

7864
# Reassemble the tags for the wheel file
79-
impls = sorted({tag.split("-")[0] for tag in tags})
80-
abivers = sorted({tag.split("-")[1] for tag in tags})
81-
platforms = sorted({tag.split("-")[2] for tag in tags})
82-
tagline = "-".join([".".join(impls), ".".join(abivers), ".".join(platforms)])
65+
tagline = compute_tagline(tags)
8366

8467
# Repack the wheel
8568
wheel_path = os.path.join(dest_dir, f"{name_version}-{tagline}.whl")
@@ -88,3 +71,54 @@ def pack(directory: str, dest_dir: str, build_number: str | None):
8871
wf.write_files(directory)
8972

9073
print("OK")
74+
75+
76+
def read_tags(input_str: bytes) -> tuple[list[str], str | None]:
77+
"""Read tags from a string.
78+
79+
:param input_str: A string containing one or more tags, separated by spaces
80+
:return: A list of tags and a list of build tags
81+
"""
82+
83+
tags = []
84+
existing_build_number = None
85+
for line in input_str.splitlines():
86+
if line.startswith(b"Tag: "):
87+
tags.append(line.split(b" ")[1].rstrip().decode("ascii"))
88+
elif line.startswith(b"Build: "):
89+
existing_build_number = line.split(b" ")[1].rstrip().decode("ascii")
90+
91+
return tags, existing_build_number
92+
93+
94+
def set_build_number(wheel_file_content: str, build_number: str | None) -> str:
95+
"""Compute a build tag and add/replace/remove as necessary.
96+
97+
:param wheel_file_content: The contents of .dist-info/WHEEL
98+
:param build_number: The build tags present in .dist-info/WHEEL
99+
:return: The (modified) contents of .dist-info/WHEEL
100+
"""
101+
replacement = (
102+
("Build: %s\r\n" % build_number).encode("ascii") if build_number else b""
103+
)
104+
105+
wheel_file_content, num_replaced = BUILD_NUM_RE.subn(
106+
replacement, wheel_file_content
107+
)
108+
109+
if not num_replaced:
110+
wheel_file_content += replacement
111+
112+
return wheel_file_content
113+
114+
115+
def compute_tagline(tags: list[str]) -> str:
116+
"""Compute a tagline from a list of tags.
117+
118+
:param tags: A list of tags
119+
:return: A tagline
120+
"""
121+
impls = sorted({tag.split("-")[0] for tag in tags})
122+
abivers = sorted({tag.split("-")[1] for tag in tags})
123+
platforms = sorted({tag.split("-")[2] for tag in tags})
124+
return "-".join([".".join(impls), ".".join(abivers), ".".join(platforms)])

src/wheel/cli/tags.py

Lines changed: 103 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from __future__ import print_function
1+
from __future__ import annotations
22

33
import itertools
44
import os
@@ -8,7 +8,7 @@
88
from contextlib import contextmanager
99

1010
from ..wheelfile import WheelFile
11-
from .pack import pack
11+
from .pack import pack, read_tags, set_build_number
1212
from .unpack import unpack
1313

1414
try:
@@ -84,14 +84,13 @@ def compute_tags(original_tags, new_tags):
8484

8585

8686
def tags(
87-
wheels,
88-
python_tags=None,
89-
abi_tags=None,
90-
platform_tags=None,
91-
build_number=None,
92-
remove=False,
93-
):
94-
# type: (List[str], Optional[List[str]], Optional[List[str]], Optional[List[str]], Optional[int], bool) -> Iterator[str] # noqa: E501
87+
wheels: list[str],
88+
python_tags: list[str] | None = None,
89+
abi_tags: list[str] | None = None,
90+
platform_tags: list[str] = None,
91+
build_number: int | None = None,
92+
remove: bool = False,
93+
) -> Iterator[str]:
9594
"""Change the tags on a wheel file.
9695
9796
The tags are left unchanged if they are not specified. To specify "none",
@@ -106,54 +105,103 @@ def tags(
106105
"""
107106

108107
for wheel in wheels:
109-
with temporary_directory() as tmpdir, InWheelCtx(wheel, tmpdir) as wfctx:
110-
namever = wfctx.parsed_filename.group("namever")
111-
build = wfctx.parsed_filename.group("build")
112-
original_python_tags = wfctx.parsed_filename.group("pyver").split(".")
113-
original_abi_tags = wfctx.parsed_filename.group("abi").split(".")
114-
orignial_plat_tags = wfctx.parsed_filename.group("plat").split(".")
115-
116-
if build_number is not None:
117-
build = str(build_number)
118-
119-
final_python_tags = compute_tags(original_python_tags, python_tags)
120-
final_abi_tags = compute_tags(original_abi_tags, abi_tags)
121-
final_plat_tags = compute_tags(orignial_plat_tags, platform_tags)
122-
123-
final_tags = [
124-
".".join(sorted(final_python_tags)),
125-
".".join(sorted(final_abi_tags)),
126-
".".join(sorted(final_plat_tags)),
108+
with WheelFile(wheel, "r") as f:
109+
wheel_info = f.read(f.dist_info_path + "/WHEEL")
110+
111+
original_wheel_name = os.path.basename(f.filename)
112+
namever = f.parsed_filename.group("namever")
113+
build = f.parsed_filename.group("build")
114+
original_python_tags = f.parsed_filename.group("pyver").split(".")
115+
original_abi_tags = f.parsed_filename.group("abi").split(".")
116+
orignial_plat_tags = f.parsed_filename.group("plat").split(".")
117+
118+
tags, existing_build_number = read_tags(wheel_info)
119+
120+
impls = {tag.split("-")[0] for tag in tags}
121+
abivers = {tag.split("-")[1] for tag in tags}
122+
platforms = {tag.split("-")[2] for tag in tags}
123+
124+
if impls != set(original_python_tags):
125+
raise AssertionError(f"{impls} != {original_python_tags}")
126+
127+
if abivers != set(original_abi_tags):
128+
raise AssertionError(f"{abivers} != {original_abi_tags}")
129+
130+
if platforms != set(orignial_plat_tags):
131+
raise AssertionError(f"{platforms} != {orignial_plat_tags}")
132+
133+
if existing_build_number != build:
134+
raise AssertionError(
135+
f"Incorrect filename '{build}' & "
136+
f"*.dist-info/WHEEL '{existing_build_number}' build numbers"
137+
)
138+
139+
# Start changing as needed
140+
if build_number is not None:
141+
build = str(build_number)
142+
143+
final_python_tags = compute_tags(original_python_tags, python_tags)
144+
final_abi_tags = compute_tags(original_abi_tags, abi_tags)
145+
final_plat_tags = compute_tags(orignial_plat_tags, platform_tags)
146+
147+
final_tags = [
148+
".".join(sorted(final_python_tags)),
149+
".".join(sorted(final_abi_tags)),
150+
".".join(sorted(final_plat_tags)),
151+
]
152+
153+
if build:
154+
final_tags.insert(0, build)
155+
final_tags.insert(0, namever)
156+
157+
final_wheel_name = "-".join(final_tags) + ".whl"
158+
159+
if original_wheel_name != final_wheel_name:
160+
tags = [
161+
f"{a}-{b}-{c}"
162+
for a, b, c in itertools.product(
163+
final_python_tags, final_abi_tags, final_plat_tags
164+
)
127165
]
128166

129-
if build:
130-
final_tags.insert(0, build)
131-
final_tags.insert(0, namever)
167+
original_wheel_path = os.path.join(
168+
os.path.dirname(f.filename), original_wheel_name
169+
)
170+
final_wheel_path = os.path.join(
171+
os.path.dirname(f.filename), final_wheel_name
172+
)
173+
174+
with WheelFile(original_wheel_path, "r") as fin, WheelFile(
175+
final_wheel_path, "w"
176+
) as fout:
177+
fout.comment = fin.comment # preserve the comment
178+
for item in fin.infolist():
179+
if item.filename == f.dist_info_path + "/RECORD":
180+
continue
181+
if item.filename == f.dist_info_path + "/WHEEL":
182+
content = fin.read(item)
183+
content = set_tags(content, tags)
184+
content = set_build_number(content, build)
185+
fout.writestr(item, content)
186+
else:
187+
fout.writestr(item, fin.read(item))
188+
189+
if remove:
190+
os.remove(original_wheel_path)
132191

133-
original_wheel_name = os.path.basename(wfctx.filename)
134-
final_wheel_name = "-".join(final_tags) + ".whl"
192+
yield final_wheel_name
135193

136-
if original_wheel_name != final_wheel_name:
137194

138-
wheelinfo = os.path.join(
139-
tmpdir, namever, wfctx.wheel.dist_info_path, "WHEEL"
140-
)
141-
with open(wheelinfo, "rb+") as f:
142-
lines = [line for line in f if not line.startswith(b"Tag:")]
143-
for a, b, c in itertools.product(
144-
final_python_tags, final_abi_tags, final_plat_tags
145-
):
146-
lines.append(
147-
"Tag: {}-{}-{}\r\n".format(a, b, c).encode("ascii")
148-
)
149-
f.seek(0)
150-
f.truncate()
151-
f.write(b"".join(lines))
152-
153-
wfctx.build_number = build
154-
wfctx.dirname = os.path.dirname(wheel)
155-
156-
if remove:
157-
os.remove(wheel)
195+
def set_tags(in_string: bytes, tags: list[str]) -> bytes:
196+
"""Set the tags in the .dist-info/WHEEL file contents.
158197
159-
yield final_wheel_name
198+
:param in_string: The string to modify.
199+
:param tags: The tags to set.
200+
"""
201+
202+
lines = [line for line in in_string.splitlines() if not line.startswith(b"Tag:")]
203+
for tag in tags:
204+
lines.append(b"Tag: " + tag.encode("ascii"))
205+
in_string = b"\r\n".join(lines) + b"\r\n"
206+
207+
return in_string

tests/cli/test_tags.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from zipfile import ZipFile
23

34
import py
45
import pytest
@@ -166,3 +167,31 @@ def test_tags_command_del(capsys, wheelpath):
166167
assert "test-1.0-py2.py3.py4-cp33m-linux_x86_64.whl" == newname
167168
output_file = wheelpath.dirpath(newname)
168169
output_file.remove()
170+
171+
172+
def test_permission_bits(capsys, wheelpath):
173+
args = [
174+
"tags",
175+
"--python-tag",
176+
".py4",
177+
str(wheelpath),
178+
]
179+
p = parser()
180+
args = p.parse_args(args)
181+
args.func(args)
182+
183+
newname = capsys.readouterr().out.strip()
184+
assert "test-1.0-py2.py3.py4-none-any.whl" == newname
185+
output_file = wheelpath.dirpath(newname)
186+
187+
with ZipFile(str(output_file), "r") as outf:
188+
with ZipFile(str(wheelpath), "r") as inf:
189+
for member in inf.namelist():
190+
if not member.endswith("/RECORD"):
191+
out_attr = outf.getinfo(member).external_attr
192+
inf_attr = inf.getinfo(member).external_attr
193+
assert (
194+
out_attr == inf_attr
195+
), f"{member} 0x{out_attr:012o} != 0x{inf_attr:012o}"
196+
197+
output_file.remove()

0 commit comments

Comments
 (0)