Coverage for pygeodesy/errors.py: 93%

291 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-10 14:08 -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, issubclassof, itemsorted, _xinstanceof, _zip # _MODS 

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

16# from pygeodesy import errors # _MODS, _MODS.getattr 

17from pygeodesy.internals import _plural, _tailof 

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

19 _COMMASPACE_, _datum_, _ellipsoidal_, _incompatible_, _invalid_, \ 

20 _keyword_, _len_, _not_, _or_, _SPACE_, _specified_, _UNDER_, \ 

21 _vs_, _with_ 

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

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

24# from pygeodesy.vector3dBase import Vector3dBase # _MODS 

25 

26from copy import copy as _copy 

27 

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

29__version__ = '24.06.08' 

30 

31_argument_ = 'argument' 

32_box_ = 'box' 

33_expected_ = 'expected' 

34_limiterrors = True # in .formy 

35_name_value_ = repr('name=value') 

36_rangerrors = True # in .dms 

37_region_ = 'region' 

38_vs__ = _SPACE_(NN, _vs_, NN) 

39 

40try: 

41 _exception_chaining = None # not available 

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

43 

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

45 _exception_chaining = True # turned on, std 

46 raise AttributeError() # allow exception chaining 

47 

48 _exception_chaining = False # turned off 

49 

50 def _error_cause(inst, cause=None): 

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

52 

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

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

55 

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

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

58 or C{None} to avoid exception chaining. 

59 

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

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

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

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

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

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

66 ''' 

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

68 return inst 

69 

70except AttributeError: # Python 2+ 

71 

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

73 return inst # no-op 

74 

75 

76class _AssertionError(AssertionError): 

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

78 ''' 

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

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

81 

82 

83class _AttributeError(AttributeError): 

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

85 ''' 

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

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

88 

89 

90class _ImportError(ImportError): 

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

92 ''' 

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

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

95 

96 

97class _IndexError(IndexError): 

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

99 ''' 

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

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

102 

103 

104class _KeyError(KeyError): 

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

106 ''' 

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

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

109 

110 

111class _NameError(NameError): 

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

113 ''' 

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

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

116 

117 

118class _NotImplementedError(NotImplementedError): 

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

120 ''' 

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

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

123 

124 

125class _OverflowError(OverflowError): 

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

127 ''' 

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

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

130 

131 

132class _TypeError(TypeError): 

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

134 ''' 

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

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

137 

138 

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

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

141 ''' 

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

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

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

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

146 

147 

148class _UnexpectedError(TypeError): # note, a TypeError! 

149 '''(INTERNAL) Format a C{TypeError} I{without exception chaining}. 

150 ''' 

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

152 n = len(kwds) 

153 if args: 

154 a = _plural(_argument_, len(args)) 

155 n = _and(a, _plural(_keyword_, n)) if n else a 

156 else: 

157 n = _plural(_SPACE_(_keyword_, _argument_), n) 

158 u = _MODS.streprs.unstr(_SPACE_(n, NN), *args, **kwds) 

159 # _error_init(TypeError, self, (u,), txt_not_=_expected_) 

160 TypeError.__init__(self, _SPACE_(u, _not_, _expected_)) 

161 

162 

163class _ValueError(ValueError): 

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

165 ''' 

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

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

168 

169 

170class _ZeroDivisionError(ZeroDivisionError): 

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

172 ''' 

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

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

175 

176 

177class AuxError(_ValueError): 

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

179 ''' 

180 pass 

181 

182 

183class ClipError(_ValueError): 

184 '''Clip box or clip region issue. 

185 ''' 

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

187 '''New L{ClipError}. 

188 

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

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

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

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

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

194 for exception chaining. 

195 ''' 

196 if len(name_n_corners) == 3: 

197 t, n, v = name_n_corners 

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

199 name_n_corners = n, v 

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

201 

202 

203class CrossError(_ValueError): 

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

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

206 ''' 

207 pass 

208 

209 

210class GeodesicError(_ValueError): 

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

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

213 ''' 

214 pass 

215 

216 

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

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

219 ''' 

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

221 '''New L{IntersectionError}. 

222 ''' 

223 if args: 

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

225 else: 

226 _ValueError.__init__(self, **kwds) 

227 

228 

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

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

231 ''' 

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

233 '''New L{LenError}. 

234 

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

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

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

238 (C{keyword arguments}). 

239 ''' 

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

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

242 return ns, vs, txt, cause 

243 

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

245 ns = _COMMASPACE_.join(ns) 

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

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

248 t = _SPACE_(t, _len_, vs) 

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

250 

251 

252class LimitError(_ValueError): 

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

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

255 L{pygeodesy.equirectangular4}, C{nearestOn*} and C{simplify*} 

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

257 

258 @see: Subclass L{UnitError}. 

259 ''' 

260 pass 

261 

262 

263class MGRSError(_ValueError): 

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

265 ''' 

266 pass 

267 

268 

269class NumPyError(_ValueError): 

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

271 ''' 

272 pass 

273 

274 

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

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

277 ''' 

278 pass 

279 

280 

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

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

283 ''' 

284 pass 

285 

286 

287class RangeError(_ValueError): 

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

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

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

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

292 L{pygeodesy.clipRadians}. 

293 

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

295 ''' 

296 pass 

297 

298 

299class RhumbError(_ValueError): 

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

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

302 ''' 

303 pass 

304 

305 

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

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

308 ''' 

309 pass 

310 

311 

312class SciPyError(PointsError): 

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

314 ''' 

315 pass 

316 

317 

318class SciPyWarning(PointsError): 

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

320 

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

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

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

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

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

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

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

328 ''' 

329 pass 

330 

331 

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

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

334 or L{RefFrame} conversion issue. 

335 ''' 

336 pass 

337 

338 

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

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

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

342 ''' 

343 pass 

344 

345 

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

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

348 ''' 

349 pass 

350 

351 

352def _an(noun): 

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

354 on the pronounciation of the first letter. 

355 ''' 

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

357 return _SPACE_(a, noun) 

358 

359 

360def _and(*words): 

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

362 ''' 

363 return _and_or(_and_, *words) 

364 

365 

366def _and_or(last, *words): 

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

368 ''' 

369 t, w = NN, list(words) 

370 if w: 

371 t = w.pop() 

372 if w: 

373 w = _COMMASPACE_.join(w) 

374 t = _SPACE_(w, last, t) 

375 return t 

376 

377 

378def crosserrors(raiser=None): 

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

380 

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

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

383 leave the setting unchanged. 

384 

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

386 

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

388 ''' 

389 V = _MODS.vector3dBase.Vector3dBase 

390 t = V._crosserrors # XXX class attr! 

391 if raiser in (True, False): 

392 V._crosserrors = raiser 

393 return t 

394 

395 

396def _error_init(Error, inst, args, fmt_name_value='%s (%r)', txt_not_=NN, 

397 txt__=None, txt=NN, cause=None, **kwds): 

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

399 

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

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

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

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

404 particular for name conflicts with keyword 

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

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

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

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

409 @kwarg txt__: Alternate C{B{txt}=B{txt__}.__name__}. 

410 @kwarg txt_not_: Negative explanation C{B{txt}=_not_(B{txt_not_})}. 

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

412 exception chaining (supported in Python 3+). 

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

414 ''' 

415 def _fmtuple(pairs): 

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

417 

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

419 if n > 2: 

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

421 s = _MODS.basics.isodd(n) 

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

423 t += args[-1:] 

424 elif n == 2: 

425 t = (fmt_name_value % args), 

426 elif n: # == 1 

427 t = str(args[0]), 

428 if kwds: 

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

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

431 

432 x = _not_(txt_not_) if txt_not_ else (txt if txt__ is None 

433 else txt__.__name__) 

434 if x is not None: 

435 x = str(x) or (str(cause) if cause else _invalid_) 

436 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_ 

437 t = C(t, x) 

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

439# x = NN # XXX or t? 

440 Error.__init__(inst, t) 

441# inst.__x_txt__ = x # hold explanation 

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

443 _error_under(inst) 

444 

445 

446def _error_under(inst): 

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

448 ''' 

449 n = inst.__class__.__name__ # _tailof? 

450 if n.startswith(_UNDER_): 

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

452 return inst 

453 

454 

455def exception_chaining(exc=None): 

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

457 

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

459 

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

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

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

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

464 or C{None}. 

465 

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

467 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any 

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

469 ''' 

470 return _exception_chaining if exc is None else \ 

471 getattr(exc, '__cause__', None) 

472 

473 

474def _incompatible(this): 

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

476 ''' 

477 return _SPACE_(_incompatible_, _with_, this) 

478 

479 

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

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

482 

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

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

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

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

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

488 

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

490 ''' 

491 return _XError(Error, **txt_name_values_cause) 

492 

493 

494def isError(exc): 

495 '''Check a (caught) exception. 

496 

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

498 

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

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

501 of C{None} if neither. 

502 ''' 

503 def _X(exc): 

504 X = type(exc) 

505 m = X.__module__ 

506 return _MODS.basics.issubclassof(X, *_XErrors) or \ 

507 ((m is __name__ or m == __name__) and 

508 _tailof(X.__name__).startswith(_UNDER_)) 

509 

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

511 _X(exc) if isinstance(exc, Exception) else None) 

512 

513 

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

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

516 

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

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

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

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

521 

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

523 ''' 

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

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

526 

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

528 

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

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

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

532 

533 

534def limiterrors(raiser=None): 

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

536 

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

538 ignore L{LimitError} exceptions. Use 

539 C{None} to leave the setting unchanged. 

540 

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

542 ''' 

543 global _limiterrors 

544 t = _limiterrors 

545 if raiser in (True, False): 

546 _limiterrors = raiser 

547 return t 

548 

549 

550def _or(*words): 

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

552 ''' 

553 return _and_or(_or_, *words) 

554 

555 

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

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

558 

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

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

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

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

563 argument to override the default. 

564 

565 @return: Parser result. 

566 

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

568 ''' 

569 try: 

570 return parser(*args) 

571 except Exception as x: 

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

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

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

575 

576 

577def rangerrors(raiser=None): 

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

579 

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

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

582 the setting unchanged. 

583 

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

585 ''' 

586 global _rangerrors 

587 t = _rangerrors 

588 if raiser in (True, False): 

589 _rangerrors = raiser 

590 return t 

591 

592 

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

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

595 E = SciPyWarning 

596 else: 

597 E = SciPyError # PYCHOK not really 

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

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

600 

601 

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

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

604 ''' 

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

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

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

608 

609 

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

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

612 ''' 

613 if len(name_default) == 1: 

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

615 return getattr(obj, n, d) 

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

617 

618 

619def _xcallable(**names_callables): 

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

621 ''' 

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

623 if not callable(c): 

624 raise _TypeError(n, c, txt_not_=callable.__name__) # txt__ 

625 

626 

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

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

629 ''' 

630 if Error: 

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

632 if e1 != e2: 

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

634 elif datum1 != datum2: 

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

636 _not_, repr(datum2.name)) 

637 raise _AssertionError(t) 

638 

639 

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

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

642 ''' 

643 if len(name_value) == 1: 

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

645 try: 

646 if v.isEllipsoidal: 

647 return v 

648 except AttributeError: 

649 pass 

650 raise _TypeError(n, v, txt_not_=_ellipsoidal_) 

651 raise _xAssertionError(_xellipsoidal, name_value) 

652 

653 

654def _xellipsoidall(point): # see _xellipsoidal 

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

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

657 ''' 

658 m = _MODS.ellipsoidalBase 

659 ll = isinstance(point, m.LatLonEllipsoidalBase) 

660 if not ll: 

661 b = _MODS.basics 

662 b._xinstanceof(m.CartesianEllipsoidalBase, 

663 m.LatLonEllipsoidalBase, point=point) 

664 return ll 

665 

666 

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

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

669 ''' 

670 try: # C{_Error} style 

671 return Error(*args, **kwds) 

672 except TypeError: # no keyword arguments 

673 pass 

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

675 E = Error(str(e)) 

676 if _exception_chaining: 

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

678 return E 

679 

680 

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

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

683 

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

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

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

687 ''' 

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

689 

690 

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

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

693 

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

695 ''' 

696 x = isError(exc) 

697 if x: 

698 E = type(exc) 

699 elif x is None: 

700 E = _AssertionError 

701 else: # get _Error from Error 

702 n = NN(_UNDER_, _tailof(type(exc).__name__)) 

703 E = _MODS.getattr(__name__, n, _NotImplementedError) 

704 x = E is not _NotImplementedError 

705 return E, (str(exc) if x else repr(exc)) 

706 

707 

708_XErrors = (_AssertionError, _AttributeError, # some isError's 

709 _TypeError, _ValueError, _ZeroDivisionError) 

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

711# _X2Error = {AssertionError: _AssertionError, ... 

712# ZeroDivisionError: _ZeroDivisionError} 

713 

714try: 

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

716 

717 def _xkwds(kwds, **dflts): 

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

719 ''' 

720 return (dflts | kwds) if kwds else dflts 

721 

722except AttributeError: 

723 

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

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

726 ''' 

727 d = dflts 

728 if kwds: 

729 d = _copy(d) 

730 d.update(kwds) 

731 return d 

732 

733 

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

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

736# ''' 

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

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

739# if b is None: # invalid bool attr 

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

741# raise _AttributeError(t, txt_not_='applicable') 

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

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

744 

745 

746def _xkwds_get(kwds, **name_default): 

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

748 C{default} if not present. 

749 ''' 

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

751 for n, v in name_default.items(): 

752 return kwds.get(n, v) 

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

754 

755 

756def _xkwds_get_(kwds, **names_defaults): 

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

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

759 ''' 

760 if not isinstance(kwds, dict): 

761 raise _xAssertionError(_xkwds_get_, kwds) 

762 for n, v in _MODS.basics.itemsorted(names_defaults): 

763 yield kwds.get(n, v) 

764 

765 

766def _xkwds_get1(kwds, **name_default): 

767 '''(INTERNAL) Get one C{kwds} value by C{name} or the 

768 C{default} if not present. 

769 ''' 

770 v, kwds = _xkwds_pop2(kwds, **name_default) 

771 if kwds: 

772 raise _UnexpectedError(**kwds) 

773 return v 

774 

775 

776def _xkwds_item2(kwds): 

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

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

779 ''' 

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

781 for item in kwds.items(): 

782 return item 

783 raise _xAssertionError(_xkwds_item2, kwds) 

784 

785 

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

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

788 ''' 

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

790 

791 

792def _xkwds_pop2(kwds, **name_default): 

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

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

795 ''' 

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

797 for n, v in name_default.items(): 

798 if n in kwds: 

799 kwds = _copy(kwds) 

800 v = kwds.pop(n, v) 

801 return v, kwds 

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

803 

804 

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

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

807 ''' 

808 X, m = _xkwds_item2(Xorder) 

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

810 return m 

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

812 raise Error(X, m, txt_not_=_or(*t)) 

813 

814# **) MIT License 

815# 

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

817# 

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

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

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

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

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

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

824# 

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

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

827# 

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

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

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

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

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

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

834# OTHER DEALINGS IN THE SOFTWARE.