Merge release-4-6 into release-5-0
[gromacs.git] / doxygen / reporter.py
blobe541c54235f84f2fbf5c419f8d2e3397f49e6328
1 #!/usr/bin/python
3 # This file is part of the GROMACS molecular simulation package.
5 # Copyright (c) 2014, by the GROMACS development team, led by
6 # Mark Abraham, David van der Spoel, Berk Hess, and Erik Lindahl,
7 # and including many others, as listed in the AUTHORS file in the
8 # top-level source directory and at http://www.gromacs.org.
10 # GROMACS is free software; you can redistribute it and/or
11 # modify it under the terms of the GNU Lesser General Public License
12 # as published by the Free Software Foundation; either version 2.1
13 # of the License, or (at your option) any later version.
15 # GROMACS is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 # Lesser General Public License for more details.
20 # You should have received a copy of the GNU Lesser General Public
21 # License along with GROMACS; if not, see
22 # http://www.gnu.org/licenses, or write to the Free Software Foundation,
23 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
25 # If you want to redistribute modifications to GROMACS, please
26 # consider that scientific software is very special. Version
27 # control is crucial - bugs must be traceable. We will be happy to
28 # consider code for inclusion in the official distribution, but
29 # derived work must not be called official GROMACS. Details are found
30 # in the README & COPYING files - if they are missing, get the
31 # official version at http://www.gromacs.org.
33 # To help us fund GROMACS development, we humbly ask that you cite
34 # the research papers on the package. Check out http://www.gromacs.org.
36 import sys
38 from fnmatch import fnmatch
40 """Central issue reporting implementation.
42 This module implements a Reporter class that is used by other Python modules in
43 this directory to report issues. This allows central customization of the
44 output format, and also a central implementation for redirecting/copying
45 the output into a log file. This class also implements sorting for the
46 messages such that all issues from a single file are reported next to each
47 other in the output, as well as filtering to make it possible to suppress
48 certain messages.
49 """
51 class Location(object):
53 """Location for a reported message."""
55 def __init__(self, filename, line):
56 """Create a location with the given file and line number.
58 One or both of the parameters can be None, but filename should be
59 specified if line is.
60 """
61 self.filename = filename
62 self.line = line
64 def __nonzero__(self):
65 """Make empty locations False in boolean context."""
66 return self.filename is not None
68 def __str__(self):
69 """Format the location as a string."""
70 if self.line:
71 return '{0}:{1}'.format(self.filename, self.line)
72 elif self.filename:
73 return self.filename
74 else:
75 return '<unknown>'
77 def __cmp__(self, other):
78 """Sort locations based on file name and line number."""
79 result = cmp(self.filename, other.filename)
80 if not self.filename or result != 0:
81 return result
82 return cmp(self.line, other.line)
84 class Message(object):
86 """Single reported message.
88 This class stores the contents of a reporter message for later output to
89 allow sorting the output messages reasonably by the reported location.
90 """
92 def __init__(self, message, details=None, filename=None, location=None):
93 """Create a message object.
95 The message parameter provides the actual text, while optional details
96 provides a list of extra lines that provide context information for the
97 error. filename and location provide two alternative ways of
98 specifying the location of the issue:
99 - if filename is provided, the issue is reported in that file, without
100 a line number
101 - if location is provided, it should be a Location instance
103 if filename:
104 self.location = Location(filename, None)
105 elif location:
106 self.location = location
107 else:
108 self.location = Location(None, None)
109 self.message = message
110 self.details = details
112 def __cmp__(self, other):
113 """Sort messages based on file name and line number."""
114 return cmp(self.location, other.location)
116 class Filter(object):
118 """Filter expression to exclude messages."""
120 def __init__(self, filterline):
121 """Initialize a filter from a line in a filter file."""
122 self._orgline = filterline
123 filepattern, text = filterline.split(':', 1)
124 if filepattern == '*':
125 self._filematcher = lambda x: x is not None
126 elif filepattern:
127 self._filematcher = lambda x: x and fnmatch(x, '*/' + filepattern)
128 else:
129 self._filematcher = lambda x: x is None
130 self._textpattern = text.strip()
131 self._count = 0
133 def matches(self, message):
134 """Check whether the filter matches a message."""
135 if not self._filematcher(message.location.filename):
136 return False
137 if not fnmatch(message.message, self._textpattern):
138 return False
139 self._count += 1
140 return True
142 def get_match_count(self):
143 """Return the number of times this filter has matched."""
144 return self._count
146 def get_text(self):
147 """Return original line used to specify the filter."""
148 return self._orgline
150 class Reporter(object):
152 """Collect and write out issues found by checker scripts."""
154 def __init__(self, logfile=None, quiet=False):
155 """Initialize the reporter.
157 If logfile is set to a file name, all issues will be written to this
158 file in addition to stderr.
160 If quiet is set to True, the reporter will suppress all output.
162 self._logfp = None
163 if logfile:
164 self._logfp = open(logfile, 'w')
165 self._messages = []
166 self._filters = []
167 self._quiet = quiet
169 def _write(self, message):
170 """Implement actual message writing."""
171 wholemsg = ''
172 if message.location:
173 wholemsg += str(message.location) + ': '
174 wholemsg += message.message
175 if message.details:
176 wholemsg += '\n ' + '\n '.join(message.details)
177 wholemsg += '\n'
178 sys.stderr.write(wholemsg)
179 if self._logfp:
180 self._logfp.write(wholemsg)
182 def _report(self, message):
183 """Handle a single reporter message."""
184 if self._quiet:
185 return
186 for filterobj in self._filters:
187 if filterobj.matches(message):
188 return
189 if not message.location:
190 self._write(message)
191 else:
192 self._messages.append(message)
194 def load_filters(self, filterfile):
195 """Load filters for excluding messages from a file."""
196 with open(filterfile, 'r') as fp:
197 for filterline in fp:
198 filterline = filterline.strip()
199 if not filterline or filterline.startswith('#'):
200 continue
201 self._filters.append(Filter(filterline))
203 def write_pending(self):
204 """Write out pending messages in sorted order."""
205 self._messages.sort()
206 for message in self._messages:
207 self._write(message)
208 self._messages = []
210 def report_unused_filters(self):
211 """Report filters that did not match any messages."""
212 for filterobj in self._filters:
213 if filterobj.get_match_count() == 0:
214 # TODO: Consider adding the input filter file as location
215 text = 'warning: unused filter: ' + filterobj.get_text()
216 self._write(Message(text))
218 def close_log(self):
219 """Close the log file if one exists."""
220 assert not self._messages
221 if self._logfp:
222 self._logfp.close()
223 self._logfp = None
225 def xml_assert(self, xmlpath, message):
226 """Report issues in Doxygen XML that violate assumptions in the script."""
227 self._report(Message('warning: ' + message, filename=xmlpath))
229 def input_error(self, message):
230 """Report issues in input files."""
231 self._report(Message('error: ' + message))
233 def file_error(self, fileobj, message):
234 """Report file-level issues."""
235 self._report(Message('error: ' + message,
236 location=fileobj.get_reporter_location()))
238 def code_issue(self, entity, message, details=None):
239 """Report an issue in a code construct (not documentation related)."""
240 self._report(Message('warning: ' + message, details,
241 location=entity.get_reporter_location()))
243 def doc_error(self, entity, message):
244 """Report an issue in documentation."""
245 self._report(Message('error: ' + entity.get_name() + ': ' + message,
246 location=entity.get_reporter_location()))
248 def doc_note(self, entity, message):
249 """Report a potential issue in documentation."""
250 self._report(Message('note: ' + entity.get_name() + ': ' + message,
251 location=entity.get_reporter_location()))