Coverage for pygeodesy/errors.py: 93%
268 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-02-05 16:22 -0500
« prev ^ index » next coverage.py v7.2.2, created at 2024-02-05 16:22 -0500
2# -*- coding: utf-8 -*-
4u'''Errors, exceptions, exception formatting and exception chaining.
6Error, exception classes and functions to format PyGeodesy errors,
7including the setting of I{exception chaining} in Python 3+.
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-empyty string I{OR} set env variable
12C{PYGEODESY_EXCEPTION_CHAINING=std} or to any non-empty string.
13'''
15from pygeodesy.interns import MISSING, NN, _a_, _an_, _and_, _clip_, \
16 _COLON_, _COLONSPACE_, _COMMASPACE_, _datum_, \
17 _ellipsoidal_, _EQUAL_, _incompatible_, _invalid_, \
18 _len_, _not_, _or_, _SPACE_, _specified_, _UNDER_, \
19 _vs_, _with_
20from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _getenv, _PYTHON_X_DEV
22__all__ = _ALL_LAZY.errors # _ALL_DOCS('_InvalidError', '_IsnotError') _under
23__version__ = '24.01.02'
25_box_ = 'box'
26_limiterrors = True # in .formy
27_name_value_ = repr('name=value')
28_rangerrors = True # in .dms
29_region_ = 'region'
30_vs__ = _SPACE_(NN, _vs_, NN)
32try:
33 _exception_chaining = None # not available
34 _ = Exception().__cause__ # Python 3+ exception chaining
36 if _PYTHON_X_DEV or _getenv('PYGEODESY_EXCEPTION_CHAINING', NN): # == _std_
37 _exception_chaining = True # turned on, std
38 raise AttributeError # allow exception chaining
40 _exception_chaining = False # turned off
42 def _error_cause(inst, cause=None):
43 '''(INTERNAL) Set or avoid Python 3+ exception chaining.
45 Setting C{inst.__cause__ = None} is equivalent to syntax
46 C{raise Error(...) from None} to avoid exception chaining.
48 @arg inst: An error instance (I{caught} C{Exception}).
49 @kwarg cause: A previous error instance (I{caught} C{Exception})
50 or C{None} to avoid exception chaining.
52 @see: Alex Martelli, et.al., "Python in a Nutshell", 3rd Ed., page 163,
53 O'Reilly, 2017, U{PEP-3134<https://www.Python.org/dev/peps/pep-3134>},
54 U{here<https://StackOverflow.com/questions/17091520/how-can-i-more-
55 easily-suppress-previous-exceptions-when-i-raise-my-own-exception>}
56 and U{here<https://StackOverflow.com/questions/1350671/
57 inner-exception-with-traceback-in-python>}.
58 '''
59 inst.__cause__ = cause # None, no exception chaining
60 return inst
62except AttributeError: # Python 2+
64 def _error_cause(inst, **unused): # PYCHOK expected
65 return inst # no-op
68class _AssertionError(AssertionError):
69 '''(INTERNAL) Format an C{AssertionError} with/-out exception chaining.
70 '''
71 def __init__(self, *args, **kwds):
72 _error_init(AssertionError, self, args, **kwds)
75class _AttributeError(AttributeError):
76 '''(INTERNAL) Format an C{AttributeError} with/-out exception chaining.
77 '''
78 def __init__(self, *args, **kwds):
79 _error_init(AttributeError, self, args, **kwds)
82class _ImportError(ImportError):
83 '''(INTERNAL) Format an C{ImportError} with/-out exception chaining.
84 '''
85 def __init__(self, *args, **kwds):
86 _error_init(ImportError, self, args, **kwds)
89class _IndexError(IndexError):
90 '''(INTERNAL) Format an C{IndexError} with/-out exception chaining.
91 '''
92 def __init__(self, *args, **kwds):
93 _error_init(IndexError, self, args, **kwds)
96class _KeyError(KeyError):
97 '''(INTERNAL) Format a C{KeyError} with/-out exception chaining.
98 '''
99 def __init__(self, *args, **kwds): # txt=_invalid_
100 _error_init(KeyError, self, args, **kwds)
103class _NameError(NameError):
104 '''(INTERNAL) Format a C{NameError} with/-out exception chaining.
105 '''
106 def __init__(self, *args, **kwds):
107 _error_init(NameError, self, args, **kwds)
110class _NotImplementedError(NotImplementedError):
111 '''(INTERNAL) Format a C{NotImplementedError} with/-out exception chaining.
112 '''
113 def __init__(self, *args, **kwds):
114 _error_init(NotImplementedError, self, args, **kwds)
117class _OverflowError(OverflowError):
118 '''(INTERNAL) Format an C{OverflowError} with/-out exception chaining.
119 '''
120 def __init__(self, *args, **kwds): # txt=_invalid_
121 _error_init(OverflowError, self, args, **kwds)
124class _TypeError(TypeError):
125 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining.
126 '''
127 def __init__(self, *args, **kwds):
128 _error_init(TypeError, self, args, fmt_name_value='type(%s) (%r)', **kwds)
131def _TypesError(name, value, *Types, **kwds):
132 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining.
133 '''
134 # no longer C{class _TypesError} to avoid missing value
135 # argument errors in _XError line ...E = Error(str(e))
136 t = _not_(_an(_or(*(t.__name__ for t in Types))))
137 return _TypeError(name, value, txt=t, **kwds)
140class _ValueError(ValueError):
141 '''(INTERNAL) Format a C{ValueError} with/-out exception chaining.
142 '''
143 def __init__(self, *args, **kwds): # ..., cause=None, txt=_invalid_, ...
144 _error_init(ValueError, self, args, **kwds)
147class _ZeroDivisionError(ZeroDivisionError):
148 '''(INTERNAL) Format a C{ZeroDivisionError} with/-out exception chaining.
149 '''
150 def __init__(self, *args, **kwds):
151 _error_init(ZeroDivisionError, self, args, **kwds)
154class AuxError(_ValueError):
155 '''Error raised for a L{rhumb.aux_}, C{Aux}, C{AuxDLat} or C{AuxLat} issue.
156 '''
157 pass
160class ClipError(_ValueError):
161 '''Clip box or clip region issue.
162 '''
163 def __init__(self, *name_n_corners, **txt_cause):
164 '''New L{ClipError}.
166 @arg name_n_corners: Either just a name (C{str}) or
167 name, number, corners (C{str},
168 C{int}, C{tuple}).
169 @kwarg txt_cause: Optional C{B{txt}=str} explanation
170 of the error and C{B{cause}=None}
171 for exception chaining.
172 '''
173 if len(name_n_corners) == 3:
174 t, n, v = name_n_corners
175 n = _SPACE_(t, _clip_, (_box_ if n == 2 else _region_))
176 name_n_corners = n, v
177 _ValueError.__init__(self, *name_n_corners, **txt_cause)
180class CrossError(_ValueError):
181 '''Error raised for zero or near-zero vectorial cross products,
182 occurring for coincident or colinear points, lines or bearings.
183 '''
184 pass
187class GeodesicError(_ValueError):
188 '''Error raised for lack of convergence or other issues in L{pygeodesy.geodesicx},
189 L{pygeodesy.geodesicw} or L{pygeodesy.karney}.
190 '''
191 pass
194class IntersectionError(_ValueError): # in .ellipsoidalBaseDI, .formy, ...
195 '''Error raised for line or circle intersection issues.
196 '''
197 def __init__(self, *args, **kwds):
198 '''New L{IntersectionError}.
199 '''
200 if args:
201 _ValueError.__init__(self, _SPACE_(*args), **kwds)
202 else:
203 _ValueError.__init__(self, **kwds)
206class LenError(_ValueError): # in .ecef, .fmath, .heights, .iters, .named
207 '''Error raised for mis-matching C{len} values.
208 '''
209 def __init__(self, where, **lens_txt): # txt=None
210 '''New L{LenError}.
212 @arg where: Object with C{.__name__} attribute
213 (C{class}, C{method}, or C{function}).
214 @kwarg lens_txt: Two or more C{name=len(name)} pairs
215 (C{keyword arguments}).
216 '''
217 def _ns_vs_txt_x(cause=None, txt=_invalid_, **kwds):
218 ns, vs = zip(*itemsorted(kwds)) # unzip
219 return ns, vs, txt, cause
221 ns, vs, txt, x = _ns_vs_txt_x(**lens_txt)
222 ns = _COMMASPACE_.join(ns)
223 t = _MODS.streprs.Fmt.PAREN(where.__name__, ns)
224 vs = _vs__.join(map(str, vs))
225 t = _SPACE_(t, _len_, vs)
226 _ValueError.__init__(self, t, txt=txt, cause=x)
229class LimitError(_ValueError):
230 '''Error raised for lat- or longitudinal values or deltas exceeding
231 the given B{C{limit}} in functions L{pygeodesy.equirectangular},
232 L{pygeodesy.equirectangular_}, C{nearestOn*} and C{simplify*}
233 or methods with C{limit} or C{options} keyword arguments.
235 @see: Subclass L{UnitError}.
236 '''
237 pass
240class MGRSError(_ValueError):
241 '''Military Grid Reference System (MGRS) parse or other L{Mgrs} issue.
242 '''
243 pass
246class NumPyError(_ValueError):
247 '''Error raised for C{NumPy} issues.
248 '''
249 pass
252class ParseError(_ValueError): # in .dms, .elevations, .utmupsBase
253 '''Error parsing degrees, radians or several other formats.
254 '''
255 pass
258class PointsError(_ValueError): # in .clipy, .frechet, ...
259 '''Error for an insufficient number of points.
260 '''
261 pass
264class RangeError(_ValueError):
265 '''Error raised for lat- or longitude values outside the B{C{clip}},
266 B{C{clipLat}}, B{C{clipLon}} in functions L{pygeodesy.parse3llh},
267 L{pygeodesy.parseDMS}, L{pygeodesy.parseDMS2} and L{pygeodesy.parseRad}
268 or the given B{C{limit}} in functions L{pygeodesy.clipDegrees} and
269 L{pygeodesy.clipRadians}.
271 @see: Function L{pygeodesy.rangerrors}.
272 '''
273 pass
276class RhumbError(_ValueError):
277 '''Error raised for a L{pygeodesy.rhumb.aux_}, L{pygeodesy.rhumb.ekx}
278 or L{pygeodesy.rhumb.solve} issue.
279 '''
280 pass
283class TriangleError(_ValueError): # in .resections, .vector2d
284 '''Error raised for triangle, inter- or resection issues.
285 '''
286 pass
289class SciPyError(PointsError):
290 '''Error raised for C{SciPy} issues.
291 '''
292 pass
295class SciPyWarning(PointsError):
296 '''Error thrown for C{SciPy} warnings.
298 To raise C{SciPy} warnings as L{SciPyWarning} exceptions, Python
299 C{warnings} must be filtered as U{warnings.filterwarnings('error')
300 <https://docs.Python.org/3/library/warnings.html#the-warnings-filter>}
301 I{prior to} C{import scipy} OR by setting env var U{PYTHONWARNINGS
302 <https://docs.Python.org/3/using/cmdline.html#envvar-PYTHONWARNINGS>}
303 OR by invoking C{python} with command line option U{-W<https://docs.
304 Python.org/3/using/cmdline.html#cmdoption-w>} set to C{-W error}.
305 '''
306 pass
309class TRFError(_ValueError): # in .ellipsoidalBase, .trf, .units
310 '''Terrestrial Reference Frame (TRF), L{Epoch}, L{RefFrame}
311 or L{RefFrame} conversion issue.
312 '''
313 pass
316class UnitError(LimitError): # in .named, .units
317 '''Default exception for L{units} issues for a value exceeding the
318 C{low} or C{high} limit.
319 '''
320 pass
323class VectorError(_ValueError): # in .nvectorBase, .vector3d, .vector3dBase
324 '''L{Vector3d}, C{Cartesian*} or C{*Nvector} issues.
325 '''
326 pass
329def _an(noun):
330 '''(INTERNAL) Prepend an article to a noun based
331 on the pronounciation of the first letter.
332 '''
333 a = _an_ if noun[:1].lower() in 'aeinoux' else _a_
334 return _SPACE_(a, noun)
337def _and(*words):
338 '''(INTERNAL) Join C{words} with C{", "} and C{" and "}.
339 '''
340 return _and_or(_and_, *words)
343def _and_or(last, *words):
344 '''(INTERNAL) Join C{words} with C{", "} and C{B{last}}.
345 '''
346 t, w = NN, list(words)
347 if w:
348 t = w.pop()
349 if w:
350 w = _COMMASPACE_.join(w)
351 t = _SPACE_(w, last, t)
352 return t
355def crosserrors(raiser=None):
356 '''Report or ignore vectorial cross product errors.
358 @kwarg raiser: Use C{True} to throw or C{False} to ignore
359 L{CrossError} exceptions. Use C{None} to
360 leave the setting unchanged.
362 @return: Previous setting (C{bool}).
364 @see: Property C{Vector3d[Base].crosserrors}.
365 '''
366 B = _MODS.vector3dBase.Vector3dBase
367 t = B._crosserrors # XXX class attr!
368 if raiser in (True, False):
369 B._crosserrors = raiser
370 return t
373def _error_init(Error, inst, args, fmt_name_value='%s (%r)', txt=NN,
374 cause=None, **kwds): # by .lazily
375 '''(INTERNAL) Format an error text and initialize an C{Error} instance.
377 @arg Error: The error super-class (C{Exception}).
378 @arg inst: Sub-class instance to be __init__-ed (C{_Exception}).
379 @arg args: Either just a value or several name, value, ...
380 positional arguments (C{str}, any C{type}), in
381 particular for name conflicts with keyword
382 arguments of C{error_init} or which can't be
383 given as C{name=value} keyword arguments.
384 @kwarg fmt_name_value: Format for (name, value) (C{str}).
385 @kwarg txt: Optional explanation of the error (C{str}).
386 @kwarg cause: Optional, caught error (L{Exception}), for
387 exception chaining (supported in Python 3+).
388 @kwarg kwds: Additional C{B{name}=value} pairs, if any.
389 '''
390 def _fmtuple(pairs):
391 return tuple(fmt_name_value % t for t in pairs)
393 t, n = (), len(args)
394 if n > 2:
395 s = _MODS.basics.isodd(n)
396 t = _fmtuple(zip(args[0::2], args[1::2]))
397 if s: # XXX _xzip(..., strict=s)
398 t += args[-1:]
399 elif n == 2:
400 t = (fmt_name_value % args),
401 elif n: # == 1
402 t = str(args[0]),
404 if kwds:
405 t += _fmtuple(itemsorted(kwds))
406 t = _or(*t) if t else _SPACE_(_name_value_, MISSING)
408 if txt is not None:
409 x = str(txt) or (str(cause) if cause else _invalid_)
410 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_
411 t = C(t, x)
412# else: # LenError, _xzip, .dms, .heights, .vector2d
413# x = NN # XXX or t?
414 Error.__init__(inst, t)
415# inst.__x_txt__ = x # hold explanation
416 _error_cause(inst, cause=cause if _exception_chaining else None)
417 _error_under(inst)
420def _error_under(inst):
421 '''(INTERNAL) Remove leading underscore from instance' class name.
422 '''
423 n = inst.__class__.__name__
424 if n.startswith(_UNDER_):
425 inst.__class__.__name__ = n.lstrip(_UNDER_)
426 return inst
429def exception_chaining(error=None):
430 '''Get an error's I{cause} or the exception chaining setting.
432 @kwarg error: An error instance (C{Exception}) or C{None}.
434 @return: If C{B{error} is None}, return C{True} if exception
435 chaining is enabled for PyGeodesy errors, C{False}
436 if turned off and C{None} if not available. If
437 B{C{error}} is not C{None}, return it's error
438 I{cause} or C{None}.
440 @note: To enable exception chaining for C{pygeodesy} errors,
441 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any
442 non-empty value prior to C{import pygeodesy}.
443 '''
444 return _exception_chaining if error is None else \
445 getattr(error, '__cause__', None)
448def _incompatible(this):
449 '''(INTERNAL) Format an C{"incompatible with ..."} text.
450 '''
451 return _SPACE_(_incompatible_, _with_, this)
454def _InvalidError(Error=_ValueError, **txt_name_values_cause): # txt=_invalid_, name=value [, ...]
455 '''(INTERNAL) Create an C{Error} instance.
457 @kwarg Error: The error class or sub-class (C{Exception}).
458 @kwarg txt_name_values: One or more C{B{name}=value} pairs
459 and optionally, keyword argument C{B{txt}=str}
460 to override the default C{B{txt}='invalid'} and
461 C{B{cause}=None} for exception chaining.
463 @return: An B{C{Error}} instance.
464 '''
465 return _XError(Error, **txt_name_values_cause)
468def isError(obj):
469 '''Check a (caught) exception.
471 @arg obj: The exception C({Exception}).
473 @return: C{True} if B{C{obj}} is a C{pygeodesy} error,
474 C{False} if B{C{obj}} is a standard Python error
475 of C{None} if neither.
476 '''
477 return True if isinstance(obj, _XErrors) else (
478 False if isinstance(obj, Exception) else None)
481def _IsnotError(*nouns, **name_value_Error_cause): # name=value [, Error=TypeError, cause=None]
482 '''Create a C{TypeError} for an invalid C{name=value} type.
484 @arg nouns: One or more expected class or type names, usually nouns (C{str}).
485 @kwarg name_value_Error_cause: One C{B{name}=value} pair and optionally,
486 keyword argument C{B{Error}=TypeError} to override the default
487 and C{B{cause}=None} for exception chaining.
489 @return: A C{TypeError} or an B{C{Error}} instance.
490 '''
491 x, Error = _xkwds_pop_(name_value_Error_cause, cause=None, Error=TypeError)
492 n, v = _xkwds_popitem(name_value_Error_cause) if name_value_Error_cause \
493 else (_name_value_, MISSING) # XXX else tuple(...)
494 n = _MODS.streprs.Fmt.PARENSPACED(n, repr(v))
495 t = _not_(_an(_or(*nouns)) if nouns else _specified_)
496 return _XError(Error, n, txt=t, cause=x)
499def itemsorted(adict, *args, **asorted_reverse):
500 '''Return the items of C{B{adict}} sorted I{alphabetically, case-insensitively}
501 and in I{ascending} order.
503 @arg args: Optional argument(s) for method C{B{adict}.items(B*{args})}.
504 @kwarg asorted_reverse: Use keyword argument C{B{asorted}=False} for
505 I{case-sensitive} sorting and C{B{reverse}=True} for
506 results in C{descending} order.
507 '''
508 def _un(item):
509 return item[0].lower()
511 # see .rhumb.Rhumb and ._RhumbLine
512 a, r = _xkwds_get_(asorted_reverse, asorted=True, reverse=False) \
513 if asorted_reverse else (True, False)
514 items = adict.items(*args) if args else adict.items()
515 return sorted(items, reverse=r, key=_un if a else None)
518def limiterrors(raiser=None):
519 '''Get/set the throwing of L{LimitError}s.
521 @kwarg raiser: Choose C{True} to raise or C{False} to
522 ignore L{LimitError} exceptions. Use
523 C{None} to leave the setting unchanged.
525 @return: Previous setting (C{bool}).
526 '''
527 global _limiterrors
528 t = _limiterrors
529 if raiser in (True, False):
530 _limiterrors = raiser
531 return t
534def _or(*words):
535 '''(INTERNAL) Join C{words} with C{", "} and C{" or "}.
536 '''
537 return _and_or(_or_, *words)
540def _parseX(parser, *args, **name_values_Error): # name=value[, ..., Error=ParseError]
541 '''(INTERNAL) Invoke a parser and handle exceptions.
543 @arg parser: The parser (C{callable}).
544 @arg args: Any B{C{parser}} arguments (any C{type}s).
545 @kwarg name_values_Error: Any C{B{name}=value} pairs and
546 optionally, C{B{Error}=ParseError} keyword
547 argument to override the default.
549 @return: Parser result.
551 @raise ParseError: Or the specified C{B{Error}}.
552 '''
553 try:
554 return parser(*args)
555 except Exception as x:
556 E = _xkwds_pop(name_values_Error, Error=type(x) if isError(x) else
557 ParseError)
558 raise _XError(E, **_xkwds(name_values_Error, cause=x))
561def rangerrors(raiser=None):
562 '''Get/set the throwing of L{RangeError}s.
564 @kwarg raiser: Choose C{True} to raise or C{False} to ignore
565 L{RangeError} exceptions. Use C{None} to leave
566 the setting unchanged.
568 @return: Previous setting (C{bool}).
569 '''
570 global _rangerrors
571 t = _rangerrors
572 if raiser in (True, False):
573 _rangerrors = raiser
574 return t
577def _SciPyIssue(x, *extras): # PYCHOK no cover
578 if isinstance(x, (RuntimeWarning, UserWarning)):
579 Error = SciPyWarning
580 else:
581 Error = SciPyError # PYCHOK not really
582 t = _SPACE_(str(x).strip(), *extras)
583 return Error(t, cause=x)
586def _xAssertionError(where, *args, **kwds): # in .basics
587 '''(INTERNAL) Embellish an C{AssertionError}.
588 '''
589 t = _MODS.streprs.unstr(where, *args, **kwds)
590 return _AssertionError(t)
593def _xattr(obj, **name_default): # see .strerprs._xattrs
594 '''(INTERNAL) Get an C{obj}'s attribute by C{name}.
595 '''
596 if len(name_default) == 1:
597 for n, d in name_default.items():
598 return getattr(obj, n, d)
599 raise _xAssertionError(_xattr, obj, **name_default)
602def _xdatum(datum1, datum2, Error=None):
603 '''(INTERNAL) Check for datum, ellipsoid or rhumb mis-match.
604 '''
605 if Error:
606 E1, E2 = datum1.ellipsoid, datum2.ellipsoid
607 if E1 != E2:
608 raise Error(E2.named2, txt=_incompatible(E1.named2))
609 elif datum1 != datum2:
610 t = _SPACE_(_datum_, repr(datum1.name),
611 _not_, repr(datum2.name))
612 raise _AssertionError(t)
615def _xellipsoidal(**name_value):
616 '''(INTERNAL) Check an I{ellipsoidal} item.
618 @return: The B{C{value}} if ellipsoidal.
620 @raise TypeError: Not ellipsoidal B{C{value}}.
621 '''
622 try:
623 for n, v in name_value.items():
624 if v.isEllipsoidal:
625 return v
626 break
627 else:
628 n = v = MISSING
629 except AttributeError:
630 pass
631 raise _TypeError(n, v, txt=_not_(_ellipsoidal_))
634def _XError(Error, *args, **kwds):
635 '''(INTERNAL) Format an C{Error} or C{_Error}.
636 '''
637 try: # C{_Error} style
638 return Error(*args, **kwds)
639 except TypeError: # no keyword arguments
640 pass
641 e = _ValueError(*args, **kwds)
642 E = Error(str(e))
643 if _exception_chaining:
644 _error_cause(E, cause=e.__cause__) # PYCHOK OK
645 return E
648_XErrors = _TypeError, _ValueError
649# map certain C{Exception} classes to the C{_Error}
650_X2Error = {AssertionError: _AssertionError,
651 AttributeError: _AttributeError,
652 ImportError: _ImportError,
653 IndexError: _IndexError,
654 KeyError: _KeyError,
655 NameError: _NameError,
656 NotImplementedError: _NotImplementedError,
657 OverflowError: _OverflowError,
658 TypeError: _TypeError,
659 ValueError: _ValueError,
660 ZeroDivisionError: _ZeroDivisionError}
663def _xError(x, *args, **kwds):
664 '''(INTERNAL) Embellish a (caught) exception.
666 @arg x: The exception (usually, C{_Error}).
667 @arg args: Embelishments (C{any}).
668 @kwarg kwds: Embelishments (C{any}).
669 '''
670 return _XError(type(x), *args, **_xkwds(kwds, cause=x))
673def _xError2(x): # in .fsums
674 '''(INTERNAL) Map an exception to 2-tuple (C{_Error} class, error C{txt}).
676 @arg x: The exception instance (usually, C{Exception}).
677 '''
678 X = type(x)
679 E = _X2Error.get(X, X)
680 if E is X and not isError(x):
681 E = _NotImplementedError
682 t = repr(x)
683 else:
684 t = str(x)
685 return E, t
688try:
689 _ = {}.__or__ # {} | {} # Python 3.9+
691 def _xkwds(kwds, **dflts):
692 '''(INTERNAL) Override C{dflts} with specified C{kwds}.
693 '''
694 return (dflts | kwds) if kwds else dflts
696except AttributeError:
697 from copy import copy as _copy
699 def _xkwds(kwds, **dflts): # PYCHOK expected
700 '''(INTERNAL) Override C{dflts} with specified C{kwds}.
701 '''
702 d = dflts
703 if kwds:
704 d = _copy(d)
705 d.update(kwds)
706 return d
709def _xkwds_bool(inst, **kwds): # in .frechet, .hausdorff, .heights
710 '''(INTERNAL) Set applicable C{bool} properties/attributes.
711 '''
712 for n, v in kwds.items():
713 b = getattr(inst, n, None)
714 if b is None: # invalid bool attr
715 t = _SPACE_(_EQUAL_(n, repr(v)), 'for', inst.__class__.__name__) # XXX .classname
716 raise _AttributeError(t, txt=_not_('applicable'))
717 if v in (False, True) and v != b:
718 setattr(inst, NN(_UNDER_, n), v)
721def _xkwds_get(kwds, **name_default):
722 '''(INTERNAL) Get a C{kwds} value by C{name}, or the C{default}.
723 '''
724 if len(name_default) == 1:
725 for n, d in name_default.items():
726 return kwds.get(n, d)
727 raise _xAssertionError(_xkwds_get, kwds, **name_default)
730def _xkwds_get_(kwds, **names_defaults):
731 '''(INTERNAL) Yield each C{kwds} value or its C{default}
732 in I{case-insensitive, alphabetical} order.
733 '''
734 for n, d in itemsorted(names_defaults):
735 yield kwds.get(n, d)
738def _xkwds_not(*args, **kwds):
739 '''(INTERNAL) Return C{kwds} with a value not in C{args}.
740 '''
741 return dict((n, v) for n, v in kwds.items() if v not in args)
744def _xkwds_pop(kwds, **name_default):
745 '''(INTERNAL) Pop a C{kwds} value by C{name}, or the C{default}.
746 '''
747 if len(name_default) == 1:
748 for n, d in name_default.items():
749 return kwds.pop(n, d)
750 raise _xAssertionError(_xkwds_pop, kwds, **name_default)
753def _xkwds_pop_(kwds, **names_defaults):
754 '''(INTERNAL) Pop and yield each C{kwds} value or its C{default}
755 in I{case-insensitive, alphabetical} order.
756 '''
757 for n, d in itemsorted(names_defaults):
758 yield kwds.pop(n, d)
761def _xkwds_popitem(name_value):
762 '''(INTERNAL) Return exactly one C{(name, value)} item.
763 '''
764 if len(name_value) == 1: # XXX TypeError
765 return name_value.popitem() # XXX AttributeError
766 raise _xAssertionError(_xkwds_popitem, name_value)
769def _Xorder(_Coeffs, Error, **Xorder): # in .ktm, .rhumbBase
770 '''(INTERNAL) Validate C{RAorder} or C{TMorder}.
771 '''
772 X, m = Xorder.popitem()
773 if m in _Coeffs and _MODS.basics.isint(m):
774 return m
775 t = sorted(map(str, _Coeffs.keys()))
776 raise Error(X, m, txt=_not_(_or(*t)))
779def _xzip(*args, **strict): # PYCHOK no cover
780 '''(INTERNAL) Standard C{zip(..., strict=True)}.
781 '''
782 s = _xkwds_get(strict, strict=True)
783 if s:
784 _zip = _MODS.basics._zip
785 if _zip is zip: # < (3, 10)
786 t = _MODS.streprs.unstr(_xzip.__name__, *args, strict=s)
787 raise _NotImplementedError(t, txt=None)
788 return _zip(*args)
789 return zip(*args)
791# **) MIT License
792#
793# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
794#
795# Permission is hereby granted, free of charge, to any person obtaining a
796# copy of this software and associated documentation files (the "Software"),
797# to deal in the Software without restriction, including without limitation
798# the rights to use, copy, modify, merge, publish, distribute, sublicense,
799# and/or sell copies of the Software, and to permit persons to whom the
800# Software is furnished to do so, subject to the following conditions:
801#
802# The above copyright notice and this permission notice shall be included
803# in all copies or substantial portions of the Software.
804#
805# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
806# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
807# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
808# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
809# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
810# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
811# OTHER DEALINGS IN THE SOFTWARE.