Coverage for pygeodesy/errors.py: 93%

268 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-02-09 17:20 -0500

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Errors, exceptions, exception formatting and exception chaining. 

5 

6Error, exception classes and functions to format PyGeodesy errors, 

7including the setting of I{exception chaining} in Python 3+. 

8 

9By default, I{exception chaining} is turned 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''' 

14 

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 

21 

22__all__ = _ALL_LAZY.errors # _ALL_DOCS('_InvalidError', '_IsnotError') _under 

23__version__ = '24.01.02' 

24 

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) 

31 

32try: 

33 _exception_chaining = None # not available 

34 _ = Exception().__cause__ # Python 3+ exception chaining 

35 

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 

39 

40 _exception_chaining = False # turned off 

41 

42 def _error_cause(inst, cause=None): 

43 '''(INTERNAL) Set or avoid Python 3+ exception chaining. 

44 

45 Setting C{inst.__cause__ = None} is equivalent to syntax 

46 C{raise Error(...) from None} to avoid exception chaining. 

47 

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. 

51 

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 

61 

62except AttributeError: # Python 2+ 

63 

64 def _error_cause(inst, **unused): # PYCHOK expected 

65 return inst # no-op 

66 

67 

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) 

73 

74 

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) 

80 

81 

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) 

87 

88 

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) 

94 

95 

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) 

101 

102 

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) 

108 

109 

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) 

115 

116 

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) 

122 

123 

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) 

129 

130 

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) 

138 

139 

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) 

145 

146 

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) 

152 

153 

154class AuxError(_ValueError): 

155 '''Error raised for a L{rhumb.aux_}, C{Aux}, C{AuxDLat} or C{AuxLat} issue. 

156 ''' 

157 pass 

158 

159 

160class ClipError(_ValueError): 

161 '''Clip box or clip region issue. 

162 ''' 

163 def __init__(self, *name_n_corners, **txt_cause): 

164 '''New L{ClipError}. 

165 

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) 

178 

179 

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 

185 

186 

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 

192 

193 

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) 

204 

205 

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}. 

211 

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 

220 

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) 

227 

228 

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. 

234 

235 @see: Subclass L{UnitError}. 

236 ''' 

237 pass 

238 

239 

240class MGRSError(_ValueError): 

241 '''Military Grid Reference System (MGRS) parse or other L{Mgrs} issue. 

242 ''' 

243 pass 

244 

245 

246class NumPyError(_ValueError): 

247 '''Error raised for C{NumPy} issues. 

248 ''' 

249 pass 

250 

251 

252class ParseError(_ValueError): # in .dms, .elevations, .utmupsBase 

253 '''Error parsing degrees, radians or several other formats. 

254 ''' 

255 pass 

256 

257 

258class PointsError(_ValueError): # in .clipy, .frechet, ... 

259 '''Error for an insufficient number of points. 

260 ''' 

261 pass 

262 

263 

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}. 

270 

271 @see: Function L{pygeodesy.rangerrors}. 

272 ''' 

273 pass 

274 

275 

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 

281 

282 

283class TriangleError(_ValueError): # in .resections, .vector2d 

284 '''Error raised for triangle, inter- or resection issues. 

285 ''' 

286 pass 

287 

288 

289class SciPyError(PointsError): 

290 '''Error raised for C{SciPy} issues. 

291 ''' 

292 pass 

293 

294 

295class SciPyWarning(PointsError): 

296 '''Error thrown for C{SciPy} warnings. 

297 

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 

307 

308 

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 

314 

315 

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 

321 

322 

323class VectorError(_ValueError): # in .nvectorBase, .vector3d, .vector3dBase 

324 '''L{Vector3d}, C{Cartesian*} or C{*Nvector} issues. 

325 ''' 

326 pass 

327 

328 

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) 

335 

336 

337def _and(*words): 

338 '''(INTERNAL) Join C{words} with C{", "} and C{" and "}. 

339 ''' 

340 return _and_or(_and_, *words) 

341 

342 

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 

353 

354 

355def crosserrors(raiser=None): 

356 '''Report or ignore vectorial cross product errors. 

357 

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. 

361 

362 @return: Previous setting (C{bool}). 

363 

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 

371 

372 

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. 

376 

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) 

392 

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]), 

403 

404 if kwds: 

405 t += _fmtuple(itemsorted(kwds)) 

406 t = _or(*t) if t else _SPACE_(_name_value_, MISSING) 

407 

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) 

418 

419 

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 

427 

428 

429def exception_chaining(error=None): 

430 '''Get an error's I{cause} or the exception chaining setting. 

431 

432 @kwarg error: An error instance (C{Exception}) or C{None}. 

433 

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}. 

439 

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) 

446 

447 

448def _incompatible(this): 

449 '''(INTERNAL) Format an C{"incompatible with ..."} text. 

450 ''' 

451 return _SPACE_(_incompatible_, _with_, this) 

452 

453 

454def _InvalidError(Error=_ValueError, **txt_name_values_cause): # txt=_invalid_, name=value [, ...] 

455 '''(INTERNAL) Create an C{Error} instance. 

456 

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. 

462 

463 @return: An B{C{Error}} instance. 

464 ''' 

465 return _XError(Error, **txt_name_values_cause) 

466 

467 

468def isError(obj): 

469 '''Check a (caught) exception. 

470 

471 @arg obj: The exception C({Exception}). 

472 

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) 

479 

480 

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. 

483 

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. 

488 

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) 

497 

498 

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. 

502 

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() 

510 

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) 

516 

517 

518def limiterrors(raiser=None): 

519 '''Get/set the throwing of L{LimitError}s. 

520 

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. 

524 

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 

532 

533 

534def _or(*words): 

535 '''(INTERNAL) Join C{words} with C{", "} and C{" or "}. 

536 ''' 

537 return _and_or(_or_, *words) 

538 

539 

540def _parseX(parser, *args, **name_values_Error): # name=value[, ..., Error=ParseError] 

541 '''(INTERNAL) Invoke a parser and handle exceptions. 

542 

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. 

548 

549 @return: Parser result. 

550 

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)) 

559 

560 

561def rangerrors(raiser=None): 

562 '''Get/set the throwing of L{RangeError}s. 

563 

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. 

567 

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 

575 

576 

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) 

584 

585 

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) 

591 

592 

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) 

600 

601 

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) 

613 

614 

615def _xellipsoidal(**name_value): 

616 '''(INTERNAL) Check an I{ellipsoidal} item. 

617 

618 @return: The B{C{value}} if ellipsoidal. 

619 

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_)) 

632 

633 

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 

646 

647 

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} 

661 

662 

663def _xError(x, *args, **kwds): 

664 '''(INTERNAL) Embellish a (caught) exception. 

665 

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)) 

671 

672 

673def _xError2(x): # in .fsums 

674 '''(INTERNAL) Map an exception to 2-tuple (C{_Error} class, error C{txt}). 

675 

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 

686 

687 

688try: 

689 _ = {}.__or__ # {} | {} # Python 3.9+ 

690 

691 def _xkwds(kwds, **dflts): 

692 '''(INTERNAL) Override C{dflts} with specified C{kwds}. 

693 ''' 

694 return (dflts | kwds) if kwds else dflts 

695 

696except AttributeError: 

697 from copy import copy as _copy 

698 

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 

707 

708 

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) 

719 

720 

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) 

728 

729 

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) 

736 

737 

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) 

742 

743 

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) 

751 

752 

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) 

759 

760 

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) 

767 

768 

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))) 

777 

778 

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) 

790 

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.