From 2ea6066dbdc19b69af901ab24f5b75090dc9de27 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Wed, 19 Mar 2025 11:03:47 +0100 Subject: [PATCH 1/4] Ignore newlines when processing keywords/platforms Newlines in `keywords` or `platforms` can break the produced metadata in PKG-INFO or METADATA files. Fixes: https://github.com/pypa/setuptools/issues/4887 --- distutils/dist.py | 1 + distutils/tests/test_dist.py | 13 +++++++++++++ newsfragments/4887.bugfix.rst | 1 + 3 files changed, 15 insertions(+) create mode 100644 newsfragments/4887.bugfix.rst diff --git a/distutils/dist.py b/distutils/dist.py index 69d42016a..8752e203e 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -644,6 +644,7 @@ def finalize_options(self) -> None: if value is None: continue if isinstance(value, str): + value = value.replace("\n", " ") value = [elm.strip() for elm in value.split(',')] setattr(self.metadata, attr, value) diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 2c5beebe6..6bcc2acbd 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -217,6 +217,19 @@ def test_finalize_options(self): assert dist.metadata.platforms == ['foo bar'] assert dist.metadata.keywords == ['foo bar'] + # finalize_option removes '\n' from keywords and platforms + attrs = {'keywords': 'one,two,\nthree,four\n', 'platforms': 'one,two,\nthree,four\n'} + dist = Distribution(attrs=attrs) + dist.finalize_options() + assert dist.metadata.platforms == ['one', 'two', 'three', 'four'] + assert dist.metadata.keywords == ['one', 'two', 'three', 'four'] + + attrs = {'keywords': 'one two\nthree four\n', 'platforms': 'one two\nthree four\n'} + dist = Distribution(attrs=attrs) + dist.finalize_options() + 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.bugfix.rst b/newsfragments/4887.bugfix.rst new file mode 100644 index 000000000..83b92562c --- /dev/null +++ b/newsfragments/4887.bugfix.rst @@ -0,0 +1 @@ +Fixed handling of keywords following the old specification with spaces instead of commas and containing newlines. \ No newline at end of file From ce29b82118d3f8307bb021695c8b27535d26959e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Jul 2026 10:54:55 -0400 Subject: [PATCH 2/4] Repair newline-separated keywords/platforms and deprecate the behavior Rather than silently joining newline-separated items with spaces, treat each line as a separate item (replacing newlines with commas) and warn that the behavior is deprecated, since newlines were never a valid separator for these fields. Extract the repair into a documented helper. Ref pypa/setuptools#4887 Co-Authored-By: Claude Opus 4.8 --- distutils/dist.py | 28 ++++++++++++++++++++++++++-- distutils/tests/test_dist.py | 27 ++++++++++++++++++--------- newsfragments/4887.bugfix.rst | 1 - newsfragments/4887.feature.rst | 1 + 4 files changed, 45 insertions(+), 12 deletions(-) delete mode 100644 newsfragments/4887.bugfix.rst create mode 100644 newsfragments/4887.feature.rst diff --git a/distutils/dist.py b/distutils/dist.py index 8752e203e..6a3c5ff21 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -74,6 +74,31 @@ 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). Some + projects nonetheless supply these fields as newline-separated strings + (e.g. a triple-quoted string with one item per line), so treat each line + as a separate item by replacing newlines with commas, and warn that the + behavior is deprecated. + """ + if "\n" not in value: + return value + 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.", + DeprecationWarning, + stacklevel=2, + ) + return value.strip().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 @@ -644,8 +669,7 @@ def finalize_options(self) -> None: if value is None: continue if isinstance(value, str): - value = value.replace("\n", " ") - value = [elm.strip() for elm in value.split(',')] + value = [elm.strip() for elm in _repair_newlines(value).split(',')] setattr(self.metadata, attr, value) def _show_help( diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 6bcc2acbd..041ec783a 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -217,18 +217,27 @@ def test_finalize_options(self): assert dist.metadata.platforms == ['foo bar'] assert dist.metadata.keywords == ['foo bar'] - # finalize_option removes '\n' from keywords and platforms - attrs = {'keywords': 'one,two,\nthree,four\n', 'platforms': 'one,two,\nthree,four\n'} - dist = Distribution(attrs=attrs) - dist.finalize_options() + # 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(DeprecationWarning, match="Newlines"): + dist = Distribution(attrs=attrs) assert dist.metadata.platforms == ['one', 'two', 'three', 'four'] assert dist.metadata.keywords == ['one', 'two', 'three', 'four'] - attrs = {'keywords': 'one two\nthree four\n', 'platforms': 'one two\nthree four\n'} - dist = Distribution(attrs=attrs) - dist.finalize_options() - assert dist.metadata.platforms == ['one two three four'] - assert dist.metadata.keywords == ['one two three four'] + # Surrounding newlines (e.g. from a triple-quoted string) don't + # introduce empty items. + attrs = { + 'keywords': '\none two\nthree four\n', + 'platforms': '\none two\nthree four\n', + } + with pytest.warns(DeprecationWarning): + 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() diff --git a/newsfragments/4887.bugfix.rst b/newsfragments/4887.bugfix.rst deleted file mode 100644 index 83b92562c..000000000 --- a/newsfragments/4887.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed handling of keywords following the old specification with spaces instead of commas and containing newlines. \ No newline at end of file diff --git a/newsfragments/4887.feature.rst b/newsfragments/4887.feature.rst new file mode 100644 index 000000000..69acc0e22 --- /dev/null +++ b/newsfragments/4887.feature.rst @@ -0,0 +1 @@ +Improperly newline-separated ``keywords`` and ``platforms`` are now handled by treating each line as a separate item, and that usage is now deprecated. Newlines were never a valid separator for these fields -- the `old specification `_ separated items with spaces and the current one uses commas -- and they corrupted the generated metadata (pypa/setuptools#4887). From 9e21d0f13eee6be2858daaecc5ab99e460dc1190 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Jul 2026 11:05:26 -0400 Subject: [PATCH 3/4] Address review: visible warning, drop empty items, reframe as forgiving - Emit a plain (UserWarning) rather than DeprecationWarning, which is suppressed by default and wouldn't reach setuptools users (per the distutils.log.Log precedent). - Drop empty items so consecutive/leading/trailing newlines don't create blank keywords; cover this in the tests. - Reframe the news and docstring as forgiving invalid input rather than implying newlines followed any specification. --- distutils/dist.py | 23 +++++++++++++++-------- distutils/tests/test_dist.py | 12 ++++++------ newsfragments/4887.feature.rst | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/distutils/dist.py b/distutils/dist.py index 6a3c5ff21..71455d3d2 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -81,22 +81,25 @@ def _repair_newlines(value: str) -> str: 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). Some - projects nonetheless supply these fields as newline-separated strings - (e.g. a triple-quoted string with one item per line), so treat each line - as a separate item by replacing newlines with commas, and warn that the - behavior is deprecated. + 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.", - DeprecationWarning, stacklevel=2, ) - return value.strip().replace("\n", ",") + return value.replace("\n", ",") class Distribution: @@ -669,7 +672,11 @@ def finalize_options(self) -> None: if value is None: continue if isinstance(value, str): - value = [elm.strip() for elm in _repair_newlines(value).split(',')] + value = [ + stripped + for elm in _repair_newlines(value).split(',') + if (stripped := elm.strip()) + ] setattr(self.metadata, attr, value) def _show_help( diff --git a/distutils/tests/test_dist.py b/distutils/tests/test_dist.py index 041ec783a..6d594ba7b 100644 --- a/distutils/tests/test_dist.py +++ b/distutils/tests/test_dist.py @@ -223,18 +223,18 @@ def test_finalize_options(self): 'keywords': 'one\ntwo\nthree\nfour', 'platforms': 'one\ntwo\nthree\nfour', } - with pytest.warns(DeprecationWarning, match="Newlines"): + 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'] - # Surrounding newlines (e.g. from a triple-quoted string) don't - # introduce empty items. + # Consecutive, leading, or trailing newlines must not produce + # empty items (e.g. from a triple-quoted string with blank lines). attrs = { - 'keywords': '\none two\nthree four\n', - 'platforms': '\none two\nthree four\n', + 'keywords': '\none two\n\nthree four\n', + 'platforms': '\none two\n\nthree four\n', } - with pytest.warns(DeprecationWarning): + 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'] diff --git a/newsfragments/4887.feature.rst b/newsfragments/4887.feature.rst index 69acc0e22..e7ef4c068 100644 --- a/newsfragments/4887.feature.rst +++ b/newsfragments/4887.feature.rst @@ -1 +1 @@ -Improperly newline-separated ``keywords`` and ``platforms`` are now handled by treating each line as a separate item, and that usage is now deprecated. Newlines were never a valid separator for these fields -- the `old specification `_ separated items with spaces and the current one uses commas -- and they corrupted the generated metadata (pypa/setuptools#4887). +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. From 2326db9432723fa409b076a8b99b11e1f4137d41 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Jul 2026 11:21:21 -0400 Subject: [PATCH 4/4] Use jaraco.text.SeparatedValues for keyword/platform splitting Replace the hand-rolled comma split/strip/drop-empties with the reusable jaraco.text.SeparatedValues, promoting jaraco.text to a runtime dependency. _repair_newlines still handles the invalid-newline repair and deprecation. --- distutils/dist.py | 8 +++----- pyproject.toml | 1 + 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/distutils/dist.py b/distutils/dist.py index 71455d3d2..368192c47 100644 --- a/distutils/dist.py +++ b/distutils/dist.py @@ -26,6 +26,7 @@ overload, ) +from jaraco.text import SeparatedValues from packaging.utils import canonicalize_name, canonicalize_version from ._log import log @@ -672,11 +673,8 @@ def finalize_options(self) -> None: if value is None: continue if isinstance(value, str): - value = [ - stripped - for elm in _repair_newlines(value).split(',') - if (stripped := elm.strip()) - ] + # SeparatedValues strips whitespace and discards empty items. + value = list(SeparatedValues(_repair_newlines(value))) setattr(self.metadata, attr, value) def _show_help( diff --git a/pyproject.toml b/pyproject.toml index 9cbb3590b..af3d75551 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "jaraco.functools >= 4", "more_itertools", "jaraco.collections", + "jaraco.text", ] dynamic = ["version"]