Skip to content

Commit 3465cac

Browse files
committed
Fix version produced by dist_info (#3230)
2 parents cde42d6 + b1dca40 commit 3465cac

File tree

4 files changed

+130
-3
lines changed

4 files changed

+130
-3
lines changed

changelog.d/3088.misc.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed duplicated tag with the ``dist-info`` command.

setuptools/command/dist_info.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
"""
55

66
import os
7+
import re
8+
import warnings
9+
from inspect import cleandoc
710

811
from distutils.core import Command
912
from distutils import log
13+
from setuptools.extern import packaging
1014

1115

1216
class dist_info(Command):
@@ -29,8 +33,36 @@ def run(self):
2933
egg_info.egg_base = self.egg_base
3034
egg_info.finalize_options()
3135
egg_info.run()
32-
dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info'
36+
name = _safe(self.distribution.get_name())
37+
version = _version(self.distribution.get_version())
38+
base = self.egg_base or os.curdir
39+
dist_info_dir = os.path.join(base, f"{name}-{version}.dist-info")
3340
log.info("creating '{}'".format(os.path.abspath(dist_info_dir)))
3441

3542
bdist_wheel = self.get_finalized_command('bdist_wheel')
3643
bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir)
44+
45+
46+
def _safe(component: str) -> str:
47+
"""Escape a component used to form a wheel name according to PEP 491"""
48+
return re.sub(r"[^\w\d.]+", "_", component)
49+
50+
51+
def _version(version: str) -> str:
52+
"""Convert an arbitrary string to a version string."""
53+
v = version.replace(' ', '.')
54+
try:
55+
return str(packaging.version.Version(v)).replace("-", "_")
56+
except packaging.version.InvalidVersion:
57+
msg = f"""!!\n\n
58+
###################
59+
# Invalid version #
60+
###################
61+
{version!r} is not valid according to PEP 440.\n
62+
Please make sure specify a valid version for your package.
63+
Also note that future releases of setuptools may halt the build process
64+
if an invalid version is given.
65+
\n\n!!
66+
"""
67+
warnings.warn(cleandoc(msg))
68+
return _safe(v).strip("_")

setuptools/command/egg_info.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,21 @@ def _maybe_tag(self, version):
136136
in which case the version string already contains all tags.
137137
"""
138138
return (
139-
version if self.vtags and version.endswith(self.vtags)
139+
version if self.vtags and self._already_tagged(version)
140140
else version + self.vtags
141141
)
142142

143-
def tags(self):
143+
def _already_tagged(self, version: str) -> bool:
144+
# Depending on their format, tags may change with version normalization.
145+
# So in addition the regular tags, we have to search for the normalized ones.
146+
return version.endswith(self.vtags) or version.endswith(self._safe_tags())
147+
148+
def _safe_tags(self) -> str:
149+
# To implement this we can rely on `safe_version` pretending to be version 0
150+
# followed by tags. Then we simply discard the starting 0 (fake version number)
151+
return safe_version(f"0{self.vtags}")[1:]
152+
153+
def tags(self) -> str:
144154
version = ''
145155
if self.tag_build:
146156
version += self.tag_build

setuptools/tests/test_dist_info.py

+84
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
"""Test .dist-info style distributions.
22
"""
3+
import pathlib
4+
import re
5+
import subprocess
6+
import sys
7+
from functools import partial
38

49
import pytest
510

611
import pkg_resources
12+
from setuptools.archive_util import unpack_archive
713
from .textwrap import DALS
814

915

16+
read = partial(pathlib.Path.read_text, encoding="utf-8")
17+
18+
1019
class TestDistInfo:
1120

1221
metadata_base = DALS("""
@@ -72,3 +81,78 @@ def test_conditional_dependencies(self, metadata):
7281
pkg_resources.Requirement.parse('quux>=1.1;extra=="baz"'),
7382
]
7483
assert d.extras == ['baz']
84+
85+
def test_invalid_version(self, tmp_path):
86+
config = "[metadata]\nname=proj\nversion=42\n[egg_info]\ntag_build=invalid!!!\n"
87+
(tmp_path / "setup.cfg").write_text(config, encoding="utf-8")
88+
msg = re.compile("invalid version", re.M | re.I)
89+
output = run_command("dist_info", cwd=tmp_path)
90+
assert msg.search(output)
91+
dist_info = next(tmp_path.glob("*.dist-info"))
92+
assert dist_info.name.startswith("proj-42")
93+
94+
95+
class TestWheelCompatibility:
96+
"""Make sure the .dist-info directory produced with the ``dist_info`` command
97+
is the same as the one produced by ``bdist_wheel``.
98+
"""
99+
SETUPCFG = DALS("""
100+
[metadata]
101+
name = {name}
102+
version = {version}
103+
104+
[options]
105+
install_requires = foo>=12; sys_platform != "linux"
106+
107+
[options.extras_require]
108+
test = pytest
109+
110+
[options.entry_points]
111+
console_scripts =
112+
executable-name = my_package.module:function
113+
discover =
114+
myproj = my_package.other_module:function
115+
""")
116+
117+
EGG_INFO_OPTS = [
118+
# Related: #3088 #2872
119+
("", ""),
120+
(".post", "[egg_info]\ntag_build = post\n"),
121+
(".post", "[egg_info]\ntag_build = .post\n"),
122+
(".post", "[egg_info]\ntag_build = post\ntag_date = 1\n"),
123+
(".dev", "[egg_info]\ntag_build = .dev\n"),
124+
(".dev", "[egg_info]\ntag_build = .dev\ntag_date = 1\n"),
125+
("a1", "[egg_info]\ntag_build = .a1\n"),
126+
("+local", "[egg_info]\ntag_build = +local\n"),
127+
]
128+
129+
@pytest.mark.parametrize("name", "my-proj my_proj my.proj My.Proj".split())
130+
@pytest.mark.parametrize("version", ["0.42.13"])
131+
@pytest.mark.parametrize("suffix, cfg", EGG_INFO_OPTS)
132+
def test_dist_info_is_the_same_as_in_wheel(
133+
self, name, version, tmp_path, suffix, cfg
134+
):
135+
config = self.SETUPCFG.format(name=name, version=version) + cfg
136+
137+
for i in "dir_wheel", "dir_dist":
138+
(tmp_path / i).mkdir()
139+
(tmp_path / i / "setup.cfg").write_text(config, encoding="utf-8")
140+
141+
run_command("bdist_wheel", cwd=tmp_path / "dir_wheel")
142+
wheel = next(tmp_path.glob("dir_wheel/dist/*.whl"))
143+
unpack_archive(wheel, tmp_path / "unpack")
144+
wheel_dist_info = next(tmp_path.glob("unpack/*.dist-info"))
145+
146+
run_command("dist_info", cwd=tmp_path / "dir_dist")
147+
dist_info = next(tmp_path.glob("dir_dist/*.dist-info"))
148+
149+
assert dist_info.name == wheel_dist_info.name
150+
assert dist_info.name.startswith(f"{name.replace('-', '_')}-{version}{suffix}")
151+
for file in "METADATA", "entry_points.txt":
152+
assert read(dist_info / file) == read(wheel_dist_info / file)
153+
154+
155+
def run_command(*cmd, **kwargs):
156+
opts = {"stderr": subprocess.STDOUT, "text": True, **kwargs}
157+
cmd = [sys.executable, "-c", "__import__('setuptools').setup()", *cmd]
158+
return subprocess.check_output(cmd, **opts)

0 commit comments

Comments
 (0)