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