diff --git a/Doc/distutils/setupscript.rst b/Doc/distutils/setupscript.rst --- a/Doc/distutils/setupscript.rst +++ b/Doc/distutils/setupscript.rst @@ -576,58 +576,66 @@ This information includes: | | package maintainer | | | +----------------------+---------------------------+-----------------+--------+ | ``url`` | home page for the package | URL | \(1) | +----------------------+---------------------------+-----------------+--------+ | ``description`` | short, summary | short string | | | | 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 keywordss | list of strings | (6)(8) | ++----------------------+---------------------------+-----------------+--------+ +| ``license`` | license for the package | short string | \(5) | +----------------------+---------------------------+-----------------+--------+ Notes: (1) These fields are required. (2) It is recommended that versions take the form *major.minor[.patch[.sub]]*. (3) Either the author or the maintainer must be identified. If maintainer is 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 Python list. + +(7) + The valid classifiers are listed on + `PyPI `_. + +(8) + To preserve backward compatibility, this field also accepts a Python + string. + 'short string' A single line of text, not more than 200 characters. 'long string' Multiple lines of plain text in reStructuredText format (see http://docutils.sourceforge.net/). 'list of strings' @@ -645,17 +653,17 @@ information is sometimes used to indicat (for final pre-release release testing). Some examples: 0.1.0 the first, experimental release of a package 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 Python list:: setup(..., classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Environment :: Web Environment', 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Developers', @@ -666,16 +674,22 @@ 1.0.1a2 'Operating System :: POSIX', 'Programming Language :: Python', 'Topic :: Communications :: Email', 'Topic :: Office/Business', 'Topic :: Software Development :: Bug Tracking', ], ) +.. versionchanged:: 3.5 + :class:`~distutils.core.Distribution` now raises a :exc:`TypeError` if + ``classifiers``, ``keywords`` and ``platforms`` fields are not specified + as a Python list. + + .. _debug-setup-script: Debugging the setup script ========================== Sometimes things go wrong, and the setup script doesn't do what the developer wants. diff --git a/Doc/whatsnew/3.5.rst b/Doc/whatsnew/3.5.rst --- a/Doc/whatsnew/3.5.rst +++ b/Doc/whatsnew/3.5.rst @@ -613,16 +613,22 @@ Changes in the Python API * :func:`re.split` always ignored empty pattern matches, so the ``'x*'`` pattern worked the same as ``'x+'``, and the ``'\b'`` pattern never worked. Now :func:`re.split` raises a warning if the pattern could match an empty string. For compatibility use patterns that never match an empty string (e.g. ``'x+'`` instead of ``'x*'``). Patterns that could only match an empty string (such as ``'\b'``) now raise an error. +* :class:`distutils.core.Distribution` now raises a :exc:`TypeError` if + ``classifiers``, ``keywords`` and ``platforms`` fields are not specified + as a Python list. However, to minimize backwards incompatibility concerns, + ``keywords`` and ``platforms`` fields still accept a Python string. + (Contributed by Berker Peksag in :issue:`19610`.) + Changes in the C API -------------------- * The undocumented :c:member:`~PyMemoryViewObject.format` member of the (non-public) :c:type:`PyMemoryViewObject` structure has been removed. All extensions relying on the relevant parts in ``memoryobject.h`` must be rebuilt. diff --git a/Lib/distutils/dist.py b/Lib/distutils/dist.py --- a/Lib/distutils/dist.py +++ b/Lib/distutils/dist.py @@ -1184,22 +1184,48 @@ class DistributionMetadata: return self.description or "UNKNOWN" def get_long_description(self): return self.long_description or "UNKNOWN" def get_keywords(self): return self.keywords or [] + def set_keywords(self, value): + # if keywords is a string, it will be converted to 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 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" # PEP 314 def get_requires(self): return self.requires or [] def set_requires(self, value): diff --git a/Lib/distutils/tests/test_dist.py b/Lib/distutils/tests/test_dist.py --- a/Lib/distutils/tests/test_dist.py +++ b/Lib/distutils/tests/test_dist.py @@ -332,19 +332,56 @@ class MetadataTestCase(support.TempdirMa {"name": "package", "version": "1.0", "obsoletes": ["my.pkg (splat)"]}) def test_classifier(self): 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'} dist = Distribution(attrs) meta = self.format_metadata(dist) self.assertIn('Metadata-Version: 1.1', meta) def test_long_description(self):