diff --git a/Doc/distutils/apiref.rst b/Doc/distutils/apiref.rst index ce260d9b802..490e7f35603 100644 --- a/Doc/distutils/apiref.rst +++ b/Doc/distutils/apiref.rst @@ -1950,6 +1950,19 @@ This is described in more detail in :pep:`301`. .. % todo +:mod:`distutils.command.check` --- Check the meta-data of a package +=================================================================== + +.. module:: distutils.command.check + :synopsis: Check the metadata of a package + + +The ``check`` command performs some tests on the meta-data of a package. +It makes sure for example that all required meta-data are provided through +the arguments passed to the :func:`setup` function. + +.. % todo + Creating a new Distutils command ================================ diff --git a/Doc/distutils/examples.rst b/Doc/distutils/examples.rst index b4959286578..d5918a5eb26 100644 --- a/Doc/distutils/examples.rst +++ b/Doc/distutils/examples.rst @@ -233,6 +233,58 @@ With exactly the same source tree layout, this extension can be put in the ext_modules=[Extension('foopkg.foo', ['foo.c'])], ) +Checking a package +================== + +The ``check`` command allows you to verify if your package meta-data are +meeting the minimum requirements to build a distribution. + +To run it, just call it over your :file:`setup.py` script. If something is +missing, ``check`` will display a warning. + +Let's take an example with a simple script:: + + from distutils.core import setup + + setup(name='foobar') + +Running the ``check`` command will display some warnings:: + + $ python setup.py check + running check + warning: check: missing required meta-data: version ,url + warning: check: missing meta-data: either (author and author_email) or + (maintainer and maintainer_email) must be supplied + + +If you use the reStructuredText syntax in the `long_description` field and +`docutils `_ is installed you can check if +the syntax is fine with the ``check`` command, using the `restructuredtext` +option. + +For example, if the :file:`setup.py` script is changed like this:: + + from distutils.core import setup + + desc = """\ + My description + ============= + + This is the description of the ``foobar`` package. + """ + + setup(name='foobar', version='1', author='tarek', + author_email='tarek@ziade.org', + url='http://example.com', long_description=desc) + +Where the long description is broken, ``check`` will be able to detect it +by using the `docutils` parser:: + + $ pythontrunk setup.py check --restructuredtext + running check + warning: check: Title underline too short. (line 2) + warning: check: Could not finish the parsing. + .. % \section{Multiple extension modules} .. % \label{multiple-ext} diff --git a/Lib/distutils/command/__init__.py b/Lib/distutils/command/__init__.py index add83f8740e..f7dcde48ed1 100644 --- a/Lib/distutils/command/__init__.py +++ b/Lib/distutils/command/__init__.py @@ -22,6 +22,7 @@ __all__ = ['build', 'bdist_dumb', 'bdist_rpm', 'bdist_wininst', + 'check', # These two are reserved for future use: #'bdist_sdux', #'bdist_pkgtool', diff --git a/Lib/distutils/command/check.py b/Lib/distutils/command/check.py new file mode 100644 index 00000000000..c72914952e1 --- /dev/null +++ b/Lib/distutils/command/check.py @@ -0,0 +1,143 @@ +"""distutils.command.check + +Implements the Distutils 'check' command. +""" +__revision__ = "$Id$" + +from distutils.core import Command +from distutils.errors import DistutilsSetupError + +try: + # docutils is installed + from docutils.utils import Reporter + from docutils.parsers.rst import Parser + from docutils import frontend + from docutils import nodes + from StringIO import StringIO + + class SilentReporter(Reporter): + + def __init__(self, source, report_level, halt_level, stream=None, + debug=0, encoding='ascii', error_handler='replace'): + self.messages = [] + Reporter.__init__(self, source, report_level, halt_level, stream, + debug, encoding, error_handler) + + def system_message(self, level, message, *children, **kwargs): + self.messages.append((level, message, children, kwargs)) + + HAS_DOCUTILS = True +except ImportError: + # docutils is not installed + HAS_DOCUTILS = False + +class check(Command): + """This command checks the meta-data of the package. + """ + description = ("perform some checks on the package") + user_options = [('metadata', 'm', 'Verify meta-data'), + ('restructuredtext', 'r', + ('Checks if long string meta-data syntax ' + 'are reStructuredText-compliant')), + ('strict', 's', + 'Will exit with an error if a check fails')] + + boolean_options = ['metadata', 'restructuredtext', 'strict'] + + def initialize_options(self): + """Sets default values for options.""" + self.restructuredtext = 0 + self.metadata = 1 + self.strict = 0 + self._warnings = 0 + + def finalize_options(self): + pass + + def warn(self, msg): + """Counts the number of warnings that occurs.""" + self._warnings += 1 + return Command.warn(self, msg) + + def run(self): + """Runs the command.""" + # perform the various tests + if self.metadata: + self.check_metadata() + if self.restructuredtext: + if docutils: + self.check_restructuredtext() + elif self.strict: + raise DistutilsSetupError('The docutils package is needed.') + + # let's raise an error in strict mode, if we have at least + # one warning + if self.strict and self._warnings > 1: + raise DistutilsSetupError('Please correct your package.') + + def check_metadata(self): + """Ensures that all required elements of meta-data are supplied. + + name, version, URL, (author and author_email) or + (maintainer and maintainer_email)). + + Warns if any are missing. + """ + metadata = self.distribution.metadata + + missing = [] + for attr in ('name', 'version', 'url'): + if not (hasattr(metadata, attr) and getattr(metadata, attr)): + missing.append(attr) + + if missing: + self.warn("missing required meta-data: %s" % ' ,'.join(missing)) + if metadata.author: + if not metadata.author_email: + self.warn("missing meta-data: if 'author' supplied, " + + "'author_email' must be supplied too") + elif metadata.maintainer: + if not metadata.maintainer_email: + self.warn("missing meta-data: if 'maintainer' supplied, " + + "'maintainer_email' must be supplied too") + else: + self.warn("missing meta-data: either (author and author_email) " + + "or (maintainer and maintainer_email) " + + "must be supplied") + + def check_restructuredtext(self): + """Checks if the long string fields are reST-compliant.""" + data = self.distribution.get_long_description() + for warning in self._check_rst_data(data): + line = warning[-1].get('line') + if line is None: + warning = warning[1] + else: + warning = '%s (line %s)' % (warning[1], line) + self.warn(warning) + + def _check_rst_data(self, data): + """Returns warnings when the provided data doesn't compile.""" + source_path = StringIO() + parser = Parser() + settings = frontend.OptionParser().get_default_values() + settings.tab_width = 4 + settings.pep_references = None + settings.rfc_references = None + reporter = SilentReporter(source_path, + settings.report_level, + settings.halt_level, + stream=settings.warning_stream, + debug=settings.debug, + encoding=settings.error_encoding, + error_handler=settings.error_encoding_error_handler) + + document = nodes.document(settings, reporter, source=source_path) + document.note_source(source_path, -1) + try: + parser.parse(data, document) + except AttributeError: + reporter.messages.append((-1, 'Could not finish the parsing.', + '', {})) + + return reporter.messages diff --git a/Lib/distutils/tests/test_check.py b/Lib/distutils/tests/test_check.py new file mode 100644 index 00000000000..443fa35baff --- /dev/null +++ b/Lib/distutils/tests/test_check.py @@ -0,0 +1,92 @@ +"""Tests for distutils.command.check.""" +import unittest + +from distutils.command.check import check, HAS_DOCUTILS +from distutils.tests import support +from distutils.errors import DistutilsSetupError + +class CheckTestCase(support.LoggingSilencer, + support.TempdirManager, + unittest.TestCase): + + def _run(self, metadata=None, **options): + if metadata is None: + metadata = {} + pkg_info, dist = self.create_dist(**metadata) + cmd = check(dist) + cmd.initialize_options() + for name, value in options.items(): + setattr(cmd, name, value) + cmd.ensure_finalized() + cmd.run() + return cmd + + def test_check_metadata(self): + # let's run the command with no metadata at all + # by default, check is checking the metadata + # should have some warnings + cmd = self._run() + self.assertEquals(cmd._warnings, 2) + + # now let's add the required fields + # and run it again, to make sure we don't get + # any warning anymore + metadata = {'url': 'xxx', 'author': 'xxx', + 'author_email': 'xxx', + 'name': 'xxx', 'version': 'xxx'} + cmd = self._run(metadata) + self.assertEquals(cmd._warnings, 0) + + # now with the strict mode, we should + # get an error if there are missing metadata + self.assertRaises(DistutilsSetupError, self._run, {}, **{'strict': 1}) + + # and of course, no error when all metadata are present + cmd = self._run(metadata, strict=1) + self.assertEquals(cmd._warnings, 0) + + def test_check_document(self): + if not HAS_DOCUTILS: # won't test without docutils + return + pkg_info, dist = self.create_dist() + cmd = check(dist) + + # let's see if it detects broken rest + broken_rest = 'title\n===\n\ntest' + msgs = cmd._check_rst_data(broken_rest) + self.assertEquals(len(msgs), 1) + + # and non-broken rest + rest = 'title\n=====\n\ntest' + msgs = cmd._check_rst_data(rest) + self.assertEquals(len(msgs), 0) + + def test_check_restructuredtext(self): + if not HAS_DOCUTILS: # won't test without docutils + return + # let's see if it detects broken rest in long_description + broken_rest = 'title\n===\n\ntest' + pkg_info, dist = self.create_dist(long_description=broken_rest) + cmd = check(dist) + cmd.check_restructuredtext() + self.assertEquals(cmd._warnings, 1) + + # let's see if we have an error with strict=1 + cmd = check(dist) + cmd.initialize_options() + cmd.strict = 1 + cmd.ensure_finalized() + self.assertRaises(DistutilsSetupError, cmd.run) + + # and non-broken rest + rest = 'title\n=====\n\ntest' + pkg_info, dist = self.create_dist(long_description=rest) + cmd = check(dist) + cmd.check_restructuredtext() + self.assertEquals(cmd._warnings, 0) + +def test_suite(): + return unittest.makeSuite(CheckTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/Misc/NEWS b/Misc/NEWS index 29e44d56dfb..9b7f13bc20b 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -340,6 +340,8 @@ Core and Builtins Library ------- +- Issue #5732: added a new command in Distutils: check. + - Issue #5731: Distutils bdist_wininst no longer worked on non-Windows platforms. Initial patch by Paul Moore.