1#
2# objdoc: epydoc documentation completeness checker
3# Edward Loper
4#
5# Created [01/30/01 05:18 PM]
6# $Id: checker.py 1366 2006-09-07 15:54:59Z edloper $
7#
8
9"""
10Documentation completeness checker.  This module defines a single
11class, C{DocChecker}, which can be used to check the that specified
12classes of objects are documented.
13"""
14__docformat__ = 'epytext en'
15
16##################################################
17## Imports
18##################################################
19
20import re, sys, os.path, string
21from xml.dom.minidom import Text as _Text
22from epydoc.apidoc import *
23
24# The following methods may be undocumented:
25_NO_DOCS = ['__hash__', '__repr__', '__str__', '__cmp__']
26
27# The following methods never need descriptions, authors, or
28# versions:
29_NO_BASIC = ['__hash__', '__repr__', '__str__', '__cmp__']
30
31# The following methods never need return value descriptions.
32_NO_RETURN = ['__init__', '__hash__', '__repr__', '__str__', '__cmp__']
33
34# The following methods don't need parameters documented:
35_NO_PARAM = ['__cmp__']
36
37class DocChecker:
38    """
39    Documentation completeness checker.  C{DocChecker} can be used to
40    check that specified classes of objects are documented.  To check
41    the documentation for a group of objects, you should create a
42    C{DocChecker} from a L{DocIndex<apidoc.DocIndex>} that documents
43    those objects; and then use the L{check} method to run specified
44    checks on the objects' documentation.
45
46    What checks are run, and what objects they are run on, are
47    specified by the constants defined by C{DocChecker}.  These
48    constants are divided into three groups.
49
50      - Type specifiers indicate what type of objects should be
51        checked: L{MODULE}; L{CLASS}; L{FUNC}; L{VAR}; L{IVAR};
52        L{CVAR}; L{PARAM}; and L{RETURN}.
53      - Public/private specifiers indicate whether public or private
54        objects should be checked: L{PRIVATE}.
55      - Check specifiers indicate what checks should be run on the
56        objects: L{TYPE}; L{DESCR}; L{AUTHOR};
57        and L{VERSION}.
58
59    The L{check} method is used to perform a check on the
60    documentation.  Its parameter is formed by or-ing together at
61    least one value from each specifier group:
62
63        >>> checker.check(DocChecker.MODULE | DocChecker.DESCR)
64
65    To specify multiple values from a single group, simply or their
66    values together:
67
68        >>> checker.check(DocChecker.MODULE | DocChecker.CLASS |
69        ...               DocChecker.FUNC )
70
71    @group Types: MODULE, CLASS, FUNC, VAR, IVAR, CVAR, PARAM,
72        RETURN, ALL_T
73    @type MODULE: C{int}
74    @cvar MODULE: Type specifier that indicates that the documentation
75        of modules should be checked.
76    @type CLASS: C{int}
77    @cvar CLASS: Type specifier that indicates that the documentation
78        of classes should be checked.
79    @type FUNC: C{int}
80    @cvar FUNC: Type specifier that indicates that the documentation
81        of functions should be checked.
82    @type VAR: C{int}
83    @cvar VAR: Type specifier that indicates that the documentation
84        of module variables should be checked.
85    @type IVAR: C{int}
86    @cvar IVAR: Type specifier that indicates that the documentation
87        of instance variables should be checked.
88    @type CVAR: C{int}
89    @cvar CVAR: Type specifier that indicates that the documentation
90        of class variables should be checked.
91    @type PARAM: C{int}
92    @cvar PARAM: Type specifier that indicates that the documentation
93        of function and method parameters should be checked.
94    @type RETURN: C{int}
95    @cvar RETURN: Type specifier that indicates that the documentation
96        of return values should be checked.
97    @type ALL_T: C{int}
98    @cvar ALL_T: Type specifier that indicates that the documentation
99        of all objects should be checked.
100
101    @group Checks: TYPE, AUTHOR, VERSION, DESCR, ALL_C
102    @type TYPE: C{int}
103    @cvar TYPE: Check specifier that indicates that every variable and
104        parameter should have a C{@type} field.
105    @type AUTHOR: C{int}
106    @cvar AUTHOR: Check specifier that indicates that every object
107        should have an C{author} field.
108    @type VERSION: C{int}
109    @cvar VERSION: Check specifier that indicates that every object
110        should have a C{version} field.
111    @type DESCR: C{int}
112    @cvar DESCR: Check specifier that indicates that every object
113        should have a description.
114    @type ALL_C: C{int}
115    @cvar ALL_C: Check specifier that indicates that  all checks
116        should be run.
117
118    @group Publicity: PRIVATE
119    @type PRIVATE: C{int}
120    @cvar PRIVATE: Specifier that indicates that private objects should
121        be checked.
122    """
123    # Types
124    MODULE = 1
125    CLASS  = 2
126    FUNC   = 4
127    VAR    = 8
128    #IVAR   = 16
129    #CVAR   = 32
130    PARAM  = 64
131    RETURN = 128
132    PROPERTY = 256
133    ALL_T  = 1+2+4+8+16+32+64+128+256
134
135    # Checks
136    TYPE = 256
137    AUTHOR = 1024
138    VERSION = 2048
139    DESCR = 4096
140    ALL_C = 256+512+1024+2048+4096
141
142    # Private/public
143    PRIVATE = 16384
144
145    ALL = ALL_T + ALL_C + PRIVATE
146
147    def __init__(self, docindex):
148        """
149        Create a new C{DocChecker} that can be used to run checks on
150        the documentation of the objects documented by C{docindex}
151
152        @param docindex: A documentation map containing the
153            documentation for the objects to be checked.
154        @type docindex: L{Docindex<apidoc.DocIndex>}
155        """
156        self._docindex = docindex
157
158        # Initialize instance variables
159        self._checks = 0
160        self._last_warn = None
161        self._out = sys.stdout
162        self._num_warnings = 0
163
164    def check(self, *check_sets):
165        """
166        Run the specified checks on the documentation of the objects
167        contained by this C{DocChecker}'s C{DocIndex}.  Any errors found
168        are printed to standard out.
169
170        @param check_sets: The checks that should be run on the
171            documentation.  This value is constructed by or-ing
172            together the specifiers that indicate which objects should
173            be checked, and which checks should be run.  See the
174            L{module description<checker>} for more information.
175            If no checks are specified, then a default set of checks
176            will be run.
177        @type check_sets: C{int}
178        @return: True if no problems were found.
179        @rtype: C{boolean}
180        """
181        if not check_sets:
182            check_sets = (DocChecker.MODULE | DocChecker.CLASS |
183                          DocChecker.FUNC | DocChecker.VAR |
184                          DocChecker.DESCR,)
185
186        self._warnings = {}
187        log.start_progress('Checking docs')
188        for j, checks in enumerate(check_sets):
189            self._check(checks)
190        log.end_progress()
191
192        for (warning, docs) in self._warnings.items():
193            docs = sorted(docs)
194            docnames = '\n'.join(['  - %s' % self._name(d) for d in docs])
195            log.warning('%s:\n%s' % (warning, docnames))
196
197    def _check(self, checks):
198        self._checks = checks
199
200        # Get the list of objects to check.
201        valdocs = sorted(self._docindex.reachable_valdocs(
202            imports=False, packages=False, bases=False, submodules=False,
203            subclasses=False, private = (checks & DocChecker.PRIVATE)))
204        docs = set()
205        for d in valdocs:
206            if not isinstance(d, GenericValueDoc): docs.add(d)
207        for doc in valdocs:
208            if isinstance(doc, NamespaceDoc):
209                for d in doc.variables.values():
210                    if isinstance(d.value, GenericValueDoc): docs.add(d)
211
212        for i, doc in enumerate(sorted(docs)):
213            if isinstance(doc, ModuleDoc):
214                self._check_module(doc)
215            elif isinstance(doc, ClassDoc):
216                self._check_class(doc)
217            elif isinstance(doc, RoutineDoc):
218                self._check_func(doc)
219            elif isinstance(doc, PropertyDoc):
220                self._check_property(doc)
221            elif isinstance(doc, VariableDoc):
222                self._check_var(doc)
223            else:
224                log.error("Don't know how to check %r" % doc)
225
226    def _name(self, doc):
227        name = str(doc.canonical_name)
228        if isinstance(doc, RoutineDoc): name += '()'
229        return name
230
231    def _check_basic(self, doc):
232        """
233        Check the description, author, version, and see-also fields of
234        C{doc}.  This is used as a helper function by L{_check_module},
235        L{_check_class}, and L{_check_func}.
236
237        @param doc: The documentation that should be checked.
238        @type doc: L{APIDoc}
239        @rtype: C{None}
240        """
241        if ((self._checks & DocChecker.DESCR) and
242            (doc.descr in (None, UNKNOWN))):
243            if doc.docstring in (None, UNKNOWN):
244                self.warning('Undocumented', doc)
245            else:
246                self.warning('No description', doc)
247        if self._checks & DocChecker.AUTHOR:
248            for tag, arg, descr in doc.metadata:
249                if 'author' == tag: break
250            else:
251                self.warning('No authors', doc)
252        if self._checks & DocChecker.VERSION:
253            for tag, arg, descr in doc.metadata:
254                if 'version' == tag: break
255            else:
256                self.warning('No version', doc)
257
258    def _check_module(self, doc):
259        """
260        Run checks on the module whose APIDoc is C{doc}.
261
262        @param doc: The APIDoc of the module to check.
263        @type doc: L{APIDoc}
264        @rtype: C{None}
265        """
266        if self._checks & DocChecker.MODULE:
267            self._check_basic(doc)
268
269    def _check_class(self, doc):
270        """
271        Run checks on the class whose APIDoc is C{doc}.
272
273        @param doc: The APIDoc of the class to check.
274        @type doc: L{APIDoc}
275        @rtype: C{None}
276        """
277        if self._checks & DocChecker.CLASS:
278            self._check_basic(doc)
279
280    def _check_property(self, doc):
281        if self._checks & DocChecker.PROPERTY:
282            self._check_basic(doc)
283
284    def _check_var(self, doc):
285        """
286        Run checks on the variable whose documentation is C{var} and
287        whose name is C{name}.
288
289        @param doc: The documentation for the variable to check.
290        @type doc: L{APIDoc}
291        @rtype: C{None}
292        """
293        if self._checks & DocChecker.VAR:
294            if (self._checks & (DocChecker.DESCR|DocChecker.TYPE) and
295                doc.descr in (None, UNKNOWN) and
296                doc.type_descr in (None, UNKNOWN) and
297                doc.docstring in (None, UNKNOWN)):
298                self.warning('Undocumented', doc)
299            else:
300                if (self._checks & DocChecker.DESCR and
301                    doc.descr in (None, UNKNOWN)):
302                    self.warning('No description', doc)
303                if (self._checks & DocChecker.TYPE and
304                    doc.type_descr in (None, UNKNOWN)):
305                    self.warning('No type information', doc)
306
307    def _check_func(self, doc):
308        """
309        Run checks on the function whose APIDoc is C{doc}.
310
311        @param doc: The APIDoc of the function to check.
312        @type doc: L{APIDoc}
313        @rtype: C{None}
314        """
315        name = doc.canonical_name
316        if (self._checks & DocChecker.FUNC and
317            doc.docstring in (None, UNKNOWN) and
318            doc.canonical_name[-1] not in _NO_DOCS):
319            self.warning('Undocumented', doc)
320            return
321        if (self._checks & DocChecker.FUNC and
322            doc.canonical_name[-1] not in _NO_BASIC):
323                self._check_basic(doc)
324        if (self._checks & DocChecker.RETURN and
325            doc.canonical_name[-1] not in _NO_RETURN):
326            if (doc.return_type in (None, UNKNOWN) and
327                doc.return_descr in (None, UNKNOWN)):
328                self.warning('No return descr', doc)
329        if (self._checks & DocChecker.PARAM and
330            doc.canonical_name[-1] not in _NO_PARAM):
331            if doc.arg_descrs in (None, UNKNOWN):
332                self.warning('No argument info', doc)
333            else:
334                args_with_descr = []
335                for arg, descr in doc.arg_descrs:
336                    if isinstance(arg, basestring):
337                        args_with_descr.append(arg)
338                    else:
339                        args_with_descr += arg
340                for posarg in doc.posargs:
341                    if (self._checks & DocChecker.DESCR and
342                        posarg not in args_with_descr):
343                        self.warning('Argument(s) not described', doc)
344                    if (self._checks & DocChecker.TYPE and
345                        posarg not in doc.arg_types):
346                        self.warning('Argument type(s) not described', doc)
347
348    def warning(self, msg, doc):
349        self._warnings.setdefault(msg,set()).add(doc)
350