From dcaed6b2d954786eb5369ec2e8dfdeefe3cdc6ae Mon Sep 17 00:00:00 2001 From: Berker Peksag Date: Thu, 23 Nov 2017 21:34:20 +0300 Subject: bpo-19610: setup() now raises TypeError for invalid types (GH-4519) The Distribution class now explicitly raises an exception when 'classifiers', 'keywords' and 'platforms' fields are not specified as a list. --- Doc/distutils/apiref.rst | 4 ++ Doc/distutils/setupscript.rst | 39 +++++++++++++------ Doc/whatsnew/3.7.rst | 6 +++ Lib/distutils/dist.py | 26 +++++++++++++ Lib/distutils/tests/test_dist.py | 44 ++++++++++++++++++++++ .../2017-11-23-16-15-55.bpo-19610.Dlca2P.rst | 5 +++ 6 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2017-11-23-16-15-55.bpo-19610.Dlca2P.rst diff --git a/Doc/distutils/apiref.rst b/Doc/distutils/apiref.rst index 7cde1a0..ced8837 100644 --- a/Doc/distutils/apiref.rst +++ b/Doc/distutils/apiref.rst @@ -285,6 +285,10 @@ the full reference. See the :func:`setup` function for a list of keyword arguments accepted by the Distribution constructor. :func:`setup` creates a Distribution instance. + .. versionchanged:: 3.7 + :class:`~distutils.core.Distribution` now raises a :exc:`TypeError` if + ``classifiers``, ``keywords`` and ``platforms`` fields are not specified + as a list. .. class:: Command diff --git a/Doc/distutils/setupscript.rst b/Doc/distutils/setupscript.rst index 38e0202..542ad54 100644 --- a/Doc/distutils/setupscript.rst +++ b/Doc/distutils/setupscript.rst @@ -581,17 +581,19 @@ This information includes: | | description of the | | | | | package | | | +----------------------+---------------------------+-----------------+--------+ -| ``long_description`` | longer description of the | long string | \(5) | +| ``long_description`` | longer description of the | long string | \(4) | | | package | | | +----------------------+---------------------------+-----------------+--------+ -| ``download_url`` | location where the | URL | \(4) | +| ``download_url`` | location where the | URL | | | | package may be downloaded | | | +----------------------+---------------------------+-----------------+--------+ -| ``classifiers`` | a list of classifiers | list of strings | \(4) | +| ``classifiers`` | a list of classifiers | list of strings | (6)(7) | +----------------------+---------------------------+-----------------+--------+ -| ``platforms`` | a list of platforms | list of strings | | +| ``platforms`` | a list of platforms | list of strings | (6)(8) | +----------------------+---------------------------+-----------------+--------+ -| ``license`` | license for the package | short string | \(6) | +| ``keywords`` | a list of keywords | list of strings | (6)(8) | ++----------------------+---------------------------+-----------------+--------+ +| ``license`` | license for the package | short string | \(5) | +----------------------+---------------------------+-----------------+--------+ Notes: @@ -607,22 +609,30 @@ Notes: provided, distutils lists it as the author in :file:`PKG-INFO`. (4) - These fields should not be used if your package is to be compatible with Python - versions prior to 2.2.3 or 2.3. The list is available from the `PyPI website - `_. - -(5) The ``long_description`` field is used by PyPI when you are :ref:`registering ` a package, to :ref:`build its home page `. -(6) +(5) The ``license`` field is a text indicating the license covering the package where the license is not a selection from the "License" Trove classifiers. See the ``Classifier`` field. Notice that there's a ``licence`` distribution option which is deprecated but still acts as an alias for ``license``. +(6) + This field must be a list. + +(7) + The valid classifiers are listed on + `PyPI `_. + +(8) + To preserve backward compatibility, this field also accepts a string. If + you pass a comma-separated string ``'foo, bar'``, it will be converted to + ``['foo', 'bar']``, Otherwise, it will be converted to a list of one + string. + 'short string' A single line of text, not more than 200 characters. @@ -650,7 +660,7 @@ information is sometimes used to indicate sub-releases. These are 1.0.1a2 the second alpha release of the first patch version of 1.0 -``classifiers`` are specified in a Python list:: +``classifiers`` must be specified in a list:: setup(..., classifiers=[ @@ -671,6 +681,11 @@ information is sometimes used to indicate sub-releases. These are ], ) +.. versionchanged:: 3.7 + :class:`~distutils.core.setup` now raises a :exc:`TypeError` if + ``classifiers``, ``keywords`` and ``platforms`` fields are not specified + as a list. + .. _debug-setup-script: Debugging the setup script diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index 71e8358..514c3c2 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -298,6 +298,12 @@ README.rst is now included in the list of distutils standard READMEs and therefore included in source distributions. (Contributed by Ryan Gonzalez in :issue:`11913`.) +:class:`distutils.core.setup` now raises a :exc:`TypeError` if +``classifiers``, ``keywords`` and ``platforms`` fields are not specified +as a list. However, to minimize backwards incompatibility concerns, +``keywords`` and ``platforms`` fields still accept a comma separated string. +(Contributed by Berker Peksag in :issue:`19610`.) + http.client ----------- diff --git a/Lib/distutils/dist.py b/Lib/distutils/dist.py index 62a2451..78c29ed 100644 --- a/Lib/distutils/dist.py +++ b/Lib/distutils/dist.py @@ -1188,12 +1188,38 @@ class DistributionMetadata: def get_keywords(self): return self.keywords or [] + def set_keywords(self, value): + # If 'keywords' is a string, it will be converted to a list + # by Distribution.finalize_options(). To maintain backwards + # compatibility, do not raise an exception if 'keywords' is + # a string. + if not isinstance(value, (list, str)): + msg = "'keywords' should be a 'list', not %r" + raise TypeError(msg % type(value).__name__) + self.keywords = value + def get_platforms(self): return self.platforms or ["UNKNOWN"] + def set_platforms(self, value): + # If 'platforms' is a string, it will be converted to a list + # by Distribution.finalize_options(). To maintain backwards + # compatibility, do not raise an exception if 'platforms' is + # a string. + if not isinstance(value, (list, str)): + msg = "'platforms' should be a 'list', not %r" + raise TypeError(msg % type(value).__name__) + self.platforms = value + def get_classifiers(self): return self.classifiers or [] + def set_classifiers(self, value): + if not isinstance(value, list): + msg = "'classifiers' should be a 'list', not %r" + raise TypeError(msg % type(value).__name__) + self.classifiers = value + def get_download_url(self): return self.download_url or "UNKNOWN" diff --git a/Lib/distutils/tests/test_dist.py b/Lib/distutils/tests/test_dist.py index 1f104ce..50b456e 100644 --- a/Lib/distutils/tests/test_dist.py +++ b/Lib/distutils/tests/test_dist.py @@ -195,6 +195,13 @@ class DistributionTestCase(support.LoggingSilencer, self.assertEqual(dist.metadata.platforms, ['one', 'two']) self.assertEqual(dist.metadata.keywords, ['one', 'two']) + attrs = {'keywords': 'foo bar', + 'platforms': 'foo bar'} + dist = Distribution(attrs=attrs) + dist.finalize_options() + self.assertEqual(dist.metadata.platforms, ['foo bar']) + self.assertEqual(dist.metadata.keywords, ['foo bar']) + def test_get_command_packages(self): dist = Distribution() self.assertEqual(dist.command_packages, None) @@ -338,9 +345,46 @@ class MetadataTestCase(support.TempdirManager, support.EnvironGuard, attrs = {'name': 'Boa', 'version': '3.0', 'classifiers': ['Programming Language :: Python :: 3']} dist = Distribution(attrs) + self.assertEqual(dist.get_classifiers(), + ['Programming Language :: Python :: 3']) meta = self.format_metadata(dist) self.assertIn('Metadata-Version: 1.1', meta) + def test_classifier_invalid_type(self): + attrs = {'name': 'Boa', 'version': '3.0', + 'classifiers': ('Programming Language :: Python :: 3',)} + msg = "'classifiers' should be a 'list', not 'tuple'" + with self.assertRaises(TypeError, msg=msg): + Distribution(attrs) + + def test_keywords(self): + attrs = {'name': 'Monty', 'version': '1.0', + 'keywords': ['spam', 'eggs', 'life of brian']} + dist = Distribution(attrs) + self.assertEqual(dist.get_keywords(), + ['spam', 'eggs', 'life of brian']) + + def test_keywords_invalid_type(self): + attrs = {'name': 'Monty', 'version': '1.0', + 'keywords': ('spam', 'eggs', 'life of brian')} + msg = "'keywords' should be a 'list', not 'tuple'" + with self.assertRaises(TypeError, msg=msg): + Distribution(attrs) + + def test_platforms(self): + attrs = {'name': 'Monty', 'version': '1.0', + 'platforms': ['GNU/Linux', 'Some Evil Platform']} + dist = Distribution(attrs) + self.assertEqual(dist.get_platforms(), + ['GNU/Linux', 'Some Evil Platform']) + + def test_platforms_invalid_types(self): + attrs = {'name': 'Monty', 'version': '1.0', + 'platforms': ('GNU/Linux', 'Some Evil Platform')} + msg = "'platforms' should be a 'list', not 'tuple'" + with self.assertRaises(TypeError, msg=msg): + Distribution(attrs) + def test_download_url(self): attrs = {'name': 'Boa', 'version': '3.0', 'download_url': 'http://example.org/boa'} diff --git a/Misc/NEWS.d/next/Library/2017-11-23-16-15-55.bpo-19610.Dlca2P.rst b/Misc/NEWS.d/next/Library/2017-11-23-16-15-55.bpo-19610.Dlca2P.rst new file mode 100644 index 0000000..5ea87a4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-11-23-16-15-55.bpo-19610.Dlca2P.rst @@ -0,0 +1,5 @@ +``setup()`` now raises :exc:`TypeError` for invalid types. + +The ``distutils.dist.Distribution`` class now explicitly raises an exception +when ``classifiers``, ``keywords`` and ``platforms`` fields are not +specified as a list. -- cgit v0.12