1 2# -*- coding: utf-8 -*- 3 4u'''Errors, exceptions and exception chaining. 5 6Error, exception classes and functions to format PyGeodesy errors, 7including the setting of I{exception chaining} in Python 3+. 8 9By default, I{exception chaining} is turned off. To enable 10I{exception chaining}, use command line option C{python -X dev} or 11set environment variable C{PYTHONDEVMODE} to C{1} or any non-empyty 12string OR set environment variable C{PYGEODESY_EXCEPTION_CHAINING} 13to C{'std'} or any other non-empty string. 14''' 15from pygeodesy.interns import MISSING, NN, _a_,_an_, _and_, \ 16 _COLON_, _COMMA_, _COMMASPACE_, \ 17 _datum_, _ellipsoidal_, _EQUAL_, \ 18 _invalid_, _len_, _name_, _no_, \ 19 _not_, _or_, _SPACE_, _UNDER_, __vs__ 20from pygeodesy.lazily import _ALL_LAZY, _getenv, _PYTHON_X_DEV 21 22__all__ = _ALL_LAZY.errors # _ALL_DOCS('_InvalidError', '_IsnotError') 23__version__ = '21.08.14' 24 25_default_ = 'default' 26_kwargs_ = 'kwargs' 27_limiterrors = True # imported by .formy 28_multiple_ = 'multiple' 29_name_value_ = repr('name=value') 30_rangerrors = True # imported by .dms 31_specified_ = 'specified' 32_value_ = 'value' 33 34try: 35 _exception_chaining = None # not available 36 _ = Exception().__cause__ # Python 3+ exception chaining 37 38 if _PYTHON_X_DEV or _getenv('PYGEODESY_EXCEPTION_CHAINING', NN): # == _std_ 39 _exception_chaining = True # turned on, std 40 raise AttributeError # allow exception chaining 41 42 _exception_chaining = False # turned off 43 44 def _error_chain(inst, other=None): 45 '''(INTERNAL) Set or avoid Python 3+ exception chaining. 46 47 Setting C{inst.__cause__ = None} is equivalent to syntax 48 C{raise Error(...) from None} to avoid exception chaining. 49 50 @arg inst: An error instance (C{Exception}). 51 @kwarg other: The previous error instance (C{Exception}) or 52 C{None} to avoid exception chaining. 53 54 @see: Alex Martelli, et.al., "Python in a Nutshell", 3rd Ed., page 163, 55 O'Reilly, 2017, U{PEP-3134<https://www.Python.org/dev/peps/pep-3134>}, 56 U{here <https://StackOverflow.com/questions/17091520/how-can-i-more- 57 easily-suppress-previous-exceptions-when-i-raise-my-own-exception>} 58 and U{here<https://StackOverflow.com/questions/1350671/ 59 inner-exception-with-traceback-in-python>}. 60 ''' 61 inst.__cause__ = other # None, no exception chaining 62 return inst 63 64except AttributeError: # Python 2+ 65 66 def _error_chain(inst, **unused): # PYCHOK expected 67 return inst # no-op 68 69 70class _AssertionError(AssertionError): 71 '''(INTERNAL) Format an C{AssertionError} without exception chaining. 72 ''' 73 def __init__(self, *name_value, **txt_name_values): # txt=_invalid_ 74 _error_init(AssertionError, self, name_value, **txt_name_values) 75 76 77class _AttributeError(AttributeError): 78 '''(INTERNAL) Format an C{AttributeError} without exception chaining. 79 ''' 80 def __init__(self, *name_value, **txt_name_values): # txt=_invalid_ 81 _error_init(AttributeError, self, name_value, **txt_name_values) 82 83 84class _ImportError(ImportError): 85 '''(INTERNAL) Format an C{ImportError} without exception chaining. 86 ''' 87 def __init__(self, *name_value, **txt_name_values): # txt=_invalid_ 88 _error_init(ImportError, self, name_value, **txt_name_values) 89 90 91class _IndexError(IndexError): 92 '''(INTERNAL) Format an C{IndexError} without exception chaining. 93 ''' 94 def __init__(self, *name_value, **txt_name_values): # txt=_invalid_ 95 _error_init(IndexError, self, name_value, **txt_name_values) 96 97 98class _NameError(NameError): 99 '''(INTERNAL) Format a C{NameError} without exception chaining. 100 ''' 101 def __init__(self, *name_value, **txt_name_values): # txt=_invalid_ 102 _error_init(NameError, self, name_value, **txt_name_values) 103 104 105class _NotImplementedError(NotImplementedError): 106 '''(INTERNAL) Format a C{NotImplementedError} without exception chaining. 107 ''' 108 def __init__(self, *name_value, **txt_name_values): # txt=_invalid_ 109 _error_init(NotImplementedError, self, name_value, **txt_name_values) 110 111 112class _OverflowError(OverflowError): 113 '''(INTERNAL) Format an C{OverflowError} without exception chaining. 114 ''' 115 def __init__(self, *name_value, **txt_name_values): # txt=_invalid_ 116 _error_init(OverflowError, self, name_value, **txt_name_values) 117 118 119class _TypeError(TypeError): 120 '''(INTERNAL) Format a C{TypeError} without exception chaining. 121 ''' 122 def __init__(self, *name_value, **txt_name_values): 123 _error_init(TypeError, self, name_value, fmt_name_value='type(%s) (%r)', 124 **txt_name_values) 125 126 127class _TypesError(_TypeError): 128 '''(INTERNAL) Format a C{TypeError} without exception chaining. 129 ''' 130 def __init__(self, name, value, *Types): 131 t = _not_(_an(_or(*(t.__name__ for t in Types)))) 132 _TypeError.__init__(self, name, value, txt=t) 133 134 135class _ValueError(ValueError): 136 '''(INTERNAL) Format a C{ValueError} without exception chaining. 137 ''' 138 def __init__(self, *name_value, **txt_name_values): # name, value, txt=_invalid_ 139 _error_init(ValueError, self, name_value, **txt_name_values) 140 141 142class CrossError(_ValueError): 143 '''Error raised for zero or near-zero vectorial cross products, 144 occurring for coincident or colinear points, paths or bearings. 145 ''' 146 pass 147 148 149class IntersectionError(_ValueError): 150 '''Error raised for path or circle intersection issues. 151 ''' 152 def __init__(self, *args, **kwds): # txt=_invalid_ 153 '''New L{IntersectionError}. 154 ''' 155 if args: 156 _ValueError.__init__(self, _SPACE_(*args), **kwds) 157 else: 158 _ValueError.__init__(self, **kwds) 159 160 161class LenError(_ValueError): 162 '''Error raised for mis-matching C{len} values. 163 ''' 164 def __init__(self, where, **lens_txt): # txt=None 165 '''New L{LenError}. 166 167 @arg where: Object with C{.__name__} attribute 168 (C{class}, C{method}, or C{function}). 169 @kwarg lens_txt: Two or more C{name=len(name)} pairs 170 (C{keyword arguments}). 171 ''' 172 from pygeodesy.streprs import Fmt as _Fmt 173 x = _xkwds_pop(lens_txt, txt=_invalid_) 174 ns, vs = zip(*sorted(lens_txt.items())) 175 ns = _COMMASPACE_.join(ns) 176 vs = __vs__.join(map(str, vs)) 177 t = _SPACE_(_Fmt.PAREN(where.__name__, ns), _len_, vs) 178 _ValueError.__init__(self, t, txt=x) 179 180 181class LimitError(_ValueError): 182 '''Error raised for lat- or longitudinal deltas exceeding 183 the B{C{limit}} in functions L{equirectangular} and 184 L{equirectangular_} and C{nearestOn*} and C{simplify*} 185 functions or methods. 186 ''' 187 pass 188 189 190class NumPyError(_ValueError): 191 '''Error raised for C{NumPy} errors. 192 ''' 193 pass 194 195 196class ParseError(_ValueError): 197 '''Error parsing degrees, radians or several other formats. 198 ''' 199 pass 200 201 202class PointsError(_ValueError): 203 '''Error for an insufficient number of points. 204 ''' 205 pass 206 207 208class RangeError(_ValueError): 209 '''Error raised for lat- or longitude values outside the B{C{clip}}, 210 B{C{clipLat}}, B{C{clipLon}} or B{C{limit}} range in function 211 L{clipDegrees}, L{clipRadians}, L{parse3llh}, L{parseDMS}, 212 L{parseDMS2} or L{parseRad}. 213 214 @see: Function L{rangerrors}. 215 ''' 216 pass 217 218 219class SciPyError(PointsError): 220 '''Error raised for C{SciPy} errors. 221 ''' 222 pass 223 224 225class SciPyWarning(PointsError): 226 '''Error thrown for C{SciPy} warnings. 227 228 To raise C{SciPy} warnings as L{SciPyWarning} exceptions, Python 229 C{warnings} must be filtered as U{warnings.filterwarnings('error') 230 <https://docs.Python.org/3/library/warnings.html#the-warnings-filter>} 231 I{prior to} C{import scipy} or by setting environment variable 232 U{PYTHONWARNINGS<https://docs.Python.org/3/using/cmdline.html 233 #envvar-PYTHONWARNINGS>} or with C{python} command line option 234 U{-W<https://docs.Python.org/3/using/cmdline.html#cmdoption-w>} 235 as C{error}. 236 ''' 237 pass 238 239 240class TRFError(_ValueError): 241 '''Terrestrial Reference Frame (TRF), L{Epoch}, L{RefFrame} 242 or L{RefFrame} conversion issue. 243 ''' 244 pass 245 246 247class UnitError(_ValueError): 248 '''Default exception for L{units} issues. 249 ''' 250 pass 251 252 253class VectorError(_ValueError): 254 '''L{Vector3d}, C{Cartesian*} or C{*Nvector} issues. 255 ''' 256 pass 257 258 259def _an(noun): 260 '''(INTERNAL) Prepend an article to a noun based 261 on the pronounciation of the first letter. 262 ''' 263 return _SPACE_((_an_ if noun[:1].lower() in 'aeinoux' else _a_), noun) 264 265 266def _and(*words): 267 '''(INTERNAL) Join C{words} with C{", "} and C{" and "}. 268 ''' 269 t, w = NN, list(words) 270 if w: 271 t = w.pop() 272 if w: 273 w = _COMMASPACE_.join(w) 274 t = _SPACE_(w, _and_, t) 275 return t 276 277 278def crosserrors(raiser=None): 279 '''Report or ignore vectorial cross product errors. 280 281 @kwarg raiser: Use C{True} to throw or C{False} to ignore 282 L{CrossError} exceptions. Use C{None} to 283 leave the setting unchanged. 284 285 @return: Previous setting (C{bool}). 286 287 @see: Property C{Vector3d[Base].crosserrors}. 288 ''' 289 from pygeodesy.vector3dBase import Vector3dBase 290 t = Vector3dBase._crosserrors # XXX class attr! 291 if raiser in (True, False): 292 Vector3dBase._crosserrors = raiser 293 return t 294 295 296def _datum_datum(datum1, datum2, Error=None): 297 '''(INTERNAL) Check for datum or ellipsoid match. 298 ''' 299 if Error: 300 E1, E2 = datum1.ellipsoid, datum2.ellipsoid 301 if E1 != E2: 302 raise Error(E2.named2, txt=_incompatible(E1.named2)) 303 elif datum1 != datum2: 304 t = _SPACE_(_datum_, repr(datum1.name), _not_, repr(datum2.name)) 305 raise _AssertionError(t) 306 307 308def _error_init(Error, inst, name_value, fmt_name_value='%s (%r)', 309 txt=_invalid_, **name_values): # by .lazily 310 '''(INTERNAL) Format an error text and initialize an C{Error} instance. 311 312 @arg Error: The error super-class (C{Exception}). 313 @arg inst: Sub-class instance to be initialized (C{_Exception}). 314 @arg name_value: Either just a value or several name, value, ... 315 positional arguments (C{str}, any C{type}), in 316 particular for name conflicts with keyword 317 arguments of C{error_init} or which can't be 318 used as C{name=value} keyword arguments. 319 @kwarg fmt_name_value: Format for (name, value) (C{str}). 320 @kwarg txt: Optional explanation of the error (C{str}). 321 @kwarg name_values: One or more C{B{name}=value} pairs overriding 322 any B{C{name_value}} positional arguments. 323 ''' 324 if name_values: 325 t = _or(*sorted(fmt_name_value % t for t in name_values.items())) 326 elif len(name_value) > 1: 327 t = _or(*sorted(fmt_name_value % t for t in zip(name_value[0::2], 328 name_value[1::2]))) 329 elif name_value: 330 t = str(name_value[0]) 331 else: 332 t = _SPACE_(_name_value_, str(MISSING)) 333 334 if txt is not None: 335 c = _COMMA_ if _COLON_ in t else _COLON_ 336 x = str(txt) or _invalid_ 337 t = NN(t, c, _SPACE_, x) 338# else: 339# x = NN # XXX or t? 340 Error.__init__(inst, t) 341# inst.__x_txt__ = x # hold explanation 342 _error_chain(inst) # no Python 3+ exception chaining 343 _error_under(inst) 344 345 346def _error_under(inst): 347 '''(INTERNAL) Remove leading underscore from instance' class name. 348 ''' 349 n = inst.__class__.__name__ 350 if n.startswith(_UNDER_): 351 inst.__class__.__name__ = n.lstrip(_UNDER_) 352 return inst 353 354 355def exception_chaining(error=None): 356 '''Get the previous exception's or exception chaining setting. 357 358 @kwarg error: An error instance (C{Exception}) or C{None}. 359 360 @return: If C{B{error} is None}, return C{True} if exception 361 chaining is enabled for PyGeodesy errors, C{False} 362 if turned off and C{None} if not available. If 363 B{C{error}} is not C{None}, return the previous, 364 chained error or C{None} otherwise. 365 366 @note: Set C{env} variable C{PYGEODESY_EXCEPTION_CHAINING} 367 to any non-empty value prior to C{import pygeodesy} 368 to enable exception chaining for C{pygeodesy} errors. 369 ''' 370 return _exception_chaining if error is None else \ 371 getattr(error, '__cause__', None) 372 373 374def _incompatible(this): 375 '''(INTERNAL) Format an incompatible text. 376 ''' 377 return 'incompatible with ' + str(this) 378 379 380def _InvalidError(Error=_ValueError, **txt_name_values): # txt=_invalid_, name=value [, ...] 381 '''(INTERNAL) Create an C{Error} instance. 382 383 @kwarg Error: The error class or sub-class (C{Exception}). 384 @kwarg txt_name_values: One or more C{B{name}=value} pairs 385 and optionally, a C{B{txt}=...} 386 keyword argument to override the 387 default C{B{txt}='invalid'} 388 389 @return: An B{C{Error}} instance. 390 ''' 391 try: 392 e = Error(**txt_name_values) 393 except TypeError: # std *Error takes no keyword arguments 394 e = _ValueError(**txt_name_values) 395 e = Error(str(e)) 396 _error_chain(e) 397 _error_under(e) 398 return e 399 400 401def _IsnotError(*nouns, **name_value_Error): # name=value [, Error=TypeeError] 402 '''Create a C{TypeError} for an invalid C{name=value} type. 403 404 @arg nouns: One or more expected class or type names, 405 usually nouns (C{str}). 406 @kwarg name_value_Error: One C{B{name}=value} pair and 407 optionally, an C{B{Error}=...} 408 keyword argument to override 409 the default C{B{Error}=TypeError}. 410 411 @return: A C{TypeError} or an B{C{Error}} instance. 412 ''' 413 from pygeodesy.streprs import Fmt as _Fmt 414 Error = _xkwds_pop(name_value_Error, Error=TypeError) 415 n, v = _xkwds_popitem(name_value_Error) if name_value_Error else ( 416 _name_value_, MISSING) # XXX else tuple(...) 417 t = _or(*nouns) or _specified_ 418 if len(nouns) > 1: 419 t = _an(t) 420 e = Error(_SPACE_(n, _Fmt.PAREN(repr(v)), _not_, t)) 421 _error_chain(e) 422 _error_under(e) 423 return e 424 425 426def limiterrors(raiser=None): 427 '''Get/set the throwing of L{LimitError}s. 428 429 @kwarg raiser: Choose C{True} to raise or C{False} to 430 ignore L{LimitError} exceptions. Use 431 C{None} to leave the setting unchanged. 432 433 @return: Previous setting (C{bool}). 434 ''' 435 global _limiterrors 436 t = _limiterrors 437 if raiser in (True, False): 438 _limiterrors = raiser 439 return t 440 441 442def _or(*words): 443 '''(INTERNAL) Join C{words} with C{", "} and C{" or "}. 444 ''' 445 t, w = NN, list(words) 446 if w: 447 t = w.pop() 448 if w: 449 w = _COMMASPACE_.join(w) 450 t = _SPACE_(w, _or_, t) 451 return t 452 453 454def _parseX(parser, *args, **name_values_Error): # name=value[, ..., Error=ParseError] 455 '''(INTERNAL) Invoke a parser and handle exceptions. 456 457 @arg parser: The parser (C{callable}). 458 @arg args: Any parser positional arguments (any C{type}s). 459 @kwarg name_values_Error: One or more C{B{name}=value} pairs 460 and optionally, an C{B{Error}=...} 461 keyword argument to override the 462 default C{B{Error}=ParseError}. 463 464 @return: Parser result. 465 466 @raise ParseError: Or the specified C{B{Error}=...}. 467 468 @raise RangeError: If that error occurred. 469 ''' 470 try: 471 return parser(*args) 472 473 except RangeError as x: 474 t = str(x) 475 E = type(x) 476 _ = _xkwds_pop(name_values_Error, Error=None) 477 except (AttributeError, IndexError, TypeError, ValueError) as x: 478 t = str(x) 479 E = _xkwds_pop(name_values_Error, Error=ParseError) 480 raise _InvalidError(Error=E, txt=t, **name_values_Error) 481 482 483def rangerrors(raiser=None): 484 '''Get/set the throwing of L{RangeError}s. 485 486 @kwarg raiser: Choose C{True} to raise or C{False} to ignore 487 L{RangeError} exceptions. Use C{None} to leave 488 the setting unchanged. 489 490 @return: Previous setting (C{bool}). 491 ''' 492 global _rangerrors 493 t = _rangerrors 494 if raiser in (True, False): 495 _rangerrors = raiser 496 return t 497 498 499def _SciPyIssue(x, *extras): # PYCHOK no cover 500 if isinstance(x, (RuntimeWarning, UserWarning)): 501 X = SciPyWarning 502 else: 503 X = SciPyError # PYCHOK not really 504 t = _SPACE_(str(x).strip(), *extras) 505 return _error_chain(X(t), other=x) 506 507 508def _xellipsoidal(**name_value): 509 '''(INTERNAL) Check an I{ellipsoidal} item. 510 511 @return: The B{C{value}} if ellipsoidal. 512 513 @raise TypeError: Not ellipsoidal B{C{value}}. 514 ''' 515 try: 516 for n, v in name_value.items(): 517 if v.isEllipsoidal: 518 return v 519 break 520 else: 521 n = v = MISSING 522 except AttributeError: 523 pass 524 raise _TypeError(n, v, txt=_not_(_ellipsoidal_)) 525 526 527def _xError(x, *name_value, **kwds): 528 '''(INTERNAL) Embellish an exception. 529 530 @arg x: The exception instance (usually, C{_Error}). 531 @arg kwds: Embelishments (C{any}). 532 ''' 533 X = x.__class__ 534 t = str(x) 535 try: # C{_Error} style 536 return X(txt=t, *name_value, **kwds) 537 except TypeError: # no keyword arguments 538 pass 539 # not an C{_Error}, format as C{_Error} 540 t = str(_ValueError(txt=t, *name_value, **kwds)) 541 return x if _exception_chaining else X(t) 542 543 544try: 545 _ = {}.__or__ # {} | {} # Python 3.9+ 546 547 def _xkwds(kwds, **dflts): 548 '''(INTERNAL) Override C{dflts} with specified C{kwds}. 549 ''' 550 return (dflts | kwds) if kwds else dflts 551 552except AttributeError: 553 554 from copy import copy as _copy 555 556 def _xkwds(kwds, **dflts): # PYCHOK expected 557 '''(INTERNAL) Override C{dflts} with specified C{kwds}. 558 ''' 559 d = dflts 560 if kwds: 561 d = _copy(d) 562 d.update(kwds) 563 return d 564 565 566def _xkwds_Error(where, kwds, name_txt, txt=_default_): 567 # Helper for _xkwds_get, _xkwds_pop and _xkwds_popitem below 568 from pygeodesy.streprs import Fmt, pairs 569 f = _COMMASPACE_.join(pairs(kwds) + pairs(name_txt)) 570 f = Fmt.PAREN(where.__name__, f) 571 t = _multiple_ if name_txt else _no_ 572 t = _SPACE_(t, _EQUAL_(_name_, txt), _kwargs_) 573 return _AssertionError(f, txt=t) 574 575 576def _xkwds_get(kwds, **name_default): 577 '''(INTERNAL) Get a C{kwds} value by C{name}, or the C{default}. 578 ''' 579 if len(name_default) == 1: 580 for n, d in name_default.items(): 581 return kwds.get(n, d) 582 583 raise _xkwds_Error(_xkwds_get, kwds, name_default) 584 585 586def _xkwds_not(*args, **kwds): 587 '''(INTERNAL) Return C{kwds} with a value not in C{args}. 588 ''' 589 return dict((n, v) for n, v in kwds.items() if v not in args) 590 591 592def _xkwds_pop(kwds, **name_default): 593 '''(INTERNAL) Pop a C{kwds} value by C{name}, or the C{default}. 594 ''' 595 if len(name_default) == 1: 596 return kwds.pop(*name_default.popitem()) 597 598 raise _xkwds_Error(_xkwds_pop, kwds, name_default) 599 600 601def _xkwds_popitem(name_value): 602 '''(INTERNAL) Return exactly one C{(name, value)} item. 603 ''' 604 if len(name_value) == 1: # XXX TypeError 605 return name_value.popitem() # XXX AttributeError 606 607 raise _xkwds_Error(_xkwds_popitem, (), name_value, txt=_value_) 608 609# **) MIT License 610# 611# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved. 612# 613# Permission is hereby granted, free of charge, to any person obtaining a 614# copy of this software and associated documentation files (the "Software"), 615# to deal in the Software without restriction, including without limitation 616# the rights to use, copy, modify, merge, publish, distribute, sublicense, 617# and/or sell copies of the Software, and to permit persons to whom the 618# Software is furnished to do so, subject to the following conditions: 619# 620# The above copyright notice and this permission notice shall be included 621# in all copies or substantial portions of the Software. 622# 623# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 624# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 625# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 626# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 627# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 628# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 629# OTHER DEALINGS IN THE SOFTWARE. 630