Coverage for pygeodesy/errors.py: 92%

319 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-06 12: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, 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 _DUNDER_nameof_, _getPYGEODESY, _plural, _tailof 

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

19 _COMMASPACE_, _datum_, _ELLIPSIS_, _ellipsoidal_, _incompatible_, \ 

20 _invalid_, _keyword_, _LatLon_, _len_, _not_, _or_, _SPACE_, \ 

21 _specified_, _UNDER_, _vs_, _with_ 

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

23# from pygeodesy import streprs as _streprs # _MODS 

24# from pygeodesy.unitsBase import Str # _MODS 

25# from pygeodesy.vector3dBase import Vector3dBase # _MODS 

26 

27from copy import copy as _copy 

28 

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

30__version__ = '24.11.02' 

31 

32_argument_ = 'argument' 

33_box_ = 'box' 

34_expected_ = 'expected' 

35_limiterrors = True # in .formy 

36_name_value_ = repr('name=value') 

37_rangerrors = True # in .dms 

38_region_ = 'region' 

39_streprs = _MODS.into(streprs=__name__) 

40_vs__ = _SPACE_(NN, _vs_, NN) 

41 

42try: 

43 _exception_chaining = None # not available 

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

45 

46 if _PYTHON_X_DEV or _getPYGEODESY('EXCEPTION_CHAINING'): # == _std_ 

47 _exception_chaining = True # turned on, std 

48 raise AttributeError() # allow exception chaining 

49 

50 _exception_chaining = False # turned off 

51 

52 def _error_cause(inst, cause=None): 

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

54 

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

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

57 

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

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

60 or C{None} to avoid exception chaining. 

61 

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

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

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

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

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

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

68 ''' 

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

70 return inst 

71 

72except AttributeError: # Python 2+ 

73 

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

75 return inst # no-op 

76 

77 

78class _AssertionError(AssertionError): 

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

80 ''' 

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

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

83 

84 

85class _AttributeError(AttributeError): 

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

87 ''' 

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

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

90 

91 

92class _ImportError(ImportError): 

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

94 ''' 

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

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

97 

98 

99class _IndexError(IndexError): 

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

101 ''' 

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

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

104 

105 

106class _KeyError(KeyError): 

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

108 ''' 

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

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

111 

112 

113class _NameError(NameError): 

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

115 ''' 

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

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

118 

119 

120class _NotImplementedError(NotImplementedError): 

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

122 ''' 

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

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

125 

126 

127class _OverflowError(OverflowError): 

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

129 ''' 

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

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

132 

133 

134class _TypeError(TypeError): 

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

136 ''' 

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

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

139 

140 

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

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

143 ''' 

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

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

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

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

148 

149 

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

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

152 ''' 

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

154 n = len(kwds) 

155 if args: 

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

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

158 else: 

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

160 u = _streprs.unstr(_SPACE_(n, NN), *args, **kwds) 

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

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

163 

164 

165class _ValueError(ValueError): 

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

167 ''' 

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

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

170 

171 

172class _ZeroDivisionError(ZeroDivisionError): 

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

174 ''' 

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

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

177 

178 

179class AuxError(_ValueError): 

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

181 ''' 

182 pass 

183 

184 

185class ClipError(_ValueError): 

186 '''Clip box or clip region issue. 

187 ''' 

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

189 '''New L{ClipError}. 

190 

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

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

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

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

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

196 for exception chaining. 

197 ''' 

198 if len(name_n_corners) == 3: 

199 t, n, v = name_n_corners 

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

201 name_n_corners = n, v 

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

203 

204 

205class CrossError(_ValueError): 

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

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

208 ''' 

209 pass 

210 

211 

212class GeodesicError(_ValueError): 

213 '''Error raised for convergence or other issues in L{geodesicx<pygeodesy.geodesicx>}, 

214 L{geodesicw<pygeodesy.geodesicw>} or L{karney<pygeodesy.karney>}. 

215 ''' 

216 pass 

217 

218 

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

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

221 ''' 

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

223 '''New L{IntersectionError}. 

224 ''' 

225 if args: 

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

227 else: 

228 _ValueError.__init__(self, **kwds) 

229 

230 

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

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

233 ''' 

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

235 '''New L{LenError}. 

236 

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

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

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

240 (C{keyword arguments}). 

241 ''' 

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

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

244 return ns, vs, txt, cause 

245 

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

247 ns = _COMMASPACE_.join(ns) 

248 t = _streprs.Fmt.PAREN(where.__name__, ns) 

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

250 t = _SPACE_(t, _len_, vs) 

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

252 

253 

254class LimitError(_ValueError): 

255 '''Error raised for lat- or longitudinal values or deltas exceeding the given 

256 B{C{limit}} in functions L{equirectangular<pygeodesy.equirectangular>}, 

257 L{equirectangular4<pygeodesy.equirectangular4>}, C{nearestOn*} and 

258 C{simplify*} or methods with C{limit} or C{options} keyword arguments. 

259 

260 @see: Subclass L{UnitError}. 

261 ''' 

262 pass 

263 

264 

265class MGRSError(_ValueError): 

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

267 ''' 

268 pass 

269 

270 

271class NumPyError(_ValueError): 

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

273 ''' 

274 pass 

275 

276 

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

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

279 ''' 

280 pass 

281 

282 

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

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

285 ''' 

286 pass 

287 

288 

289class RangeError(_ValueError): 

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

291 B{C{clipLon}} in functions L{parse3llh<pygeodesy.dms.parse3llh>}, L{parseDMS<pygeodesy.dms.parseDMS>}, 

292 L{parseDMS2<pygeodesy.dms.parseDMS2>} and L{parseRad<pygeodesy.dms.parseRad>} or the B{C{limit}} set 

293 with functions L{clipDegrees<pygeodesy.dms.clipDegrees>} and L{clipRadians<pygeodesy.dms.clipRadians>}. 

294 

295 @see: Function L{rangerrors<pygeodesy.errors.rangerrors>}. 

296 ''' 

297 pass 

298 

299 

300class RhumbError(_ValueError): 

301 '''Error raised for a rhumb L{aux_<pygeodesy.rhumb.aux_>}, L{ekx<pygeodesy.rhumb.ekx>} or 

302 L{solve<pygeodesy.rhumb.solve>} issue. 

303 ''' 

304 pass 

305 

306 

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

308 '''Error raised for triangle, intersection or resection issues. 

309 ''' 

310 pass 

311 

312 

313class SciPyError(PointsError): 

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

315 ''' 

316 pass 

317 

318 

319class SciPyWarning(PointsError): 

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

321 

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

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

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

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

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

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

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

329 ''' 

330 pass 

331 

332 

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

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

335 conversion issue. 

336 ''' 

337 pass 

338 

339 

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

341 '''Default exception for L{units} issues for a value exceeding the C{low} 

342 or C{high} limit. 

343 ''' 

344 pass 

345 

346 

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

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

349 ''' 

350 pass 

351 

352 

353def _an(noun): 

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

355 on the pronounciation of the first letter. 

356 ''' 

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

358 return _SPACE_(a, noun) 

359 

360 

361def _and(*words): 

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

363 ''' 

364 return _and_or(_and_, *words) 

365 

366 

367def _and_or(last, *words): 

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

369 ''' 

370 t, w = NN, list(words) 

371 if w: 

372 t = w.pop() 

373 if w: 

374 w = _COMMASPACE_.join(w) 

375 t = _SPACE_(w, last, t) 

376 return t 

377 

378 

379def crosserrors(raiser=None): 

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

381 

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

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

384 leave the setting unchanged. 

385 

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

387 

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

389 ''' 

390 V = _MODS.vector3dBase.Vector3dBase 

391 t = V._crosserrors # XXX class attr! 

392 if raiser in (True, False): 

393 V._crosserrors = raiser 

394 return t 

395 

396 

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

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

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

400 

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

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

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

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

405 particular for name conflicts with keyword 

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

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

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

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

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

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

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

413 exception chaining (supported in Python 3+). 

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

415 ''' 

416 def _fmtuple(pairs): 

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

418 

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

420 if n > 2: 

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

422 s = _MODS.basics.isodd(n) 

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

424 t += args[-1:] 

425 elif n == 2: 

426 t = (fmt_name_value % args), 

427 elif n: # == 1 

428 t = str(args[0]), 

429 if kwds: 

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

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

432 

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

434 else txt__.__name__) 

435 if x is not None: 

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

437 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_ 

438 t = C(t, x) 

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

440# x = NN # XXX or t? 

441 Error.__init__(inst, t) 

442# inst.__x_txt__ = x # hold explanation 

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

444 _error_under(inst) 

445 

446 

447def _error_under(inst): 

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

449 ''' 

450 n = inst.__class__.__name__ # _tailof? 

451 if n.startswith(_UNDER_): 

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

453 return inst 

454 

455 

456def exception_chaining(exc=None): 

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

458 

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

460 

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

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

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

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

465 or C{None} if there is none. 

466 

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

468 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any 

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

470 ''' 

471 return _exception_chaining if exc is None else \ 

472 getattr(exc, '__cause__', None) 

473 

474 

475def _incompatible(this): 

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

477 ''' 

478 return _SPACE_(_incompatible_, _with_, this) 

479 

480 

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

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

483 

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

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

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

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

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

489 

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

491 ''' 

492 return _XError(Error, **txt_name_values_cause) 

493 

494 

495def isError(exc): 

496 '''Check a (caught) exception. 

497 

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

499 

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

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

502 of C{None} if neither. 

503 ''' 

504 def _X(exc): 

505 X = type(exc) 

506 m = X.__module__ 

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

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

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

510 

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

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

513 

514 

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

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

517 

518 @arg types__: One or more types or type names. 

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

520 keyword arguments C{B{Error}=TypeError} and C{B{cause}=None} 

521 for exception chaining. 

522 

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

524 ''' 

525 x, kwds = _xkwds_pop2(name_value_Error_cause, cause=None) 

526 E, kwds = _xkwds_pop2(kwds, Error=TypeError) 

527 n, v = _xkwds_item2(kwds) 

528 

529 n = _streprs.Fmt.PARENSPACED(n, repr(v)) 

530 t = _not_(_an(_or(*_DUNDER_nameof_(*types__))) if types__ 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, **Error_name_values): # name=value[, ..., Error=ParseError] 

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

558 

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

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

561 @kwarg Error_name_values: Optional C{B{Error}=ParseError} 

562 and number of C{B{name}=value} pairs. 

563 

564 @return: Parser result. 

565 

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

567 ''' 

568 try: 

569 return parser(*args) 

570 except Exception as x: 

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

572 E, kwds = _xkwds_pop2(Error_name_values, Error=E) 

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

574 

575 

576def rangerrors(raiser=None): 

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

578 

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

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

581 the setting unchanged. 

582 

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

584 ''' 

585 global _rangerrors 

586 t = _rangerrors 

587 if raiser in (True, False): 

588 _rangerrors = raiser 

589 return t 

590 

591 

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

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

594 E = SciPyWarning 

595 else: 

596 E = SciPyError # PYCHOK not really 

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

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

599 

600 

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

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

603 ''' 

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

605 w = _streprs.unstr(where, *args, **kwds) 

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

607 

608 

609def _xattr(obj, **name_default): 

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

611 ''' 

612 if len(name_default) == 1: 

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

614 return getattr(obj, n, d) 

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

616 

617 

618def _xattrs(inst, other, *attrs): # see .errors._xattr 

619 '''(INTERNAL) Copy attribute values from B{C{other}} to B{C{inst}}. 

620 

621 @arg inst: Object to copy attribute values to (any C{type}). 

622 @arg other: Object to copy attribute values from (any C{type}). 

623 @arg attrs: One or more attribute names (C{str}s). 

624 

625 @return: Object B{C{inst}}, updated. 

626 

627 @raise AttributeError: An B{C{attrs}} doesn't exist or isn't settable. 

628 ''' 

629 def _getattr(o, a): 

630 if hasattr(o, a): 

631 return getattr(o, a) 

632 try: 

633 n = o._DOT_(a) 

634 except AttributeError: 

635 n = _streprs.Fmt.DOT(a) 

636 raise _AttributeError(o, name=n) 

637 

638 for a in attrs: 

639 s = _getattr(other, a) 

640 g = _getattr(inst, a) 

641 if (g is None and s is not None) or g != s: 

642 setattr(inst, a, s) # not settable? 

643 return inst 

644 

645 

646def _xcallable(**names_callables): 

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

648 ''' 

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

650 if not callable(c): 

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

652 

653 

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

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

656 ''' 

657 if Error: 

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

659 if e1 != e2: 

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

661 elif datum1 != datum2: 

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

663 _not_, repr(datum2.name)) 

664 raise _AssertionError(t) 

665 

666 

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

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

669 ''' 

670 if len(name_value) == 1: 

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

672 try: 

673 if v.isEllipsoidal: 

674 return v 

675 except AttributeError: 

676 pass 

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

678 raise _xAssertionError(_xellipsoidal, name_value) 

679 

680 

681def _xellipsoidall(point): # ... elel, see _xellipsoidal 

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

683 if geodetic latlon, C{False} if cartesian or TypeError. 

684 ''' 

685 m = _MODS.ellipsoidalBase 

686 ll = isinstance(point, m.LatLonEllipsoidalBase) 

687 if not ll: 

688 b = _MODS.basics 

689 b._xinstanceof(m.CartesianEllipsoidalBase, 

690 m.LatLonEllipsoidalBase, point=point) 

691 return ll 

692 

693 

694def _xellipsoids(E1, E2, Error=_ValueError): # see .ellipsoidalBase 

695 '''(INTERNAL) Check ellipsoid mis-match, E2 vs E1. 

696 ''' 

697 if E2 != E1: 

698 raise Error(E2.named2, txt=_incompatible(E1.named2)) 

699 return E1 

700 

701 

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

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

704 ''' 

705 try: # C{_Error} style 

706 return Error(*args, **kwds) 

707 except TypeError: # no keyword arguments 

708 pass 

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

710 E = Error(str(e)) 

711 if _exception_chaining: 

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

713 return E 

714 

715 

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

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

718 

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

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

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

722 ''' 

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

724 

725 

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

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

728 

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

730 ''' 

731 x = isError(exc) 

732 if x: 

733 E = type(exc) 

734 elif x is None: 

735 E = _AssertionError 

736 else: # get _Error from Error 

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

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

739 x = E is not _NotImplementedError 

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

741 

742 

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

744 _TypeError, _ValueError, _ZeroDivisionError) 

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

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

747# ZeroDivisionError: _ZeroDivisionError} 

748 

749 

750def _xgeodesics(G1, G2, Error=_ValueError): # see .geodesici 

751 '''(INTERNAL) Check geodesics mis-match. 

752 ''' 

753 if G1.ellipsoid != G2.ellipsoid: 

754 raise Error(G1.named2, txt=_incompatible(G2.named2)) 

755 return G1 

756 

757 

758try: 

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

760 

761 def _xkwds(kwds, **dflts): 

762 '''(INTERNAL) Update C{dflts} with specified C{kwds}, 

763 i.e. C{copy(kwds).update(dflts)}. 

764 ''' 

765 return ((dflts | kwds) if kwds else dflts) if dflts else kwds 

766 

767except AttributeError: 

768 

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

770 '''(INTERNAL) Update C{dflts} with specified C{kwds}, 

771 i.e. C{copy(kwds).update(dflts)}. 

772 ''' 

773 d = dflts 

774 if kwds: 

775 d = _copy(d) 

776 d.update(kwds) 

777 return d 

778 

779 

780# def _xkwds_bool(inst, **kwds): # unused 

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

782# ''' 

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

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

785# if b is None: # invalid bool attr 

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

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

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

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

790 

791 

792# def _xkwds_from(orig, *args, **kwds): # unused 

793# '''(INTERNAL) Return the items from C{orig} with the keys 

794# from C{kwds} and a value not in C{args} and C{kwds}. 

795# ''' 

796# def _items(orig, args, items): 

797# for n, m in items: 

798# if n in orig: # n in (orig.keys() & kwds.keys()) 

799# t = orig[n] 

800# if t is not m and t not in args: 

801# yield n, t 

802# 

803# return _items(orig, args, kwds.items()) 

804 

805 

806def _xkwds_get(kwds, **name_default): 

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

808 C{default} if not present. 

809 ''' 

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

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

812 return kwds.get(n, v) 

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

814 

815 

816# def _xkwds_get_(kwds, **names_defaults): # unused 

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

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

819# ''' 

820# if not isinstance(kwds, dict): 

821# raise _xAssertionError(_xkwds_get_, kwds) 

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

823# yield kwds.get(n, v) 

824 

825 

826def _xkwds_get1(kwds, **name_default): 

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

828 C{default} if not present. Raise an C{_UnexpectedError} 

829 with any remaining keyword arguments. 

830 ''' 

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

832 if kwds: 

833 raise _UnexpectedError(**kwds) 

834 return v 

835 

836 

837def _xkwds_item2(kwds): 

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

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

840 ''' 

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

842 for item in kwds.items(): 

843 return item 

844 raise _xAssertionError(_xkwds_item2, kwds) 

845 

846 

847def _xkwds_kwds(kwds, **names_defaults): # in .geodesici # PYCHOK no cover 

848 '''(INTERNAL) Return a C{dict} of C{named_defaults} items replaced with C{kwds}. 

849 ''' 

850 if not isinstance(kwds, dict): 

851 raise _xAssertionError(_xkwds_kwds, kwds) 

852 _g = kwds.get 

853 return dict((n, _g(n, v)) for n, v in names_defaults.items()) 

854 

855 

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

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

858 ''' 

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

860 

861 

862def _xkwds_pop(kwds, **name_default): 

863 '''(INTERNAL) Pop an item by C{name} from C{kwds} and 

864 return its value, otherwise return the C{default}. 

865 ''' 

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

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

868 return kwds.pop(n, v) 

869 raise _xAssertionError(_xkwds_pop, kwds, **name_default) 

870 

871 

872def _xkwds_pop2(kwds, **name_default): 

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

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

875 ''' 

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

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

878 if n in kwds: 

879 kwds = _copy(kwds) 

880 v = kwds.pop(n, v) 

881 return v, kwds 

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

883 

884 

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

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

887 ''' 

888 X, m = _xkwds_item2(Xorder) 

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

890 return m 

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

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

893 

894 

895def _xsError(X, xs, i, x, *n, **kwds): # in .fmath, ._fstats, .fsums 

896 '''(INTERNAL) Error for C{xs} or C{x}, item C{xs[i]}. 

897 ''' 

898 def _xs(*xs): 

899 if len(xs) > 4: 

900 xs = xs[:3] + (_ELLIPSIS_,) + xs[-1:] 

901 return xs 

902 

903 return ((_xError(X, n[0], _xs(*xs), **kwds) if n else 

904 _xError(X, xs=_xs(*xs), **kwds)) if x is xs else 

905 _xError(X, _streprs.Fmt.INDEX(xs=i), x, **kwds)) 

906 

907 

908def _xStrError(*Refs, **name_value_Error): # in .gars, .geohash, .wgrs 

909 '''(INTERNAL) Create a C{TypeError} for C{Garef}, C{Geohash}, C{Wgrs}. 

910 ''' 

911 S = _MODS.unitsBase.Str 

912 r = tuple(r.__name__ for r in Refs) + (S.__name__, _LatLon_, 'LatLon*Tuple') 

913 return _IsnotError(*r, **name_value_Error) 

914 

915# **) MIT License 

916# 

917# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

918# 

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

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

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

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

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

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

925# 

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

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

928# 

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

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

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

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

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

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

935# OTHER DEALINGS IN THE SOFTWARE.