1"""distutils.command.check
2
3Implements the Distutils 'check' command.
4"""
5__revision__ = "$Id$"
6
7from distutils.core import Command
8from distutils.dist import PKG_INFO_ENCODING
9from distutils.errors import DistutilsSetupError
10
11try:
12    # docutils is installed
13    from docutils.utils import Reporter
14    from docutils.parsers.rst import Parser
15    from docutils import frontend
16    from docutils import nodes
17    from StringIO import StringIO
18
19    class SilentReporter(Reporter):
20
21        def __init__(self, source, report_level, halt_level, stream=None,
22                     debug=0, encoding='ascii', error_handler='replace'):
23            self.messages = []
24            Reporter.__init__(self, source, report_level, halt_level, stream,
25                              debug, encoding, error_handler)
26
27        def system_message(self, level, message, *children, **kwargs):
28            self.messages.append((level, message, children, kwargs))
29            return nodes.system_message(message, level=level,
30                                        type=self.levels[level],
31                                        *children, **kwargs)
32
33    HAS_DOCUTILS = True
34except ImportError:
35    # docutils is not installed
36    HAS_DOCUTILS = False
37
38class check(Command):
39    """This command checks the meta-data of the package.
40    """
41    description = ("perform some checks on the package")
42    user_options = [('metadata', 'm', 'Verify meta-data'),
43                    ('restructuredtext', 'r',
44                     ('Checks if long string meta-data syntax '
45                      'are reStructuredText-compliant')),
46                    ('strict', 's',
47                     'Will exit with an error if a check fails')]
48
49    boolean_options = ['metadata', 'restructuredtext', 'strict']
50
51    def initialize_options(self):
52        """Sets default values for options."""
53        self.restructuredtext = 0
54        self.metadata = 1
55        self.strict = 0
56        self._warnings = 0
57
58    def finalize_options(self):
59        pass
60
61    def warn(self, msg):
62        """Counts the number of warnings that occurs."""
63        self._warnings += 1
64        return Command.warn(self, msg)
65
66    def run(self):
67        """Runs the command."""
68        # perform the various tests
69        if self.metadata:
70            self.check_metadata()
71        if self.restructuredtext:
72            if HAS_DOCUTILS:
73                self.check_restructuredtext()
74            elif self.strict:
75                raise DistutilsSetupError('The docutils package is needed.')
76
77        # let's raise an error in strict mode, if we have at least
78        # one warning
79        if self.strict and self._warnings > 0:
80            raise DistutilsSetupError('Please correct your package.')
81
82    def check_metadata(self):
83        """Ensures that all required elements of meta-data are supplied.
84
85        name, version, URL, (author and author_email) or
86        (maintainer and maintainer_email)).
87
88        Warns if any are missing.
89        """
90        metadata = self.distribution.metadata
91
92        missing = []
93        for attr in ('name', 'version', 'url'):
94            if not (hasattr(metadata, attr) and getattr(metadata, attr)):
95                missing.append(attr)
96
97        if missing:
98            self.warn("missing required meta-data: %s"  % ', '.join(missing))
99        if metadata.author:
100            if not metadata.author_email:
101                self.warn("missing meta-data: if 'author' supplied, " +
102                          "'author_email' must be supplied too")
103        elif metadata.maintainer:
104            if not metadata.maintainer_email:
105                self.warn("missing meta-data: if 'maintainer' supplied, " +
106                          "'maintainer_email' must be supplied too")
107        else:
108            self.warn("missing meta-data: either (author and author_email) " +
109                      "or (maintainer and maintainer_email) " +
110                      "must be supplied")
111
112    def check_restructuredtext(self):
113        """Checks if the long string fields are reST-compliant."""
114        data = self.distribution.get_long_description()
115        if not isinstance(data, unicode):
116            data = data.decode(PKG_INFO_ENCODING)
117        for warning in self._check_rst_data(data):
118            line = warning[-1].get('line')
119            if line is None:
120                warning = warning[1]
121            else:
122                warning = '%s (line %s)' % (warning[1], line)
123            self.warn(warning)
124
125    def _check_rst_data(self, data):
126        """Returns warnings when the provided data doesn't compile."""
127        source_path = StringIO()
128        parser = Parser()
129        settings = frontend.OptionParser().get_default_values()
130        settings.tab_width = 4
131        settings.pep_references = None
132        settings.rfc_references = None
133        reporter = SilentReporter(source_path,
134                          settings.report_level,
135                          settings.halt_level,
136                          stream=settings.warning_stream,
137                          debug=settings.debug,
138                          encoding=settings.error_encoding,
139                          error_handler=settings.error_encoding_error_handler)
140
141        document = nodes.document(settings, reporter, source=source_path)
142        document.note_source(source_path, -1)
143        try:
144            parser.parse(data, document)
145        except AttributeError:
146            reporter.messages.append((-1, 'Could not finish the parsing.',
147                                      '', {}))
148
149        return reporter.messages
150