From 9f7acca7dd6fb802dc171dd803de236982b387bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Nov 2024 02:35:16 +0200 Subject: [PATCH 1/9] Refactored the `wheel convert` command to not require setuptools This also aims to improve the correctness of the egg and bdist_wininst conversions. --- docs/news.rst | 1 + src/wheel/cli/convert.py | 513 ++++++++++++++++++++++---------------- tests/cli/test_convert.py | 202 +++++++++++++-- 3 files changed, 482 insertions(+), 234 deletions(-) diff --git a/docs/news.rst b/docs/news.rst index bfd2921b..cd319be3 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -3,6 +3,7 @@ Release Notes **UNRELEASED** +- Refactored the ``convert`` command to not need setuptools to be installed - Added a redirection from ``wheel.bdist_wheel.bdist_wheel`` to ``setuptools.command.bdist_wheel.bdist_wheel`` to improve compatibility with ``setuptools``' latest fixes. diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py index 157a4a80..ace94f6a 100644 --- a/src/wheel/cli/convert.py +++ b/src/wheel/cli/convert.py @@ -2,21 +2,23 @@ import os.path import re -import shutil -import tempfile -import zipfile +from abc import ABCMeta, abstractmethod +from collections import defaultdict +from collections.abc import Iterator +from email.message import Message +from email.parser import Parser +from email.policy import EmailPolicy from glob import iglob +from pathlib import Path +from textwrap import dedent +from zipfile import ZipFile -from .._bdist_wheel import bdist_wheel +from .. import __version__ +from ..metadata import generate_requirements +from ..vendored.packaging.tags import parse_tag from ..wheelfile import WheelFile -from . import WheelError -try: - from setuptools import Distribution -except ImportError: - from distutils.dist import Distribution - -egg_info_re = re.compile( +egg_filename_re = re.compile( r""" (?P.+?)-(?P.+?) (-(?Ppy\d\.\d+) @@ -24,84 +26,177 @@ )?.egg$""", re.VERBOSE, ) - - -class _bdist_wheel_tag(bdist_wheel): - # allow the client to override the default generated wheel tag - # The default bdist_wheel implementation uses python and abi tags - # of the running python process. This is not suitable for - # generating/repackaging prebuild binaries. - - full_tag_supplied = False - full_tag = None # None or a (pytag, soabitag, plattag) triple - - def get_tag(self): - if self.full_tag_supplied and self.full_tag is not None: - return self.full_tag +egg_info_re = re.compile( + r""" + ^(?P.+?)-(?P.+?) + (-(?Ppy\d\.\d+) + )?.egg-info/""", + re.VERBOSE, +) +wininst_re = re.compile( + r"\.(?Pwin32|win-amd64)(?:-(?Ppy\d\.\d))?\.exe$" +) +pyd_re = re.compile(r"\.(?P[a-z0-9]+)-(?Pwin32|win_amd64)\.pyd$") +serialization_policy = EmailPolicy( + utf8=True, + mangle_from_=False, + max_line_length=0, +) +GENERATOR = f"wheel {__version__}" + + +def convert_requires(requires: str, metadata: Message) -> None: + extra: str | None = None + requirements: dict[str | None, list[str]] = defaultdict(list) + for line in requires.splitlines(): + line = line.strip() + if not line: + continue + + if line.startswith("[") and line.endswith("]"): + extra = line[1:-1] + continue + + requirements[extra].append(line) + + for key, value in generate_requirements(requirements): + metadata.add_header(key, value) + + +def convert_pkg_info(pkginfo: str, metadata: Message): + parsed_message = Parser().parsestr(pkginfo) + for key, value in parsed_message.items(): + key_lower = key.lower() + if value == "UNKNOWN": + continue + + if key_lower == "description": + description_lines = value.splitlines() + value = "\n".join( + ( + description_lines[0].lstrip(), + dedent("\n".join(description_lines[1:])), + "\n", + ) + ) + metadata.set_payload(value) + elif key_lower == "home-page": + metadata.add_header("Project-URL", f"Homepage, {value}") + elif key_lower == "download-url": + metadata.add_header("Project-URL", f"Download, {value}") else: - return bdist_wheel.get_tag(self) - - -def egg2wheel(egg_path: str, dest_dir: str) -> None: - filename = os.path.basename(egg_path) - match = egg_info_re.match(filename) - if not match: - raise WheelError(f"Invalid egg file name: {filename}") - - egg_info = match.groupdict() - dir = tempfile.mkdtemp(suffix="_e2w") - if os.path.isfile(egg_path): - # assume we have a bdist_egg otherwise - with zipfile.ZipFile(egg_path) as egg: - egg.extractall(dir) - else: - # support buildout-style installed eggs directories - for pth in os.listdir(egg_path): - src = os.path.join(egg_path, pth) - if os.path.isfile(src): - shutil.copy2(src, dir) - else: - shutil.copytree(src, os.path.join(dir, pth)) - - pyver = egg_info["pyver"] - if pyver: - pyver = egg_info["pyver"] = pyver.replace(".", "") - - arch = (egg_info["arch"] or "any").replace(".", "_").replace("-", "_") - - # assume all binary eggs are for CPython - abi = "cp" + pyver[2:] if arch != "any" else "none" - - root_is_purelib = egg_info["arch"] is None - if root_is_purelib: - bw = bdist_wheel(Distribution()) - else: - bw = _bdist_wheel_tag(Distribution()) - - bw.root_is_pure = root_is_purelib - bw.python_tag = pyver - bw.plat_name_supplied = True - bw.plat_name = egg_info["arch"] or "any" - if not root_is_purelib: - bw.full_tag_supplied = True - bw.full_tag = (pyver, abi, arch) - - dist_info_dir = os.path.join(dir, "{name}-{ver}.dist-info".format(**egg_info)) - bw.egg2dist(os.path.join(dir, "EGG-INFO"), dist_info_dir) - bw.write_wheelfile(dist_info_dir, generator="egg2wheel") - wheel_name = "{name}-{ver}-{pyver}-{}-{}.whl".format(abi, arch, **egg_info) - with WheelFile(os.path.join(dest_dir, wheel_name), "w") as wf: - wf.write_files(dir) - - shutil.rmtree(dir) - - -def parse_wininst_info(wininfo_name: str, egginfo_name: str | None): - """Extract metadata from filenames. - - Extracts the 4 metadataitems needed (name, version, pyversion, arch) from - the installer filename and the name of the egg-info directory embedded in - the zipfile (if any). + metadata.add_header(key, value) + + metadata.replace_header("Metadata-Version", "2.4") + + +def normalize(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower().replace("-", "_") + + +class ConvertSource(metaclass=ABCMeta): + name: str + version: str + pyver: str = "py2.py3" + abi: str = "none" + platform: str = "any" + metadata: Message + + @property + def dist_info_dir(self) -> str: + return f"{self.name}-{self.version}.dist-info" + + @abstractmethod + def generate_contents(self) -> Iterator[tuple[str, bytes]]: + pass + + +class EggFileSource(ConvertSource): + def __init__(self, path: Path): + if not (match := egg_filename_re.match(path.name)): + raise ValueError(f"Invalid egg file name: {path.name}") + + # Binary wheels are assumed to be for CPython + self.path = path + self.name = normalize(match.group("name")) + self.version = match.group("ver") + if pyver := match.group("pyver"): + self.pyver = pyver.replace(".", "") + self.abi = self.pyver.replace("py", "cp") + if arch := match.group("arch"): + self.platform = normalize(arch) + + self.metadata = Message() + + def generate_contents(self) -> Iterator[tuple[str, bytes]]: + with ZipFile(self.path, "r") as zip_file: + for zinfo in zip_file.infolist(): + # Skip any compiled bytecode files + if zinfo.filename.endswith(".pyc"): + continue + + # Skip pure directory entries + if zinfo.filename.endswith("/"): + continue + + # Handle files in the egg-info directory specially, selectively moving + # them to the dist-info directory while converting as needed + if zinfo.filename.startswith("EGG-INFO/"): + if zinfo.filename == "EGG-INFO/requires.txt": + requires = zip_file.read(zinfo.filename).decode("utf-8") + convert_requires(requires, self.metadata) + elif zinfo.filename == "EGG-INFO/PKG-INFO": + pkginfo = zip_file.read(zinfo.filename).decode("utf-8") + convert_pkg_info(pkginfo, self.metadata) + elif zinfo.filename == "EGG-INFO/entry_points.txt": + yield ( + f"{self.dist_info_dir}/entry_points.txt", + zip_file.read(zinfo.filename), + ) + + continue + + # For any other file, just pass it through + yield zinfo.filename, zip_file.read(zinfo) + + +class EggDirectorySource(EggFileSource): + def generate_contents(self) -> Iterator[tuple[str, bytes]]: + for dirpath, _, filenames in os.walk(self.path): + for filename in filenames: + path = Path(dirpath, filename) + + # Skip any compiled bytecode files + if filename.endswith(".pyc"): + continue + + if path.parent.name == "EGG-INFO": + if path.name == "requires.txt": + requires = path.read_text("utf-8") + convert_requires(requires, self.metadata) + elif path.name == "PKG-INFO": + pkginfo = path.read_text("utf-8") + convert_pkg_info(pkginfo, self.metadata) + if name := self.metadata.get("Name"): + self.name = normalize(name) + + if version := self.metadata.get("Version"): + self.version = version + elif path.name == "entry_points.txt": + yield ( + f"{self.dist_info_dir}/entry_points.txt", + path.read_bytes(), + ) + + continue + + # For any other file, just pass it through + yield str(path.relative_to(self.path)), path.read_bytes() + + +class WininstFileSource(ConvertSource): + """ + Handles distributions created with ``bdist_wininst``. The egginfo filename has the format:: @@ -129,145 +224,123 @@ def parse_wininst_info(wininfo_name: str, egginfo_name: str | None): should therefore ignore the architecture if the content is pure-python. """ - egginfo = None - if egginfo_name: - egginfo = egg_info_re.search(egginfo_name) - if not egginfo: - raise ValueError(f"Egg info filename {egginfo_name} is not valid") - - # Parse the wininst filename - # 1. Distribution name (up to the first '-') - w_name, sep, rest = wininfo_name.partition("-") - if not sep: - raise ValueError(f"Installer filename {wininfo_name} is not valid") - - # Strip '.exe' - rest = rest[:-4] - # 2. Python version (from the last '-', must start with 'py') - rest2, sep, w_pyver = rest.rpartition("-") - if sep and w_pyver.startswith("py"): - rest = rest2 - w_pyver = w_pyver.replace(".", "") - else: - # Not version specific - use py2.py3. While it is possible that - # pure-Python code is not compatible with both Python 2 and 3, there - # is no way of knowing from the wininst format, so we assume the best - # here (the user can always manually rename the wheel to be more - # restrictive if needed). - w_pyver = "py2.py3" - # 3. Version and architecture - w_ver, sep, w_arch = rest.rpartition(".") - if not sep: - raise ValueError(f"Installer filename {wininfo_name} is not valid") - - if egginfo: - w_name = egginfo.group("name") - w_ver = egginfo.group("ver") - - return {"name": w_name, "ver": w_ver, "arch": w_arch, "pyver": w_pyver} - - -def wininst2wheel(path: str, dest_dir: str) -> None: - with zipfile.ZipFile(path) as bdw: - # Search for egg-info in the archive - egginfo_name = None - for filename in bdw.namelist(): - if ".egg-info" in filename: - egginfo_name = filename - break - - info = parse_wininst_info(os.path.basename(path), egginfo_name) - - root_is_purelib = True - for zipinfo in bdw.infolist(): - if zipinfo.filename.startswith("PLATLIB"): - root_is_purelib = False - break - if root_is_purelib: - paths = {"purelib": ""} - else: - paths = {"platlib": ""} - - dist_info = "{name}-{ver}".format(**info) - datadir = f"{dist_info}.data/" - - # rewrite paths to trick ZipFile into extracting an egg - # XXX grab wininst .ini - between .exe, padding, and first zip file. - members: list[str] = [] - egginfo_name = "" - for zipinfo in bdw.infolist(): - key, basename = zipinfo.filename.split("/", 1) - key = key.lower() - basepath = paths.get(key, None) - if basepath is None: - basepath = datadir + key.lower() + "/" - oldname = zipinfo.filename - newname = basepath + basename - zipinfo.filename = newname - del bdw.NameToInfo[oldname] - bdw.NameToInfo[newname] = zipinfo - # Collect member names, but omit '' (from an entry like "PLATLIB/" - if newname: - members.append(newname) - # Remember egg-info name for the egg2dist call below - if not egginfo_name: - if newname.endswith(".egg-info"): - egginfo_name = newname - elif ".egg-info/" in newname: - egginfo_name, sep, _ = newname.rpartition("/") - dir = tempfile.mkdtemp(suffix="_b2w") - bdw.extractall(dir, members) - - # egg2wheel - abi = "none" - pyver = info["pyver"] - arch = (info["arch"] or "any").replace(".", "_").replace("-", "_") - # Wininst installers always have arch even if they are not - # architecture-specific (because the format itself is). - # So, assume the content is architecture-neutral if root is purelib. - if root_is_purelib: - arch = "any" - # If the installer is architecture-specific, it's almost certainly also - # CPython-specific. - if arch != "any": - pyver = pyver.replace("py", "cp") - wheel_name = "-".join((dist_info, pyver, abi, arch)) - if root_is_purelib: - bw = bdist_wheel(Distribution()) - else: - bw = _bdist_wheel_tag(Distribution()) - - bw.root_is_pure = root_is_purelib - bw.python_tag = pyver - bw.plat_name_supplied = True - bw.plat_name = info["arch"] or "any" - - if not root_is_purelib: - bw.full_tag_supplied = True - bw.full_tag = (pyver, abi, arch) - - dist_info_dir = os.path.join(dir, f"{dist_info}.dist-info") - bw.egg2dist(os.path.join(dir, egginfo_name), dist_info_dir) - bw.write_wheelfile(dist_info_dir, generator="wininst2wheel") - - wheel_path = os.path.join(dest_dir, wheel_name) - with WheelFile(wheel_path, "w") as wf: - wf.write_files(dir) - - shutil.rmtree(dir) + def __init__(self, path: Path): + self.path = path + self.metadata = Message() + + # Determine the initial architecture and Python version from the file name + # (if possible) + if match := wininst_re.search(path.name): + self.platform = normalize(match.group("platform")) + if pyver := match.group("pyver"): + self.pyver = pyver.replace(".", "") + self.abi = pyver.replace("py", "cp") + + # Look for an .egg-info directory and any .pyd files for more precise info + egg_info_found = pyd_found = False + with ZipFile(self.path) as zip_file: + for filename in zip_file.namelist(): + prefix, filename = filename.split("/", 1) + if not egg_info_found and (match := egg_info_re.match(filename)): + egg_info_found = True + self.name = normalize(match.group("name")) + self.version = match.group("ver") + if pyver := match.group("pyver"): + self.pyver = pyver.replace(".", "") + elif not pyd_found and (match := pyd_re.search(filename)): + pyd_found = True + self.abi = match.group("abi") + self.platform = match.group("platform") + + if egg_info_found and pyd_found: + break + + def generate_contents(self) -> Iterator[tuple[str, bytes]]: + dist_info_dir = f"{self.name}-{self.version}.dist-info" + data_dir = f"{self.name}-{self.version}.data" + with ZipFile(self.path, "r") as zip_file: + for zinfo in zip_file.infolist(): + # Skip any compiled bytecode files + if zinfo.filename.endswith(".pyc"): + continue + + # Skip pure directory entries + if zinfo.filename.endswith("/"): + continue + + # Handle files in the egg-info directory specially, selectively moving + # them to the dist-info directory while converting as needed + prefix, target_filename = zinfo.filename.split("/", 1) + if egg_info_re.search(target_filename): + basename = target_filename.rsplit("/", 1)[-1] + if basename == "requires.txt": + requires = zip_file.read(zinfo.filename).decode("utf-8") + convert_requires(requires, self.metadata) + elif basename == "PKG-INFO": + pkginfo = zip_file.read(zinfo.filename).decode("utf-8") + convert_pkg_info(pkginfo, self.metadata) + elif basename == "entry_points.txt": + yield ( + f"{dist_info_dir}/entry_points.txt", + zip_file.read(zinfo.filename), + ) + + continue + elif prefix == "SCRIPTS": + target_filename = f"{data_dir}/scripts/{target_filename}" + + # For any other file, just pass it through + yield target_filename, zip_file.read(zinfo) def convert(files: list[str], dest_dir: str, verbose: bool) -> None: for pat in files: - for installer in iglob(pat): - if os.path.splitext(installer)[1] == ".egg": - conv = egg2wheel + for archive in iglob(pat): + path = Path(archive) + if path.suffix == ".egg": + if path.is_dir(): + source: ConvertSource = EggDirectorySource(path) + else: + source = EggFileSource(path) else: - conv = wininst2wheel + source = WininstFileSource(path) if verbose: - print(f"{installer}... ", flush=True) + print(f"{archive}... ", flush=True) + + dest_path = Path(dest_dir) / ( + f"{source.name}-{source.version}-{source.pyver}-{source.abi}" + f"-{source.platform}.whl" + ) + with WheelFile(dest_path, "w") as wheelfile: + for name_or_zinfo, contents in source.generate_contents(): + wheelfile.writestr(name_or_zinfo, contents) + + # Write the METADATA file + wheelfile.writestr( + f"{source.dist_info_dir}/METADATA", + source.metadata.as_string(policy=serialization_policy).encode( + "utf-8" + ), + ) + + # Write the WHEEL file + wheel_message = Message() + wheel_message.add_header("Wheel-Version", "1.0") + wheel_message.add_header("Generator", GENERATOR) + wheel_message.add_header( + "Root-Is-Purelib", str(source.platform == "any").lower() + ) + tags = parse_tag(f"{source.pyver}-{source.abi}-{source.platform}") + for tag in sorted(tags, key=lambda tag: tag.interpreter): + wheel_message.add_header("Tag", str(tag)) + + wheelfile.writestr( + f"{source.dist_info_dir}/WHEEL", + wheel_message.as_string(policy=serialization_policy).encode( + "utf-8" + ), + ) - conv(installer, dest_dir) if verbose: print("OK") diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index 4f26b237..9cf0d3cd 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -1,28 +1,202 @@ from __future__ import annotations import os.path -import re +import zipfile +from pathlib import Path +from textwrap import dedent -from wheel.cli.convert import convert, egg_info_re -from wheel.wheelfile import WHEEL_INFO_RE +import pytest +from _pytest.fixtures import SubRequest +from pytest import TempPathFactory +import wheel +from wheel.cli.convert import convert, egg_filename_re +from wheel.wheelfile import WHEEL_INFO_RE, WheelFile -def test_egg_re(): +PKG_INFO = dedent( + """\ + Metadata-Version: 2.1 + Name: Sampledist + Version: 1.0.0 + Author: Alex Grönholm + Author-email: alex.gronholm@example.com + Description: Sample Distribution + =================== + Test description + """ +) +EXPECTED_METADATA = dedent( + """\ + Metadata-Version: 2.4 + Name: Sampledist + Version: 1.0.0 + Author: Alex Grönholm + Author-email: alex.gronholm@example.com + + Sample Distribution + =================== + Test description + + """ +).encode("utf-8") + + +@pytest.fixture( + params=[ + pytest.param(("py3.7", "win32"), id="win32"), + pytest.param(("py3.7", "win_amd64"), id="amd64"), + pytest.param((None, "any"), id="pure"), + ] +) +def pyver_arch(request: SubRequest) -> tuple[str | None, str]: + return request.param + + +@pytest.fixture +def pyver(pyver_arch: tuple[str | None, str]) -> str | None: + return pyver_arch[0] + + +@pytest.fixture +def arch(pyver_arch: tuple[str | None, str]) -> str: + return pyver_arch[1] + + +@pytest.fixture +def expected_wheelfile(arch: str) -> bytes: + root_is_purelib = str(arch == "any").lower() + text = dedent( + f"""\ + Wheel-Version: 1.0 + Generator: wheel {wheel.__version__} + Root-Is-Purelib: {root_is_purelib} + """ + ) + if arch == "any": + text += "Tag: py2-none-any\nTag: py3-none-any\n\n" + else: + text += f"Tag: py37-cp37-{arch}\n\n" + + return text.encode("utf-8") + + +@pytest.fixture +def bdist_wininst_path(arch: str, pyver: str | None, tmp_path: Path) -> str: + # As bdist_wininst is no longer present in Python, and carrying .exe files in the + # tarball is risky, we have to fake this a bit + if pyver: + filename = f"Sampledist-1.0.0-{arch.replace('_', '-')}-{pyver}.exe" + pyver_suffix = f"-{pyver}" + else: + filename = f"Sampledist-1.0.0-{arch.replace('_', '-')}.exe" + pyver_suffix = "" + + bdist_path = tmp_path / filename + prefix = "PURELIB" if arch == "any" else "PLATLIB" + with zipfile.ZipFile(bdist_path, "w") as zip: + zip.writestr(f"{prefix}/", b"") + zip.writestr(f"{prefix}/sampledist/", b"") + zip.writestr(f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/", b"") + zip.writestr(f"{prefix}/sampledist/__init__.py", b"") + if arch != "any": + zip.writestr(f"{prefix}/sampledist/_extmodule.cp37-{arch}.pyd", b"") + + zip.writestr( + f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/dependency_links.txt", + b"", + ) + zip.writestr( + f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/PKG-INFO", + PKG_INFO.encode("utf-8"), + ) + zip.writestr( + f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/SOURCES.txt", b"" + ) + zip.writestr( + f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/top_level.txt", b"" + ) + zip.writestr(f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/zip-safe", b"") + zip.writestr("SCRIPTS/somecommand", b"#!python\nprint('hello')") + + return str(bdist_path) + + +@pytest.fixture +def egg_path(arch: str, pyver: str | None, tmp_path: Path) -> str: + if pyver: + filename = f"Sampledist-1.0.0-{pyver}-{arch}.egg" + else: + filename = "Sampledist-1.0.0.egg" + + bdist_path = tmp_path / filename + with zipfile.ZipFile(bdist_path, "w") as zip: + zip.writestr("sampledist/", b"") + zip.writestr("EGG-INFO/", b"") + zip.writestr("sampledist/__init__.py", b"") + zip.writestr(f"sampledist/_extmodule.cp37-{arch}.pyd", b"") + zip.writestr("EGG-INFO/dependency_links.txt", b"") + zip.writestr("EGG-INFO/PKG-INFO", PKG_INFO.encode("utf-8")) + zip.writestr("EGG-INFO/SOURCES.txt", b"") + zip.writestr("EGG-INFO/top_level.txt", b"") + zip.writestr("EGG-INFO/zip-safe", b"") + + return str(bdist_path) + + +def test_egg_re() -> None: """Make sure egg_info_re matches.""" egg_names_path = os.path.join(os.path.dirname(__file__), "eggnames.txt") with open(egg_names_path, encoding="utf-8") as egg_names: for line in egg_names: line = line.strip() if line: - assert egg_info_re.match(line), line + assert egg_filename_re.match(line), line -def test_convert_egg(egg_paths, tmp_path): - convert(egg_paths, str(tmp_path), verbose=False) - wheel_names = [path.name for path in tmp_path.iterdir()] - assert len(wheel_names) == len(egg_paths) - assert all(WHEEL_INFO_RE.match(filename) for filename in wheel_names) - assert all( - re.match(r"^[\w\d.]+-\d\.\d-\w+\d+-[\w\d]+-[\w\d]+\.whl$", fname) - for fname in wheel_names - ) +def test_convert_egg_file( + egg_path: str, tmp_path: Path, arch: str, expected_wheelfile: bytes +) -> None: + convert([egg_path], str(tmp_path), verbose=False) + wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") + assert WHEEL_INFO_RE.match(wheel_path.name) + with WheelFile(wheel_path) as wf: + assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA + assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile + + +def test_convert_egg_directory( + egg_path: str, + tmp_path: Path, + tmp_path_factory: TempPathFactory, + arch: str, + expected_wheelfile: bytes, +) -> None: + with zipfile.ZipFile(egg_path) as egg_file: + egg_dir_path = tmp_path_factory.mktemp("eggdir") / Path(egg_path).name + egg_dir_path.mkdir() + egg_file.extractall(egg_dir_path) + + convert([str(egg_dir_path)], str(tmp_path), verbose=False) + wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") + assert WHEEL_INFO_RE.match(wheel_path.name) + with WheelFile(wheel_path) as wf: + assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA + assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile + + +def test_convert_bdist_wininst( + bdist_wininst_path: str, + tmp_path: Path, + arch: str, + expected_wheelfile: bytes, +) -> None: + convert([bdist_wininst_path], str(tmp_path), verbose=False) + wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") + assert WHEEL_INFO_RE.match(wheel_path.name) + with WheelFile(wheel_path) as wf: + assert ( + wf.read("sampledist-1.0.0.data/scripts/somecommand") + == b"#!python\nprint('hello')" + ) + assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA + assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile From 4bdf84eb863fb944c1ef2c6f9bc092bd15105dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Nov 2024 02:50:57 +0200 Subject: [PATCH 2/9] Added test coverage for convert_requires() --- tests/cli/test_convert.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index 9cf0d3cd..07feae18 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -24,7 +24,16 @@ =================== Test description """ -) +).encode("utf-8") +REQUIRES_TXT = dedent( + """\ + somepackage>=1.5 + otherpackage>=1.7 + + [:python_version < '3'] + six + """ +).encode("utf-8") EXPECTED_METADATA = dedent( """\ Metadata-Version: 2.4 @@ -32,6 +41,9 @@ Version: 1.0.0 Author: Alex Grönholm Author-email: alex.gronholm@example.com + Requires-Dist: somepackage>=1.5 + Requires-Dist: otherpackage>=1.7 + Requires-Dist: six; python_version < "3" Sample Distribution =================== @@ -107,7 +119,7 @@ def bdist_wininst_path(arch: str, pyver: str | None, tmp_path: Path) -> str: ) zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/PKG-INFO", - PKG_INFO.encode("utf-8"), + PKG_INFO, ) zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/SOURCES.txt", b"" @@ -115,6 +127,10 @@ def bdist_wininst_path(arch: str, pyver: str | None, tmp_path: Path) -> str: zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/top_level.txt", b"" ) + zip.writestr( + f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/requires.txt", + REQUIRES_TXT, + ) zip.writestr(f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/zip-safe", b"") zip.writestr("SCRIPTS/somecommand", b"#!python\nprint('hello')") @@ -135,9 +151,10 @@ def egg_path(arch: str, pyver: str | None, tmp_path: Path) -> str: zip.writestr("sampledist/__init__.py", b"") zip.writestr(f"sampledist/_extmodule.cp37-{arch}.pyd", b"") zip.writestr("EGG-INFO/dependency_links.txt", b"") - zip.writestr("EGG-INFO/PKG-INFO", PKG_INFO.encode("utf-8")) + zip.writestr("EGG-INFO/PKG-INFO", PKG_INFO) zip.writestr("EGG-INFO/SOURCES.txt", b"") zip.writestr("EGG-INFO/top_level.txt", b"") + zip.writestr("EGG-INFO/requires.txt", REQUIRES_TXT) zip.writestr("EGG-INFO/zip-safe", b"") return str(bdist_path) From 2a7d2a0e36743a1366cabf7104152d67ee88ad1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Nov 2024 02:56:31 +0200 Subject: [PATCH 3/9] Sort the file names before iteration --- src/wheel/cli/convert.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py index ace94f6a..3a092020 100644 --- a/src/wheel/cli/convert.py +++ b/src/wheel/cli/convert.py @@ -130,40 +130,40 @@ def __init__(self, path: Path): def generate_contents(self) -> Iterator[tuple[str, bytes]]: with ZipFile(self.path, "r") as zip_file: - for zinfo in zip_file.infolist(): + for filename in sorted(zip_file.namelist()): # Skip any compiled bytecode files - if zinfo.filename.endswith(".pyc"): + if filename.endswith(".pyc"): continue # Skip pure directory entries - if zinfo.filename.endswith("/"): + if filename.endswith("/"): continue # Handle files in the egg-info directory specially, selectively moving # them to the dist-info directory while converting as needed - if zinfo.filename.startswith("EGG-INFO/"): - if zinfo.filename == "EGG-INFO/requires.txt": - requires = zip_file.read(zinfo.filename).decode("utf-8") + if filename.startswith("EGG-INFO/"): + if filename == "EGG-INFO/requires.txt": + requires = zip_file.read(filename).decode("utf-8") convert_requires(requires, self.metadata) - elif zinfo.filename == "EGG-INFO/PKG-INFO": - pkginfo = zip_file.read(zinfo.filename).decode("utf-8") + elif filename == "EGG-INFO/PKG-INFO": + pkginfo = zip_file.read(filename).decode("utf-8") convert_pkg_info(pkginfo, self.metadata) - elif zinfo.filename == "EGG-INFO/entry_points.txt": + elif filename == "EGG-INFO/entry_points.txt": yield ( f"{self.dist_info_dir}/entry_points.txt", - zip_file.read(zinfo.filename), + zip_file.read(filename), ) continue # For any other file, just pass it through - yield zinfo.filename, zip_file.read(zinfo) + yield filename, zip_file.read(filename) class EggDirectorySource(EggFileSource): def generate_contents(self) -> Iterator[tuple[str, bytes]]: for dirpath, _, filenames in os.walk(self.path): - for filename in filenames: + for filename in sorted(filenames): path = Path(dirpath, filename) # Skip any compiled bytecode files @@ -259,30 +259,30 @@ def generate_contents(self) -> Iterator[tuple[str, bytes]]: dist_info_dir = f"{self.name}-{self.version}.dist-info" data_dir = f"{self.name}-{self.version}.data" with ZipFile(self.path, "r") as zip_file: - for zinfo in zip_file.infolist(): + for filename in sorted(zip_file.namelist()): # Skip any compiled bytecode files - if zinfo.filename.endswith(".pyc"): + if filename.endswith(".pyc"): continue # Skip pure directory entries - if zinfo.filename.endswith("/"): + if filename.endswith("/"): continue # Handle files in the egg-info directory specially, selectively moving # them to the dist-info directory while converting as needed - prefix, target_filename = zinfo.filename.split("/", 1) + prefix, target_filename = filename.split("/", 1) if egg_info_re.search(target_filename): basename = target_filename.rsplit("/", 1)[-1] if basename == "requires.txt": - requires = zip_file.read(zinfo.filename).decode("utf-8") + requires = zip_file.read(filename).decode("utf-8") convert_requires(requires, self.metadata) elif basename == "PKG-INFO": - pkginfo = zip_file.read(zinfo.filename).decode("utf-8") + pkginfo = zip_file.read(filename).decode("utf-8") convert_pkg_info(pkginfo, self.metadata) elif basename == "entry_points.txt": yield ( f"{dist_info_dir}/entry_points.txt", - zip_file.read(zinfo.filename), + zip_file.read(filename), ) continue @@ -290,7 +290,7 @@ def generate_contents(self) -> Iterator[tuple[str, bytes]]: target_filename = f"{data_dir}/scripts/{target_filename}" # For any other file, just pass it through - yield target_filename, zip_file.read(zinfo) + yield target_filename, zip_file.read(filename) def convert(files: list[str], dest_dir: str, verbose: bool) -> None: From aa83978393e4c2255633d5187b07cafb55acef61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Nov 2024 03:06:56 +0200 Subject: [PATCH 4/9] Increased coverage --- pyproject.toml | 3 +++ src/wheel/cli/convert.py | 13 ------------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cd5bdd42..461de8bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,6 +89,9 @@ exclude_also = [ [tool.coverage.report] show_missing = true +exclude_also = [ + "@abstractmethod", +] [tool.ruff] extend-exclude = ["src/wheel/vendored"] diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py index 3a092020..3361fd4f 100644 --- a/src/wheel/cli/convert.py +++ b/src/wheel/cli/convert.py @@ -131,10 +131,6 @@ def __init__(self, path: Path): def generate_contents(self) -> Iterator[tuple[str, bytes]]: with ZipFile(self.path, "r") as zip_file: for filename in sorted(zip_file.namelist()): - # Skip any compiled bytecode files - if filename.endswith(".pyc"): - continue - # Skip pure directory entries if filename.endswith("/"): continue @@ -165,11 +161,6 @@ def generate_contents(self) -> Iterator[tuple[str, bytes]]: for dirpath, _, filenames in os.walk(self.path): for filename in sorted(filenames): path = Path(dirpath, filename) - - # Skip any compiled bytecode files - if filename.endswith(".pyc"): - continue - if path.parent.name == "EGG-INFO": if path.name == "requires.txt": requires = path.read_text("utf-8") @@ -260,10 +251,6 @@ def generate_contents(self) -> Iterator[tuple[str, bytes]]: data_dir = f"{self.name}-{self.version}.data" with ZipFile(self.path, "r") as zip_file: for filename in sorted(zip_file.namelist()): - # Skip any compiled bytecode files - if filename.endswith(".pyc"): - continue - # Skip pure directory entries if filename.endswith("/"): continue From 29bf75c2ac6201643100af7f8ae959b35cdddd03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Nov 2024 03:11:40 +0200 Subject: [PATCH 5/9] Increased coverage further --- tests/cli/test_convert.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index 07feae18..31c8a427 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -20,6 +20,8 @@ Version: 1.0.0 Author: Alex Grönholm Author-email: alex.gronholm@example.com + Home-page: https://example.com + Download-URL: https://example.com/sampledist Description: Sample Distribution =================== Test description @@ -41,6 +43,8 @@ Version: 1.0.0 Author: Alex Grönholm Author-email: alex.gronholm@example.com + Project-URL: Homepage, https://example.com + Project-URL: Download, https://example.com/sampledist Requires-Dist: somepackage>=1.5 Requires-Dist: otherpackage>=1.7 Requires-Dist: six; python_version < "3" From f45026ec704527fc7104df10ea1bf628eae36042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Nov 2024 03:13:40 +0200 Subject: [PATCH 6/9] Increased coverage even more --- tests/cli/test_convert.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index 31c8a427..69784fb3 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -131,6 +131,9 @@ def bdist_wininst_path(arch: str, pyver: str | None, tmp_path: Path) -> str: zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/top_level.txt", b"" ) + zip.writestr( + f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/entry_points.txt", b"" + ) zip.writestr( f"{prefix}/Sampledist-1.0.0{pyver_suffix}.egg-info/requires.txt", REQUIRES_TXT, @@ -158,6 +161,7 @@ def egg_path(arch: str, pyver: str | None, tmp_path: Path) -> str: zip.writestr("EGG-INFO/PKG-INFO", PKG_INFO) zip.writestr("EGG-INFO/SOURCES.txt", b"") zip.writestr("EGG-INFO/top_level.txt", b"") + zip.writestr("EGG-INFO/entry_points.txt", b"") zip.writestr("EGG-INFO/requires.txt", REQUIRES_TXT) zip.writestr("EGG-INFO/zip-safe", b"") @@ -183,6 +187,7 @@ def test_convert_egg_file( with WheelFile(wheel_path) as wf: assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile + assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" def test_convert_egg_directory( @@ -203,6 +208,7 @@ def test_convert_egg_directory( with WheelFile(wheel_path) as wf: assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile + assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" def test_convert_bdist_wininst( @@ -221,3 +227,4 @@ def test_convert_bdist_wininst( ) assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile + assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" From 5b4a98c9820728cf7f0416e6fe42cd75c0f8cf29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Nov 2024 20:36:37 +0200 Subject: [PATCH 7/9] Test for a multi-line License header --- .pre-commit-config.yaml | 2 +- pyproject.toml | 1 + tests/cli/test_convert.py | 90 +++++++++++++++++++++------------------ 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce5da10f..90d584ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,8 +14,8 @@ repos: - id: end-of-file-fixer - id: mixed-line-ending args: ["--fix=lf"] - - id: requirements-txt-fixer - id: trailing-whitespace + exclude: "tests/cli/test_convert.py" - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.9 diff --git a/pyproject.toml b/pyproject.toml index 461de8bd..d9a59d43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ build-backend = "flit_core.buildapi" name = "wheel" description = "A built-package format for Python" readme = "README.rst" +license = {file = "LICENSE.txt"} classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index 69784fb3..e8da2adb 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -13,48 +13,56 @@ from wheel.cli.convert import convert, egg_filename_re from wheel.wheelfile import WHEEL_INFO_RE, WheelFile -PKG_INFO = dedent( - """\ - Metadata-Version: 2.1 - Name: Sampledist - Version: 1.0.0 - Author: Alex Grönholm - Author-email: alex.gronholm@example.com - Home-page: https://example.com - Download-URL: https://example.com/sampledist - Description: Sample Distribution - =================== - Test description - """ -).encode("utf-8") -REQUIRES_TXT = dedent( - """\ - somepackage>=1.5 - otherpackage>=1.7 - - [:python_version < '3'] - six - """ -).encode("utf-8") -EXPECTED_METADATA = dedent( - """\ - Metadata-Version: 2.4 - Name: Sampledist - Version: 1.0.0 - Author: Alex Grönholm - Author-email: alex.gronholm@example.com - Project-URL: Homepage, https://example.com - Project-URL: Download, https://example.com/sampledist - Requires-Dist: somepackage>=1.5 - Requires-Dist: otherpackage>=1.7 - Requires-Dist: six; python_version < "3" - - Sample Distribution +PKG_INFO = """\ +Metadata-Version: 2.1 +Name: Sampledist +Version: 1.0.0 +Author: Alex Grönholm +Author-email: alex.gronholm@example.com +Home-page: https://example.com +Download-URL: https://example.com/sampledist +License: Sample license text + second row + third row + + fourth row +Description: Sample Distribution =================== - Test description - - """ -).encode("utf-8") + + Test description +""".encode() # noqa: W293 + +REQUIRES_TXT = b"""\ +somepackage>=1.5 +otherpackage>=1.7 + +[:python_version < '3'] +six +""" + +EXPECTED_METADATA = """\ +Metadata-Version: 2.4 +Name: Sampledist +Version: 1.0.0 +Author: Alex Grönholm +Author-email: alex.gronholm@example.com +Project-URL: Homepage, https://example.com +Project-URL: Download, https://example.com/sampledist +License: Sample license text + second row + third row + + fourth row +Requires-Dist: somepackage>=1.5 +Requires-Dist: otherpackage>=1.7 +Requires-Dist: six; python_version < "3" + +Sample Distribution +=================== + +Test description + +""".encode() # noqa: W293 @pytest.fixture( From f65bcc20cb48e45e1d45fd97a3e1ed6802ad944d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Thu, 7 Nov 2024 23:37:37 +0200 Subject: [PATCH 8/9] Only configure setuptools logging if bdist_wheel is imported Also fix the output of `wheel convert` to add the final "OK" on the same line as the source file name. Fixes #632. --- docs/news.rst | 1 + src/wheel/_bdist_wheel.py | 9 +++++++++ src/wheel/cli/convert.py | 2 +- src/wheel/util.py | 9 --------- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/news.rst b/docs/news.rst index cd319be3..9acf9186 100644 --- a/docs/news.rst +++ b/docs/news.rst @@ -4,6 +4,7 @@ Release Notes **UNRELEASED** - Refactored the ``convert`` command to not need setuptools to be installed +- Don't configure setuptools logging unless running ``bdist_wheel`` - Added a redirection from ``wheel.bdist_wheel.bdist_wheel`` to ``setuptools.command.bdist_wheel.bdist_wheel`` to improve compatibility with ``setuptools``' latest fixes. diff --git a/src/wheel/_bdist_wheel.py b/src/wheel/_bdist_wheel.py index 3cc7165e..88973ebf 100644 --- a/src/wheel/_bdist_wheel.py +++ b/src/wheel/_bdist_wheel.py @@ -34,6 +34,15 @@ if TYPE_CHECKING: import types +# ensure Python logging is configured +try: + __import__("setuptools.logging") +except ImportError: + # setuptools < ?? + from . import _setuptools_logging + + _setuptools_logging.configure() + def safe_name(name: str) -> str: """Convert an arbitrary string to a standard distribution name diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py index 3361fd4f..b1b31439 100644 --- a/src/wheel/cli/convert.py +++ b/src/wheel/cli/convert.py @@ -293,7 +293,7 @@ def convert(files: list[str], dest_dir: str, verbose: bool) -> None: source = WininstFileSource(path) if verbose: - print(f"{archive}... ", flush=True) + print(f"{archive}... ", flush=True, end="") dest_path = Path(dest_dir) / ( f"{source.name}-{source.version}-{source.pyver}-{source.abi}" diff --git a/src/wheel/util.py b/src/wheel/util.py index d98d98cb..c928aa40 100644 --- a/src/wheel/util.py +++ b/src/wheel/util.py @@ -5,15 +5,6 @@ log = logging.getLogger("wheel") -# ensure Python logging is configured -try: - __import__("setuptools.logging") -except ImportError: - # setuptools < ?? - from . import _setuptools_logging - - _setuptools_logging.configure() - def urlsafe_b64encode(data: bytes) -> bytes: """urlsafe_b64encode without padding""" From 13876db3ad86d422bff1f5bc8ff22715070fa265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Fri, 8 Nov 2024 09:55:22 +0200 Subject: [PATCH 9/9] Tweaked the verbose output and added test coverage for it --- src/wheel/cli/convert.py | 2 +- tests/cli/test_convert.py | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/wheel/cli/convert.py b/src/wheel/cli/convert.py index b1b31439..ec5ecf3a 100644 --- a/src/wheel/cli/convert.py +++ b/src/wheel/cli/convert.py @@ -293,7 +293,7 @@ def convert(files: list[str], dest_dir: str, verbose: bool) -> None: source = WininstFileSource(path) if verbose: - print(f"{archive}... ", flush=True, end="") + print(f"{archive}...", flush=True, end="") dest_path = Path(dest_dir) / ( f"{source.name}-{source.version}-{source.pyver}-{source.abi}" diff --git a/tests/cli/test_convert.py b/tests/cli/test_convert.py index e8da2adb..a1bfd5d1 100644 --- a/tests/cli/test_convert.py +++ b/tests/cli/test_convert.py @@ -7,11 +7,11 @@ import pytest from _pytest.fixtures import SubRequest -from pytest import TempPathFactory +from pytest import CaptureFixture, TempPathFactory import wheel from wheel.cli.convert import convert, egg_filename_re -from wheel.wheelfile import WHEEL_INFO_RE, WheelFile +from wheel.wheelfile import WheelFile PKG_INFO = """\ Metadata-Version: 2.1 @@ -187,16 +187,21 @@ def test_egg_re() -> None: def test_convert_egg_file( - egg_path: str, tmp_path: Path, arch: str, expected_wheelfile: bytes + egg_path: str, + tmp_path: Path, + arch: str, + expected_wheelfile: bytes, + capsys: CaptureFixture, ) -> None: - convert([egg_path], str(tmp_path), verbose=False) + convert([egg_path], str(tmp_path), verbose=True) wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") - assert WHEEL_INFO_RE.match(wheel_path.name) with WheelFile(wheel_path) as wf: assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" + assert capsys.readouterr().out == f"{egg_path}...OK\n" + def test_convert_egg_directory( egg_path: str, @@ -204,30 +209,32 @@ def test_convert_egg_directory( tmp_path_factory: TempPathFactory, arch: str, expected_wheelfile: bytes, + capsys: CaptureFixture, ) -> None: with zipfile.ZipFile(egg_path) as egg_file: egg_dir_path = tmp_path_factory.mktemp("eggdir") / Path(egg_path).name egg_dir_path.mkdir() egg_file.extractall(egg_dir_path) - convert([str(egg_dir_path)], str(tmp_path), verbose=False) + convert([str(egg_dir_path)], str(tmp_path), verbose=True) wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") - assert WHEEL_INFO_RE.match(wheel_path.name) with WheelFile(wheel_path) as wf: assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" + assert capsys.readouterr().out == f"{egg_dir_path}...OK\n" + def test_convert_bdist_wininst( bdist_wininst_path: str, tmp_path: Path, arch: str, expected_wheelfile: bytes, + capsys: CaptureFixture, ) -> None: - convert([bdist_wininst_path], str(tmp_path), verbose=False) + convert([bdist_wininst_path], str(tmp_path), verbose=True) wheel_path = next(path for path in tmp_path.iterdir() if path.suffix == ".whl") - assert WHEEL_INFO_RE.match(wheel_path.name) with WheelFile(wheel_path) as wf: assert ( wf.read("sampledist-1.0.0.data/scripts/somecommand") @@ -236,3 +243,5 @@ def test_convert_bdist_wininst( assert wf.read("sampledist-1.0.0.dist-info/METADATA") == EXPECTED_METADATA assert wf.read("sampledist-1.0.0.dist-info/WHEEL") == expected_wheelfile assert wf.read("sampledist-1.0.0.dist-info/entry_points.txt") == b"" + + assert capsys.readouterr().out == f"{bdist_wininst_path}...OK\n"