1
# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*-
2
# vi: set ft=python sts=4 ts=4 sw=4 et:
3
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
4
#
5
#   See COPYING file distributed along with the NiBabel package for the
6
#   copyright and license terms.
7
#
8
### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
9 33
""" Battery runner classes and Report classes
10

11
These classes / objects are for generic checking / fixing batteries
12

13
The ``BatteryRunner`` class will run a series of checks on a single
14
object.
15

16
A check is a callable, of signature ``func(obj, fix=False)`` which
17
returns a tuple ``(obj, Report)`` for ``func(obj, False)`` or
18
``func(obj, True)``, where the obj may be a modified object, or a
19
different object, if ``fix==True``.
20

21
To run checks only, and return problem report objects:
22

23
>>> def chk(obj, fix=False): # minimal check
24
...     return obj, Report()
25
>>> btrun = BatteryRunner((chk,))
26
>>> reports = btrun.check_only('a string')
27

28
To run checks and fixes, returning fixed object and problem report
29
sequence, with possible fix messages:
30

31
>>> fixed_obj, report_seq = btrun.check_fix('a string')
32

33
Reports are iterable things, where the elements in the iterations are
34
``Problems``, with attributes ``error``, ``problem_level``,
35
``problem_msg``, and possibly empty ``fix_msg``.  The ``problem_level``
36
is an integer, giving the level of problem, from 0 (no problem) to 50
37
(very bad problem).  The levels follow the log levels from the logging
38
module (e.g 40 equivalent to "error" level, 50 to "critical").  The
39
``error`` can be one of ``None`` if no error to suggest, or an Exception
40
class that the user might consider raising for this sitation.  The
41
``problem_msg`` and ``fix_msg`` are human readable strings that should
42
explain what happened.
43

44
=======================
45
 More about ``checks``
46
=======================
47

48
Checks are callables returning objects and reports, like ``chk`` below,
49
such that::
50

51
   obj, report = chk(obj, fix=False)
52
   obj, report = chk(obj, fix=True)
53

54
For example, for the Analyze header, we need to check the datatype::
55

56
    def chk_datatype(hdr, fix=True):
57
        rep = Report(hdr, HeaderDataError)
58
        code = int(hdr['datatype'])
59
        try:
60
            dtype = AnalyzeHeader._data_type_codes.dtype[code]
61
        except KeyError:
62
            rep.problem_level = 40
63
            rep.problem_msg = 'data code not recognized'
64
        else:
65
            if dtype.type is np.void:
66
                rep.problem_level = 40
67
                rep.problem_msg = 'data code not supported'
68
            else:
69
                return hdr, rep
70
        if fix:
71
            rep.fix_problem_msg = 'not attempting fix'
72
        return hdr, rep
73

74
or the bitpix::
75

76
    def chk_bitpix(hdr, fix=True):
77
        rep = Report(HeaderDataError)
78
        code = int(hdr['datatype'])
79
        try:
80
            dt = AnalyzeHeader._data_type_codes.dtype[code]
81
        except KeyError:
82
            rep.problem_level = 10
83
            rep.problem_msg = 'no valid datatype to fix bitpix'
84
            return hdr, rep
85
        bitpix = dt.itemsize * 8
86
        if bitpix == hdr['bitpix']:
87
            return hdr, rep
88
        rep.problem_level = 10
89
        rep.problem_msg = 'bitpix does not match datatype')
90
        if fix:
91
            hdr['bitpix'] = bitpix # inplace modification
92
            rep.fix_msg = 'setting bitpix to match datatype'
93
        return hdr, ret
94

95
or the pixdims::
96

97
    def chk_pixdims(hdr, fix=True):
98
        rep = Report(hdr, HeaderDataError)
99
        if not np.any(hdr['pixdim'][1:4] < 0):
100
            return hdr, rep
101
        rep.problem_level = 40
102
        rep.problem_msg = 'pixdim[1,2,3] should be positive'
103
        if fix:
104
            hdr['pixdim'][1:4] = np.abs(hdr['pixdim'][1:4])
105
            rep.fix_msg = 'setting to abs of pixdim values'
106
        return hdr, rep
107

108
"""
109

110

111 33
class BatteryRunner(object):
112
    """ Class to run set of checks """
113

114 33
    def __init__(self, checks):
115
        """ Initialize instance from sequence of `checks`
116

117
        Parameters
118
        ----------
119
        checks : sequence
120
           sequence of checks, where checks are callables matching
121
           signature ``obj, rep = chk(obj, fix=False)``.  Checks are run
122
           in the order they are passed.
123

124
        Examples
125
        --------
126
        >>> def chk(obj, fix=False): # minimal check
127
        ...     return obj, Report()
128
        >>> btrun = BatteryRunner((chk,))
129
        """
130 33
        self._checks = checks
131

132 33
    def check_only(self, obj):
133
        """ Run checks on `obj` returning reports
134

135
        Parameters
136
        ----------
137
        obj : anything
138
           object on which to run checks
139

140
        Returns
141
        -------
142
        reports : sequence
143
           sequence of report objects reporting on result of running
144
           checks (without fixes) on `obj`
145
        """
146 33
        reports = []
147 33
        for check in self._checks:
148 33
            obj, rep = check(obj, False)
149 33
            reports.append(rep)
150 33
        return reports
151

152 33
    def check_fix(self, obj):
153
        """ Run checks, with fixes, on `obj` returning `obj`, reports
154

155
        Parameters
156
        ----------
157
        obj : anything
158
           object on which to run checks, fixes
159

160
        Returns
161
        -------
162
        obj : anything
163
           possibly modified or replaced `obj`, after fixes
164
        reports : sequence
165
           sequence of reports on checks, fixes
166
        """
167 33
        reports = []
168 33
        for check in self._checks:
169 33
            obj, report = check(obj, True)
170 33
            reports.append(report)
171 33
        return obj, reports
172

173 33
    def __len__(self):
174 33
        return len(self._checks)
175

176

177 33
class Report(object):
178

179 33
    def __init__(self,
180
                 error=Exception,
181
                 problem_level=0,
182
                 problem_msg='',
183
                 fix_msg=''):
184
        """ Initialize report with values
185

186
        Parameters
187
        ----------
188
        error : None or Exception
189
           Error to raise if raising error for this check.  If None,
190
           no error can be raised for this check (it was probably
191
           normal).
192
        problem_level : int
193
           level of problem.  From 0 (no problem) to 50 (severe
194
           problem).  If the report originates from a fix, then this
195
           is the level of the problem remaining after the fix.
196
           Default is 0
197
        problem_msg : string
198
           String describing problem detected. Default is ''
199
        fix_msg : string
200
           String describing any fix applied.  Default is ''.
201

202
        Examples
203
        --------
204
        >>> rep = Report()
205
        >>> rep.problem_level
206
        0
207
        >>> rep = Report(TypeError, 10)
208
        >>> rep.problem_level
209
        10
210
        """
211 33
        self.error = error
212 33
        self.problem_level = problem_level
213 33
        self.problem_msg = problem_msg
214 33
        self.fix_msg = fix_msg
215

216 33
    def __getstate__(self):
217
        """ State that defines object
218

219
        Returns
220
        -------
221
        tup : tuple
222
        """
223 33
        return self.error, self.problem_level, self.problem_msg, self.fix_msg
224

225 33
    def __eq__(self, other):
226
        """ are two BatteryRunner-like objects equal?
227

228
        Parameters
229
        ----------
230
        other : object
231
           report-like object to test equality
232

233
        Examples
234
        --------
235
        >>> rep = Report(problem_level=10)
236
        >>> rep2 = Report(problem_level=10)
237
        >>> rep == rep2
238
        True
239
        >>> rep3 = Report(problem_level=20)
240
        >>> rep == rep3
241
        False
242
        """
243 33
        return self.__getstate__() == other.__getstate__()
244

245 33
    def __ne__(self, other):
246
        """ are two BatteryRunner-like objects not equal?
247

248
        See docstring for __eq__
249
        """
250 0
        return not self == other
251

252 33
    def __str__(self):
253
        """ Printable string for object """
254 33
        return self.__dict__.__str__()
255

256 33
    @property
257 5
    def message(self):
258
        """ formatted message string, including fix message if present
259
        """
260 33
        if self.fix_msg:
261 33
            return '; '.join((self.problem_msg, self.fix_msg))
262 33
        return self.problem_msg
263

264 33
    def log_raise(self, logger, error_level=40):
265
        """ Log problem, raise error if problem >= `error_level`
266

267
        Parameters
268
        ----------
269
        logger : log
270
           log object, implementing ``log`` method
271
        error_level : int, optional
272
           If ``self.problem_level`` >= `error_level`, raise error
273
        """
274 33
        logger.log(self.problem_level, self.message)
275 33
        if self.problem_level and self.problem_level >= error_level:
276 33
            if self.error:
277 33
                raise self.error(self.problem_msg)
278

279 33
    def write_raise(self, stream, error_level=40, log_level=30):
280
        """ Write report to `stream`
281

282
        Parameters
283
        ----------
284
        stream : file-like
285
           implementing ``write`` method
286
        error_level : int, optional
287
           level at which to raise error for problem detected in
288
           ``self``
289
        log_level : int, optional
290
           Such that if `log_level` is >= ``self.problem_level`` we
291
           write the report to `stream`, otherwise we write nothing.
292
        """
293 33
        if self.problem_level >= log_level:
294 33
            stream.write(f'Level {self.problem_level}: {self.message}\n')
295 33
        if self.problem_level and self.problem_level >= error_level:
296 33
            if self.error:
297 33
                raise self.error(self.problem_msg)

Read our documentation on viewing source code .

Loading