|
| 1 | +""" |
| 2 | +exceptions.py |
| 3 | +
|
| 4 | +Copyright 2020 Slycot team |
| 5 | +
|
| 6 | +This program is free software; you can redistribute it and/or modify |
| 7 | +it under the terms of the GNU General Public License version 2 as |
| 8 | +published by the Free Software Foundation. |
| 9 | +
|
| 10 | +This program is distributed in the hope that it will be useful, |
| 11 | +but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | +GNU General Public License for more details. |
| 14 | +
|
| 15 | +You should have received a copy of the GNU General Public License |
| 16 | +along with this program; if not, write to the Free Software |
| 17 | +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, |
| 18 | +MA 02110-1301, USA. |
| 19 | +""" |
| 20 | + |
| 21 | +import re |
| 22 | + |
| 23 | +from warnings import warn |
| 24 | + |
| 25 | + |
| 26 | +class SlycotError(RuntimeError): |
| 27 | + """Slycot exception base class""" |
| 28 | + |
| 29 | + def __init__(self, message, info): |
| 30 | + super(SlycotError, self).__init__(message) |
| 31 | + self.info = info |
| 32 | + |
| 33 | + |
| 34 | +class SlycotParameterError(SlycotError, ValueError): |
| 35 | + """A Slycot input parameter had an illegal value. |
| 36 | +
|
| 37 | + In case of a wrong input value, the SLICOT routines return a negative |
| 38 | + info parameter indicating which parameter was illegal. |
| 39 | + """ |
| 40 | + |
| 41 | + pass |
| 42 | + |
| 43 | + |
| 44 | +class SlycotArithmeticError(SlycotError, ArithmeticError): |
| 45 | + """A Slycot computation failed""" |
| 46 | + |
| 47 | + pass |
| 48 | + |
| 49 | + |
| 50 | +class SlycotWarning(UserWarning): |
| 51 | + """Slycot Warning""" |
| 52 | + |
| 53 | + def __init__(self, message, iwarn, info): |
| 54 | + super(SlycotWarning, self).__init__(message) |
| 55 | + self.info = info |
| 56 | + self.iwarn = iwarn |
| 57 | + |
| 58 | + |
| 59 | +class SlycotResultWarning(SlycotWarning): |
| 60 | + """Slycot computation result warning |
| 61 | +
|
| 62 | + A Slycot routine returned a nonzero info parameter that warns about the |
| 63 | + returned results, but the results might still be usable. |
| 64 | + """ |
| 65 | + |
| 66 | + pass |
| 67 | + |
| 68 | + |
| 69 | +def _parse_docsection(section_name, docstring, checkvars): |
| 70 | + slycot_error = None |
| 71 | + message = None |
| 72 | + docline = iter(docstring.splitlines()) |
| 73 | + try: |
| 74 | + |
| 75 | + info_eval = False |
| 76 | + while section_name not in next(docline): |
| 77 | + continue |
| 78 | + section_indent = next(docline).index("-") |
| 79 | + |
| 80 | + for l in docline: |
| 81 | + # ignore blank lines |
| 82 | + if not l.strip(): |
| 83 | + continue |
| 84 | + |
| 85 | + # reached next section without match |
| 86 | + if l[section_indent] == "-": |
| 87 | + break |
| 88 | + |
| 89 | + # Exception Type |
| 90 | + ematch = re.match( |
| 91 | + r'(\s*)(Slycot.*(Error|Warning))', l) |
| 92 | + if ematch: |
| 93 | + error_indent = len(ematch.group(1)) |
| 94 | + slycot_error = ematch.group(2) |
| 95 | + |
| 96 | + # new infospec |
| 97 | + if slycot_error: |
| 98 | + imatch = re.match( |
| 99 | + r'(\s{' + str(error_indent + 1) + r',}):(.+):\s*(.*)', l) |
| 100 | + if imatch: |
| 101 | + infospec_indent = len(imatch.group(1)) |
| 102 | + infospec = imatch.group(2) |
| 103 | + # Don't handle the standard case unless we have i |
| 104 | + if infospec == "info = -i": |
| 105 | + if 'i' not in checkvars.keys(): |
| 106 | + continue |
| 107 | + infospec_ = infospec.replace(" = ", " == ") |
| 108 | + try: |
| 109 | + info_eval = eval(infospec_, checkvars) |
| 110 | + except NameError: |
| 111 | + raise RuntimeError("Unknown variable in infospec: " |
| 112 | + + infospec) |
| 113 | + except SyntaxError: |
| 114 | + raise RuntimeError("Invalid infospec: " + infospec) |
| 115 | + if info_eval: |
| 116 | + message = imatch.group(3).strip() + '\n' |
| 117 | + mmatch = re.match( |
| 118 | + r'(\s{' + str(infospec_indent+1) + r',})(.*)', |
| 119 | + next(docline)) |
| 120 | + if not mmatch: |
| 121 | + break # docstring |
| 122 | + body_indent = len(mmatch.group(1)) |
| 123 | + message += mmatch.group(2) + '\n' |
| 124 | + for l in docline: |
| 125 | + if l and not l[:body_indent].isspace(): |
| 126 | + break # message body |
| 127 | + message += l[body_indent:] + '\n' |
| 128 | + break # docstring |
| 129 | + except StopIteration: |
| 130 | + pass |
| 131 | + return (slycot_error, message) |
| 132 | + |
| 133 | + |
| 134 | +def raise_if_slycot_error(info, arg_list=None, docstring=None, checkvars=None): |
| 135 | + """Raise exceptions or warnings if slycot info returned is non-zero. |
| 136 | +
|
| 137 | + Parameters |
| 138 | + ---------- |
| 139 | + info: int or list of int |
| 140 | + The parameter INFO or [IWARN, INFO] returned by the SLICOT subroutine |
| 141 | + arg_list: list of str, optional |
| 142 | + A list of arguments (possibly hidden by the wrapper) of the SLICOT |
| 143 | + subroutine |
| 144 | + docstring: str, optional |
| 145 | + The docstring of the Slycot function |
| 146 | + checkvars: dict, optional |
| 147 | + dict of variables for evaluation of <infospec> and formatting the |
| 148 | + exception message |
| 149 | +
|
| 150 | + Notes |
| 151 | + ----- |
| 152 | + If the numpydoc compliant docstring has a "Raises" section with one or |
| 153 | + multiple definition terms ``SlycotError`` or a subclass of it, |
| 154 | + the matching exception text is used. |
| 155 | +
|
| 156 | + To raise warnings, define a "Warns" section using a ``SlycotWarning`` |
| 157 | + definition or a subclass of it. |
| 158 | +
|
| 159 | + The definition body must contain a reST compliant field list with |
| 160 | + ':<infospec>:' as field name, where <infospec> is a python parseable |
| 161 | + expression using the arguments `iwarn`, `info` and any additional variables |
| 162 | + provided in `checkvars` (usually obtained by calling `locals()`. |
| 163 | + A single " = " is treated as " == ". |
| 164 | +
|
| 165 | + The body of the field list contains the exception or warning message and |
| 166 | + can contain replacement fields in format string syntax using the variables |
| 167 | + in `checkvars`. |
| 168 | +
|
| 169 | + For negative info, the argument as indicated in arg_list was erroneous and |
| 170 | + a generic SlycotParameterError is raised if matching infospec was defined. |
| 171 | +
|
| 172 | + Example |
| 173 | + ------- |
| 174 | + >>> def fun(info): |
| 175 | + ... '''Example function |
| 176 | + ... |
| 177 | + ... Raises |
| 178 | + ... ------ |
| 179 | + ... SlycotArithmeticError |
| 180 | + ... :info = 1: INFO is 1 |
| 181 | + ... :info > 1 and info < n: |
| 182 | + ... INFO is {info}, which is between 1 and {n} |
| 183 | + ... :n <= info < m: |
| 184 | + ... {info} is in [{n}, {m:10.2g})! |
| 185 | + ... |
| 186 | + ... Warns |
| 187 | + ... ----- |
| 188 | + ... SlycotResultWarning |
| 189 | + ... :info >= 120: {info} is too large |
| 190 | + ... SlycotResultWarning |
| 191 | + ... :iwarn == 1: IWARN is 1 |
| 192 | + ... ''' |
| 193 | + ... n, m = 4, 120. |
| 194 | + ... raise_if_slycot_error(info, |
| 195 | + ... arg_list=["a", "b", "c"], |
| 196 | + ... docstring=(fun.__doc__ if type(info) is list |
| 197 | + ... else fun.__doc__[:-60]), |
| 198 | + ... checkvars=locals()) |
| 199 | + ... |
| 200 | + >>> fun(0) |
| 201 | + >>> fun(-1) |
| 202 | + SlycotParameterError: |
| 203 | + The following argument had an illegal value: a |
| 204 | + >>> fun(1) |
| 205 | + SlycotArithmeticError: |
| 206 | + INFO is 1 |
| 207 | + >>> fun(2) |
| 208 | + SlycotArithmeticError: |
| 209 | + INFO is 2, which is between 1 and 4 |
| 210 | + >>> fun(4) |
| 211 | + SlycotArithmeticError: |
| 212 | + 4 is in [4, 1.2e+02)! |
| 213 | + >>> fun(120) |
| 214 | + SlycotResultWarning: |
| 215 | + 120 is too large |
| 216 | + >>> fun([1,0]) |
| 217 | + SlycotResultWarning: |
| 218 | + IWARN is 1 |
| 219 | + """ |
| 220 | + try: |
| 221 | + iwarn, info = info |
| 222 | + except TypeError: |
| 223 | + iwarn = None |
| 224 | + if not checkvars: |
| 225 | + checkvars = {} |
| 226 | + if docstring and (iwarn or info): |
| 227 | + # possibly override info with mandatory argument |
| 228 | + checkvars['info'] = info |
| 229 | + # do not possibly override iwarn if not provided |
| 230 | + if iwarn is not None: |
| 231 | + checkvars['iwarn'] = iwarn |
| 232 | + |
| 233 | + exception, message = _parse_docsection("Raises", docstring, checkvars) |
| 234 | + if exception and message: |
| 235 | + fmessage = '\n' + message.format(**checkvars).strip() |
| 236 | + raise globals()[exception](fmessage, info) |
| 237 | + |
| 238 | + warning, message = _parse_docsection("Warns", docstring, checkvars) |
| 239 | + if warning and message: |
| 240 | + fmessage = '\n' + message.format(**checkvars).strip() |
| 241 | + warn(globals()[warning](fmessage, iwarn, info)) |
| 242 | + return |
| 243 | + |
| 244 | + if info < 0 and arg_list: |
| 245 | + message = ("The following argument had an illegal value: {}" |
| 246 | + "".format(arg_list[-info-1])) |
| 247 | + raise SlycotParameterError(message, info) |
| 248 | + |
| 249 | + # catch all |
| 250 | + if info > 0: |
| 251 | + raise SlycotError("Caught unhandled nonzero INFO value {}" |
| 252 | + "".format(info), |
| 253 | + info) |
| 254 | + if iwarn is None and 'iwarn' in checkvars: |
| 255 | + iwarn = checkvars['iwarn'] |
| 256 | + if iwarn: |
| 257 | + warn(SlycotWarning("Caught unhandled nonzero IWARN value {}" |
| 258 | + "".format(iwarn), |
| 259 | + iwarn, info)) |
0 commit comments