Coverage for pygeodesy/errors.py: 92%
320 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
2# -*- coding: utf-8 -*-
4u'''Errors, exceptions, exception formatting and exception chaining.
6Error, exception classes and functions to format PyGeodesy errors, including
7the setting of I{exception chaining} for Python 3.9+.
9By default, I{exception chaining} is turned I{off}. To enable I{exception
10chaining}, use command line option C{python -X dev} I{OR} set env variable
11C{PYTHONDEVMODE=1} or to any non-empty string I{OR} set env variable
12C{PYGEODESY_EXCEPTION_CHAINING=std} or to any non-empty string.
13'''
14# from pygeodesy import basics as _basics # _MODS.into
15# from pygeodesy.ellipsoidalBase import CartesianEllipsoidalBase, LatLonEllipsoidalBase # _MODS
16# from pygeodesy import errors # _MODS, _MODS.getattr
17from pygeodesy.internals import _envPYGEODESY, _plural, _tailof, typename
18from pygeodesy.interns import MISSING, NN, _a_, _an_, _and_, _clip_, _COLON_, _COLONSPACE_, \
19 _COMMASPACE_, _datum_, _ELLIPSIS_, _ellipsoidal_, _incompatible_, \
20 _invalid_, _keyword_, _LatLon_, _len_, _not_, _or_, _SPACE_, \
21 _specified_, _UNDER_, _vs_, _with_
22from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _PYTHON_X_DEV
23# from pygeodesy import streprs as _streprs # _MODS.into
24# from pygeodesy.unitsBase import Str # _MODS
25# from pygeodesy.vector3dBase import Vector3dBase # _MODS
27from copy import copy as _copy
29__all__ = _ALL_LAZY.errors # _ALL_DOCS('_InvalidError', '_IsnotError') _under
30__version__ = '25.04.21'
32_argument_ = 'argument'
33_basics = _MODS.into(basics=__name__)
34_box_ = 'box'
35_expected_ = 'expected'
36_limiterrors = True # in .formy
37_name_value_ = repr('name=value')
38_rangerrors = True # in .dms
39_region_ = 'region'
40_streprs = _MODS.into(streprs=__name__)
41_vs__ = _SPACE_(NN, _vs_, NN)
43try:
44 _exception_chaining = None # not available
45 _ = Exception().__cause__ # Python 3.9+ exception chaining
47 if _PYTHON_X_DEV or _envPYGEODESY('EXCEPTION_CHAINING'): # == _std_
48 _exception_chaining = True # turned on, std
49 raise AttributeError() # allow exception chaining
51 _exception_chaining = False # turned off
53 def _error_cause(inst, cause=None):
54 '''(INTERNAL) Set or avoid Python 3+ exception chaining.
56 Setting C{inst.__cause__ = None} is equivalent to syntax
57 C{raise Error(...) from None} to avoid exception chaining.
59 @arg inst: An error instance (I{caught} C{Exception}).
60 @kwarg cause: A previous error instance (I{caught} C{Exception})
61 or C{None} to avoid exception chaining.
63 @see: Alex Martelli, et.al., "Python in a Nutshell", 3rd Ed., page 163,
64 O'Reilly, 2017, U{PEP-3134<https://www.Python.org/dev/peps/pep-3134>},
65 U{here<https://StackOverflow.com/questions/17091520/how-can-i-more-
66 easily-suppress-previous-exceptions-when-i-raise-my-own-exception>}
67 and U{here<https://StackOverflow.com/questions/1350671/
68 inner-exception-with-traceback-in-python>}.
69 '''
70 inst.__cause__ = cause # None, no exception chaining
71 return inst
73except AttributeError: # Python 2+
75 def _error_cause(inst, **unused): # PYCHOK expected
76 return inst # no-op
79class _AssertionError(AssertionError):
80 '''(INTERNAL) Format an C{AssertionError} with/-out exception chaining.
81 '''
82 def __init__(self, *args, **kwds):
83 _error_init(AssertionError, self, args, **kwds)
86class _AttributeError(AttributeError):
87 '''(INTERNAL) Format an C{AttributeError} with/-out exception chaining.
88 '''
89 def __init__(self, *args, **kwds):
90 _error_init(AttributeError, self, args, **kwds)
93class _ImportError(ImportError):
94 '''(INTERNAL) Format an C{ImportError} with/-out exception chaining.
95 '''
96 def __init__(self, *args, **kwds):
97 _error_init(ImportError, self, args, **kwds)
100class _IndexError(IndexError):
101 '''(INTERNAL) Format an C{IndexError} with/-out exception chaining.
102 '''
103 def __init__(self, *args, **kwds):
104 _error_init(IndexError, self, args, **kwds)
107class _KeyError(KeyError):
108 '''(INTERNAL) Format a C{KeyError} with/-out exception chaining.
109 '''
110 def __init__(self, *args, **kwds): # txt=_invalid_
111 _error_init(KeyError, self, args, **kwds)
114class _NameError(NameError):
115 '''(INTERNAL) Format a C{NameError} with/-out exception chaining.
116 '''
117 def __init__(self, *args, **kwds):
118 _error_init(NameError, self, args, **kwds)
121class _NotImplementedError(NotImplementedError):
122 '''(INTERNAL) Format a C{NotImplementedError} with/-out exception chaining.
123 '''
124 def __init__(self, *args, **kwds):
125 _error_init(NotImplementedError, self, args, **kwds)
128class _OverflowError(OverflowError):
129 '''(INTERNAL) Format an C{OverflowError} with/-out exception chaining.
130 '''
131 def __init__(self, *args, **kwds): # txt=_invalid_
132 _error_init(OverflowError, self, args, **kwds)
135class _TypeError(TypeError):
136 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining.
137 '''
138 def __init__(self, *args, **kwds):
139 _error_init(TypeError, self, args, fmt_name_value='type(%s) (%r)', **kwds)
142def _TypesError(name, value, *Types, **kwds):
143 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining.
144 '''
145 # no longer C{class _TypesError} to avoid missing value
146 # argument errors in _XError line ...E = Error(str(e))
147 t = _an(_or(*map(typename, Types, Types)))
148 return _TypeError(name, value, txt=_not_(t), **kwds)
151class _UnexpectedError(TypeError): # note, a TypeError!
152 '''(INTERNAL) Format a C{TypeError} I{without exception chaining}.
153 '''
154 def __init__(self, *args, **kwds):
155 n = len(kwds)
156 if args:
157 a = _plural(_argument_, len(args))
158 n = _and(a, _plural(_keyword_, n)) if n else a
159 else:
160 n = _plural(_SPACE_(_keyword_, _argument_), n)
161 u = _streprs.unstr(_SPACE_(n, NN), *args, **kwds)
162 # _error_init(TypeError, self, (u,), txt_not_=_expected_)
163 TypeError.__init__(self, _SPACE_(u, _not_, _expected_))
166class _ValueError(ValueError):
167 '''(INTERNAL) Format a C{ValueError} with/-out exception chaining.
168 '''
169 def __init__(self, *args, **kwds): # ..., cause=None, txt=_invalid_, ...
170 _error_init(ValueError, self, args, **kwds)
173class _ZeroDivisionError(ZeroDivisionError):
174 '''(INTERNAL) Format a C{ZeroDivisionError} with/-out exception chaining.
175 '''
176 def __init__(self, *args, **kwds):
177 _error_init(ZeroDivisionError, self, args, **kwds)
180class AuxError(_ValueError):
181 '''Error raised for a L{rhumb.aux_}, C{Aux}, C{AuxDLat} or C{AuxLat} issue.
182 '''
183 pass
186class ClipError(_ValueError):
187 '''Clip box or clip region issue.
188 '''
189 def __init__(self, *name_n_corners, **txt_cause):
190 '''New L{ClipError}.
192 @arg name_n_corners: Either just a name (C{str}) or
193 name, number, corners (C{str},
194 C{int}, C{tuple}).
195 @kwarg txt_cause: Optional C{B{txt}=str} explanation
196 of the error and C{B{cause}=None}
197 for exception chaining.
198 '''
199 if len(name_n_corners) == 3:
200 t, n, v = name_n_corners
201 n = _SPACE_(t, _clip_, (_box_ if n == 2 else _region_))
202 name_n_corners = n, v
203 _ValueError.__init__(self, *name_n_corners, **txt_cause)
206class CrossError(_ValueError):
207 '''Error raised for zero or near-zero vectorial cross products,
208 occurring for coincident or colinear points, lines or bearings.
209 '''
210 pass
213class GeodesicError(_ValueError):
214 '''Error raised for convergence or other issues in L{geodesicx<pygeodesy.geodesicx>},
215 L{geodesicw<pygeodesy.geodesicw>} or L{karney<pygeodesy.karney>}.
216 '''
217 pass
220class IntersectionError(_ValueError): # in .ellipsoidalBaseDI, .formy, ...
221 '''Error raised for line or circle intersection issues.
222 '''
223 def __init__(self, *args, **kwds):
224 '''New L{IntersectionError}.
225 '''
226 if args:
227 _ValueError.__init__(self, _SPACE_(*args), **kwds)
228 else:
229 _ValueError.__init__(self, **kwds)
232class LenError(_ValueError): # in .ecef, .fmath, .heights, .iters, .named
233 '''Error raised for mis-matching C{len} values.
234 '''
235 def __init__(self, where, **lens_txt): # txt=None
236 '''New L{LenError}.
238 @arg where: Object with C{.__name__} attribute
239 (C{class}, C{method}, or C{function}).
240 @kwarg lens_txt: Two or more C{name=len(name)} pairs
241 (C{keyword arguments}).
242 '''
243 def _ns_vs_txt_x(cause=None, txt=_invalid_, **kwds):
244 ns, vs = zip(*_basics.itemsorted(kwds)) # unzip
245 return ns, vs, txt, cause
247 ns, vs, txt, x = _ns_vs_txt_x(**lens_txt)
248 ns = _COMMASPACE_.join(ns)
249 t = _streprs.Fmt.PAREN(typename(where), ns)
250 vs = _vs__.join(map(str, vs))
251 t = _SPACE_(t, _len_, vs)
252 _ValueError.__init__(self, t, txt=txt, cause=x)
255class LimitError(_ValueError):
256 '''Error raised for lat- or longitudinal values or deltas exceeding the given
257 B{C{limit}} in functions L{equirectangular<pygeodesy.equirectangular>},
258 L{equirectangular4<pygeodesy.equirectangular4>}, C{nearestOn*} and
259 C{simplify*} or methods with C{limit} or C{options} keyword arguments.
261 @see: Subclass L{UnitError}.
262 '''
263 pass
266class MGRSError(_ValueError):
267 '''Military Grid Reference System (MGRS) parse or other L{Mgrs} issue.
268 '''
269 pass
272class NumPyError(_ValueError):
273 '''Error raised for C{NumPy} issues.
274 '''
275 pass
278class ParseError(_ValueError): # in .dms, .elevations, .utmupsBase
279 '''Error parsing degrees, radians or several other formats.
280 '''
281 pass
284class PointsError(_ValueError): # in .clipy, .frechet, ...
285 '''Error for an insufficient number of points.
286 '''
287 pass
290class RangeError(_ValueError):
291 '''Error raised for lat- or longitude values outside the B{C{clip}}, B{C{clipLat}},
292 B{C{clipLon}} in functions L{parse3llh<pygeodesy.dms.parse3llh>}, L{parseDMS<pygeodesy.dms.parseDMS>},
293 L{parseDMS2<pygeodesy.dms.parseDMS2>} and L{parseRad<pygeodesy.dms.parseRad>} or the B{C{limit}} set
294 with functions L{clipDegrees<pygeodesy.dms.clipDegrees>} and L{clipRadians<pygeodesy.dms.clipRadians>}.
296 @see: Function L{rangerrors<pygeodesy.errors.rangerrors>}.
297 '''
298 pass
301class RhumbError(_ValueError):
302 '''Error raised for a rhumb L{aux_<pygeodesy.rhumb.aux_>}, L{ekx<pygeodesy.rhumb.ekx>} or
303 L{solve<pygeodesy.rhumb.solve>} issue.
304 '''
305 pass
308class TriangleError(_ValueError): # in .resections, .vector2d
309 '''Error raised for triangle, intersection or resection issues.
310 '''
311 pass
314class SciPyError(PointsError):
315 '''Error raised for C{SciPy} issues.
316 '''
317 pass
320class SciPyWarning(PointsError):
321 '''Error thrown for C{SciPy} warnings.
323 To raise C{SciPy} warnings as L{SciPyWarning} exceptions, Python
324 C{warnings} must be filtered as U{warnings.filterwarnings('error')
325 <https://docs.Python.org/3/library/warnings.html#the-warnings-filter>}
326 I{prior to} C{import scipy} OR by setting env var U{PYTHONWARNINGS
327 <https://docs.Python.org/3/using/cmdline.html#envvar-PYTHONWARNINGS>}
328 OR by invoking C{python} with command line option U{-W<https://docs.
329 Python.org/3/using/cmdline.html#cmdoption-w>} set to C{-W error}.
330 '''
331 pass
334class TRFError(_ValueError): # in .ellipsoidalBase, .trf, .units
335 '''Terrestrial Reference Frame (TRF), L{Epoch}, L{RefFrame} or L{RefFrame}
336 conversion issue.
337 '''
338 pass
341class UnitError(LimitError): # in .named, .units
342 '''Default exception for L{units} issues for a value exceeding the C{low}
343 or C{high} limit.
344 '''
345 pass
348class VectorError(_ValueError): # in .nvectorBase, .vector3d, .vector3dBase
349 '''L{Vector3d}, C{Cartesian*} or C{*Nvector} issues.
350 '''
351 pass
354def _an(noun):
355 '''(INTERNAL) Prepend an article to a noun based
356 on the pronounciation of the first letter.
357 '''
358 a = _an_ if noun[:1].lower() in 'aeinoux' else _a_
359 return _SPACE_(a, noun)
362def _and(*words):
363 '''(INTERNAL) Join C{words} with C{", "} and C{" and "}.
364 '''
365 return _and_or(_and_, *words)
368def _and_or(last, *words):
369 '''(INTERNAL) Join C{words} with C{", "} and C{B{last}}.
370 '''
371 t, w = NN, list(words)
372 if w:
373 t = w.pop()
374 if w:
375 w = _COMMASPACE_.join(w)
376 t = _SPACE_(w, last, t)
377 return t
380def crosserrors(raiser=None):
381 '''Report or ignore vectorial cross product errors.
383 @kwarg raiser: Use C{True} to throw, C{False} to ignore
384 L{CrossError} exceptions or C{None} to
385 leave the setting unchanged.
387 @return: Previous setting (C{bool}).
389 @see: Property C{Vector3d[Base].crosserrors}.
390 '''
391 V = _MODS.vector3dBase.Vector3dBase
392 t = V._crosserrors # XXX class attr!
393 if raiser is not None:
394 V._crosserrors = bool(raiser)
395 return t
398def _error_init(Error, inst, args, fmt_name_value='%s (%r)', txt_not_=NN,
399 txt__=None, txt=NN, cause=None, **kwds):
400 '''(INTERNAL) Format an error text and initialize an C{Error} instance.
402 @arg Error: The error super-class (C{Exception}).
403 @arg inst: Sub-class instance to be __init__-ed (C{_Exception}).
404 @arg args: Either just a value or several name, value, ...
405 positional arguments (C{str}, any C{type}), in
406 particular for name conflicts with keyword
407 arguments of C{error_init} or which can't be
408 given as C{name=value} keyword arguments.
409 @kwarg fmt_name_value: Format for (name, value) (C{str}).
410 @kwarg txt: Optional explanation of the error (C{str}).
411 @kwarg txt__: Alternate C{B{txt}=B{txt__}.__name__}.
412 @kwarg txt_not_: Negative explanation C{B{txt}=_not_(B{txt_not_})}.
413 @kwarg cause: Optional, caught error (L{Exception}), for
414 exception chaining (supported in Python 3+).
415 @kwarg kwds: Additional C{B{name}=value} pairs, if any.
416 '''
417 def _fmtuple(pairs):
418 return tuple(fmt_name_value % t for t in pairs)
420 t, n = (), len(args)
421 if n > 2:
422 t = _fmtuple(zip(args[0::2], args[1::2]))
423 s = _basics.isodd(n)
424 if s: # XXX _xzip(..., strict=s)
425 t += args[-1:]
426 elif n == 2:
427 t = (fmt_name_value % args),
428 elif n: # == 1
429 t = str(args[0]),
430 if kwds:
431 t += _fmtuple(_basics.itemsorted(kwds))
432 t = _or(*t) if t else _SPACE_(_name_value_, MISSING)
434 x = _not_(txt_not_) if txt_not_ else (txt if txt__ is None
435 else typename(txt__))
436 if x is not None:
437 x = str(x) or (str(cause) if cause else _invalid_)
438 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_
439 t = C(t, x)
440# else: # LenError, _xzip, .dms, .heights, .vector2d
441# x = NN # XXX or t?
442 Error.__init__(inst, t)
443# inst.__x_txt__ = x # hold explanation
444 _error_cause(inst, cause=cause if _exception_chaining else None)
445 _error_under(inst)
448def _error_under(inst):
449 '''(INTERNAL) Remove leading underscore from instance' class name.
450 '''
451 t = type(inst)
452 n = typename(t) # _tailof?
453 if n.startswith(_UNDER_):
454 t.__name__ = n.lstrip(_UNDER_)
455 return inst
458def exception_chaining(exc=None):
459 '''Get an error's I{cause} or the exception chaining setting.
461 @kwarg exc: An error instance (C{Exception}) or C{None}.
463 @return: If C{B{exc} is None}, return C{True} if exception
464 chaining is enabled for PyGeodesy errors, C{False}
465 if turned off and C{None} if not available. If
466 C{B{exc} is not None}, return it's error I{cause}
467 or C{None} if there is none.
469 @note: To enable exception chaining for C{pygeodesy} errors,
470 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any
471 non-empty value prior to C{import pygeodesy}.
472 '''
473 return _exception_chaining if exc is None else \
474 getattr(exc, '__cause__', None) # _DCAUSE_
477def _incompatible(this):
478 '''(INTERNAL) Format an C{"incompatible with ..."} text.
479 '''
480 return _SPACE_(_incompatible_, _with_, this)
483def _InvalidError(Error=_ValueError, **txt_name_values_cause): # txt=_invalid_, name=value [, ...]
484 '''(INTERNAL) Create an C{Error} instance.
486 @kwarg Error: The error class or sub-class (C{Exception}).
487 @kwarg txt_name_values: One or more C{B{name}=value} pairs
488 and optionally, keyword argument C{B{txt}=str}
489 to override the default C{B{txt}='invalid'} and
490 C{B{cause}=None} for exception chaining.
492 @return: An B{C{Error}} instance.
493 '''
494 return _XError(Error, **txt_name_values_cause)
497def isError(exc):
498 '''Check a (caught) exception.
500 @arg exc: The exception C({Exception}).
502 @return: C{True} if B{C{exc}} is a C{pygeodesy} error,
503 C{False} if B{C{exc}} is a standard Python error
504 of C{None} if neither.
505 '''
506 def _X(exc):
507 X = type(exc)
508 m = X.__module__
509 return _basics.issubclassof(X, *_XErrors) or \
510 ((m is __name__ or m == __name__) and
511 _tailof(typename(X)).startswith(_UNDER_))
513 return True if isinstance(exc, _XErrors) else (
514 _X(exc) if isinstance(exc, Exception) else None)
517def _IsnotError(*types__, **name_value_Error_cause): # name=value [, Error=TypeError, cause=None]
518 '''Create a C{TypeError} for an invalid C{name=value} type.
520 @arg types__: One or more types or type names.
521 @kwarg name_value_Error_cause: One C{B{name}=value} pair and optionally,
522 keyword arguments C{B{Error}=TypeError} and C{B{cause}=None}
523 for exception chaining.
525 @return: A C{TypeError} or an B{C{Error}} instance.
526 '''
527 x, kwds = _xkwds_pop2(name_value_Error_cause, cause=None)
528 E, kwds = _xkwds_pop2(kwds, Error=TypeError)
529 n, v = _xkwds_item2(kwds)
531 n = _streprs.Fmt.PARENSPACED(n, repr(v))
532 t = _an(_or(*map(typename, types__, types__))) if types__ else _specified_
533 return _XError(E, n, txt=_not_(t), cause=x)
536def limiterrors(raiser=None):
537 '''Get/set the throwing of L{LimitError}s.
539 @kwarg raiser: Use C{True} to raise, C{False} to
540 ignore L{LimitError} exceptions or
541 C{None} to leave the setting unchanged.
543 @return: Previous setting (C{bool}).
544 '''
545 global _limiterrors
546 t = _limiterrors
547 if raiser is not None:
548 _limiterrors = bool(raiser)
549 return t
552def _or(*words):
553 '''(INTERNAL) Join C{words} with C{", "} and C{" or "}.
554 '''
555 return _and_or(_or_, *words)
558def _parseX(parser, *args, **Error_name_values): # name=value[, ..., Error=ParseError]
559 '''(INTERNAL) Invoke a parser and handle exceptions.
561 @arg parser: The parser (C{callable(*B{args}}).
562 @arg args: Any B{C{parser}} arguments (any C{type}s).
563 @kwarg Error_name_values: Optional C{B{Error}=ParseError}
564 and number of C{B{name}=value} pairs.
566 @return: Parser result.
568 @raise ParseError: Or the specified C{B{Error}}.
569 '''
570 try:
571 return parser(*args)
572 except Exception as x:
573 E = type(x) if isError(x) else ParseError
574 E, kwds = _xkwds_pop2(Error_name_values, Error=E)
575 raise _XError(E, **_xkwds(kwds, cause=x))
578def rangerrors(raiser=None):
579 '''Get/set the throwing of L{RangeError}s.
581 @kwarg raiser: Use C{True} to raise or C{False} to ignore
582 L{RangeError} exceptions or C{None} to leave
583 the setting unchanged.
585 @return: Previous setting (C{bool}).
586 '''
587 global _rangerrors
588 t = _rangerrors
589 if raiser is not None:
590 _rangerrors = bool(raiser)
591 return t
594def _SciPyIssue(exc, *extras): # PYCHOK no cover
595 if isinstance(exc, (RuntimeWarning, UserWarning)):
596 E = SciPyWarning
597 else:
598 E = SciPyError # PYCHOK not really
599 t = _SPACE_(str(exc).strip(), *extras)
600 return E(t, txt=None, cause=exc)
603def _xAssertionError(where, *args, **kwds):
604 '''(INTERNAL) Embellish an C{AssertionError} with/-out exception chaining.
605 '''
606 x, kwds = _xkwds_pop2(kwds, cause=None)
607 w = _streprs.unstr(where, *args, **kwds)
608 return _AssertionError(w, txt=None, cause=x)
611def _xattr(obj, **name_default):
612 '''(INTERNAL) Get an C{obj}'s attribute by C{name}.
613 '''
614 if len(name_default) == 1:
615 for n, d in name_default.items():
616 return getattr(obj, n, d)
617 raise _xAssertionError(_xattr, obj, **name_default)
620def _xattrs(inst, other, *attrs): # see .errors._xattr
621 '''(INTERNAL) Copy attribute values from B{C{other}} to B{C{inst}}.
623 @arg inst: Object to copy attribute values to (any C{type}).
624 @arg other: Object to copy attribute values from (any C{type}).
625 @arg attrs: One or more attribute names (C{str}s).
627 @return: Object B{C{inst}}, updated.
629 @raise AttributeError: An B{C{attrs}} doesn't exist or isn't settable.
630 '''
631 def _getattr(o, a):
632 if hasattr(o, a):
633 return getattr(o, a)
634 try:
635 n = o._DOT_(a)
636 except AttributeError:
637 n = _streprs.Fmt.DOT(a)
638 raise _AttributeError(o, name=n)
640 for a in attrs:
641 s = _getattr(other, a)
642 g = _getattr(inst, a)
643 if (g is None and s is not None) or g != s:
644 setattr(inst, a, s) # not settable?
645 return inst
648def _xcallable(**names_callables):
649 '''(INTERNAL) Check one or more C{callable}s.
650 '''
651 for n, c in names_callables.items():
652 if not callable(c):
653 raise _TypeError(n, c, txt_not_=typename(callable)) # txt__
656def _xdatum(datum1, datum2, Error=None):
657 '''(INTERNAL) Check for datum, ellipsoid or rhumb mis-match.
658 '''
659 if Error:
660 e1, e2 = datum1.ellipsoid, datum2.ellipsoid
661 if e1 != e2:
662 raise Error(e2.named2, txt=_incompatible(e1.named2))
663 elif datum1 != datum2:
664 t = _SPACE_(_datum_, repr(datum1.name),
665 _not_, repr(datum2.name))
666 raise _AssertionError(t)
669def _xellipsoidal(**name_value): # see _xellipsoidall elel
670 '''(INTERNAL) Check an I{ellipsoidal} item and return its value.
671 '''
672 if len(name_value) == 1:
673 for n, v in name_value.items():
674 try:
675 if v.isEllipsoidal:
676 return v
677 except AttributeError:
678 pass
679 raise _TypeError(n, v, txt_not_=_ellipsoidal_)
680 raise _xAssertionError(_xellipsoidal, name_value)
683def _xellipsoidall(point): # ... elel, see _xellipsoidal
684 '''(INTERNAL) Check an ellipsoidal C{point}, return C{True}
685 if geodetic latlon, C{False} if cartesian or TypeError.
686 '''
687 m = _MODS.ellipsoidalBase
688 ll = isinstance(point, m.LatLonEllipsoidalBase)
689 if not ll:
690 _basics._xinstanceof(m.CartesianEllipsoidalBase,
691 m.LatLonEllipsoidalBase, point=point)
692 return ll
695def _xellipsoids(E1, E2, Error=_ValueError): # see .ellipsoidalBase
696 '''(INTERNAL) Check ellipsoid mis-match, E2 vs E1.
697 '''
698 if E2 != E1:
699 raise Error(E2.named2, txt=_incompatible(E1.named2))
700 return E1
703def _XError(Error, *args, **kwds):
704 '''(INTERNAL) Format an C{Error} or C{_Error}.
705 '''
706 try: # C{_Error} style
707 return Error(*args, **kwds)
708 except TypeError: # no keyword arguments
709 pass
710 e = _ValueError(*args, **kwds)
711 E = Error(str(e))
712 if _exception_chaining:
713 _error_cause(E, cause=e.__cause__) # PYCHOK OK
714 return E
717def _xError(exc, *args, **kwds):
718 '''(INTERNAL) Embellish a (caught) exception.
720 @arg exc: The exception (usually, C{_Error}).
721 @arg args: Embelishments (C{any}).
722 @kwarg kwds: Embelishments (C{any}).
723 '''
724 return _XError(type(exc), *args, **_xkwds(kwds, cause=exc))
727def _xError2(exc): # in .constants, .fsums, .lazily, .vector2d
728 '''(INTERNAL) Map an exception to 2-tuple (C{_Error} class, error C{txt}).
730 @arg exc: The exception instance (usually, C{Exception}).
731 '''
732 x = isError(exc)
733 if x:
734 E = type(exc)
735 elif x is None:
736 E = _AssertionError
737 else: # get _Error from Error
738 n = NN(_UNDER_, _tailof(typename(type(exc))))
739 E = _MODS.getattr(__name__, n, _NotImplementedError)
740 x = E is not _NotImplementedError
741 return E, (str(exc) if x else repr(exc))
744_XErrors = (_AssertionError, _AttributeError, # some isError's
745 _TypeError, _ValueError, _ZeroDivisionError)
746# map certain C{Exception} classes to the C{_Error}
747# _X2Error = {AssertionError: _AssertionError, ...
748# ZeroDivisionError: _ZeroDivisionError}
751def _xgeodesics(G1, G2, Error=_ValueError): # see .geodesici
752 '''(INTERNAL) Check geodesics mis-match.
753 '''
754 if G1.ellipsoid != G2.ellipsoid:
755 raise Error(G1.named2, txt=_incompatible(G2.named2))
756 return G1
759try:
760 _ = {}.__or__ # {} | {} # Python 3.9+
762 def _xkwds(kwds, **dflts):
763 '''(INTERNAL) Update C{dflts} with specified C{kwds},
764 i.e. C{copy(kwds).update(dflts)}.
765 '''
766 return ((dflts | kwds) if kwds else dflts) if dflts else kwds
768except AttributeError:
770 def _xkwds(kwds, **dflts): # PYCHOK expected
771 '''(INTERNAL) Update C{dflts} with specified C{kwds},
772 i.e. C{copy(kwds).update(dflts)}.
773 '''
774 d = dflts
775 if kwds:
776 d = _copy(d)
777 d.update(kwds)
778 return d
781# def _xkwds_bool(inst, **kwds): # unused
782# '''(INTERNAL) Set applicable C{bool} properties/attributes.
783# '''
784# for n, v in kwds.items():
785# b = getattr(inst, n, None)
786# if b is None: # invalid bool attr
787# t = _SPACE_(_EQUAL_(n, repr(v)), 'for', typename(type(inst)) # XXX .classname
788# raise _AttributeError(t, txt_not_='applicable')
789# if _basics.isbool(v) and v != b:
790# setattr(inst, NN(_UNDER_, n), v)
793# def _xkwds_from(orig, *args, **kwds): # unused
794# '''(INTERNAL) Return the items from C{orig} with the keys
795# from C{kwds} and a value not in C{args} and C{kwds}.
796# '''
797# def _items(orig, args, items):
798# for n, m in items:
799# if n in orig: # n in (orig.keys() & kwds.keys())
800# t = orig[n]
801# if t is not m and t not in args:
802# yield n, t
803#
804# return _items(orig, args, kwds.items())
807def _xkwds_get(kwds, **name_default):
808 '''(INTERNAL) Get a C{kwds} value by C{name} or the
809 C{default} if not present.
810 '''
811 if isinstance(kwds, dict) and len(name_default) == 1:
812 for n, v in name_default.items():
813 return kwds.get(n, v)
814 raise _xAssertionError(_xkwds_get, kwds, **name_default)
817# def _xkwds_get_(kwds, **names_defaults): # unused
818# '''(INTERNAL) Yield each C{kwds} value or its C{default}
819# in I{case-insensitive, alphabetical} C{name} order.
820# '''
821# if not isinstance(kwds, dict):
822# raise _xAssertionError(_xkwds_get_, kwds)
823# for n, v in _basics.itemsorted(names_defaults):
824# yield kwds.get(n, v)
827def _xkwds_get1(kwds, **name_default):
828 '''(INTERNAL) Get one C{kwds} value by C{name} or the
829 C{default} if not present. Raise an C{_UnexpectedError}
830 with any remaining keyword arguments.
831 '''
832 v, kwds = _xkwds_pop2(kwds, **name_default)
833 if kwds:
834 raise _UnexpectedError(**kwds)
835 return v
838def _xkwds_item2(kwds):
839 '''(INTERNAL) Return the 2-tuple C{item}, keeping the
840 single-item C{kwds} I{unmodified}.
841 '''
842 if isinstance(kwds, dict) and len(kwds) == 1:
843 for item in kwds.items():
844 return item
845 raise _xAssertionError(_xkwds_item2, kwds)
848def _xkwds_kwds(kwds, **names_defaults): # in .geodesici # PYCHOK no cover
849 '''(INTERNAL) Return a C{dict} of C{named_defaults} items replaced with C{kwds}.
850 '''
851 if not isinstance(kwds, dict):
852 raise _xAssertionError(_xkwds_kwds, kwds)
853 _g = kwds.get
854 return dict((n, _g(n, v)) for n, v in names_defaults.items())
857def _xkwds_not(*args, **kwds):
858 '''(INTERNAL) Return C{kwds} with a value not in C{args}.
859 '''
860 return dict((n, v) for n, v in kwds.items() if v not in args)
863def _xkwds_pop(kwds, **name_default):
864 '''(INTERNAL) Pop an item by C{name} from C{kwds} and
865 return its value, otherwise return the C{default}.
866 '''
867 if isinstance(kwds, dict) and len(name_default) == 1:
868 for n, v in name_default.items():
869 return kwds.pop(n, v)
870 raise _xAssertionError(_xkwds_pop, kwds, **name_default)
873def _xkwds_pop2(kwds, **name_default):
874 '''(INTERNAL) Pop a C{kwds} item by C{name} and return the value and
875 reduced C{kwds} copy, otherwise the C{default} and original C{kwds}.
876 '''
877 if isinstance(kwds, dict) and len(name_default) == 1:
878 for n, v in name_default.items():
879 if n in kwds:
880 kwds = _copy(kwds)
881 v = kwds.pop(n, v)
882 return v, kwds
883 raise _xAssertionError(_xkwds_pop2, kwds, **name_default)
886def _Xorder(_Coeffs, Error, **Xorder): # in .auxLat, .ktm, .rhumb.bases, .rhumb.ekx
887 '''(INTERNAL) Validate C{RAorder} or C{TMorder}.
888 '''
889 X, m = _xkwds_item2(Xorder)
890 if m in _Coeffs and _basics.isint(m):
891 return m
892 t = sorted(map(str, _Coeffs.keys()))
893 raise Error(X, m, txt_not_=_or(*t))
896def _xsError(X, xs, i, x, *n, **kwds): # in .fmath, ._fstats, .fsums
897 '''(INTERNAL) Error for C{xs} or C{x}, item C{xs[i]}.
898 '''
899 def _xs(*xs):
900 if len(xs) > 4:
901 xs = xs[:3] + (_ELLIPSIS_,) + xs[-1:]
902 return xs
904 return ((_xError(X, n[0], _xs(*xs), **kwds) if n else
905 _xError(X, xs=_xs(*xs), **kwds)) if x is xs else
906 _xError(X, _streprs.Fmt.INDEX(xs=i), x, **kwds))
909def _xStrError(*Refs, **name_value_Error): # in .gars, .geohash, .wgrs
910 '''(INTERNAL) Create a C{TypeError} for C{Garef}, C{Geohash}, C{Wgrs}.
911 '''
912 s = typename(_MODS.unitsBase.Str)
913 t = tuple(map(typename, Refs)) + (s, _LatLon_, 'LatLon*Tuple')
914 return _IsnotError(*t, **name_value_Error)
916# **) MIT License
917#
918# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
919#
920# Permission is hereby granted, free of charge, to any person obtaining a
921# copy of this software and associated documentation files (the "Software"),
922# to deal in the Software without restriction, including without limitation
923# the rights to use, copy, modify, merge, publish, distribute, sublicense,
924# and/or sell copies of the Software, and to permit persons to whom the
925# Software is furnished to do so, subject to the following conditions:
926#
927# The above copyright notice and this permission notice shall be included
928# in all copies or substantial portions of the Software.
929#
930# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
931# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
932# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
933# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
934# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
935# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
936# OTHER DEALINGS IN THE SOFTWARE.