Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid undocumented pypa/wheel API in dist_info #3905

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 54 additions & 47 deletions setuptools/command/dist_info.py
Expand Up @@ -5,16 +5,25 @@

import os
import shutil
import sys
from contextlib import contextmanager
from distutils import log
from distutils.core import Command
from distutils import dir_util # prefer dir_util for log/cache consistency
from pathlib import Path

from .. import _normalization
from ..warnings import SetuptoolsDeprecationWarning


_IGNORE = {
"PKG-INFO",
"requires.txt",
"SOURCES.txt",
"not-zip-safe",
"dependency_links.txt",
}
# Files to be ignored when copying the egg-info into dist-info


class dist_info(Command):
"""
This command is private and reserved for internal use of setuptools,
Expand All @@ -41,9 +50,10 @@ class dist_info(Command):
('tag-build=', 'b', "Specify explicit tag to add to version number"),
('no-date', 'D', "Don't include date stamp [default]"),
('keep-egg-info', None, "*TRANSITIONAL* will be removed in the future"),
('use-cached', None, "*TRANSITIONAL* will be removed in the future"),
]

boolean_options = ['tag-date', 'keep-egg-info']
boolean_options = ['tag-date', 'keep-egg-info', 'use-cached']
negative_opt = {'no-date': 'tag-date'}

def initialize_options(self):
Expand All @@ -54,6 +64,7 @@ def initialize_options(self):
self.tag_date = None
self.tag_build = None
self.keep_egg_info = False
self.use_cached = False

def finalize_options(self):
if self.egg_base:
Expand All @@ -67,9 +78,18 @@ def finalize_options(self):
project_dir = dist.src_root or os.curdir
self.output_dir = Path(self.output_dir or project_dir)

egg_info = self.reinitialize_command("egg_info")
egg_info = self.reinitialize_command("egg_info", reinit_subcommands=True)
egg_info.egg_base = str(self.output_dir)
self._sync_tag_details(egg_info)
egg_info.finalize_options()
self.egg_info = egg_info

name = _normalization.safer_name(dist.get_name())
version = _normalization.safer_best_effort_version(dist.get_version())
self.name = f"{name}-{version}"
self.dist_info_dir = Path(self.output_dir, f"{self.name}.dist-info")

def _sync_tag_details(self, egg_info):
if self.tag_date:
egg_info.tag_date = self.tag_date
else:
Expand All @@ -80,48 +100,35 @@ def finalize_options(self):
else:
self.tag_build = egg_info.tag_build

egg_info.finalize_options()
self.egg_info = egg_info

name = _normalization.safer_name(dist.get_name())
version = _normalization.safer_best_effort_version(dist.get_version())
self.name = f"{name}-{version}"
self.dist_info_dir = os.path.join(self.output_dir, f"{self.name}.dist-info")

@contextmanager
def _maybe_bkp_dir(self, dir_path: str, requires_bkp: bool):
if requires_bkp:
bkp_name = f"{dir_path}.__bkp__"
_rm(bkp_name, ignore_errors=True)
_copy(dir_path, bkp_name, dirs_exist_ok=True, symlinks=True)
try:
yield
finally:
_rm(dir_path, ignore_errors=True)
shutil.move(bkp_name, dir_path)
else:
yield

def run(self):
self.output_dir.mkdir(parents=True, exist_ok=True)
self.egg_info.run()
egg_info_dir = self.egg_info.egg_info
assert os.path.isdir(egg_info_dir), ".egg-info dir should have been created"

log.info("creating '{}'".format(os.path.abspath(self.dist_info_dir)))
bdist_wheel = self.get_finalized_command('bdist_wheel')
if self.use_cached and (self.dist_info_dir / "METADATA").is_file():
return

# TODO: if bdist_wheel if merged into setuptools, just add "keep_egg_info" there
with self._maybe_bkp_dir(egg_info_dir, self.keep_egg_info):
bdist_wheel.egg2dist(egg_info_dir, self.dist_info_dir)


def _rm(dir_name, **opts):
if os.path.isdir(dir_name):
shutil.rmtree(dir_name, **opts)


def _copy(src, dst, **opts):
if sys.version_info < (3, 8):
opts.pop("dirs_exist_ok", None)
shutil.copytree(src, dst, **opts)
self.mkpath(str(self.output_dir))
self.egg_info.run()
egg_info_dir = Path(self.egg_info.egg_info)
dist_info_dir = self.dist_info_dir

assert egg_info_dir.is_dir(), ".egg-info dir should have been created"
log.info(f"creating {str(os.path.abspath(dist_info_dir))!r}")

# The egg-info dir should now be basically equivalent to the dist-info dir
# If in the future we don't want to use egg_info, we have to create the files:
# METADATA, entry-points.txt
shutil.copytree(egg_info_dir, dist_info_dir, ignore=lambda _, __: _IGNORE)
metadata_file = dist_info_dir / "METADATA"
self.copy_file(egg_info_dir / "PKG-INFO", metadata_file)
if self.distribution.dependency_links:
self.copy_file(egg_info_dir / "dependency_links.txt", dist_info_dir)

for dest, orig in self._license_paths():
dest = dist_info_dir / dest
self.mkpath(str(dest.parent))
self.copy_file(orig, dest)

if not self.keep_egg_info:
dir_util.remove_tree(egg_info_dir, self.verbose, self.dry_run)

def _license_paths(self):
for file in self.distribution.metadata.license_files or ():
yield os.path.basename(file), file
30 changes: 27 additions & 3 deletions setuptools/tests/test_dist_info.py
Expand Up @@ -5,11 +5,13 @@
import shutil
import subprocess
import sys
from email import message_from_string
from functools import partial

import pytest

import pkg_resources
from setuptools import _reqs
from setuptools.archive_util import unpack_archive
from .textwrap import DALS

Expand Down Expand Up @@ -131,7 +133,7 @@ def test_output_dir(self, tmp_path, keep_egg_info):

class TestWheelCompatibility:
"""Make sure the .dist-info directory produced with the ``dist_info`` command
is the same as the one produced by ``bdist_wheel``.
is the same(ish) as the one produced by ``bdist_wheel``.
"""

SETUPCFG = DALS(
Expand Down Expand Up @@ -189,8 +191,30 @@ def test_dist_info_is_the_same_as_in_wheel(

assert dist_info.name == wheel_dist_info.name
assert dist_info.name.startswith(f"{name.replace('-', '_')}-{version}{suffix}")
for file in "METADATA", "entry_points.txt":
assert read(dist_info / file) == read(wheel_dist_info / file)

assert (dist_info / "entry_points.txt").read_text(encoding="utf-8") == (
wheel_dist_info / "entry_points.txt"
).read_text(encoding="utf-8")

wheel_metadata = (wheel_dist_info / "METADATA").read_text(encoding="utf-8")
metadata = (dist_info / "METADATA").read_text(encoding="utf-8")

# Compare metadata but normalize requirements formatting
wheel_msg = message_from_string(wheel_metadata)
wheel_deps = set(_reqs.parse(wheel_msg.get_all("Requires-Dist")))
wheel_extras = set(wheel_msg.get_all("Provides-Extra"))
del wheel_msg["Requires-Dist"]
del wheel_msg["Provides-Extra"]

metadata_msg = message_from_string(metadata)
metadata_deps = set(_reqs.parse(metadata_msg.get_all("Requires-Dist")))
metadata_extras = set(metadata_msg.get_all("Provides-Extra"))
del metadata_msg["Requires-Dist"]
del metadata_msg["Provides-Extra"]

assert metadata_msg.as_string() == wheel_msg.as_string()
assert metadata_deps == wheel_deps
assert metadata_extras == wheel_extras


def run_command_inner(*cmd, **kwargs):
Expand Down