diff --git a/newsfragments/4766.bugfix.rst b/newsfragments/4766.bugfix.rst new file mode 100644 index 0000000000..fcd54785d8 --- /dev/null +++ b/newsfragments/4766.bugfix.rst @@ -0,0 +1 @@ +Fix wheel file naming to follow binary distribution specification -- by :user:`di` diff --git a/setuptools/_normalization.py b/setuptools/_normalization.py index 467b643d46..9541a55d6c 100644 --- a/setuptools/_normalization.py +++ b/setuptools/_normalization.py @@ -134,7 +134,13 @@ def filename_component_broken(value: str) -> str: def safer_name(value: str) -> str: """Like ``safe_name`` but can be used as filename component for wheel""" # See bdist_wheel.safer_name - return filename_component(safe_name(value)) + return ( + # Per https://packaging.python.org/en/latest/specifications/name-normalization/#name-normalization + re.sub(r"[-_.]+", "-", safe_name(value)) + .lower() + # Per https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode + .replace("-", "_") + ) def safer_best_effort_version(value: str) -> str: diff --git a/setuptools/command/bdist_wheel.py b/setuptools/command/bdist_wheel.py index bcd176f98e..27b36232ec 100644 --- a/setuptools/command/bdist_wheel.py +++ b/setuptools/command/bdist_wheel.py @@ -23,19 +23,13 @@ from wheel.wheelfile import WheelFile from .. import Command, __version__, _shutil +from .._normalization import safer_name from ..warnings import SetuptoolsDeprecationWarning from .egg_info import egg_info as egg_info_cls from distutils import log -def safe_name(name: str) -> str: - """Convert an arbitrary string to a standard distribution name - Any runs of non-alphanumeric/. characters are replaced with a single '-'. - """ - return re.sub("[^A-Za-z0-9.]+", "-", name) - - def safe_version(version: str) -> str: """ Convert an arbitrary string to a standard version string @@ -133,10 +127,6 @@ def get_abi_tag() -> str | None: return abi -def safer_name(name: str) -> str: - return safe_name(name).replace("-", "_") - - def safer_version(version: str) -> str: return safe_version(version).replace("-", "_") diff --git a/setuptools/tests/test_bdist_wheel.py b/setuptools/tests/test_bdist_wheel.py index d51dfbeb6d..776d21d729 100644 --- a/setuptools/tests/test_bdist_wheel.py +++ b/setuptools/tests/test_bdist_wheel.py @@ -246,9 +246,9 @@ def test_no_scripts(wheel_paths): def test_unicode_record(wheel_paths): - path = next(path for path in wheel_paths if "unicode.dist" in path) + path = next(path for path in wheel_paths if "unicode_dist" in path) with ZipFile(path) as zf: - record = zf.read("unicode.dist-0.1.dist-info/RECORD") + record = zf.read("unicode_dist-0.1.dist-info/RECORD") assert "åäö_日本語.py".encode() in record diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 533eb9f45e..e65ab310e7 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -8,7 +8,7 @@ from setuptools import Distribution from setuptools.dist import check_package_data, check_specifier -from .test_easy_install import make_nspkg_sdist +from .test_easy_install import make_trivial_sdist from .test_find_packages import ensure_files from .textwrap import DALS @@ -25,7 +25,7 @@ def test_dist_fetch_build_egg(tmpdir): def sdist_with_index(distname, version): dist_dir = index.mkdir(distname) dist_sdist = f'{distname}-{version}.tar.gz' - make_nspkg_sdist(str(dist_dir.join(dist_sdist)), distname, version) + make_trivial_sdist(str(dist_dir.join(dist_sdist)), distname, version) with dist_dir.join('index.html').open('w') as fp: fp.write( DALS( diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py index 31e6e95a68..426694e019 100644 --- a/setuptools/tests/test_dist_info.py +++ b/setuptools/tests/test_dist_info.py @@ -188,7 +188,7 @@ def test_dist_info_is_the_same_as_in_wheel( dist_info = next(tmp_path.glob("dir_dist/*.dist-info")) assert dist_info.name == wheel_dist_info.name - assert dist_info.name.startswith(f"{name.replace('-', '_')}-{version}{suffix}") + assert dist_info.name.startswith(f"my_proj-{version}{suffix}") for file in "METADATA", "entry_points.txt": assert read(dist_info / file) == read(wheel_dist_info / file) diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index e9b96027ce..b58b0b6666 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -26,6 +26,7 @@ import setuptools.command.easy_install as ei from pkg_resources import Distribution as PRDistribution, normalize_path, working_set from setuptools import sandbox +from setuptools._normalization import safer_name from setuptools.command.easy_install import PthDistributions from setuptools.dist import Distribution from setuptools.sandbox import run_setup @@ -670,11 +671,11 @@ def test_setup_requires_override_nspkg(self, use_setup_cfg): with contexts.save_pkg_resources_state(): with contexts.tempdir() as temp_dir: - foobar_1_archive = os.path.join(temp_dir, 'foo.bar-0.1.tar.gz') + foobar_1_archive = os.path.join(temp_dir, 'foo_bar-0.1.tar.gz') make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1') # Now actually go ahead an extract to the temp dir and add the # extracted path to sys.path so foo.bar v0.1 is importable - foobar_1_dir = os.path.join(temp_dir, 'foo.bar-0.1') + foobar_1_dir = os.path.join(temp_dir, 'foo_bar-0.1') os.mkdir(foobar_1_dir) with tarfile.open(foobar_1_archive) as tf: tf.extraction_filter = lambda member, path: member @@ -697,7 +698,7 @@ def test_setup_requires_override_nspkg(self, use_setup_cfg): len(foo.__path__) == 2): print('FAIL') - if 'foo.bar-0.2' not in foo.__path__[0]: + if 'foo_bar-0.2' not in foo.__path__[0]: print('FAIL') """ ) @@ -718,8 +719,8 @@ def test_setup_requires_override_nspkg(self, use_setup_cfg): # Don't even need to install the package, just # running the setup.py at all is sufficient run_setup(test_setup_py, ['--name']) - except pkg_resources.VersionConflict: - self.fail( + except pkg_resources.VersionConflict: # pragma: nocover + pytest.fail( 'Installing setup.py requirements caused a VersionConflict' ) @@ -1120,6 +1121,8 @@ def make_nspkg_sdist(dist_path, distname, version): package with the same name as distname. The top-level package is designated a namespace package). """ + # Assert that the distname contains at least one period + assert '.' in distname parts = distname.split('.') nspackage = parts[0] @@ -1207,10 +1210,11 @@ def create_setup_requires_package( package itself is just 'test_pkg'. """ + normalized_distname = safer_name(distname) test_setup_attrs = { 'name': 'test_pkg', 'version': '0.0', - 'setup_requires': [f'{distname}=={version}'], + 'setup_requires': [f'{normalized_distname}=={version}'], 'dependency_links': [os.path.abspath(path)], } if setup_attrs: @@ -1259,7 +1263,7 @@ def create_setup_requires_package( with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f: f.write(setup_py_template % test_setup_attrs) - foobar_path = os.path.join(path, f'{distname}-{version}.tar.gz') + foobar_path = os.path.join(path, f'{normalized_distname}-{version}.tar.gz') make_package(foobar_path, distname, version) return test_pkg