Coverage for pygeodesy/errors.py: 95%

270 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-04-02 13:52 -0400

1 

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

3 

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

5 

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

7the setting of I{exception chaining} for Python 3.9+. 

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-empty string I{OR} set env variable 

12C{PYGEODESY_EXCEPTION_CHAINING=std} or to any non-empty string. 

13''' 

14# from pygeodesy.basics import isint, isodd, itemsorted, _xinstanceof, _zip # _MODS 

15# from pygeodesy.ellipsoidalBase import CartesianEllipsoidalBase, LatLonEllipsoidalBase # _MODS 

16# from pygeodesy import errors # _MODS.getattr 

17from pygeodesy.interns import MISSING, NN, _a_, _an_, _and_, _clip_, _COLON_, \ 

18 _COLONSPACE_, _COMMASPACE_, _datum_, _ellipsoidal_, \ 

19 _incompatible_, _invalid_, _len_, _not_, _or_, _SPACE_, \ 

20 _specified_, _UNDER_, _vs_, _with_, _tailof 

21from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _getenv, _PYTHON_X_DEV 

22# from pygeodesy.streprs import Fmt, unstr # _MODS 

23# from pygeodesy.vector3dBase import Vector3dBase # _MODS 

24 

25from copy import copy as _copy 

26 

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

28__version__ = '24.03.24' 

29 

30_box_ = 'box' 

31_limiterrors = True # in .formy 

32_name_value_ = repr('name=value') 

33_rangerrors = True # in .dms 

34_region_ = 'region' 

35_vs__ = _SPACE_(NN, _vs_, NN) 

36 

37try: 

38 _exception_chaining = None # not available 

39 _ = Exception().__cause__ # Python 3.9+ exception chaining 

40 

41 if _PYTHON_X_DEV or _getenv('PYGEODESY_EXCEPTION_CHAINING', NN): # == _std_ 

42 _exception_chaining = True # turned on, std 

43 raise AttributeError # allow exception chaining 

44 

45 _exception_chaining = False # turned off 

46 

47 def _error_cause(inst, cause=None): 

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

49 

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

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

52 

53 @arg inst: An error instance (I{caught} C{Exception}). 

54 @kwarg cause: A previous error instance (I{caught} C{Exception}) 

55 or C{None} to avoid exception chaining. 

56 

57 @see: Alex Martelli, et.al., "Python in a Nutshell", 3rd Ed., page 163, 

58 O'Reilly, 2017, U{PEP-3134<https://www.Python.org/dev/peps/pep-3134>}, 

59 U{here<https://StackOverflow.com/questions/17091520/how-can-i-more- 

60 easily-suppress-previous-exceptions-when-i-raise-my-own-exception>} 

61 and U{here<https://StackOverflow.com/questions/1350671/ 

62 inner-exception-with-traceback-in-python>}. 

63 ''' 

64 inst.__cause__ = cause # None, no exception chaining 

65 return inst 

66 

67except AttributeError: # Python 2+ 

68 

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

70 return inst # no-op 

71 

72 

73class _AssertionError(AssertionError): 

74 '''(INTERNAL) Format an C{AssertionError} with/-out exception chaining. 

75 ''' 

76 def __init__(self, *args, **kwds): 

77 _error_init(AssertionError, self, args, **kwds) 

78 

79 

80class _AttributeError(AttributeError): 

81 '''(INTERNAL) Format an C{AttributeError} with/-out exception chaining. 

82 ''' 

83 def __init__(self, *args, **kwds): 

84 _error_init(AttributeError, self, args, **kwds) 

85 

86 

87class _ImportError(ImportError): 

88 '''(INTERNAL) Format an C{ImportError} with/-out exception chaining. 

89 ''' 

90 def __init__(self, *args, **kwds): 

91 _error_init(ImportError, self, args, **kwds) 

92 

93 

94class _IndexError(IndexError): 

95 '''(INTERNAL) Format an C{IndexError} with/-out exception chaining. 

96 ''' 

97 def __init__(self, *args, **kwds): 

98 _error_init(IndexError, self, args, **kwds) 

99 

100 

101class _KeyError(KeyError): 

102 '''(INTERNAL) Format a C{KeyError} with/-out exception chaining. 

103 ''' 

104 def __init__(self, *args, **kwds): # txt=_invalid_ 

105 _error_init(KeyError, self, args, **kwds) 

106 

107 

108class _NameError(NameError): 

109 '''(INTERNAL) Format a C{NameError} with/-out exception chaining. 

110 ''' 

111 def __init__(self, *args, **kwds): 

112 _error_init(NameError, self, args, **kwds) 

113 

114 

115class _NotImplementedError(NotImplementedError): 

116 '''(INTERNAL) Format a C{NotImplementedError} with/-out exception chaining. 

117 ''' 

118 def __init__(self, *args, **kwds): 

119 _error_init(NotImplementedError, self, args, **kwds) 

120 

121 

122class _OverflowError(OverflowError): 

123 '''(INTERNAL) Format an C{OverflowError} with/-out exception chaining. 

124 ''' 

125 def __init__(self, *args, **kwds): # txt=_invalid_ 

126 _error_init(OverflowError, self, args, **kwds) 

127 

128 

129class _TypeError(TypeError): 

130 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining. 

131 ''' 

132 def __init__(self, *args, **kwds): 

133 _error_init(TypeError, self, args, fmt_name_value='type(%s) (%r)', **kwds) 

134 

135 

136def _TypesError(name, value, *Types, **kwds): 

137 '''(INTERNAL) Format a C{TypeError} with/-out exception chaining. 

138 ''' 

139 # no longer C{class _TypesError} to avoid missing value 

140 # argument errors in _XError line ...E = Error(str(e)) 

141 t = _not_(_an(_or(*(t.__name__ for t in Types)))) 

142 return _TypeError(name, value, txt=t, **kwds) 

143 

144 

145class _ValueError(ValueError): 

146 '''(INTERNAL) Format a C{ValueError} with/-out exception chaining. 

147 ''' 

148 def __init__(self, *args, **kwds): # ..., cause=None, txt=_invalid_, ... 

149 _error_init(ValueError, self, args, **kwds) 

150 

151 

152class _ZeroDivisionError(ZeroDivisionError): 

153 '''(INTERNAL) Format a C{ZeroDivisionError} with/-out exception chaining. 

154 ''' 

155 def __init__(self, *args, **kwds): 

156 _error_init(ZeroDivisionError, self, args, **kwds) 

157 

158 

159class AuxError(_ValueError): 

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

161 ''' 

162 pass 

163 

164 

165class ClipError(_ValueError): 

166 '''Clip box or clip region issue. 

167 ''' 

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

169 '''New L{ClipError}. 

170 

171 @arg name_n_corners: Either just a name (C{str}) or 

172 name, number, corners (C{str}, 

173 C{int}, C{tuple}). 

174 @kwarg txt_cause: Optional C{B{txt}=str} explanation 

175 of the error and C{B{cause}=None} 

176 for exception chaining. 

177 ''' 

178 if len(name_n_corners) == 3: 

179 t, n, v = name_n_corners 

180 n = _SPACE_(t, _clip_, (_box_ if n == 2 else _region_)) 

181 name_n_corners = n, v 

182 _ValueError.__init__(self, *name_n_corners, **txt_cause) 

183 

184 

185class CrossError(_ValueError): 

186 '''Error raised for zero or near-zero vectorial cross products, 

187 occurring for coincident or colinear points, lines or bearings. 

188 ''' 

189 pass 

190 

191 

192class GeodesicError(_ValueError): 

193 '''Error raised for lack of convergence or other issues in L{pygeodesy.geodesicx}, 

194 L{pygeodesy.geodesicw} or L{pygeodesy.karney}. 

195 ''' 

196 pass 

197 

198 

199class IntersectionError(_ValueError): # in .ellipsoidalBaseDI, .formy, ... 

200 '''Error raised for line or circle intersection issues. 

201 ''' 

202 def __init__(self, *args, **kwds): 

203 '''New L{IntersectionError}. 

204 ''' 

205 if args: 

206 _ValueError.__init__(self, _SPACE_(*args), **kwds) 

207 else: 

208 _ValueError.__init__(self, **kwds) 

209 

210 

211class LenError(_ValueError): # in .ecef, .fmath, .heights, .iters, .named 

212 '''Error raised for mis-matching C{len} values. 

213 ''' 

214 def __init__(self, where, **lens_txt): # txt=None 

215 '''New L{LenError}. 

216 

217 @arg where: Object with C{.__name__} attribute 

218 (C{class}, C{method}, or C{function}). 

219 @kwarg lens_txt: Two or more C{name=len(name)} pairs 

220 (C{keyword arguments}). 

221 ''' 

222 def _ns_vs_txt_x(cause=None, txt=_invalid_, **kwds): 

223 ns, vs = zip(*_MODS.basics.itemsorted(kwds)) # unzip 

224 return ns, vs, txt, cause 

225 

226 ns, vs, txt, x = _ns_vs_txt_x(**lens_txt) 

227 ns = _COMMASPACE_.join(ns) 

228 t = _MODS.streprs.Fmt.PAREN(where.__name__, ns) 

229 vs = _vs__.join(map(str, vs)) 

230 t = _SPACE_(t, _len_, vs) 

231 _ValueError.__init__(self, t, txt=txt, cause=x) 

232 

233 

234class LimitError(_ValueError): 

235 '''Error raised for lat- or longitudinal values or deltas exceeding 

236 the given B{C{limit}} in functions L{pygeodesy.equirectangular}, 

237 L{pygeodesy.equirectangular_}, C{nearestOn*} and C{simplify*} 

238 or methods with C{limit} or C{options} keyword arguments. 

239 

240 @see: Subclass L{UnitError}. 

241 ''' 

242 pass 

243 

244 

245class MGRSError(_ValueError): 

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

247 ''' 

248 pass 

249 

250 

251class NumPyError(_ValueError): 

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

253 ''' 

254 pass 

255 

256 

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

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

259 ''' 

260 pass 

261 

262 

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

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

265 ''' 

266 pass 

267 

268 

269class RangeError(_ValueError): 

270 '''Error raised for lat- or longitude values outside the B{C{clip}}, 

271 B{C{clipLat}}, B{C{clipLon}} in functions L{pygeodesy.parse3llh}, 

272 L{pygeodesy.parseDMS}, L{pygeodesy.parseDMS2} and L{pygeodesy.parseRad} 

273 or the given B{C{limit}} in functions L{pygeodesy.clipDegrees} and 

274 L{pygeodesy.clipRadians}. 

275 

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

277 ''' 

278 pass 

279 

280 

281class RhumbError(_ValueError): 

282 '''Error raised for a L{pygeodesy.rhumb.aux_}, L{pygeodesy.rhumb.ekx} 

283 or L{pygeodesy.rhumb.solve} issue. 

284 ''' 

285 pass 

286 

287 

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

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

290 ''' 

291 pass 

292 

293 

294class SciPyError(PointsError): 

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

296 ''' 

297 pass 

298 

299 

300class SciPyWarning(PointsError): 

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

302 

303 To raise C{SciPy} warnings as L{SciPyWarning} exceptions, Python 

304 C{warnings} must be filtered as U{warnings.filterwarnings('error') 

305 <https://docs.Python.org/3/library/warnings.html#the-warnings-filter>} 

306 I{prior to} C{import scipy} OR by setting env var U{PYTHONWARNINGS 

307 <https://docs.Python.org/3/using/cmdline.html#envvar-PYTHONWARNINGS>} 

308 OR by invoking C{python} with command line option U{-W<https://docs. 

309 Python.org/3/using/cmdline.html#cmdoption-w>} set to C{-W error}. 

310 ''' 

311 pass 

312 

313 

314class TRFError(_ValueError): # in .ellipsoidalBase, .trf, .units 

315 '''Terrestrial Reference Frame (TRF), L{Epoch}, L{RefFrame} 

316 or L{RefFrame} conversion issue. 

317 ''' 

318 pass 

319 

320 

321class UnitError(LimitError): # in .named, .units 

322 '''Default exception for L{units} issues for a value exceeding the 

323 C{low} or C{high} limit. 

324 ''' 

325 pass 

326 

327 

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

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

330 ''' 

331 pass 

332 

333 

334def _an(noun): 

335 '''(INTERNAL) Prepend an article to a noun based 

336 on the pronounciation of the first letter. 

337 ''' 

338 a = _an_ if noun[:1].lower() in 'aeinoux' else _a_ 

339 return _SPACE_(a, noun) 

340 

341 

342def _and(*words): 

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

344 ''' 

345 return _and_or(_and_, *words) 

346 

347 

348def _and_or(last, *words): 

349 '''(INTERNAL) Join C{words} with C{", "} and C{B{last}}. 

350 ''' 

351 t, w = NN, list(words) 

352 if w: 

353 t = w.pop() 

354 if w: 

355 w = _COMMASPACE_.join(w) 

356 t = _SPACE_(w, last, t) 

357 return t 

358 

359 

360def crosserrors(raiser=None): 

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

362 

363 @kwarg raiser: Use C{True} to throw or C{False} to ignore 

364 L{CrossError} exceptions. Use C{None} to 

365 leave the setting unchanged. 

366 

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

368 

369 @see: Property C{Vector3d[Base].crosserrors}. 

370 ''' 

371 V = _MODS.vector3dBase.Vector3dBase 

372 t = V._crosserrors # XXX class attr! 

373 if raiser in (True, False): 

374 V._crosserrors = raiser 

375 return t 

376 

377 

378def _error_init(Error, inst, args, fmt_name_value='%s (%r)', txt=NN, 

379 cause=None, **kwds): # by .lazily 

380 '''(INTERNAL) Format an error text and initialize an C{Error} instance. 

381 

382 @arg Error: The error super-class (C{Exception}). 

383 @arg inst: Sub-class instance to be __init__-ed (C{_Exception}). 

384 @arg args: Either just a value or several name, value, ... 

385 positional arguments (C{str}, any C{type}), in 

386 particular for name conflicts with keyword 

387 arguments of C{error_init} or which can't be 

388 given as C{name=value} keyword arguments. 

389 @kwarg fmt_name_value: Format for (name, value) (C{str}). 

390 @kwarg txt: Optional explanation of the error (C{str}). 

391 @kwarg cause: Optional, caught error (L{Exception}), for 

392 exception chaining (supported in Python 3+). 

393 @kwarg kwds: Additional C{B{name}=value} pairs, if any. 

394 ''' 

395 def _fmtuple(pairs): 

396 return tuple(fmt_name_value % t for t in pairs) 

397 

398 t, n = (), len(args) 

399 if n > 2: 

400 s = _MODS.basics.isodd(n) 

401 t = _fmtuple(zip(args[0::2], args[1::2])) 

402 if s: # XXX _xzip(..., strict=s) 

403 t += args[-1:] 

404 elif n == 2: 

405 t = (fmt_name_value % args), 

406 elif n: # == 1 

407 t = str(args[0]), 

408 

409 if kwds: 

410 t += _fmtuple(_MODS.basics.itemsorted(kwds)) 

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

412 

413 if txt is not None: 

414 x = str(txt) or (str(cause) if cause else _invalid_) 

415 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_ 

416 t = C(t, x) 

417# else: # LenError, _xzip, .dms, .heights, .vector2d 

418# x = NN # XXX or t? 

419 Error.__init__(inst, t) 

420# inst.__x_txt__ = x # hold explanation 

421 _error_cause(inst, cause=cause if _exception_chaining else None) 

422 _error_under(inst) 

423 

424 

425def _error_under(inst): 

426 '''(INTERNAL) Remove leading underscore from instance' class name. 

427 ''' 

428 n = inst.__class__.__name__ 

429 if n.startswith(_UNDER_): 

430 inst.__class__.__name__ = n.lstrip(_UNDER_) 

431 return inst 

432 

433 

434def exception_chaining(exc=None): 

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

436 

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

438 

439 @return: If C{B{exc} is None}, return C{True} if exception 

440 chaining is enabled for PyGeodesy errors, C{False} 

441 if turned off and C{None} if not available. If 

442 B{C{exc}} is not C{None}, return it's error I{cause} 

443 or C{None}. 

444 

445 @note: To enable exception chaining for C{pygeodesy} errors, 

446 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any 

447 non-empty value prior to C{import pygeodesy}. 

448 ''' 

449 return _exception_chaining if exc is None else \ 

450 getattr(exc, '__cause__', None) 

451 

452 

453def _incompatible(this): 

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

455 ''' 

456 return _SPACE_(_incompatible_, _with_, this) 

457 

458 

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

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

461 

462 @kwarg Error: The error class or sub-class (C{Exception}). 

463 @kwarg txt_name_values: One or more C{B{name}=value} pairs 

464 and optionally, keyword argument C{B{txt}=str} 

465 to override the default C{B{txt}='invalid'} and 

466 C{B{cause}=None} for exception chaining. 

467 

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

469 ''' 

470 return _XError(Error, **txt_name_values_cause) 

471 

472 

473def isError(exc): 

474 '''Check a (caught) exception. 

475 

476 @arg exc: The exception C({Exception}). 

477 

478 @return: C{True} if B{C{exc}} is a C{pygeodesy} error, 

479 C{False} if B{C{exc}} is a standard Python error 

480 of C{None} if neither. 

481 ''' 

482 return True if isinstance(exc, _XErrors) else ( 

483 False if isinstance(exc, Exception) else None) 

484 

485 

486def _IsnotError(*nouns, **name_value_Error_cause): # name=value [, Error=TypeError, cause=None] 

487 '''Create a C{TypeError} for an invalid C{name=value} type. 

488 

489 @arg nouns: One or more expected class or type names, usually nouns (C{str}). 

490 @kwarg name_value_Error_cause: One C{B{name}=value} pair and optionally, 

491 keyword argument C{B{Error}=TypeError} to override the default 

492 and C{B{cause}=None} for exception chaining. 

493 

494 @return: A C{TypeError} or an B{C{Error}} instance. 

495 ''' 

496 def _n_v_E_x(cause=None, Error=TypeError, **name_value): 

497 return _xkwds_item2(name_value) + (Error, cause) 

498 

499 n, v, E, x = _n_v_E_x(**name_value_Error_cause) 

500 

501 n = _MODS.streprs.Fmt.PARENSPACED(n, repr(v)) 

502 t = _not_(_an(_or(*nouns)) if nouns else _specified_) 

503 return _XError(E, n, txt=t, cause=x) 

504 

505 

506def limiterrors(raiser=None): 

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

508 

509 @kwarg raiser: Choose C{True} to raise or C{False} to 

510 ignore L{LimitError} exceptions. Use 

511 C{None} to leave the setting unchanged. 

512 

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

514 ''' 

515 global _limiterrors 

516 t = _limiterrors 

517 if raiser in (True, False): 

518 _limiterrors = raiser 

519 return t 

520 

521 

522def _or(*words): 

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

524 ''' 

525 return _and_or(_or_, *words) 

526 

527 

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

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

530 

531 @arg parser: The parser (C{callable}). 

532 @arg args: Any B{C{parser}} arguments (any C{type}s). 

533 @kwarg name_values_Error: Any C{B{name}=value} pairs and 

534 optionally, C{B{Error}=ParseError} keyword 

535 argument to override the default. 

536 

537 @return: Parser result. 

538 

539 @raise ParseError: Or the specified C{B{Error}}. 

540 ''' 

541 try: 

542 return parser(*args) 

543 except Exception as x: 

544 E = type(x) if isError(x) else ParseError 

545 E, kwds = _xkwds_pop2(name_values_Error, Error=E) 

546 raise _XError(E, **_xkwds(kwds, cause=x)) 

547 

548 

549def rangerrors(raiser=None): 

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

551 

552 @kwarg raiser: Choose C{True} to raise or C{False} to ignore 

553 L{RangeError} exceptions. Use C{None} to leave 

554 the setting unchanged. 

555 

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

557 ''' 

558 global _rangerrors 

559 t = _rangerrors 

560 if raiser in (True, False): 

561 _rangerrors = raiser 

562 return t 

563 

564 

565def _SciPyIssue(exc, *extras): # PYCHOK no cover 

566 if isinstance(exc, (RuntimeWarning, UserWarning)): 

567 E = SciPyWarning 

568 else: 

569 E = SciPyError # PYCHOK not really 

570 t = _SPACE_(str(exc).strip(), *extras) 

571 return E(t, txt=None, cause=exc) 

572 

573 

574def _xAssertionError(where, *args, **kwds): 

575 '''(INTERNAL) Embellish an C{AssertionError} with/-out exception chaining. 

576 ''' 

577 x, kwds = _xkwds_pop2(kwds, cause=None) 

578 w = _MODS.streprs.unstr(where, *args, **kwds) 

579 return _AssertionError(w, txt=None, cause=x) 

580 

581 

582def _xattr(obj, **name_default): # see .strerprs._xattrs 

583 '''(INTERNAL) Get an C{obj}'s attribute by C{name}. 

584 ''' 

585 if len(name_default) == 1: 

586 for n, d in name_default.items(): 

587 return getattr(obj, n, d) 

588 raise _xAssertionError(_xattr, obj, **name_default) 

589 

590 

591def _xcallable(**names_callables): 

592 '''(INTERNAL) Check one or more C{callable}s. 

593 ''' 

594 for n, c in names_callables.items(): 

595 if not callable(c): 

596 raise _TypeError(n, c, txt=_not_(callable.__name__)) 

597 

598 

599def _xdatum(datum1, datum2, Error=None): 

600 '''(INTERNAL) Check for datum, ellipsoid or rhumb mis-match. 

601 ''' 

602 if Error: 

603 e1, e2 = datum1.ellipsoid, datum2.ellipsoid 

604 if e1 != e2: 

605 raise Error(e2.named2, txt=_incompatible(e1.named2)) 

606 elif datum1 != datum2: 

607 t = _SPACE_(_datum_, repr(datum1.name), 

608 _not_, repr(datum2.name)) 

609 raise _AssertionError(t) 

610 

611 

612def _xellipsoidal(**name_value): # see _xellipsoidall elel 

613 '''(INTERNAL) Check an I{ellipsoidal} item and return its value. 

614 ''' 

615 if len(name_value) == 1: 

616 for n, v in name_value.items(): 

617 try: 

618 if v.isEllipsoidal: 

619 return v 

620 except AttributeError: 

621 pass 

622 raise _TypeError(n, v, txt=_not_(_ellipsoidal_)) 

623 raise _xAssertionError(_xellipsoidal, name_value) 

624 

625 

626def _xellipsoidall(point): # see _xellipsoidal 

627 '''(INTERNAL) Check an ellipsoidal C{point}, return C{True} 

628 if geodetic latlon or C{False} if cartesian. 

629 ''' 

630 m = _MODS.ellipsoidalBase 

631 ll = isinstance(point, m.LatLonEllipsoidalBase) 

632 if not ll: 

633 b = _MODS.basics 

634 b._xinstanceof(m.CartesianEllipsoidalBase, 

635 m.LatLonEllipsoidalBase, point=point) 

636 return ll 

637 

638 

639def _XError(Error, *args, **kwds): 

640 '''(INTERNAL) Format an C{Error} or C{_Error}. 

641 ''' 

642 try: # C{_Error} style 

643 return Error(*args, **kwds) 

644 except TypeError: # no keyword arguments 

645 pass 

646 e = _ValueError(*args, **kwds) 

647 E = Error(str(e)) 

648 if _exception_chaining: 

649 _error_cause(E, cause=e.__cause__) # PYCHOK OK 

650 return E 

651 

652 

653def _xError(exc, *args, **kwds): 

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

655 

656 @arg exc: The exception (usually, C{_Error}). 

657 @arg args: Embelishments (C{any}). 

658 @kwarg kwds: Embelishments (C{any}). 

659 ''' 

660 return _XError(type(exc), *args, **_xkwds(kwds, cause=exc)) 

661 

662 

663def _xError2(exc): # in .constants, .fsums, .lazily, .vector2d 

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

665 

666 @arg exc: The exception instance (usually, C{Exception}). 

667 ''' 

668 m = __name__ # 'pygeodesy.errors' 

669 X = type(exc) 

670 n = NN(_UNDER_, _tailof(X.__name__)) 

671 E = _MODS.getattr(m, n, X) # == _X2Error.get(X, X) 

672 if E is X and not isError(exc): 

673 E = _NotImplementedError 

674 t = repr(exc) 

675 else: 

676 t = str(exc) 

677 return E, t 

678 

679 

680_XErrors = _TypeError, _ValueError 

681# map certain C{Exception} classes to the C{_Error} 

682# _X2Error = {AssertionError: _AssertionError, 

683# AttributeError: _AttributeError, 

684# ImportError: _ImportError, 

685# IndexError: _IndexError, 

686# KeyError: _KeyError, 

687# NameError: _NameError, 

688# NotImplementedError: _NotImplementedError, 

689# OverflowError: _OverflowError, 

690# TypeError: _TypeError, 

691# ValueError: _ValueError, 

692# ZeroDivisionError: _ZeroDivisionError} 

693 

694try: 

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

696 

697 def _xkwds(kwds, **dflts): 

698 '''(INTERNAL) Update C{dflts} with specified C{kwds}. 

699 ''' 

700 return (dflts | kwds) if kwds else dflts 

701 

702except AttributeError: 

703 

704 def _xkwds(kwds, **dflts): # PYCHOK expected 

705 '''(INTERNAL) Update C{dflts} with specified C{kwds}. 

706 ''' 

707 d = dflts 

708 if kwds: 

709 d = _copy(d) 

710 d.update(kwds) 

711 return d 

712 

713 

714# def _xkwds_bool(inst, **kwds): # no longer in .frechet, .hausdorff, .heights 

715# '''(INTERNAL) Set applicable C{bool} properties/attributes. 

716# ''' 

717# for n, v in kwds.items(): 

718# b = getattr(inst, n, None) 

719# if b is None: # invalid bool attr 

720# t = _SPACE_(_EQUAL_(n, repr(v)), 'for', inst.__class__.__name__) # XXX .classname 

721# raise _AttributeError(t, txt=_not_('applicable')) 

722# if v in (False, True) and v != b: 

723# setattr(inst, NN(_UNDER_, n), v) 

724 

725 

726def _xkwds_get(kwds, **name_default): 

727 '''(INTERNAL) Get a C{kwds} value by C{name} or the 

728 C{default} if not present. 

729 ''' 

730 if isinstance(kwds, dict) and len(name_default) == 1: 

731 for n, d in name_default.items(): 

732 return kwds.get(n, d) 

733 raise _xAssertionError(_xkwds_get, kwds, **name_default) 

734 

735 

736def _xkwds_get_(kwds, **names_defaults): 

737 '''(INTERNAL) Yield each C{kwds} value or its C{default} 

738 in I{case-insensitive, alphabetical} C{name} order. 

739 ''' 

740 if not isinstance(kwds, dict): 

741 raise _xAssertionError(_xkwds_get_, kwds) 

742 for n, d in _MODS.basics.itemsorted(names_defaults): 

743 yield kwds.get(n, d) 

744 

745 

746def _xkwds_item2(kwds): 

747 '''(INTERNAL) Return the 2-tuple C{item}, keeping the 

748 single-item C{kwds} I{unmodified}. 

749 ''' 

750 if isinstance(kwds, dict) and len(kwds) == 1: 

751 for item in kwds.items(): 

752 return item 

753 raise _xAssertionError(_xkwds_item2, kwds) 

754 

755 

756def _xkwds_not(*args, **kwds): 

757 '''(INTERNAL) Return C{kwds} with a value not in C{args}. 

758 ''' 

759 return dict((n, v) for n, v in kwds.items() if v not in args) 

760 

761 

762def _xkwds_pop2(kwds, **name_default): 

763 '''(INTERNAL) Pop a C{kwds} item by C{name} and return the value and 

764 reduced C{kwds} copy, otherwise the C{default} and original C{kwds}. 

765 ''' 

766 if isinstance(kwds, dict) and len(name_default) == 1: 

767 for n, d in name_default.items(): 

768 if n in kwds: 

769 kwds = _copy(kwds) 

770 d = kwds.pop(n, d) 

771 return d, kwds 

772 raise _xAssertionError(_xkwds_pop2, kwds, **name_default) 

773 

774 

775def _Xorder(_Coeffs, Error, **Xorder): # in .auxLat, .ktm, .rhumb.bases, .rhumb.ekx 

776 '''(INTERNAL) Validate C{RAorder} or C{TMorder}. 

777 ''' 

778 X, m = _xkwds_item2(Xorder) 

779 if m in _Coeffs and _MODS.basics.isint(m): 

780 return m 

781 t = sorted(map(str, _Coeffs.keys())) 

782 raise Error(X, m, txt=_not_(_or(*t))) 

783 

784# **) MIT License 

785# 

786# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

787# 

788# Permission is hereby granted, free of charge, to any person obtaining a 

789# copy of this software and associated documentation files (the "Software"), 

790# to deal in the Software without restriction, including without limitation 

791# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

792# and/or sell copies of the Software, and to permit persons to whom the 

793# Software is furnished to do so, subject to the following conditions: 

794# 

795# The above copyright notice and this permission notice shall be included 

796# in all copies or substantial portions of the Software. 

797# 

798# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

799# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

800# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

801# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

802# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

803# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

804# OTHER DEALINGS IN THE SOFTWARE.