Coverage for pygeodesy/errors.py: 93%
275 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-09-20 13:43 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-09-20 13:43 -0400
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_, _name_, _no_, _not_, _or_, _SPACE_, \
19 _specified_, _UNDER_, _value_, _vs_, _with_
20from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _getenv, \
21 _pairs, _PYTHON_X_DEV
23__all__ = _ALL_LAZY.errors # _ALL_DOCS('_InvalidError', '_IsnotError') _under
24__version__ = '23.08.30'
26_box_ = 'box'
27_default_ = 'default'
28_kwargs_ = 'kwargs' # XXX _kwds_?
29_limiterrors = True # imported by .formy
30_multiple_ = 'multiple'
31_name_value_ = repr('name=value')
32_rangerrors = True # imported by .dms
33_region_ = 'region'
34_vs__ = _SPACE_(NN, _vs_, NN)
36try:
37 _exception_chaining = None # not available
38 _ = Exception().__cause__ # Python 3+ exception chaining
40 if _PYTHON_X_DEV or _getenv('PYGEODESY_EXCEPTION_CHAINING', NN): # == _std_
41 _exception_chaining = True # turned on, std
42 raise AttributeError # allow exception chaining
44 _exception_chaining = False # turned off
46 def _error_cause(inst, cause=None):
47 '''(INTERNAL) Set or avoid Python 3+ exception chaining.
49 Setting C{inst.__cause__ = None} is equivalent to syntax
50 C{raise Error(...) from None} to avoid exception chaining.
52 @arg inst: An error instance (I{caught} C{Exception}).
53 @kwarg cause: A previous error instance (I{caught} C{Exception})
54 or C{None} to avoid exception chaining.
56 @see: Alex Martelli, et.al., "Python in a Nutshell", 3rd Ed., page 163,
57 O'Reilly, 2017, U{PEP-3134<https://www.Python.org/dev/peps/pep-3134>},
58 U{here<https://StackOverflow.com/questions/17091520/how-can-i-more-
59 easily-suppress-previous-exceptions-when-i-raise-my-own-exception>}
60 and U{here<https://StackOverflow.com/questions/1350671/
61 inner-exception-with-traceback-in-python>}.
62 '''
63 inst.__cause__ = cause # None, no exception chaining
64 return inst
66except AttributeError: # Python 2+
68 def _error_cause(inst, **unused): # PYCHOK expected
69 return inst # no-op
72class _AssertionError(AssertionError):
73 '''(INTERNAL) Format an C{AssertionError} with/-out exception chaining.
74 '''
75 def __init__(self, *args, **kwds):
76 _error_init(AssertionError, self, args, **kwds)
79class _AttributeError(AttributeError):
80 '''(INTERNAL) Format an C{AttributeError} with/-out exception chaining.
81 '''
82 def __init__(self, *args, **kwds):
83 _error_init(AttributeError, self, args, **kwds)
86class _ImportError(ImportError):
87 '''(INTERNAL) Format an C{ImportError} with/-out exception chaining.
88 '''
89 def __init__(self, *args, **kwds):
90 _error_init(ImportError, self, args, **kwds)
93class _IndexError(IndexError):
94 '''(INTERNAL) Format an C{IndexError} with/-out exception chaining.
95 '''
96 def __init__(self, *args, **kwds):
97 _error_init(IndexError, self, args, **kwds)
100class _KeyError(KeyError):
101 '''(INTERNAL) Format a C{KeyError} with/-out exception chaining.
102 '''
103 def __init__(self, *args, **kwds): # txt=_invalid_
104 _error_init(KeyError, self, args, **kwds)
107class _NameError(NameError):
108 '''(INTERNAL) Format a C{NameError} with/-out exception chaining.
109 '''
110 def __init__(self, *args, **kwds):
111 _error_init(NameError, self, args, **kwds)
114class _NotImplementedError(NotImplementedError):
115 '''(INTERNAL) Format a C{NotImplementedError} with/-out exception chaining.
116 '''
117 def __init__(self, *args, **kwds):
118 _error_init(NotImplementedError, self, args, **kwds)
121class _OverflowError(OverflowError):
122 '''(INTERNAL) Format an C{OverflowError} with/-out exception chaining.
123 '''
124 def __init__(self, *args, **kwds): # txt=_invalid_
125 _error_init(OverflowError, self, args, **kwds)
128class _TypeError(TypeError):
129 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining.
130 '''
131 def __init__(self, *args, **kwds):
132 _error_init(TypeError, self, args, fmt_name_value='type(%s) (%r)', **kwds)
135class _TypesError(_TypeError):
136 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining.
137 '''
138 def __init__(self, name, value, *Types, **kwds):
139 t = _not_(_an(_or(*(t.__name__ for t in Types))))
140 _TypeError.__init__(self, name, value, txt=t, **kwds)
143class _ValueError(ValueError):
144 '''(INTERNAL) Format a C{ValueError} with/-out exception chaining.
145 '''
146 def __init__(self, *args, **kwds): # ..., cause=None, txt=_invalid_, ...
147 _error_init(ValueError, self, args, **kwds)
150class _ZeroDivisionError(ZeroDivisionError):
151 '''(INTERNAL) Format a C{ZeroDivisionError} with/-out exception chaining.
152 '''
153 def __init__(self, *args, **kwds):
154 _error_init(ZeroDivisionError, self, args, **kwds)
157class AuxError(_ValueError):
158 '''Error raised for an L{rhumbaux} C{Aux}, C{AuxDLat} or C{AuxLat} issue.
159 '''
160 pass
163class ClipError(_ValueError):
164 '''Clip box or clip region issue.
165 '''
166 def __init__(self, *name_n_corners, **txt_cause):
167 '''New L{ClipError}.
169 @arg name_n_corners: Either just a name (C{str}) or
170 name, number, corners (C{str},
171 C{int}, C{tuple}).
172 @kwarg txt_cause: Optional C{B{txt}=str} explanation
173 of the error and C{B{cause}=None}
174 for exception chaining.
175 '''
176 if len(name_n_corners) == 3:
177 t, n, v = name_n_corners
178 n = _SPACE_(t, _clip_, (_box_ if n == 2 else _region_))
179 name_n_corners = n, v
180 _ValueError.__init__(self, *name_n_corners, **txt_cause)
183class CrossError(_ValueError):
184 '''Error raised for zero or near-zero vectorial cross products,
185 occurring for coincident or colinear points, lines or bearings.
186 '''
187 pass
190class GeodesicError(_ValueError):
191 '''Error raised for L{pygeodesy.geodesicx} lack of convergence
192 or other L{pygeodesy.geodesicx} or L{pygeodesy.karney} issues.
193 '''
194 pass
197class IntersectionError(_ValueError): # in .ellipsoidalBaseDI, .formy, ...
198 '''Error raised for line or circle intersection issues.
199 '''
200 def __init__(self, *args, **kwds):
201 '''New L{IntersectionError}.
202 '''
203 if args:
204 _ValueError.__init__(self, _SPACE_(*args), **kwds)
205 else:
206 _ValueError.__init__(self, **kwds)
209class LenError(_ValueError): # in .ecef, .fmath, .heights, .iters, .named
210 '''Error raised for mis-matching C{len} values.
211 '''
212 def __init__(self, where, **lens_txt): # txt=None
213 '''New L{LenError}.
215 @arg where: Object with C{.__name__} attribute
216 (C{class}, C{method}, or C{function}).
217 @kwarg lens_txt: Two or more C{name=len(name)} pairs
218 (C{keyword arguments}).
219 '''
220 def _ns_vs_txt_x(cause=None, txt=_invalid_, **kwds):
221 ns, vs = zip(*itemsorted(kwds)) # unzip
222 return ns, vs, txt, cause
224 ns, vs, txt, x = _ns_vs_txt_x(**lens_txt)
225 ns = _COMMASPACE_.join(ns)
226 t = _MODS.streprs.Fmt.PAREN(where.__name__, ns)
227 vs = _vs__.join(map(str, vs))
228 t = _SPACE_(t, _len_, vs)
229 _ValueError.__init__(self, t, txt=txt, cause=x)
232class LimitError(_ValueError):
233 '''Error raised for lat- or longitudinal values or deltas exceeding
234 the given B{C{limit}} in functions L{pygeodesy.equirectangular},
235 L{pygeodesy.equirectangular_}, C{nearestOn*} and C{simplify*}
236 or methods with C{limit} or C{options} keyword arguments.
238 @see: Subclass L{UnitError}.
239 '''
240 pass
243class MGRSError(_ValueError):
244 '''Military Grid Reference System (MGRS) parse or other L{Mgrs} issue.
245 '''
246 pass
249class NumPyError(_ValueError):
250 '''Error raised for C{NumPy} issues.
251 '''
252 pass
255class ParseError(_ValueError): # in .dms, .elevations, .utmupsBase
256 '''Error parsing degrees, radians or several other formats.
257 '''
258 pass
261class PointsError(_ValueError): # in .clipy, .frechet, ...
262 '''Error for an insufficient number of points.
263 '''
264 pass
267class RangeError(_ValueError):
268 '''Error raised for lat- or longitude values outside the B{C{clip}},
269 B{C{clipLat}}, B{C{clipLon}} in functions L{pygeodesy.parse3llh},
270 L{pygeodesy.parseDMS}, L{pygeodesy.parseDMS2} and L{pygeodesy.parseRad}
271 or the given B{C{limit}} in functions L{pygeodesy.clipDegrees} and
272 L{pygeodesy.clipRadians}.
274 @see: Function L{pygeodesy.rangerrors}.
275 '''
276 pass
279class RhumbError(_ValueError):
280 '''Error raised for L{pygeodesy.rhumbaux}, L{pygeodesy.rhumbsolve}
281 or L{pygeodesy.rhumbx} issues.
282 '''
283 pass
286class TriangleError(_ValueError): # in .resections, .vector2d
287 '''Error raised for triangle, inter- or resection issues.
288 '''
289 pass
292class SciPyError(PointsError):
293 '''Error raised for C{SciPy} issues.
294 '''
295 pass
298class SciPyWarning(PointsError):
299 '''Error thrown for C{SciPy} warnings.
301 To raise C{SciPy} warnings as L{SciPyWarning} exceptions, Python
302 C{warnings} must be filtered as U{warnings.filterwarnings('error')
303 <https://docs.Python.org/3/library/warnings.html#the-warnings-filter>}
304 I{prior to} C{import scipy} OR by setting env var U{PYTHONWARNINGS
305 <https://docs.Python.org/3/using/cmdline.html#envvar-PYTHONWARNINGS>}
306 OR by invoking C{python} with command line option U{-W<https://docs.
307 Python.org/3/using/cmdline.html#cmdoption-w>} set to C{-W error}.
308 '''
309 pass
312class TRFError(_ValueError): # in .ellipsoidalBase, .trf, .units
313 '''Terrestrial Reference Frame (TRF), L{Epoch}, L{RefFrame}
314 or L{RefFrame} conversion issue.
315 '''
316 pass
319class UnitError(LimitError): # in .named, .units
320 '''Default exception for L{units} issues for a value exceeding the
321 C{low} or C{high} limit.
322 '''
323 pass
326class VectorError(_ValueError): # in .nvectorBase, .vector3d, .vector3dBase
327 '''L{Vector3d}, C{Cartesian*} or C{*Nvector} issues.
328 '''
329 pass
332def _an(noun):
333 '''(INTERNAL) Prepend an article to a noun based
334 on the pronounciation of the first letter.
335 '''
336 a = _an_ if noun[:1].lower() in 'aeinoux' else _a_
337 return _SPACE_(a, noun)
340def _and(*words):
341 '''(INTERNAL) Join C{words} with C{", "} and C{" and "}.
342 '''
343 return _and_or(_and_, *words)
346def _and_or(last, *words):
347 '''(INTERNAL) Join C{words} with C{", "} and C{B{last}}.
348 '''
349 t, w = NN, list(words)
350 if w:
351 t = w.pop()
352 if w:
353 w = _COMMASPACE_.join(w)
354 t = _SPACE_(w, last, t)
355 return t
358def crosserrors(raiser=None):
359 '''Report or ignore vectorial cross product errors.
361 @kwarg raiser: Use C{True} to throw or C{False} to ignore
362 L{CrossError} exceptions. Use C{None} to
363 leave the setting unchanged.
365 @return: Previous setting (C{bool}).
367 @see: Property C{Vector3d[Base].crosserrors}.
368 '''
369 B = _MODS.vector3dBase.Vector3dBase
370 t = B._crosserrors # XXX class attr!
371 if raiser in (True, False):
372 B._crosserrors = raiser
373 return t
376def _error_init(Error, inst, args, fmt_name_value='%s (%r)', txt=NN,
377 cause=None, **kwds): # by .lazily
378 '''(INTERNAL) Format an error text and initialize an C{Error} instance.
380 @arg Error: The error super-class (C{Exception}).
381 @arg inst: Sub-class instance to be __init__-ed (C{_Exception}).
382 @arg args: Either just a value or several name, value, ...
383 positional arguments (C{str}, any C{type}), in
384 particular for name conflicts with keyword
385 arguments of C{error_init} or which can't be
386 given as C{name=value} keyword arguments.
387 @kwarg fmt_name_value: Format for (name, value) (C{str}).
388 @kwarg txt: Optional explanation of the error (C{str}).
389 @kwarg cause: Optional, caught error (L{Exception}), for
390 exception chaining (supported in Python 3+).
391 @kwarg kwds: Additional C{B{name}=value} pairs, if any.
392 '''
393 def _fmtuple(pairs):
394 return tuple(fmt_name_value % t for t in pairs)
396 t, n = (), len(args)
397 if n > 2:
398 s = _MODS.basics.isodd(n)
399 t = _fmtuple(zip(args[0::2], args[1::2]))
400 if s: # XXX _xzip(..., strict=s)
401 t += args[-1:]
402 elif n == 2:
403 t = (fmt_name_value % args),
404 elif n: # == 1
405 t = str(args[0]),
407 if kwds:
408 t += _fmtuple(itemsorted(kwds))
409 t = _or(*t) if t else _SPACE_(_name_value_, MISSING)
411 if txt is not None:
412 x = str(txt) or (str(cause) if cause else _invalid_)
413 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_
414 t = C(t, x)
415# else: # LenError, _xzip, .dms, .heights, .vector2d
416# x = NN # XXX or t?
417 Error.__init__(inst, t)
418# inst.__x_txt__ = x # hold explanation
419 _error_cause(inst, cause=cause if _exception_chaining else None)
420 _error_under(inst)
423def _error_under(inst):
424 '''(INTERNAL) Remove leading underscore from instance' class name.
425 '''
426 n = inst.__class__.__name__
427 if n.startswith(_UNDER_):
428 inst.__class__.__name__ = n.lstrip(_UNDER_)
429 return inst
432def exception_chaining(error=None):
433 '''Get an error's I{cause} or the exception chaining setting.
435 @kwarg error: An error instance (C{Exception}) or C{None}.
437 @return: If C{B{error} is None}, return C{True} if exception
438 chaining is enabled for PyGeodesy errors, C{False}
439 if turned off and C{None} if not available. If
440 B{C{error}} is not C{None}, return it's error
441 I{cause} or C{None}.
443 @note: To enable exception chaining for C{pygeodesy} errors,
444 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any
445 non-empty value prior to C{import pygeodesy}.
446 '''
447 return _exception_chaining if error is None else \
448 getattr(error, '__cause__', None)
451def _incompatible(this):
452 '''(INTERNAL) Format an C{"incompatible with ..."} text.
453 '''
454 return _SPACE_(_incompatible_, _with_, this)
457def _InvalidError(Error=_ValueError, **txt_name_values_cause): # txt=_invalid_, name=value [, ...]
458 '''(INTERNAL) Create an C{Error} instance.
460 @kwarg Error: The error class or sub-class (C{Exception}).
461 @kwarg txt_name_values: One or more C{B{name}=value} pairs
462 and optionally, keyword argument C{B{txt}=str}
463 to override the default C{B{txt}='invalid'} and
464 C{B{cause}=None} for exception chaining.
466 @return: An B{C{Error}} instance.
467 '''
468 return _XError(Error, **txt_name_values_cause)
471def isError(obj):
472 '''Check a (caught) exception.
474 @arg obj: The exception C({Exception}).
476 @return: C{True} if B{C{obj}} is a C{pygeodesy} error,
477 C{False} if B{C{obj}} is a standard Python error
478 of C{None} if neither.
479 '''
480 return True if isinstance(obj, _XErrors) else (
481 False if isinstance(obj, Exception) else None)
484def _IsnotError(*nouns, **name_value_Error_cause): # name=value [, Error=TypeError, cause=None]
485 '''Create a C{TypeError} for an invalid C{name=value} type.
487 @arg nouns: One or more expected class or type names, usually nouns (C{str}).
488 @kwarg name_value_Error_cause: One C{B{name}=value} pair and optionally,
489 keyword argument C{B{Error}=TypeError} to override the default
490 and C{B{cause}=None} for exception chaining.
492 @return: A C{TypeError} or an B{C{Error}} instance.
493 '''
494 x, Error = _xkwds_pop_(name_value_Error_cause, cause=None, Error=TypeError)
495 n, v = _xkwds_popitem(name_value_Error_cause) if name_value_Error_cause \
496 else (_name_value_, MISSING) # XXX else tuple(...)
497 n = _MODS.streprs.Fmt.PARENSPACED(n, repr(v))
498 t = _not_(_an(_or(*nouns)) if nouns else _specified_)
499 return _XError(Error, n, txt=t, cause=x)
502def itemsorted(adict, *args, **asorted_reverse):
503 '''Return the items of C{B{adict}} sorted I{alphabetically, case-insensitively}
504 and in I{ascending} order.
506 @arg args: Optional argument(s) for method C{B{adict}.items(B*{args})}.
507 @kwarg asorted_reverse: Use keyword argument C{B{asorted}=False} for
508 I{case-sensitive} sorting and C{B{reverse}=True} for
509 results in C{descending} order.
510 '''
511 def _un(item):
512 return item[0].lower()
514 # see .rhumb.Rhumb and ._RhumbLine
515 a, r = _xkwds_get_(asorted_reverse, asorted=True, reverse=False) \
516 if asorted_reverse else (True, False)
517 items = adict.items(*args) if args else adict.items()
518 return sorted(items, reverse=r, key=_un if a else None)
521def limiterrors(raiser=None):
522 '''Get/set the throwing of L{LimitError}s.
524 @kwarg raiser: Choose C{True} to raise or C{False} to
525 ignore L{LimitError} exceptions. Use
526 C{None} to leave the setting unchanged.
528 @return: Previous setting (C{bool}).
529 '''
530 global _limiterrors
531 t = _limiterrors
532 if raiser in (True, False):
533 _limiterrors = raiser
534 return t
537def _or(*words):
538 '''(INTERNAL) Join C{words} with C{", "} and C{" or "}.
539 '''
540 return _and_or(_or_, *words)
543def _parseX(parser, *args, **name_values_Error): # name=value[, ..., Error=ParseError]
544 '''(INTERNAL) Invoke a parser and handle exceptions.
546 @arg parser: The parser (C{callable}).
547 @arg args: Any B{C{parser}} arguments (any C{type}s).
548 @kwarg name_values_Error: Any C{B{name}=value} pairs and
549 optionally, C{B{Error}=ParseError} keyword
550 argument to override the default.
552 @return: Parser result.
554 @raise ParseError: Or the specified C{B{Error}}.
555 '''
556 try:
557 return parser(*args)
558 except Exception as x:
559 E = _xkwds_pop(name_values_Error, Error=type(x) if isError(x) else
560 ParseError)
561 raise _XError(E, **_xkwds(name_values_Error, cause=x))
564def rangerrors(raiser=None):
565 '''Get/set the throwing of L{RangeError}s.
567 @kwarg raiser: Choose C{True} to raise or C{False} to ignore
568 L{RangeError} exceptions. Use C{None} to leave
569 the setting unchanged.
571 @return: Previous setting (C{bool}).
572 '''
573 global _rangerrors
574 t = _rangerrors
575 if raiser in (True, False):
576 _rangerrors = raiser
577 return t
580def _SciPyIssue(x, *extras): # PYCHOK no cover
581 if isinstance(x, (RuntimeWarning, UserWarning)):
582 Error = SciPyWarning
583 else:
584 Error = SciPyError # PYCHOK not really
585 t = _SPACE_(str(x).strip(), *extras)
586 return Error(t, cause=x)
589def _xattr(obj, **name_default): # see .strerprs._xattrs
590 '''(INTERNAL) Get an C{obj}'s attribute by C{name}.
591 '''
592 if len(name_default) == 1:
593 for n, d in name_default.items():
594 return getattr(obj, n, d)
595 raise _xkwds_Error(_xattr, {}, name_default)
598def _xdatum(datum1, datum2, Error=None):
599 '''(INTERNAL) Check for datum, ellipsoid or rhumb mis-match.
600 '''
601 if Error:
602 E1, E2 = datum1.ellipsoid, datum2.ellipsoid
603 if E1 != E2:
604 raise Error(E2.named2, txt=_incompatible(E1.named2))
605 elif datum1 != datum2:
606 t = _SPACE_(_datum_, repr(datum1.name), _not_, repr(datum2.name))
607 raise _AssertionError(t)
610def _xellipsoidal(**name_value):
611 '''(INTERNAL) Check an I{ellipsoidal} item.
613 @return: The B{C{value}} if ellipsoidal.
615 @raise TypeError: Not ellipsoidal B{C{value}}.
616 '''
617 try:
618 for n, v in name_value.items():
619 if v.isEllipsoidal:
620 return v
621 break
622 else:
623 n = v = MISSING
624 except AttributeError:
625 pass
626 raise _TypeError(n, v, txt=_not_(_ellipsoidal_))
629def _XError(Error, *args, **kwds):
630 '''(INTERNAL) Format an C{Error} or C{_Error}.
631 '''
632 try: # C{_Error} style
633 return Error(*args, **kwds)
634 except TypeError: # no keyword arguments
635 pass
636 e = _ValueError(*args, **kwds)
637 E = Error(str(e))
638 if _exception_chaining:
639 _error_cause(E, cause=e.__cause__) # PYCHOK OK
640 return E
643_XErrors = _TypeError, _ValueError
644# map certain C{Exception} classes to the C{_Error}
645_X2Error = {AssertionError: _AssertionError,
646 AttributeError: _AttributeError,
647 ImportError: _ImportError,
648 IndexError: _IndexError,
649 KeyError: _KeyError,
650 NameError: _NameError,
651 NotImplementedError: _NotImplementedError,
652 OverflowError: _OverflowError,
653 TypeError: _TypeError,
654 ValueError: _ValueError,
655 ZeroDivisionError: _ZeroDivisionError}
658def _xError(x, *args, **kwds):
659 '''(INTERNAL) Embellish a (caught) exception.
661 @arg x: The exception (usually, C{_Error}).
662 @arg args: Embelishments (C{any}).
663 @kwarg kwds: Embelishments (C{any}).
664 '''
665 return _XError(type(x), *args, **_xkwds(kwds, cause=x))
668def _xError2(x): # in .fsums
669 '''(INTERNAL) Map an exception to 2-tuple (C{_Error} class, error C{txt}).
671 @arg x: The exception instance (usually, C{Exception}).
672 '''
673 X = type(x)
674 E = _X2Error.get(X, X)
675 if E is X and not isError(x):
676 E = _NotImplementedError
677 t = repr(x)
678 else:
679 t = str(x)
680 return E, t
683try:
684 _ = {}.__or__ # {} | {} # Python 3.9+
686 def _xkwds(kwds, **dflts):
687 '''(INTERNAL) Override C{dflts} with specified C{kwds}.
688 '''
689 return (dflts | kwds) if kwds else dflts
691except AttributeError:
692 from copy import copy as _copy
694 def _xkwds(kwds, **dflts): # PYCHOK expected
695 '''(INTERNAL) Override C{dflts} with specified C{kwds}.
696 '''
697 d = dflts
698 if kwds:
699 d = _copy(d)
700 d.update(kwds)
701 return d
704def _xkwds_bool(inst, **kwds): # in .frechet, .hausdorff, .heights
705 '''(INTERNAL) Set applicable C{bool} properties/attributes.
706 '''
707 for n, v in kwds.items():
708 b = getattr(inst, n, None)
709 if b is None: # invalid bool attr
710 t = _SPACE_(_EQUAL_(n, repr(v)), 'for', inst.__class__.__name__) # XXX .classname
711 raise _AttributeError(t, txt=_not_('applicable'))
712 if v in (False, True) and v != b:
713 setattr(inst, NN(_UNDER_, n), v)
716def _xkwds_Error(where, kwds, name_txt, txt=_default_):
717 # Helper for _xkwds_get, _xkwds_pop and _xkwds_popitem below
718 f = _COMMASPACE_.join(_pairs(kwds) + _pairs(name_txt))
719 f = _MODS.streprs.Fmt.PAREN(where.__name__, f)
720 t = _multiple_ if name_txt else _no_
721 t = _SPACE_(t, _EQUAL_(_name_, txt), _kwargs_)
722 return _AssertionError(f, txt=t)
725def _xkwds_get(kwds, **name_default):
726 '''(INTERNAL) Get a C{kwds} value by C{name}, or the C{default}.
727 '''
728 if len(name_default) == 1:
729 for n, d in name_default.items():
730 return kwds.get(n, d)
731 raise _xkwds_Error(_xkwds_get, kwds, name_default)
734def _xkwds_get_(kwds, **names_defaults):
735 '''(INTERNAL) Yield each C{kwds} value or its C{default}
736 in I{case-insensitive, alphabetical} order.
737 '''
738 for n, d in itemsorted(names_defaults):
739 yield kwds.get(n, d)
742def _xkwds_not(*args, **kwds):
743 '''(INTERNAL) Return C{kwds} with a value not in C{args}.
744 '''
745 return dict((n, v) for n, v in kwds.items() if v not in args)
748def _xkwds_pop(kwds, **name_default):
749 '''(INTERNAL) Pop a C{kwds} value by C{name}, or the C{default}.
750 '''
751 if len(name_default) == 1:
752 for n, d in name_default.items():
753 return kwds.pop(n, d)
754 raise _xkwds_Error(_xkwds_pop, kwds, name_default)
757def _xkwds_pop_(kwds, **names_defaults):
758 '''(INTERNAL) Pop and yield each C{kwds} value or its C{default}
759 in I{case-insensitive, alphabetical} order.
760 '''
761 for n, d in itemsorted(names_defaults):
762 yield kwds.pop(n, d)
765def _xkwds_popitem(name_value):
766 '''(INTERNAL) Return exactly one C{(name, value)} item.
767 '''
768 if len(name_value) == 1: # XXX TypeError
769 return name_value.popitem() # XXX AttributeError
770 raise _xkwds_Error(_xkwds_popitem, (), name_value, txt=_value_)
773def _Xorder(_Coeffs, Error, **Xorder): # in .ktm, .rhumbBase
774 '''(INTERNAL) Validate C{RAorder} or C{TMorder}.
775 '''
776 X, m = Xorder.popitem()
777 if m in _Coeffs and _MODS.basics.isint(m):
778 return m
779 t = sorted(map(str, _Coeffs.keys()))
780 raise Error(X, m, txt=_not_(_or(*t)))
783def _xzip(*args, **strict): # PYCHOK no cover
784 '''(INTERNAL) Standard C{zip(..., strict=True)}.
785 '''
786 s = _xkwds_get(strict, strict=True)
787 if s:
788 _zip = _MODS.basics._zip
789 if _zip is zip: # < (3, 10)
790 t = _MODS.streprs.unstr(_xzip.__name__, *args, strict=s)
791 raise _NotImplementedError(t, txt=None)
792 return _zip(*args)
793 return zip(*args)
795# **) MIT License
796#
797# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
798#
799# Permission is hereby granted, free of charge, to any person obtaining a
800# copy of this software and associated documentation files (the "Software"),
801# to deal in the Software without restriction, including without limitation
802# the rights to use, copy, modify, merge, publish, distribute, sublicense,
803# and/or sell copies of the Software, and to permit persons to whom the
804# Software is furnished to do so, subject to the following conditions:
805#
806# The above copyright notice and this permission notice shall be included
807# in all copies or substantial portions of the Software.
808#
809# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
810# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
811# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
812# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
813# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
814# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
815# OTHER DEALINGS IN THE SOFTWARE.