Skip to content

Commit 8837dd0

Browse files
authored
bpo-19610: Warn if distutils is provided something other than a list to some fields (#4685)
* Rather than raise TypeError, warn and call list() on the value. * Fix tests, revise NEWS and whatsnew text. * Revise documentation, a string is okay as well. * Ensure 'requires' and 'obsoletes' are real lists. * Test that requires and obsoletes are turned to lists.
1 parent 9625bf5 commit 8837dd0

File tree

5 files changed

+66
-47
lines changed

5 files changed

+66
-47
lines changed

Doc/distutils/apiref.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,9 @@ the full reference.
286286
Distribution constructor. :func:`setup` creates a Distribution instance.
287287

288288
.. versionchanged:: 3.7
289-
:class:`~distutils.core.Distribution` now raises a :exc:`TypeError` if
290-
``classifiers``, ``keywords`` and ``platforms`` fields are not specified
291-
as a list.
289+
:class:`~distutils.core.Distribution` now warns if ``classifiers``,
290+
``keywords`` and ``platforms`` fields are not specified as a list or
291+
a string.
292292

293293
.. class:: Command
294294

Doc/whatsnew/3.7.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,8 @@ README.rst is now included in the list of distutils standard READMEs and
298298
therefore included in source distributions.
299299
(Contributed by Ryan Gonzalez in :issue:`11913`.)
300300

301-
:class:`distutils.core.setup` now raises a :exc:`TypeError` if
302-
``classifiers``, ``keywords`` and ``platforms`` fields are not specified
303-
as a list. However, to minimize backwards incompatibility concerns,
304-
``keywords`` and ``platforms`` fields still accept a comma separated string.
301+
:class:`distutils.core.setup` now warns if the ``classifiers``, ``keywords``
302+
and ``platforms`` fields are not specified as a list or a string.
305303
(Contributed by Berker Peksag in :issue:`19610`.)
306304

307305
http.client

Lib/distutils/dist.py

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@
2727
command_re = re.compile(r'^[a-zA-Z]([a-zA-Z0-9_]*)$')
2828

2929

30+
def _ensure_list(value, fieldname):
31+
if isinstance(value, str):
32+
# a string containing comma separated values is okay. It will
33+
# be converted to a list by Distribution.finalize_options().
34+
pass
35+
elif not isinstance(value, list):
36+
# passing a tuple or an iterator perhaps, warn and convert
37+
typename = type(value).__name__
38+
msg = f"Warning: '{fieldname}' should be a list, got type '{typename}'"
39+
log.log(log.WARN, msg)
40+
value = list(value)
41+
return value
42+
43+
3044
class Distribution:
3145
"""The core of the Distutils. Most of the work hiding behind 'setup'
3246
is really done within a Distribution instance, which farms the work out
@@ -257,10 +271,7 @@ def __init__(self, attrs=None):
257271
setattr(self, key, val)
258272
else:
259273
msg = "Unknown distribution option: %s" % repr(key)
260-
if warnings is not None:
261-
warnings.warn(msg)
262-
else:
263-
sys.stderr.write(msg + "\n")
274+
warnings.warn(msg)
264275

265276
# no-user-cfg is handled before other command line args
266277
# because other args override the config files, and this
@@ -1189,36 +1200,19 @@ def get_keywords(self):
11891200
return self.keywords or []
11901201

11911202
def set_keywords(self, value):
1192-
# If 'keywords' is a string, it will be converted to a list
1193-
# by Distribution.finalize_options(). To maintain backwards
1194-
# compatibility, do not raise an exception if 'keywords' is
1195-
# a string.
1196-
if not isinstance(value, (list, str)):
1197-
msg = "'keywords' should be a 'list', not %r"
1198-
raise TypeError(msg % type(value).__name__)
1199-
self.keywords = value
1203+
self.keywords = _ensure_list(value, 'keywords')
12001204

12011205
def get_platforms(self):
12021206
return self.platforms or ["UNKNOWN"]
12031207

12041208
def set_platforms(self, value):
1205-
# If 'platforms' is a string, it will be converted to a list
1206-
# by Distribution.finalize_options(). To maintain backwards
1207-
# compatibility, do not raise an exception if 'platforms' is
1208-
# a string.
1209-
if not isinstance(value, (list, str)):
1210-
msg = "'platforms' should be a 'list', not %r"
1211-
raise TypeError(msg % type(value).__name__)
1212-
self.platforms = value
1209+
self.platforms = _ensure_list(value, 'platforms')
12131210

12141211
def get_classifiers(self):
12151212
return self.classifiers or []
12161213

12171214
def set_classifiers(self, value):
1218-
if not isinstance(value, list):
1219-
msg = "'classifiers' should be a 'list', not %r"
1220-
raise TypeError(msg % type(value).__name__)
1221-
self.classifiers = value
1215+
self.classifiers = _ensure_list(value, 'classifiers')
12221216

12231217
def get_download_url(self):
12241218
return self.download_url or "UNKNOWN"
@@ -1231,7 +1225,7 @@ def set_requires(self, value):
12311225
import distutils.versionpredicate
12321226
for v in value:
12331227
distutils.versionpredicate.VersionPredicate(v)
1234-
self.requires = value
1228+
self.requires = list(value)
12351229

12361230
def get_provides(self):
12371231
return self.provides or []
@@ -1250,7 +1244,7 @@ def set_obsoletes(self, value):
12501244
import distutils.versionpredicate
12511245
for v in value:
12521246
distutils.versionpredicate.VersionPredicate(v)
1253-
self.obsoletes = value
1247+
self.obsoletes = list(value)
12541248

12551249
def fix_help_options(options):
12561250
"""Convert a 4-tuple 'help_options' list as found in various command

Lib/distutils/tests/test_dist.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
from distutils.dist import Distribution, fix_help_options, DistributionMetadata
1212
from distutils.cmd import Command
1313

14-
from test.support import TESTFN, captured_stdout, run_unittest
14+
from test.support import (
15+
TESTFN, captured_stdout, captured_stderr, run_unittest
16+
)
1517
from distutils.tests import support
1618
from distutils import log
1719

@@ -319,6 +321,13 @@ def test_requires_illegal(self):
319321
"version": "1.0",
320322
"requires": ["my.pkg (splat)"]})
321323

324+
def test_requires_to_list(self):
325+
attrs = {"name": "package",
326+
"requires": iter(["other"])}
327+
dist = Distribution(attrs)
328+
self.assertIsInstance(dist.metadata.requires, list)
329+
330+
322331
def test_obsoletes(self):
323332
attrs = {"name": "package",
324333
"version": "1.0",
@@ -341,6 +350,12 @@ def test_obsoletes_illegal(self):
341350
"version": "1.0",
342351
"obsoletes": ["my.pkg (splat)"]})
343352

353+
def test_obsoletes_to_list(self):
354+
attrs = {"name": "package",
355+
"obsoletes": iter(["other"])}
356+
dist = Distribution(attrs)
357+
self.assertIsInstance(dist.metadata.obsoletes, list)
358+
344359
def test_classifier(self):
345360
attrs = {'name': 'Boa', 'version': '3.0',
346361
'classifiers': ['Programming Language :: Python :: 3']}
@@ -353,9 +368,14 @@ def test_classifier(self):
353368
def test_classifier_invalid_type(self):
354369
attrs = {'name': 'Boa', 'version': '3.0',
355370
'classifiers': ('Programming Language :: Python :: 3',)}
356-
msg = "'classifiers' should be a 'list', not 'tuple'"
357-
with self.assertRaises(TypeError, msg=msg):
358-
Distribution(attrs)
371+
with captured_stderr() as error:
372+
d = Distribution(attrs)
373+
# should have warning about passing a non-list
374+
self.assertIn('should be a list', error.getvalue())
375+
# should be converted to a list
376+
self.assertIsInstance(d.metadata.classifiers, list)
377+
self.assertEqual(d.metadata.classifiers,
378+
list(attrs['classifiers']))
359379

360380
def test_keywords(self):
361381
attrs = {'name': 'Monty', 'version': '1.0',
@@ -367,9 +387,13 @@ def test_keywords(self):
367387
def test_keywords_invalid_type(self):
368388
attrs = {'name': 'Monty', 'version': '1.0',
369389
'keywords': ('spam', 'eggs', 'life of brian')}
370-
msg = "'keywords' should be a 'list', not 'tuple'"
371-
with self.assertRaises(TypeError, msg=msg):
372-
Distribution(attrs)
390+
with captured_stderr() as error:
391+
d = Distribution(attrs)
392+
# should have warning about passing a non-list
393+
self.assertIn('should be a list', error.getvalue())
394+
# should be converted to a list
395+
self.assertIsInstance(d.metadata.keywords, list)
396+
self.assertEqual(d.metadata.keywords, list(attrs['keywords']))
373397

374398
def test_platforms(self):
375399
attrs = {'name': 'Monty', 'version': '1.0',
@@ -381,9 +405,13 @@ def test_platforms(self):
381405
def test_platforms_invalid_types(self):
382406
attrs = {'name': 'Monty', 'version': '1.0',
383407
'platforms': ('GNU/Linux', 'Some Evil Platform')}
384-
msg = "'platforms' should be a 'list', not 'tuple'"
385-
with self.assertRaises(TypeError, msg=msg):
386-
Distribution(attrs)
408+
with captured_stderr() as error:
409+
d = Distribution(attrs)
410+
# should have warning about passing a non-list
411+
self.assertIn('should be a list', error.getvalue())
412+
# should be converted to a list
413+
self.assertIsInstance(d.metadata.platforms, list)
414+
self.assertEqual(d.metadata.platforms, list(attrs['platforms']))
387415

388416
def test_download_url(self):
389417
attrs = {'name': 'Boa', 'version': '3.0',
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
``setup()`` now raises :exc:`TypeError` for invalid types.
1+
``setup()`` now warns about invalid types for some fields.
22

3-
The ``distutils.dist.Distribution`` class now explicitly raises an exception
4-
when ``classifiers``, ``keywords`` and ``platforms`` fields are not
5-
specified as a list.
3+
The ``distutils.dist.Distribution`` class now warns when ``classifiers``,
4+
``keywords`` and ``platforms`` fields are not specified as a list or a string.

0 commit comments

Comments
 (0)