summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Grönholm <alex.gronholm@nextday.fi>2021-12-26 20:50:02 +0200
committerAlex Grönholm <alex.gronholm@nextday.fi>2021-12-26 20:50:02 +0200
commit13cc63b105794ad7d014212036a0c1474546c9a4 (patch)
treefaa1776d300e49e829dddb3621b6e9a1cc8f642c
parent83eff2cd2ead5895ddbd8deb10096c85f22c8527 (diff)
parentdcac1db3a6b4be9e8d1d5173970f234ee29768e4 (diff)
downloadwheel-git-13cc63b105794ad7d014212036a0c1474546c9a4.tar.gz
Merge branch 'main' into remove-distutils
-rw-r--r--src/wheel/bdist_wheel.py7
-rwxr-xr-xsrc/wheel/cli/convert.py2
-rw-r--r--src/wheel/cli/pack.py4
-rw-r--r--src/wheel/cli/unpack.py8
-rw-r--r--src/wheel/metadata.py97
-rw-r--r--src/wheel/pkginfo.py17
-rw-r--r--src/wheel/util.py28
-rw-r--r--src/wheel/wheelfile.py45
-rw-r--r--tests/test_pkginfo.py24
-rw-r--r--tests/test_wheelfile.py45
10 files changed, 90 insertions, 187 deletions
diff --git a/src/wheel/bdist_wheel.py b/src/wheel/bdist_wheel.py
index fd3e2e3..da81611 100644
--- a/src/wheel/bdist_wheel.py
+++ b/src/wheel/bdist_wheel.py
@@ -12,7 +12,7 @@ import sys
import sysconfig
import warnings
from collections import OrderedDict
-from email.generator import BytesGenerator
+from email.generator import BytesGenerator, Generator
from glob import iglob
from io import BytesIO
from shutil import rmtree
@@ -25,7 +25,6 @@ from setuptools import Command
from . import __version__ as wheel_version
from .macosx_libfile import calculate_macosx_platform_tag
from .metadata import pkginfo_to_metadata
-from .pkginfo import write_pkg_info
from .util import log
from .vendored.packaging import tags
from .wheelfile import WheelFile
@@ -521,7 +520,9 @@ class bdist_wheel(Command):
if not dependency_links:
adios(dependency_links_path)
- write_pkg_info(os.path.join(distinfo_path, "METADATA"), pkg_info)
+ pkg_info_path = os.path.join(distinfo_path, "METADATA")
+ with open(pkg_info_path, "w", encoding="utf-8") as out:
+ Generator(out, mangle_from_=False, maxheaderlen=0).flatten(pkg_info)
for license_path in self.license_paths:
filename = os.path.basename(license_path)
diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py
index 90afad3..88743fa 100755
--- a/src/wheel/cli/convert.py
+++ b/src/wheel/cli/convert.py
@@ -37,7 +37,7 @@ class _bdist_wheel_tag(bdist_wheel):
return bdist_wheel.get_tag(self)
-def egg2wheel(egg_path, dest_dir):
+def egg2wheel(egg_path: str, dest_dir: str):
filename = os.path.basename(egg_path)
match = egg_info_re.match(filename)
if not match:
diff --git a/src/wheel/cli/pack.py b/src/wheel/cli/pack.py
index 349f722..b50bf22 100644
--- a/src/wheel/cli/pack.py
+++ b/src/wheel/cli/pack.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import os.path
import re
@@ -8,7 +10,7 @@ DIST_INFO_RE = re.compile(r"^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))\.dist-inf
BUILD_NUM_RE = re.compile(br"Build: (\d\w*)$")
-def pack(directory, dest_dir, build_number):
+def pack(directory: str, dest_dir: str, build_number: str | None):
"""Repack a previously unpacked wheel directory into a new wheel file.
The .dist-info/WHEEL file must contain one or more tags so that the target
diff --git a/src/wheel/cli/unpack.py b/src/wheel/cli/unpack.py
index ffd0e81..c6409d4 100644
--- a/src/wheel/cli/unpack.py
+++ b/src/wheel/cli/unpack.py
@@ -1,9 +1,11 @@
-import os.path
+from __future__ import annotations
+
+from pathlib import Path
from ..wheelfile import WheelFile
-def unpack(path, dest="."):
+def unpack(path: str, dest: str = ".") -> None:
"""Unpack a wheel.
Wheel content will be unpacked to {dest}/{name}-{ver}, where {name}
@@ -14,7 +16,7 @@ def unpack(path, dest="."):
"""
with WheelFile(path) as wf:
namever = wf.parsed_filename.group("namever")
- destination = os.path.join(dest, namever)
+ destination = Path(dest) / namever
print(f"Unpacking to: {destination}...", end="", flush=True)
wf.extractall(destination)
diff --git a/src/wheel/metadata.py b/src/wheel/metadata.py
index ace796d..71e887e 100644
--- a/src/wheel/metadata.py
+++ b/src/wheel/metadata.py
@@ -1,16 +1,18 @@
"""
Tools for converting old- to new-style metadata.
"""
+from __future__ import annotations
import os.path
import textwrap
+from email.message import Message
+from email.parser import Parser
+from typing import Iterator, Tuple
-import pkg_resources
+from pkg_resources import Requirement, safe_extra, split_sections
-from .pkginfo import read_pkg_info
-
-def requires_to_requires_dist(requirement):
+def requires_to_requires_dist(requirement: Requirement) -> str:
"""Return the version specifier for a requirement in PEP 345/566 fashion."""
if getattr(requirement, "url", None):
return " @ " + requirement.url
@@ -18,23 +20,28 @@ def requires_to_requires_dist(requirement):
requires_dist = []
for op, ver in requirement.specs:
requires_dist.append(op + ver)
- if not requires_dist:
+
+ if requires_dist:
+ return " (" + ",".join(sorted(requires_dist)) + ")"
+ else:
return ""
- return " (%s)" % ",".join(sorted(requires_dist))
-def convert_requirements(requirements):
+def convert_requirements(requirements: list[str]) -> Iterator[str]:
"""Yield Requires-Dist: strings for parsed requirements strings."""
for req in requirements:
- parsed_requirement = pkg_resources.Requirement.parse(req)
+ parsed_requirement = Requirement.parse(req)
spec = requires_to_requires_dist(parsed_requirement)
extras = ",".join(sorted(parsed_requirement.extras))
if extras:
- extras = "[%s]" % extras
- yield (parsed_requirement.project_name + extras + spec)
+ extras = f"[{extras}]"
+
+ yield parsed_requirement.project_name + extras + spec
-def generate_requirements(extras_require):
+def generate_requirements(
+ extras_require: dict[str, list[str]]
+) -> Iterator[Tuple[str, str]]:
"""
Convert requirements from a setup()-style dictionary to
('Requires-Dist', 'requirement') and ('Provides-Extra', 'extra') tuples.
@@ -48,7 +55,7 @@ def generate_requirements(extras_require):
if ":" in extra: # setuptools extra:condition syntax
extra, condition = extra.split(":", 1)
- extra = pkg_resources.safe_extra(extra)
+ extra = safe_extra(extra)
if extra:
yield "Provides-Extra", extra
if condition:
@@ -62,11 +69,13 @@ def generate_requirements(extras_require):
yield "Requires-Dist", new_req + condition
-def pkginfo_to_metadata(egg_info_path, pkginfo_path):
+def pkginfo_to_metadata(egg_info_path: str, pkginfo_path: str) -> Message:
"""
Convert .egg-info directory with PKG-INFO to the Metadata 2.1 format
"""
- pkg_info = read_pkg_info(pkginfo_path)
+ with open(pkginfo_path, encoding="utf-8") as headers:
+ pkg_info = Parser().parse(headers)
+
pkg_info.replace_header("Metadata-Version", "2.1")
# Those will be regenerated from `requires.txt`.
del pkg_info["Provides-Extra"]
@@ -76,9 +85,7 @@ def pkginfo_to_metadata(egg_info_path, pkginfo_path):
with open(requires_path) as requires_file:
requires = requires_file.read()
- parsed_requirements = sorted(
- pkg_resources.split_sections(requires), key=lambda x: x[0] or ""
- )
+ parsed_requirements = sorted(split_sections(requires), key=lambda x: x[0] or "")
for extra, reqs in parsed_requirements:
for key, value in generate_requirements({extra: reqs}):
if (key, value) not in pkg_info.items():
@@ -86,51 +93,17 @@ def pkginfo_to_metadata(egg_info_path, pkginfo_path):
description = pkg_info["Description"]
if description:
- pkg_info.set_payload(dedent_description(pkg_info))
+ description_lines = pkg_info["Description"].splitlines()
+ dedented_description = "\n".join(
+ # if the first line of long_description is blank,
+ # the first line here will be indented.
+ (
+ description_lines[0].lstrip(),
+ textwrap.dedent("\n".join(description_lines[1:])),
+ "\n",
+ )
+ )
+ pkg_info.set_payload(dedented_description)
del pkg_info["Description"]
return pkg_info
-
-
-def pkginfo_unicode(pkg_info, field):
- """Hack to coax Unicode out of an email Message() - Python 3.3+"""
- text = pkg_info[field]
- field = field.lower()
- if not isinstance(text, str):
- for item in pkg_info.raw_items():
- if item[0].lower() == field:
- text = item[1].encode("ascii", "surrogateescape").decode("utf-8")
- break
-
- return text
-
-
-def dedent_description(pkg_info):
- """
- Dedent and convert pkg_info['Description'] to Unicode.
- """
- description = pkg_info["Description"]
-
- # Python 3 Unicode handling, sorta.
- surrogates = False
- if not isinstance(description, str):
- surrogates = True
- description = pkginfo_unicode(pkg_info, "Description")
-
- description_lines = description.splitlines()
- description_dedent = "\n".join(
- # if the first line of long_description is blank,
- # the first line here will be indented.
- (
- description_lines[0].lstrip(),
- textwrap.dedent("\n".join(description_lines[1:])),
- "\n",
- )
- )
-
- if surrogates:
- description_dedent = description_dedent.encode("utf8").decode(
- "ascii", "surrogateescape"
- )
-
- return description_dedent
diff --git a/src/wheel/pkginfo.py b/src/wheel/pkginfo.py
deleted file mode 100644
index 9ca2a54..0000000
--- a/src/wheel/pkginfo.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""Tools for reading and writing PKG-INFO / METADATA without caring
-about the encoding."""
-
-from email.generator import BytesGenerator
-from email.parser import Parser
-
-
-def read_pkg_info(path):
- with open(path, encoding="ascii", errors="surrogateescape") as headers:
- message = Parser().parse(headers)
-
- return message
-
-
-def write_pkg_info(path, message):
- with open(path, "wb") as out:
- BytesGenerator(out, mangle_from_=False, maxheaderlen=0).flatten(message)
diff --git a/src/wheel/util.py b/src/wheel/util.py
index 1b97175..f4d8149 100644
--- a/src/wheel/util.py
+++ b/src/wheel/util.py
@@ -1,39 +1,21 @@
+from __future__ import annotations
+
import base64
import sys
-def native(s, encoding="utf-8"):
- if isinstance(s, bytes):
- return s.decode(encoding)
- else:
- return s
-
-
-def urlsafe_b64encode(data):
+def urlsafe_b64encode(data: bytes) -> bytes:
"""urlsafe_b64encode without padding"""
return base64.urlsafe_b64encode(data).rstrip(b"=")
-def urlsafe_b64decode(data):
+def urlsafe_b64decode(data: bytes) -> bytes:
"""urlsafe_b64decode without padding"""
pad = b"=" * (4 - (len(data) & 3))
return base64.urlsafe_b64decode(data + pad)
-def as_unicode(s):
- if isinstance(s, bytes):
- return s.decode("utf-8")
- return s
-
-
-def as_bytes(s):
- if isinstance(s, str):
- return s.encode("utf-8")
- else:
- return s
-
-
-def log(msg, *, error=False):
+def log(msg: str, *, error: bool = False) -> None:
stream = sys.stderr if error else sys.stdout
try:
print(msg, file=stream, flush=True)
diff --git a/src/wheel/wheelfile.py b/src/wheel/wheelfile.py
index 1fdd614..fdf4ac4 100644
--- a/src/wheel/wheelfile.py
+++ b/src/wheel/wheelfile.py
@@ -9,14 +9,7 @@ from io import StringIO, TextIOWrapper
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
from wheel.cli import WheelError
-from wheel.util import (
- as_bytes,
- as_unicode,
- log,
- native,
- urlsafe_b64decode,
- urlsafe_b64encode,
-)
+from wheel.util import log, urlsafe_b64decode, urlsafe_b64encode
# Non-greedy matching of an optional build number may be too clever (more
# invalid wheel filenames will match). Separate regex for .dist-info?
@@ -93,18 +86,14 @@ class WheelFile(ZipFile):
)
def open(self, name_or_info, mode="r", pwd=None):
- def _update_crc(newdata, eof=None):
- if eof is None:
- eof = ef._eof
- update_crc_orig(newdata)
- else: # Python 2
- update_crc_orig(newdata, eof)
-
+ def _update_crc(newdata):
+ eof = ef._eof
+ update_crc_orig(newdata)
running_hash.update(newdata)
if eof and running_hash.digest() != expected_hash:
- raise WheelError(f"Hash mismatch for file '{native(ef_name)}'")
+ raise WheelError(f"Hash mismatch for file '{ef_name}'")
- ef_name = as_unicode(
+ ef_name = (
name_or_info.filename if isinstance(name_or_info, ZipInfo) else name_or_info
)
if (
@@ -112,7 +101,7 @@ class WheelFile(ZipFile):
and not ef_name.endswith("/")
and ef_name not in self._file_hashes
):
- raise WheelError(f"No hash found for file '{native(ef_name)}'")
+ raise WheelError(f"No hash found for file '{ef_name}'")
ef = ZipFile.open(self, name_or_info, mode, pwd)
if mode == "r" and not ef_name.endswith("/"):
@@ -159,8 +148,11 @@ class WheelFile(ZipFile):
zinfo.compress_type = compress_type or self.compression
self.writestr(zinfo, data, compress_type)
- def writestr(self, zinfo_or_arcname, bytes, compress_type=None):
- ZipFile.writestr(self, zinfo_or_arcname, bytes, compress_type)
+ def writestr(self, zinfo_or_arcname, data, compress_type=None):
+ if isinstance(data, str):
+ data = data.encode("utf-8")
+
+ ZipFile.writestr(self, zinfo_or_arcname, data, compress_type)
fname = (
zinfo_or_arcname.filename
if isinstance(zinfo_or_arcname, ZipInfo)
@@ -168,11 +160,12 @@ class WheelFile(ZipFile):
)
log(f"adding '{fname}'")
if fname != self.record_path:
- hash_ = self._default_algorithm(bytes)
- self._file_hashes[fname] = hash_.name, native(
- urlsafe_b64encode(hash_.digest())
+ hash_ = self._default_algorithm(data)
+ self._file_hashes[fname] = (
+ hash_.name,
+ urlsafe_b64encode(hash_.digest()).decode("ascii"),
)
- self._file_sizes[fname] = len(bytes)
+ self._file_sizes[fname] = len(data)
def close(self):
# Write RECORD
@@ -186,9 +179,9 @@ class WheelFile(ZipFile):
)
)
writer.writerow((format(self.record_path), "", ""))
- zinfo = ZipInfo(native(self.record_path), date_time=get_zipinfo_datetime())
+ zinfo = ZipInfo(self.record_path, date_time=get_zipinfo_datetime())
zinfo.compress_type = self.compression
zinfo.external_attr = 0o664 << 16
- self.writestr(zinfo, as_bytes(data.getvalue()))
+ self.writestr(zinfo, data.getvalue())
ZipFile.close(self)
diff --git a/tests/test_pkginfo.py b/tests/test_pkginfo.py
deleted file mode 100644
index 544f9d5..0000000
--- a/tests/test_pkginfo.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from email.parser import Parser
-
-from wheel.pkginfo import write_pkg_info
-
-
-def test_pkginfo_mangle_from(tmpdir):
- """
- Test that write_pkginfo() will not prepend a ">" to a line starting with "From".
- """
- metadata = """\
-Metadata-Version: 2.1
-Name: foo
-
-From blahblah
-
-====
-Test
-====
-
-"""
- message = Parser().parsestr(metadata)
- pkginfo_file = tmpdir.join("PKGINFO")
- write_pkg_info(str(pkginfo_file), message)
- assert pkginfo_file.read_text("ascii") == metadata
diff --git a/tests/test_wheelfile.py b/tests/test_wheelfile.py
index 25dc472..b6e4eb2 100644
--- a/tests/test_wheelfile.py
+++ b/tests/test_wheelfile.py
@@ -4,7 +4,6 @@ from zipfile import ZIP_DEFLATED, ZipFile
import pytest
from wheel.cli import WheelError
-from wheel.util import as_bytes, native
from wheel.wheelfile import WheelFile
@@ -37,7 +36,7 @@ def test_bad_wheel_filename(filename):
def test_missing_record(wheel_path):
with ZipFile(wheel_path, "w") as zf:
- zf.writestr(native("hello/héllö.py"), as_bytes('print("Héllö, w0rld!")\n'))
+ zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n')
exc = pytest.raises(WheelError, WheelFile, wheel_path)
exc.match("^Missing test-1.0.dist-info/RECORD file$")
@@ -45,12 +44,10 @@ def test_missing_record(wheel_path):
def test_unsupported_hash_algorithm(wheel_path):
with ZipFile(wheel_path, "w") as zf:
- zf.writestr(native("hello/héllö.py"), as_bytes('print("Héllö, w0rld!")\n'))
+ zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n')
zf.writestr(
"test-1.0.dist-info/RECORD",
- as_bytes(
- "hello/héllö.py,sha000=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25"
- ),
+ "hello/héllö.py,sha000=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25",
)
exc = pytest.raises(WheelError, WheelFile, wheel_path)
@@ -65,10 +62,8 @@ def test_unsupported_hash_algorithm(wheel_path):
def test_weak_hash_algorithm(wheel_path, algorithm, digest):
hash_string = f"{algorithm}={digest}"
with ZipFile(wheel_path, "w") as zf:
- zf.writestr(native("hello/héllö.py"), as_bytes('print("Héllö, w0rld!")\n'))
- zf.writestr(
- "test-1.0.dist-info/RECORD", as_bytes(f"hello/héllö.py,{hash_string},25")
- )
+ zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n')
+ zf.writestr("test-1.0.dist-info/RECORD", f"hello/héllö.py,{hash_string},25")
exc = pytest.raises(WheelError, WheelFile, wheel_path)
exc.match(fr"^Weak hash algorithm \({algorithm}\) is not permitted by PEP 427$")
@@ -90,10 +85,8 @@ def test_weak_hash_algorithm(wheel_path, algorithm, digest):
def test_testzip(wheel_path, algorithm, digest):
hash_string = f"{algorithm}={digest}"
with ZipFile(wheel_path, "w") as zf:
- zf.writestr(native("hello/héllö.py"), as_bytes('print("Héllö, world!")\n'))
- zf.writestr(
- "test-1.0.dist-info/RECORD", as_bytes(f"hello/héllö.py,{hash_string},25")
- )
+ zf.writestr("hello/héllö.py", 'print("Héllö, world!")\n')
+ zf.writestr("test-1.0.dist-info/RECORD", f"hello/héllö.py,{hash_string},25")
with WheelFile(wheel_path) as wf:
wf.testzip()
@@ -101,45 +94,43 @@ def test_testzip(wheel_path, algorithm, digest):
def test_testzip_missing_hash(wheel_path):
with ZipFile(wheel_path, "w") as zf:
- zf.writestr(native("hello/héllö.py"), as_bytes('print("Héllö, world!")\n'))
+ zf.writestr("hello/héllö.py", 'print("Héllö, world!")\n')
zf.writestr("test-1.0.dist-info/RECORD", "")
with WheelFile(wheel_path) as wf:
exc = pytest.raises(WheelError, wf.testzip)
- exc.match(native("^No hash found for file 'hello/héllö.py'$"))
+ exc.match("^No hash found for file 'hello/héllö.py'$")
def test_testzip_bad_hash(wheel_path):
with ZipFile(wheel_path, "w") as zf:
- zf.writestr(native("hello/héllö.py"), as_bytes('print("Héllö, w0rld!")\n'))
+ zf.writestr("hello/héllö.py", 'print("Héllö, w0rld!")\n')
zf.writestr(
"test-1.0.dist-info/RECORD",
- as_bytes(
- "hello/héllö.py,sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25"
- ),
+ "hello/héllö.py,sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25",
)
with WheelFile(wheel_path) as wf:
exc = pytest.raises(WheelError, wf.testzip)
- exc.match(native("^Hash mismatch for file 'hello/héllö.py'$"))
+ exc.match("^Hash mismatch for file 'hello/héllö.py'$")
def test_write_str(wheel_path):
with WheelFile(wheel_path, "w") as wf:
- wf.writestr(native("hello/héllö.py"), as_bytes('print("Héllö, world!")\n'))
- wf.writestr(native("hello/h,ll,.py"), as_bytes('print("Héllö, world!")\n'))
+ wf.writestr("hello/héllö.py", 'print("Héllö, world!")\n')
+ wf.writestr("hello/h,ll,.py", 'print("Héllö, world!")\n')
with ZipFile(wheel_path, "r") as zf:
infolist = zf.infolist()
assert len(infolist) == 3
- assert infolist[0].filename == native("hello/héllö.py")
+ assert infolist[0].filename == "hello/héllö.py"
assert infolist[0].file_size == 25
- assert infolist[1].filename == native("hello/h,ll,.py")
+ assert infolist[1].filename == "hello/h,ll,.py"
assert infolist[1].file_size == 25
assert infolist[2].filename == "test-1.0.dist-info/RECORD"
record = zf.read("test-1.0.dist-info/RECORD")
- assert record == as_bytes(
+ assert record.decode("utf-8") == (
"hello/héllö.py,sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25\n"
'"hello/h,ll,.py",sha256=bv-QV3RciQC2v3zL8Uvhd_arp40J5A9xmyubN34OVwo,25\n'
"test-1.0.dist-info/RECORD,,\n"
@@ -154,7 +145,7 @@ def test_timestamp(tmpdir_factory, wheel_path, monkeypatch):
build_dir.join(filename).write(filename + "\n")
# The earliest date representable in TarInfos, 1980-01-01
- monkeypatch.setenv(native("SOURCE_DATE_EPOCH"), native("315576060"))
+ monkeypatch.setenv("SOURCE_DATE_EPOCH", "315576060")
with WheelFile(wheel_path, "w") as wf:
wf.write_files(str(build_dir))