Skip to content

Commit 2b6dfd0

Browse files
committed
Test compatibility between setuptools and wheel metadata for real use cases
1 parent 5400015 commit 2b6dfd0

File tree

1 file changed

+136
-73
lines changed

1 file changed

+136
-73
lines changed

setuptools/tests/test_core_metadata.py

Lines changed: 136 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1+
from __future__ import annotations
2+
13
import functools
24
import importlib
35
import io
46
from email import message_from_string
7+
from email.message import Message
8+
from pathlib import Path
9+
from unittest.mock import Mock
510

611
import pytest
712
from packaging.metadata import Metadata
13+
from packaging.requirements import Requirement
814

915
from setuptools import _reqs, sic
1016
from setuptools._core_metadata import rfc822_escape, rfc822_unescape
1117
from setuptools.command.egg_info import egg_info, write_requirements
18+
from setuptools.config import expand, setupcfg
1219
from setuptools.dist import Distribution
1320

21+
from .config.downloads import retrieve_file, urls_from_file
22+
1423
EXAMPLE_BASE_INFO = dict(
1524
name="package",
1625
version="0.0.1",
@@ -303,84 +312,138 @@ def test_maintainer_author(name, attrs, tmpdir):
303312
assert line in pkg_lines_set
304313

305314

306-
def test_parity_with_metadata_from_pypa_wheel(tmp_path):
307-
attrs = dict(
308-
**EXAMPLE_BASE_INFO,
309-
# Example with complex requirement definition
310-
python_requires=">=3.8",
311-
install_requires="""
312-
packaging==23.2
313-
more-itertools==8.8.0; extra == "other"
314-
jaraco.text==3.7.0
315-
importlib-resources==5.10.2; python_version<"3.8"
316-
importlib-metadata==6.0.0 ; python_version<"3.8"
317-
colorama>=0.4.4; sys_platform == "win32"
318-
""",
319-
extras_require={
320-
"testing": """
321-
pytest >= 6
322-
pytest-checkdocs >= 2.4
323-
tomli ; \\
324-
# Using stdlib when possible
325-
python_version < "3.11"
326-
ini2toml[lite]>=0.9
327-
""",
328-
"other": [],
329-
},
330-
)
331-
# Generate a PKG-INFO file using setuptools
332-
dist = Distribution(attrs)
333-
with io.StringIO() as fp:
334-
dist.metadata.write_pkg_file(fp)
335-
pkg_info = fp.getvalue()
315+
class TestParityWithMetadataFromPyPaWheel:
316+
def base_example(self):
317+
attrs = dict(
318+
**EXAMPLE_BASE_INFO,
319+
# Example with complex requirement definition
320+
python_requires=">=3.8",
321+
install_requires="""
322+
packaging==23.2
323+
more-itertools==8.8.0; extra == "other"
324+
jaraco.text==3.7.0
325+
importlib-resources==5.10.2; python_version<"3.8"
326+
importlib-metadata==6.0.0 ; python_version<"3.8"
327+
colorama>=0.4.4; sys_platform == "win32"
328+
""",
329+
extras_require={
330+
"testing": """
331+
pytest >= 6
332+
pytest-checkdocs >= 2.4
333+
tomli ; \\
334+
# Using stdlib when possible
335+
python_version < "3.11"
336+
ini2toml[lite]>=0.9
337+
""",
338+
"other": [],
339+
},
340+
)
341+
# Generate a PKG-INFO file using setuptools
342+
return Distribution(attrs)
343+
344+
def test_requires_dist(self, tmp_path):
345+
dist = self.base_example()
346+
pkg_info = _get_pkginfo(dist)
347+
assert _valid_metadata(pkg_info)
348+
349+
# Ensure Requires-Dist is present
350+
expected = [
351+
'Metadata-Version:',
352+
'Requires-Python: >=3.8',
353+
'Provides-Extra: other',
354+
'Provides-Extra: testing',
355+
'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"',
356+
'Requires-Dist: more-itertools==8.8.0; extra == "other"',
357+
'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"',
358+
]
359+
for line in expected:
360+
assert line in pkg_info
361+
362+
HERE = Path(__file__).parent
363+
EXAMPLES_FILE = HERE / "config/setupcfg_examples.txt"
364+
365+
@pytest.fixture(params=[None, *urls_from_file(EXAMPLES_FILE)])
366+
def dist(self, request, monkeypatch, tmp_path):
367+
"""Example of distribution with arbitrary configuration"""
368+
monkeypatch.chdir(tmp_path)
369+
monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.42"))
370+
monkeypatch.setattr(expand, "read_files", Mock(return_value="hello world"))
371+
if request.param is None:
372+
yield self.base_example()
373+
else:
374+
# Real-world usage
375+
config = retrieve_file(request.param)
376+
yield setupcfg.apply_configuration(Distribution({}), config)
377+
378+
def test_equivalent_output(self, tmp_path, dist):
379+
"""Ensure output from setuptools is equivalent to the one from `pypa/wheel`"""
380+
# Generate a METADATA file using pypa/wheel for comparison
381+
wheel_metadata = importlib.import_module("wheel.metadata")
382+
pkginfo_to_metadata = getattr(wheel_metadata, "pkginfo_to_metadata", None)
383+
384+
if pkginfo_to_metadata is None:
385+
pytest.xfail(
386+
"wheel.metadata.pkginfo_to_metadata is undefined, "
387+
"(this is likely to be caused by API changes in pypa/wheel"
388+
)
389+
390+
# Generate an simplified "egg-info" dir for pypa/wheel to convert
391+
pkg_info = _get_pkginfo(dist)
392+
egg_info_dir = tmp_path / "pkg.egg-info"
393+
egg_info_dir.mkdir(parents=True)
394+
(egg_info_dir / "PKG-INFO").write_text(pkg_info, encoding="utf-8")
395+
write_requirements(egg_info(dist), egg_info_dir, egg_info_dir / "requires.txt")
396+
397+
# Get pypa/wheel generated METADATA but normalize requirements formatting
398+
metadata_msg = pkginfo_to_metadata(egg_info_dir, egg_info_dir / "PKG-INFO")
399+
metadata_str = _normalize_metadata(metadata_msg)
400+
pkg_info_msg = message_from_string(pkg_info)
401+
pkg_info_str = _normalize_metadata(pkg_info_msg)
402+
403+
# Compare setuptools PKG-INFO x pypa/wheel METADATA
404+
assert metadata_str == pkg_info_str
405+
406+
407+
def _normalize_metadata(msg: Message) -> str:
408+
"""Allow equivalent metadata to be compared directly"""
409+
# The main challenge regards the requirements and extras.
410+
# Both setuptools and wheel already apply some level of normalization
411+
# but they differ regarding which character is chosen, according to the
412+
# following spec it should be "-":
413+
# https://packaging.python.org/en/latest/specifications/name-normalization/
414+
415+
# Related issues:
416+
# https://github.com/pypa/packaging/issues/845
417+
# https://github.com/pypa/packaging/issues/644#issuecomment-2429813968
418+
419+
extras = {x.replace("_", "-"): x for x in msg.get_all("Provides-Extra", [])}
420+
reqs = [
421+
_normalize_req(req, extras)
422+
for req in _reqs.parse(msg.get_all("Requires-Dist", []))
423+
]
424+
del msg["Requires-Dist"]
425+
del msg["Provides-Extra"]
336426

337-
assert _valid_metadata(pkg_info)
427+
for req in sorted(reqs):
428+
msg["Requires-Dist"] = req
429+
for extra in sorted(extras):
430+
msg["Provides-Extra"] = extra
338431

339-
# Ensure Requires-Dist is present
340-
expected = [
341-
'Metadata-Version:',
342-
'Requires-Python: >=3.8',
343-
'Provides-Extra: other',
344-
'Provides-Extra: testing',
345-
'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"',
346-
'Requires-Dist: more-itertools==8.8.0; extra == "other"',
347-
'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"',
348-
]
349-
for line in expected:
350-
assert line in pkg_info
432+
return msg.as_string()
351433

352-
# Generate a METADATA file using pypa/wheel for comparison
353-
wheel_metadata = importlib.import_module("wheel.metadata")
354-
pkginfo_to_metadata = getattr(wheel_metadata, "pkginfo_to_metadata", None)
355434

356-
if pkginfo_to_metadata is None:
357-
pytest.xfail(
358-
"wheel.metadata.pkginfo_to_metadata is undefined, "
359-
"(this is likely to be caused by API changes in pypa/wheel"
360-
)
435+
def _normalize_req(req: Requirement, extras: dict[str, str]) -> str:
436+
"""Allow equivalent requirement objects to be compared directly"""
437+
as_str = str(req).replace(req.name, req.name.replace("_", "-"))
438+
for norm, orig in extras.items():
439+
as_str = as_str.replace(orig, norm)
440+
return as_str
361441

362-
# Generate an simplified "egg-info" dir for pypa/wheel to convert
363-
egg_info_dir = tmp_path / "pkg.egg-info"
364-
egg_info_dir.mkdir(parents=True)
365-
(egg_info_dir / "PKG-INFO").write_text(pkg_info, encoding="utf-8")
366-
write_requirements(egg_info(dist), egg_info_dir, egg_info_dir / "requires.txt")
367-
368-
# Get pypa/wheel generated METADATA but normalize requirements formatting
369-
metadata_msg = pkginfo_to_metadata(egg_info_dir, egg_info_dir / "PKG-INFO")
370-
metadata_deps = set(_reqs.parse(metadata_msg.get_all("Requires-Dist")))
371-
metadata_extras = set(metadata_msg.get_all("Provides-Extra"))
372-
del metadata_msg["Requires-Dist"]
373-
del metadata_msg["Provides-Extra"]
374-
pkg_info_msg = message_from_string(pkg_info)
375-
pkg_info_deps = set(_reqs.parse(pkg_info_msg.get_all("Requires-Dist")))
376-
pkg_info_extras = set(pkg_info_msg.get_all("Provides-Extra"))
377-
del pkg_info_msg["Requires-Dist"]
378-
del pkg_info_msg["Provides-Extra"]
379-
380-
# Compare setuptools PKG-INFO x pypa/wheel METADATA
381-
assert metadata_msg.as_string() == pkg_info_msg.as_string()
382-
assert metadata_deps == pkg_info_deps
383-
assert metadata_extras == pkg_info_extras
442+
443+
def _get_pkginfo(dist: Distribution):
444+
with io.StringIO() as fp:
445+
dist.metadata.write_pkg_file(fp)
446+
return fp.getvalue()
384447

385448

386449
def _valid_metadata(text: str) -> bool:

0 commit comments

Comments
 (0)