Coverage for pygeodesy/errors.py: 91%
327 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-09-09 13:03 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-09-09 13:03 -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_, _DOT_, _ELLIPSIS_, _ellipsoidal_, \
20 _EQUALSPACED_, _immutable_, _incompatible_, _invalid_, _keyword_, \
21 _LatLon_, _len_, _not_, _or_, _SPACE_, _specified_, _UNDER_, \
22 _vs_, _with_
23from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _PYTHON_X_DEV
24# from pygeodesy import streprs as _streprs # _MODS.into
25# from pygeodesy.unitsBase import Str # _MODS
26# from pygeodesy.vector3dBase import Vector3dBase # _MODS
28from copy import copy as _copy
30__all__ = _ALL_LAZY.errors # _ALL_DOCS('_InvalidError', '_IsnotError') _under
31__version__ = '25.09.04'
33_argument_ = 'argument'
34_basics = _MODS.into(basics=__name__)
35_box_ = 'box'
36_del_ = 'del'
37_expected_ = 'expected'
38_limiterrors = True # in .formy
39_name_value_ = repr('name=value')
40_rangerrors = True # in .dms
41_region_ = 'region'
42_streprs = _MODS.into(streprs=__name__)
43_vs__ = _SPACE_(NN, _vs_, NN)
45try:
46 _exception_chaining = None # not available
47 _ = Exception().__cause__ # Python 3.9+ exception chaining
49 if _PYTHON_X_DEV or _envPYGEODESY('EXCEPTION_CHAINING'): # == _std_
50 _exception_chaining = True # turned on, std
51 raise AttributeError() # allow exception chaining
53 _exception_chaining = False # turned off
55 def _error_cause(inst, cause=None):
56 '''(INTERNAL) Set or avoid Python 3+ exception chaining.
58 Setting C{inst.__cause__ = None} is equivalent to syntax
59 C{raise Error(...) from None} to avoid exception chaining.
61 @arg inst: An error instance (I{caught} C{Exception}).
62 @kwarg cause: A previous error instance (I{caught} C{Exception})
63 or C{None} to avoid exception chaining.
65 @see: Alex Martelli, et.al., "Python in a Nutshell", 3rd Ed., page 163,
66 O'Reilly, 2017, U{PEP-3134<https://www.Python.org/dev/peps/pep-3134>},
67 U{here<https://StackOverflow.com/questions/17091520/how-can-i-more-
68 easily-suppress-previous-exceptions-when-i-raise-my-own-exception>}
69 and U{here<https://StackOverflow.com/questions/1350671/
70 inner-exception-with-traceback-in-python>}.
71 '''
72 inst.__cause__ = cause # None, no exception chaining
73 return inst
75except AttributeError: # Python 2+
77 def _error_cause(inst, **unused): # PYCHOK expected
78 return inst # no-op
81class _AssertionError(AssertionError):
82 '''(INTERNAL) Format an C{AssertionError} with/-out exception chaining.
83 '''
84 def __init__(self, *args, **kwds):
85 _error_init(AssertionError, self, args, **kwds)
88class _AttributeError(AttributeError):
89 '''(INTERNAL) Format an C{AttributeError} with/-out exception chaining.
90 '''
91 def __init__(self, *args, **kwds):
92 _error_init(AttributeError, self, args, **kwds)
95class _ImportError(ImportError):
96 '''(INTERNAL) Format an C{ImportError} with/-out exception chaining.
97 '''
98 def __init__(self, *args, **kwds):
99 _error_init(ImportError, self, args, **kwds)
102class _IndexError(IndexError):
103 '''(INTERNAL) Format an C{IndexError} with/-out exception chaining.
104 '''
105 def __init__(self, *args, **kwds):
106 _error_init(IndexError, self, args, **kwds)
109class _KeyError(KeyError):
110 '''(INTERNAL) Format a C{KeyError} with/-out exception chaining.
111 '''
112 def __init__(self, *args, **kwds): # txt=_invalid_
113 _error_init(KeyError, self, args, **kwds)
116class _NameError(NameError):
117 '''(INTERNAL) Format a C{NameError} with/-out exception chaining.
118 '''
119 def __init__(self, *args, **kwds):
120 _error_init(NameError, self, args, **kwds)
123class _NotImplementedError(NotImplementedError):
124 '''(INTERNAL) Format a C{NotImplementedError} with/-out exception chaining.
125 '''
126 def __init__(self, *args, **kwds):
127 _error_init(NotImplementedError, self, args, **kwds)
130class _OverflowError(OverflowError):
131 '''(INTERNAL) Format an C{OverflowError} with/-out exception chaining.
132 '''
133 def __init__(self, *args, **kwds): # txt=_invalid_
134 _error_init(OverflowError, self, args, **kwds)
137class _TypeError(TypeError):
138 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining.
139 '''
140 def __init__(self, *args, **kwds):
141 _error_init(TypeError, self, args, fmt_name_value='type(%s) (%r)', **kwds)
144def _TypesError(name, value, *Types, **kwds):
145 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining.
146 '''
147 # no longer C{class _TypesError} to avoid missing value
148 # argument errors in _XError line ...E = Error(str(e))
149 t = _an(_or(*map(typename, Types, Types)))
150 return _TypeError(name, value, txt=_not_(t), **kwds)
153class _UnexpectedError(TypeError): # note, a TypeError!
154 '''(INTERNAL) Format a C{TypeError} I{without exception chaining}.
155 '''
156 def __init__(self, *args, **kwds):
157 n = len(kwds)
158 if args:
159 a = _plural(_argument_, len(args))
160 n = _and(a, _plural(_keyword_, n)) if n else a
161 else:
162 n = _plural(_SPACE_(_keyword_, _argument_), n)
163 u = _streprs.unstr(_SPACE_(n, NN), *args, **kwds)
164 # _error_init(TypeError, self, (u,), txt_not_=_expected_)
165 TypeError.__init__(self, _SPACE_(u, _not_, _expected_))
168class _ValueError(ValueError):
169 '''(INTERNAL) Format a C{ValueError} with/-out exception chaining.
170 '''
171 def __init__(self, *args, **kwds): # ..., cause=None, txt=_invalid_, ...
172 _error_init(ValueError, self, args, **kwds)
175class _ZeroDivisionError(ZeroDivisionError):
176 '''(INTERNAL) Format a C{ZeroDivisionError} with/-out exception chaining.
177 '''
178 def __init__(self, *args, **kwds):
179 _error_init(ZeroDivisionError, self, args, **kwds)
182class AuxError(_ValueError):
183 '''Error raised for a L{rhumb.aux_}, C{Aux}, C{AuxDLat} or C{AuxLat} issue.
184 '''
185 pass
188class ClipError(_ValueError):
189 '''Clip box or clip region issue.
190 '''
191 def __init__(self, *name_n_corners, **txt_cause):
192 '''New L{ClipError}.
194 @arg name_n_corners: Either just a name (C{str}) or
195 name, number, corners (C{str},
196 C{int}, C{tuple}).
197 @kwarg txt_cause: Optional C{B{txt}=str} explanation
198 of the error and C{B{cause}=None}
199 for exception chaining.
200 '''
201 if len(name_n_corners) == 3:
202 t, n, v = name_n_corners
203 n = _SPACE_(t, _clip_, (_box_ if n == 2 else _region_))
204 name_n_corners = n, v
205 _ValueError.__init__(self, *name_n_corners, **txt_cause)
208class CrossError(_ValueError):
209 '''Error raised for zero or near-zero vectorial cross products,
210 occurring for coincident or colinear points, lines or bearings.
211 '''
212 pass
215class GeodesicError(_ValueError):
216 '''Error raised for convergence or other issues in L{geodesicx<pygeodesy.geodesicx>},
217 L{geodesicw<pygeodesy.geodesicw>} or L{karney<pygeodesy.karney>}.
218 '''
219 pass
222class IntersectionError(_ValueError): # in .ellipsoidalBaseDI, .formy, ...
223 '''Error raised for line or circle intersection issues.
224 '''
225 def __init__(self, *args, **kwds):
226 '''New L{IntersectionError}.
227 '''
228 if args:
229 t = _COMMASPACE_(*map(repr, args))
230 _ValueError.__init__(self, t, **kwds)
231 else:
232 _ValueError.__init__(self, **kwds)
235class LenError(_ValueError): # in .ecef, .fmath, .heights, .iters, .named
236 '''Error raised for mis-matching C{len} values.
237 '''
238 def __init__(self, where, **lens_txt): # txt=None
239 '''New L{LenError}.
241 @arg where: Object with C{.__name__} attribute
242 (C{class}, C{method}, or C{function}).
243 @kwarg lens_txt: Two or more C{name=len(name)} pairs
244 (C{keyword arguments}).
245 '''
246 def _ns_vs_txt_x(cause=None, txt=_invalid_, **kwds):
247 ns, vs = zip(*_basics.itemsorted(kwds)) # unzip
248 return ns, vs, txt, cause
250 ns, vs, txt, x = _ns_vs_txt_x(**lens_txt)
251 ns = _COMMASPACE_.join(ns)
252 t = _streprs.Fmt.PAREN(typename(where), ns)
253 vs = _vs__.join(map(str, vs))
254 t = _SPACE_(t, _len_, vs)
255 _ValueError.__init__(self, t, txt=txt, cause=x)
258class LimitError(_ValueError):
259 '''Error raised for lat- or longitudinal values or deltas exceeding the given
260 B{C{limit}} in functions L{equirectangular<pygeodesy.equirectangular>},
261 L{equirectangular4<pygeodesy.equirectangular4>}, C{nearestOn*} and
262 C{simplify*} or methods with C{limit} or C{options} keyword arguments.
264 @see: Subclass L{UnitError}.
265 '''
266 pass
269class MGRSError(_ValueError):
270 '''Military Grid Reference System (MGRS) parse or other L{Mgrs} issue.
271 '''
272 pass
275class NumPyError(_ValueError):
276 '''Error raised for C{NumPy} issues.
277 '''
278 pass
281class ParseError(_ValueError): # in .dms, .elevations, .utmupsBase
282 '''Error parsing degrees, radians or several other formats.
283 '''
284 pass
287class PointsError(_ValueError): # in .clipy, .frechet, ...
288 '''Error for an insufficient number of points.
289 '''
290 pass
293class RangeError(_ValueError):
294 '''Error raised for lat- or longitude values outside the B{C{clip}}, B{C{clipLat}},
295 B{C{clipLon}} in functions L{parse3llh<pygeodesy.dms.parse3llh>}, L{parseDMS<pygeodesy.dms.parseDMS>},
296 L{parseDMS2<pygeodesy.dms.parseDMS2>} and L{parseRad<pygeodesy.dms.parseRad>} or the B{C{limit}} set
297 with functions L{clipDegrees<pygeodesy.dms.clipDegrees>} and L{clipRadians<pygeodesy.dms.clipRadians>}.
299 @see: Function L{rangerrors<pygeodesy.errors.rangerrors>}.
300 '''
301 pass
304class RhumbError(_ValueError):
305 '''Error raised for a rhumb L{aux_<pygeodesy.rhumb.aux_>}, L{ekx<pygeodesy.rhumb.ekx>} or
306 L{solve<pygeodesy.rhumb.solve>} issue.
307 '''
308 pass
311class TriangleError(_ValueError): # in .resections, .vector2d
312 '''Error raised for triangle, intersection or resection issues.
313 '''
314 pass
317class SciPyError(PointsError):
318 '''Error raised for C{SciPy} issues.
319 '''
320 pass
323class SciPyWarning(PointsError):
324 '''Error thrown for C{SciPy} warnings.
326 To raise C{SciPy} warnings as L{SciPyWarning} exceptions, Python
327 C{warnings} must be filtered as U{warnings.filterwarnings('error')
328 <https://docs.Python.org/3/library/warnings.html#the-warnings-filter>}
329 I{prior to} C{import scipy} OR by setting env var U{PYTHONWARNINGS
330 <https://docs.Python.org/3/using/cmdline.html#envvar-PYTHONWARNINGS>}
331 OR by invoking C{python} with command line option U{-W<https://docs.
332 Python.org/3/using/cmdline.html#cmdoption-w>} set to C{-W error}.
333 '''
334 pass
337class TRFError(_ValueError): # in .ellipsoidalBase, .trf, .units
338 '''Terrestrial Reference Frame (TRF), L{Epoch}, L{RefFrame} or L{RefFrame}
339 conversion issue.
340 '''
341 pass
344class UnitError(LimitError): # in .named, .units
345 '''Default exception for L{units} issues for a value exceeding the C{low}
346 or C{high} limit.
347 '''
348 pass
351class VectorError(_ValueError): # in .nvectorBase, .vector3d, .vector3dBase
352 '''L{Vector3d}, C{Cartesian*} or C{*Nvector} issues.
353 '''
354 pass
357def _an(noun):
358 '''(INTERNAL) Prepend an article to a noun based
359 on the pronounciation of the first letter.
360 '''
361 a = _an_ if noun[:1].lower() in 'aeinoux' else _a_
362 return _SPACE_(a, noun)
365def _and(*words):
366 '''(INTERNAL) Join C{words} with C{", "} and C{" and "}.
367 '''
368 return _and_or(_and_, *words)
371def _and_or(last, *words):
372 '''(INTERNAL) Join C{words} with C{", "} and C{B{last}}.
373 '''
374 t, w = NN, list(words)
375 if w:
376 t = w.pop()
377 if w:
378 w = _COMMASPACE_.join(w)
379 t = _SPACE_(w, last, t)
380 return t
383def crosserrors(raiser=None):
384 '''Report or ignore vectorial cross product errors.
386 @kwarg raiser: Use C{True} to throw, C{False} to ignore
387 L{CrossError} exceptions or C{None} to
388 leave the setting unchanged.
390 @return: Previous setting (C{bool}).
392 @see: Property C{Vector3d[Base].crosserrors}.
393 '''
394 V = _MODS.vector3dBase.Vector3dBase
395 t = V._crosserrors # XXX class attr!
396 if raiser is not None:
397 V._crosserrors = bool(raiser)
398 return t
401def _error_init(Error, inst, args, fmt_name_value='%s (%r)', txt_not_=NN,
402 txt__=None, txt=NN, cause=None, **kwds):
403 '''(INTERNAL) Format an error text and initialize an C{Error} instance.
405 @arg Error: The error super-class (C{Exception}).
406 @arg inst: Sub-class instance to be __init__-ed (C{_Exception}).
407 @arg args: Either just a value or several name, value, ...
408 positional arguments (C{str}, any C{type}), in
409 particular for name conflicts with keyword
410 arguments of C{error_init} or which can't be
411 given as C{name=value} keyword arguments.
412 @kwarg fmt_name_value: Format for (name, value) (C{str}).
413 @kwarg txt: Optional explanation of the error (C{str}).
414 @kwarg txt__: Alternate C{B{txt}=B{txt__}.__name__}.
415 @kwarg txt_not_: Negative explanation C{B{txt}=_not_(B{txt_not_})}.
416 @kwarg cause: Optional, caught error (L{Exception}), for
417 exception chaining (supported in Python 3+).
418 @kwarg kwds: Additional C{B{name}=value} pairs, if any.
419 '''
420 def _fmtuple(pairs):
421 return tuple(fmt_name_value % t for t in pairs)
423 t, n = (), len(args)
424 if n > 2:
425 t = _fmtuple(zip(args[0::2], args[1::2]))
426 s = _basics.isodd(n)
427 if s: # XXX _xzip(..., strict=s)
428 t += args[-1:]
429 elif n == 2:
430 t = (fmt_name_value % args),
431 elif n: # == 1
432 t = str(args[0]),
433 if kwds:
434 t += _fmtuple(_basics.itemsorted(kwds))
435 t = _or(*t) if t else _SPACE_(_name_value_, MISSING)
437 x = _not_(txt_not_) if txt_not_ else (txt if txt__ is None
438 else typename(txt__))
439 if x is not None:
440 x = str(x) or (str(cause) if cause else _invalid_)
441 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_
442 t = C(t, x)
443# else: # LenError, _xzip, .dms, .heights, .vector2d
444# x = NN # XXX or t?
445 Error.__init__(inst, t)
446# inst.__x_txt__ = x # hold explanation
447 _error_cause(inst, cause=cause if _exception_chaining else None)
448 _error_under(inst)
451def _error_under(inst):
452 '''(INTERNAL) Remove leading underscore from instance' class name.
453 '''
454 t = type(inst)
455 n = typename(t) # _tailof?
456 if n.startswith(_UNDER_):
457 t.__name__ = n.lstrip(_UNDER_)
458 return inst
461def exception_chaining(exc=None):
462 '''Get an error's I{cause} or the exception chaining setting.
464 @kwarg exc: An error instance (C{Exception}) or C{None}.
466 @return: If C{B{exc} is None}, return C{True} if exception
467 chaining is enabled for PyGeodesy errors, C{False}
468 if turned off and C{None} if not available. If
469 C{B{exc} is not None}, return it's error I{cause}
470 or C{None} if there is none.
472 @note: To enable exception chaining for C{pygeodesy} errors,
473 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any
474 non-empty value prior to C{import pygeodesy}.
475 '''
476 return _exception_chaining if exc is None else \
477 getattr(exc, '__cause__', None) # _DCAUSE_
480def _ImmutableError(inst, attr, value=_del_, Error=_TypeError): # PYCHOK self
481 '''(INTERNAL) Format an C{immutable _TypeError}.
482 '''
483 n = typename(inst)
484 n = _DOT_(_xattr(inst, name=n), attr)
485 t = _SPACE_(_del_, n) if value is _del_ else \
486 _EQUALSPACED_(n, repr(value))
487 return Error(_immutable_, txt=t)
490def _incompatible(this):
491 '''(INTERNAL) Format an C{"incompatible with ..."} text.
492 '''
493 return _SPACE_(_incompatible_, _with_, this)
496def _InvalidError(Error=_ValueError, **txt_name_values_cause): # txt=_invalid_, name=value [, ...]
497 '''(INTERNAL) Create an C{Error} instance.
499 @kwarg Error: The error class or sub-class (C{Exception}).
500 @kwarg txt_name_values: One or more C{B{name}=value} pairs
501 and optionally, keyword argument C{B{txt}=str}
502 to override the default C{B{txt}='invalid'} and
503 C{B{cause}=None} for exception chaining.
505 @return: An B{C{Error}} instance.
506 '''
507 return _XError(Error, **txt_name_values_cause)
510def isError(exc):
511 '''Check a (caught) exception.
513 @arg exc: The exception C({Exception}).
515 @return: C{True} if B{C{exc}} is a C{pygeodesy} error,
516 C{False} if B{C{exc}} is a standard Python error
517 of C{None} if neither.
518 '''
519 def _X(exc):
520 X = type(exc)
521 m = X.__module__
522 return _basics.issubclassof(X, *_XErrors) or \
523 ((m is __name__ or m == __name__) and
524 _tailof(typename(X)).startswith(_UNDER_))
526 return True if isinstance(exc, _XErrors) else (
527 _X(exc) if isinstance(exc, Exception) else None)
530def _IsnotError(*types__, **name_value_Error_cause): # name=value [, Error=TypeError, cause=None]
531 '''Create a C{TypeError} for an invalid C{name=value} type.
533 @arg types__: One or more types or type names.
534 @kwarg name_value_Error_cause: One C{B{name}=value} pair and optionally,
535 keyword arguments C{B{Error}=TypeError} and C{B{cause}=None}
536 for exception chaining.
538 @return: A C{TypeError} or an B{C{Error}} instance.
539 '''
540 x, kwds = _xkwds_pop2(name_value_Error_cause, cause=None)
541 E, kwds = _xkwds_pop2(kwds, Error=TypeError)
542 n, v = _xkwds_item2(kwds)
544 n = _streprs.Fmt.PARENSPACED(n, repr(v))
545 t = _an(_or(*map(typename, types__, types__))) if types__ else _specified_
546 return _XError(E, n, txt=_not_(t), cause=x)
549def limiterrors(raiser=None):
550 '''Get/set the throwing of L{LimitError}s.
552 @kwarg raiser: Use C{True} to raise, C{False} to
553 ignore L{LimitError} exceptions or
554 C{None} to leave the setting unchanged.
556 @return: Previous setting (C{bool}).
557 '''
558 global _limiterrors
559 t = _limiterrors
560 if raiser is not None:
561 _limiterrors = bool(raiser)
562 return t
565def _or(*words):
566 '''(INTERNAL) Join C{words} with C{", "} and C{" or "}.
567 '''
568 return _and_or(_or_, *words)
571def _parseX(parser, *args, **Error_name_values): # name=value[, ..., Error=ParseError]
572 '''(INTERNAL) Invoke a parser and handle exceptions.
574 @arg parser: The parser (C{callable(*B{args}}).
575 @arg args: Any B{C{parser}} arguments (any C{type}s).
576 @kwarg Error_name_values: Optional C{B{Error}=ParseError}
577 and number of C{B{name}=value} pairs.
579 @return: Parser result.
581 @raise ParseError: Or the specified C{B{Error}}.
582 '''
583 try:
584 return parser(*args)
585 except Exception as x:
586 E = type(x) if isError(x) else ParseError
587 E, kwds = _xkwds_pop2(Error_name_values, Error=E)
588 raise _XError(E, **_xkwds(kwds, cause=x))
591def rangerrors(raiser=None):
592 '''Get/set the throwing of L{RangeError}s.
594 @kwarg raiser: Use C{True} to raise or C{False} to ignore
595 L{RangeError} exceptions or C{None} to leave
596 the setting unchanged.
598 @return: Previous setting (C{bool}).
599 '''
600 global _rangerrors
601 t = _rangerrors
602 if raiser is not None:
603 _rangerrors = bool(raiser)
604 return t
607def _SciPyIssue(exc, *extras): # PYCHOK no cover
608 if isinstance(exc, (RuntimeWarning, UserWarning)):
609 E = SciPyWarning
610 else:
611 E = SciPyError # PYCHOK not really
612 t = _SPACE_(str(exc).strip(), *extras)
613 return E(t, txt=None, cause=exc)
616def _xAssertionError(where, *args, **kwds):
617 '''(INTERNAL) Embellish an C{AssertionError} with/-out exception chaining.
618 '''
619 x, kwds = _xkwds_pop2(kwds, cause=None)
620 w = _streprs.unstr(where, *args, **kwds)
621 return _AssertionError(w, txt=None, cause=x)
624def _xattr(obj, **name_default):
625 '''(INTERNAL) Get an C{obj}'s attribute by C{name}.
626 '''
627 if len(name_default) == 1:
628 for n, d in name_default.items():
629 return getattr(obj, n, d)
630 raise _xAssertionError(_xattr, obj, **name_default)
633def _xattrs(inst, other, *attrs): # see .errors._xattr
634 '''(INTERNAL) Copy attribute values from B{C{other}} to B{C{inst}}.
636 @arg inst: Object to copy attribute values to (any C{type}).
637 @arg other: Object to copy attribute values from (any C{type}).
638 @arg attrs: One or more attribute names (C{str}s).
640 @return: Object B{C{inst}}, updated.
642 @raise AttributeError: An B{C{attrs}} doesn't exist or isn't settable.
643 '''
644 def _getattr(o, a):
645 if hasattr(o, a):
646 return getattr(o, a)
647 try:
648 n = o._DOT_(a)
649 except AttributeError:
650 n = _streprs.Fmt.DOT(a)
651 raise _AttributeError(o, name=n)
653 for a in attrs:
654 s = _getattr(other, a)
655 g = _getattr(inst, a)
656 if (g is None and s is not None) or g != s:
657 setattr(inst, a, s) # not settable?
658 return inst
661def _xcallable(**names_callables):
662 '''(INTERNAL) Check one or more C{callable}s.
663 '''
664 for n, c in names_callables.items():
665 if not callable(c):
666 raise _TypeError(n, c, txt_not_=typename(callable)) # txt__
669def _xdatum(datum1, datum2, Error=None):
670 '''(INTERNAL) Check for datum, ellipsoid or rhumb mis-match.
671 '''
672 if Error:
673 e1, e2 = datum1.ellipsoid, datum2.ellipsoid
674 if e1 != e2:
675 raise Error(e2.named2, txt=_incompatible(e1.named2))
676 elif datum1 != datum2:
677 t = _SPACE_(_datum_, repr(datum1.name),
678 _not_, repr(datum2.name))
679 raise _AssertionError(t)
682def _xellipsoidal(**name_value): # see _xellipsoidall elel
683 '''(INTERNAL) Check an I{ellipsoidal} item and return its value.
684 '''
685 if len(name_value) == 1:
686 for n, v in name_value.items():
687 try:
688 if v.isEllipsoidal:
689 return v
690 except AttributeError:
691 pass
692 raise _TypeError(n, v, txt_not_=_ellipsoidal_)
693 raise _xAssertionError(_xellipsoidal, name_value)
696def _xellipsoidall(point): # ... elel, see _xellipsoidal
697 '''(INTERNAL) Check an ellipsoidal C{point}, return C{True}
698 if geodetic latlon, C{False} if cartesian or TypeError.
699 '''
700 m = _MODS.ellipsoidalBase
701 ll = isinstance(point, m.LatLonEllipsoidalBase)
702 if not ll:
703 _basics._xinstanceof(m.CartesianEllipsoidalBase,
704 m.LatLonEllipsoidalBase, point=point)
705 return ll
708def _xellipsoids(E1, E2, Error=_ValueError): # see .ellipsoidalBase
709 '''(INTERNAL) Check ellipsoid mis-match, E2 vs E1.
710 '''
711 if E2 != E1:
712 raise Error(E2.named2, txt=_incompatible(E1.named2))
713 return E1
716def _XError(Error, *args, **kwds):
717 '''(INTERNAL) Format an C{Error} or C{_Error}.
718 '''
719 try: # C{_Error} style
720 return Error(*args, **kwds)
721 except TypeError: # no keyword arguments
722 pass
723 e = _ValueError(*args, **kwds)
724 E = Error(str(e))
725 if _exception_chaining:
726 _error_cause(E, cause=e.__cause__) # PYCHOK OK
727 return E
730def _xError(exc, *args, **kwds):
731 '''(INTERNAL) Embellish a (caught) exception.
733 @arg exc: The exception (usually, C{_Error}).
734 @arg args: Embelishments (C{any}).
735 @kwarg kwds: Embelishments (C{any}).
736 '''
737 return _XError(type(exc), *args, **_xkwds(kwds, cause=exc))
740def _xError2(exc): # in .constants, .fsums, .lazily, .vector2d
741 '''(INTERNAL) Map an exception to 2-tuple (C{_Error} class, error C{txt}).
743 @arg exc: The exception instance (usually, C{Exception}).
744 '''
745 x = isError(exc)
746 if x:
747 E = type(exc)
748 elif x is None:
749 E = _AssertionError
750 else: # get _Error from Error
751 n = NN(_UNDER_, _tailof(typename(type(exc))))
752 E = _MODS.getattr(__name__, n, _NotImplementedError)
753 x = E is not _NotImplementedError
754 return E, (str(exc) if x else repr(exc))
757_XErrors = (_AssertionError, _AttributeError, # some isError's
758 _TypeError, _ValueError, _ZeroDivisionError)
759# map certain C{Exception} classes to the C{_Error}
760# _X2Error = {AssertionError: _AssertionError, ...
761# ZeroDivisionError: _ZeroDivisionError}
764def _xgeodesics(G1, G2, Error=_ValueError): # see .geodesici
765 '''(INTERNAL) Check geodesics mis-match.
766 '''
767 if G1.ellipsoid != G2.ellipsoid:
768 raise Error(G1.named2, txt=_incompatible(G2.named2))
769 return G1
772try:
773 _ = {}.__or__ # {} | {} # Python 3.9+
775 def _xkwds(kwds, **dflts):
776 '''(INTERNAL) Update C{dflts} with specified C{kwds},
777 i.e. C{copy(kwds).update(dflts)}.
778 '''
779 return ((dflts | kwds) if kwds else dflts) if dflts else kwds
781except AttributeError:
783 def _xkwds(kwds, **dflts): # PYCHOK expected
784 '''(INTERNAL) Update C{dflts} with specified C{kwds},
785 i.e. C{copy(kwds).update(dflts)}.
786 '''
787 d = dflts
788 if kwds:
789 d = _copy(d)
790 d.update(kwds)
791 return d
794# def _xkwds_bool(inst, **kwds): # unused
795# '''(INTERNAL) Set applicable C{bool} properties/attributes.
796# '''
797# for n, v in kwds.items():
798# b = getattr(inst, n, None)
799# if b is None: # invalid bool attr
800# t = _SPACE_(_EQUAL_(n, repr(v)), 'for', typename(type(inst)) # XXX .classname
801# raise _AttributeError(t, txt_not_='applicable')
802# if _basics.isbool(v) and v != b:
803# setattr(inst, NN(_UNDER_, n), v)
806# def _xkwds_from(orig, *args, **kwds): # unused
807# '''(INTERNAL) Return the items from C{orig} with the keys
808# from C{kwds} and a value not in C{args} and C{kwds}.
809# '''
810# def _items(orig, args, items):
811# for n, m in items:
812# if n in orig: # n in (orig.keys() & kwds.keys())
813# t = orig[n]
814# if t is not m and t not in args:
815# yield n, t
816#
817# return _items(orig, args, kwds.items())
820def _xkwds_get(kwds, **name_default):
821 '''(INTERNAL) Get a C{kwds} value by C{name} or the
822 C{default} if not present.
823 '''
824 if isinstance(kwds, dict) and len(name_default) == 1:
825 for n, v in name_default.items():
826 return kwds.get(n, v)
827 raise _xAssertionError(_xkwds_get, kwds, **name_default)
830# def _xkwds_get_(kwds, **names_defaults): # unused
831# '''(INTERNAL) Yield each C{kwds} value or its C{default}
832# in I{case-insensitive, alphabetical} C{name} order.
833# '''
834# if not isinstance(kwds, dict):
835# raise _xAssertionError(_xkwds_get_, kwds)
836# for n, v in _basics.itemsorted(names_defaults):
837# yield kwds.get(n, v)
840def _xkwds_get1(kwds, **name_default):
841 '''(INTERNAL) Get one C{kwds} value by C{name} or the
842 C{default} if not present. Raise an C{_UnexpectedError}
843 with any remaining keyword arguments.
844 '''
845 v, kwds = _xkwds_pop2(kwds, **name_default)
846 if kwds:
847 raise _UnexpectedError(**kwds)
848 return v
851def _xkwds_item2(kwds):
852 '''(INTERNAL) Return the 2-tuple C{item}, keeping the
853 single-item C{kwds} I{unmodified}.
854 '''
855 if isinstance(kwds, dict) and len(kwds) == 1:
856 for item in kwds.items():
857 return item
858 raise _xAssertionError(_xkwds_item2, kwds)
861def _xkwds_kwds(kwds, **names_defaults): # in .geodesici # PYCHOK no cover
862 '''(INTERNAL) Return a C{dict} of C{named_defaults} items replaced with C{kwds}.
863 '''
864 if not isinstance(kwds, dict):
865 raise _xAssertionError(_xkwds_kwds, kwds)
866 _g = kwds.get
867 return dict((n, _g(n, v)) for n, v in names_defaults.items())
870def _xkwds_not(*args, **kwds):
871 '''(INTERNAL) Return C{kwds} with a value not in C{args}.
872 '''
873 return dict((n, v) for n, v in kwds.items() if v not in args)
876def _xkwds_pop(kwds, **name_default):
877 '''(INTERNAL) Pop an item by C{name} from C{kwds} and
878 return its value, otherwise return the C{default}.
879 '''
880 if isinstance(kwds, dict) and len(name_default) == 1:
881 for n, v in name_default.items():
882 return kwds.pop(n, v)
883 raise _xAssertionError(_xkwds_pop, kwds, **name_default)
886def _xkwds_pop2(kwds, **name_default):
887 '''(INTERNAL) Pop a C{kwds} item by C{name} and return the value and
888 reduced C{kwds} copy, otherwise the C{default} and original C{kwds}.
889 '''
890 if isinstance(kwds, dict) and len(name_default) == 1:
891 for n, v in name_default.items():
892 if n in kwds:
893 kwds = _copy(kwds)
894 v = kwds.pop(n, v)
895 return v, kwds
896 raise _xAssertionError(_xkwds_pop2, kwds, **name_default)
899def _Xorder(_Coeffs, Error, **Xorder): # in .auxLat, .ktm, .rhumb.bases, .rhumb.ekx
900 '''(INTERNAL) Validate C{RAorder} or C{TMorder}.
901 '''
902 X, m = _xkwds_item2(Xorder)
903 if m in _Coeffs and _basics.isint(m):
904 return m
905 t = sorted(map(str, _Coeffs.keys()))
906 raise Error(X, m, txt_not_=_or(*t))
909def _xsError(X, xs, i, x, *n, **kwds): # in .fmath, ._fstats, .fsums
910 '''(INTERNAL) Error for C{xs} or C{x}, item C{xs[i]}.
911 '''
912 def _xs(*xs):
913 if len(xs) > 4:
914 xs = xs[:3] + (_ELLIPSIS_,) + xs[-1:]
915 return xs
917 return ((_xError(X, n[0], _xs(*xs), **kwds) if n else
918 _xError(X, xs=_xs(*xs), **kwds)) if x is xs else
919 _xError(X, _streprs.Fmt.INDEX(xs=i), x, **kwds))
922def _xStrError(*Refs, **name_value_Error): # in .gars, .geohash, .wgrs
923 '''(INTERNAL) Create a C{TypeError} for C{Garef}, C{Geohash}, C{Wgrs}.
924 '''
925 s = typename(_MODS.unitsBase.Str)
926 t = tuple(map(typename, Refs)) + (s, _LatLon_, 'LatLon*Tuple')
927 return _IsnotError(*t, **name_value_Error)
929# **) MIT License
930#
931# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
932#
933# Permission is hereby granted, free of charge, to any person obtaining a
934# copy of this software and associated documentation files (the "Software"),
935# to deal in the Software without restriction, including without limitation
936# the rights to use, copy, modify, merge, publish, distribute, sublicense,
937# and/or sell copies of the Software, and to permit persons to whom the
938# Software is furnished to do so, subject to the following conditions:
939#
940# The above copyright notice and this permission notice shall be included
941# in all copies or substantial portions of the Software.
942#
943# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
944# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
945# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
946# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
947# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
948# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
949# OTHER DEALINGS IN THE SOFTWARE.