diff --git a/distutils/dist.py b/distutils/dist.py index fcb5f084..5a54798e 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -25,6 +25,7 @@ overload, ) +from jaraco.text import SeparatedValues from packaging.utils import canonicalize_name, canonicalize_version from ._log import log @@ -75,6 +76,34 @@ def _ensure_list(value: str | Iterable[str], fieldname) -> str | list[str]: return value +def _repair_newlines(value: str) -> str: + """ + Repair a ``keywords`` or ``platforms`` value that improperly uses + newlines to separate items. + + These fields separate items with commas (and, in the old specification, + PEP 345, with spaces). Newlines were never a valid separator and corrupt + the generated ``PKG-INFO``/``METADATA`` (pypa/setuptools#4887). As a + forgiving measure, values that supply these fields as newline-separated + strings (e.g. a triple-quoted string with one item per line) have each + line treated as a separate item by replacing newlines with commas, but + the behavior is deprecated. Empty items produced by consecutive or + trailing separators are dropped by the caller. + """ + if "\n" not in value: + return value + # A plain warning (UserWarning) rather than DeprecationWarning so that + # it's shown by default; DeprecationWarning is suppressed outside of test + # runners and this needs to reach setuptools users (cf. distutils.log.Log). + warnings.warn( + "Newlines are not a valid separator for the 'keywords' and " + "'platforms' fields and their use is deprecated. Separate items " + "with commas instead. This will raise an error in the future.", + stacklevel=2, + ) + return value.replace("\n", ",") + + class Distribution: """The core of the Distutils. Most of the work hiding behind 'setup' is really done within a Distribution instance, which farms the work out @@ -643,7 +672,8 @@ def finalize_options(self) -> None: if value is None: continue if isinstance(value, str): - value = [elm.strip() for elm in value.split(',')] + # SeparatedValues strips whitespace and discards empty items. + value = list(SeparatedValues(_repair_newlines(value))) setattr(self.metadata, attr, value) def _show_help( diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 2c5beebe..6d594ba7 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -217,6 +217,28 @@ def test_finalize_options(self): assert dist.metadata.platforms == ['foo bar'] assert dist.metadata.keywords == ['foo bar'] + # Newlines are an invalid (deprecated) separator; each line is + # treated as a separate item (pypa/setuptools#4887). + attrs = { + 'keywords': 'one\ntwo\nthree\nfour', + 'platforms': 'one\ntwo\nthree\nfour', + } + with pytest.warns(UserWarning, match="Newlines"): + dist = Distribution(attrs=attrs) + assert dist.metadata.platforms == ['one', 'two', 'three', 'four'] + assert dist.metadata.keywords == ['one', 'two', 'three', 'four'] + + # Consecutive, leading, or trailing newlines must not produce + # empty items (e.g. from a triple-quoted string with blank lines). + attrs = { + 'keywords': '\none two\n\nthree four\n', + 'platforms': '\none two\n\nthree four\n', + } + with pytest.warns(UserWarning, match="Newlines"): + dist = Distribution(attrs=attrs) + assert dist.metadata.platforms == ['one two', 'three four'] + assert dist.metadata.keywords == ['one two', 'three four'] + def test_get_command_packages(self): dist = Distribution() assert dist.command_packages is None diff --git a/newsfragments/4887.feature.rst b/newsfragments/4887.feature.rst new file mode 100644 index 00000000..e7ef4c06 --- /dev/null +++ b/newsfragments/4887.feature.rst @@ -0,0 +1 @@ +Newline-separated ``keywords`` and ``platforms``, which are invalid and corrupt the generated metadata (pypa/setuptools#4887), are now handled forgivingly: each line is treated as a separate item and a deprecation warning is emitted. Newlines were never a valid separator for these fields -- the `old specification `_ separated items with spaces and the current one uses commas. diff --git a/pyproject.toml b/pyproject.toml index e196e15d..9b0d5509 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "packaging", "jaraco.functools >= 4", "more_itertools", + "jaraco.text", ] dynamic = ["version"]