diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py index 9df625cee7..81d59c3833 100644 --- a/setuptools/command/dist_info.py +++ b/setuptools/command/dist_info.py @@ -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, @@ -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): @@ -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: @@ -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: @@ -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 diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py index a76dbeb3f2..556d16fc9f 100644 --- a/setuptools/tests/test_dist_info.py +++ b/setuptools/tests/test_dist_info.py @@ -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 @@ -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( @@ -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):