diff options
author | Alex Grönholm <alex.gronholm@nextday.fi> | 2021-12-26 20:50:02 +0200 |
---|---|---|
committer | Alex Grönholm <alex.gronholm@nextday.fi> | 2021-12-26 20:50:02 +0200 |
commit | 13cc63b105794ad7d014212036a0c1474546c9a4 (patch) | |
tree | faa1776d300e49e829dddb3621b6e9a1cc8f642c | |
parent | 83eff2cd2ead5895ddbd8deb10096c85f22c8527 (diff) | |
parent | dcac1db3a6b4be9e8d1d5173970f234ee29768e4 (diff) | |
download | wheel-git-13cc63b105794ad7d014212036a0c1474546c9a4.tar.gz |
Merge branch 'main' into remove-distutils
-rw-r--r-- | src/wheel/bdist_wheel.py | 7 | ||||
-rwxr-xr-x | src/wheel/cli/convert.py | 2 | ||||
-rw-r--r-- | src/wheel/cli/pack.py | 4 | ||||
-rw-r--r-- | src/wheel/cli/unpack.py | 8 | ||||
-rw-r--r-- | src/wheel/metadata.py | 97 | ||||
-rw-r--r-- | src/wheel/pkginfo.py | 17 | ||||
-rw-r--r-- | src/wheel/util.py | 28 | ||||
-rw-r--r-- | src/wheel/wheelfile.py | 45 | ||||
-rw-r--r-- | tests/test_pkginfo.py | 24 | ||||
-rw-r--r-- | tests/test_wheelfile.py | 45 |
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)) |