Coverage for pygeodesy/errors.py: 93%

274 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-12-24 11:18 -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_, _name_, _no_, _not_, _or_, _SPACE_, \ 

19 _specified_, _UNDER_, _value_, _vs_, _with_ 

20from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _getenv, \ 

21 _pairs, _PYTHON_X_DEV 

22 

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

24__version__ = '23.12.14' 

25 

26_box_ = 'box' 

27_default_ = 'default' 

28_kwargs_ = 'kwargs' # XXX _kwds_? 

29_limiterrors = True # in .formy 

30_multiple_ = 'multiple' 

31_name_value_ = repr('name=value') 

32_rangerrors = True # in .dms 

33_region_ = 'region' 

34_vs__ = _SPACE_(NN, _vs_, NN) 

35 

36try: 

37 _exception_chaining = None # not available 

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

39 

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

41 _exception_chaining = True # turned on, std 

42 raise AttributeError # allow exception chaining 

43 

44 _exception_chaining = False # turned off 

45 

46 def _error_cause(inst, cause=None): 

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

48 

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

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

51 

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

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

54 or C{None} to avoid exception chaining. 

55 

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

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

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

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

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

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

62 ''' 

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

64 return inst 

65 

66except AttributeError: # Python 2+ 

67 

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

69 return inst # no-op 

70 

71 

72class _AssertionError(AssertionError): 

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

74 ''' 

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

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

77 

78 

79class _AttributeError(AttributeError): 

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

81 ''' 

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

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

84 

85 

86class _ImportError(ImportError): 

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

88 ''' 

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

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

91 

92 

93class _IndexError(IndexError): 

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

95 ''' 

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

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

98 

99 

100class _KeyError(KeyError): 

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

102 ''' 

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

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

105 

106 

107class _NameError(NameError): 

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

109 ''' 

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

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

112 

113 

114class _NotImplementedError(NotImplementedError): 

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

116 ''' 

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

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

119 

120 

121class _OverflowError(OverflowError): 

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

123 ''' 

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

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

126 

127 

128class _TypeError(TypeError): 

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

130 ''' 

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

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

133 

134 

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

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

137 ''' 

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

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

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

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

142 

143 

144class _ValueError(ValueError): 

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

146 ''' 

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

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

149 

150 

151class _ZeroDivisionError(ZeroDivisionError): 

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

153 ''' 

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

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

156 

157 

158class AuxError(_ValueError): 

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

160 ''' 

161 pass 

162 

163 

164class ClipError(_ValueError): 

165 '''Clip box or clip region issue. 

166 ''' 

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

168 '''New L{ClipError}. 

169 

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

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

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

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

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

175 for exception chaining. 

176 ''' 

177 if len(name_n_corners) == 3: 

178 t, n, v = name_n_corners 

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

180 name_n_corners = n, v 

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

182 

183 

184class CrossError(_ValueError): 

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

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

187 ''' 

188 pass 

189 

190 

191class GeodesicError(_ValueError): 

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

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

194 ''' 

195 pass 

196 

197 

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

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

200 ''' 

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

202 '''New L{IntersectionError}. 

203 ''' 

204 if args: 

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

206 else: 

207 _ValueError.__init__(self, **kwds) 

208 

209 

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

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

212 ''' 

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

214 '''New L{LenError}. 

215 

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

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

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

219 (C{keyword arguments}). 

220 ''' 

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

222 ns, vs = zip(*itemsorted(kwds)) # unzip 

223 return ns, vs, txt, cause 

224 

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

226 ns = _COMMASPACE_.join(ns) 

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

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

229 t = _SPACE_(t, _len_, vs) 

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

231 

232 

233class LimitError(_ValueError): 

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

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

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

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

238 

239 @see: Subclass L{UnitError}. 

240 ''' 

241 pass 

242 

243 

244class MGRSError(_ValueError): 

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

246 ''' 

247 pass 

248 

249 

250class NumPyError(_ValueError): 

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

252 ''' 

253 pass 

254 

255 

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

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

258 ''' 

259 pass 

260 

261 

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

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

264 ''' 

265 pass 

266 

267 

268class RangeError(_ValueError): 

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

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

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

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

273 L{pygeodesy.clipRadians}. 

274 

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

276 ''' 

277 pass 

278 

279 

280class RhumbError(_ValueError): 

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

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

283 ''' 

284 pass 

285 

286 

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

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

289 ''' 

290 pass 

291 

292 

293class SciPyError(PointsError): 

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

295 ''' 

296 pass 

297 

298 

299class SciPyWarning(PointsError): 

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

301 

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

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

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

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

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

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

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

309 ''' 

310 pass 

311 

312 

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

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

315 or L{RefFrame} conversion issue. 

316 ''' 

317 pass 

318 

319 

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

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

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

323 ''' 

324 pass 

325 

326 

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

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

329 ''' 

330 pass 

331 

332 

333def _an(noun): 

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

335 on the pronounciation of the first letter. 

336 ''' 

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

338 return _SPACE_(a, noun) 

339 

340 

341def _and(*words): 

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

343 ''' 

344 return _and_or(_and_, *words) 

345 

346 

347def _and_or(last, *words): 

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

349 ''' 

350 t, w = NN, list(words) 

351 if w: 

352 t = w.pop() 

353 if w: 

354 w = _COMMASPACE_.join(w) 

355 t = _SPACE_(w, last, t) 

356 return t 

357 

358 

359def crosserrors(raiser=None): 

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

361 

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

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

364 leave the setting unchanged. 

365 

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

367 

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

369 ''' 

370 B = _MODS.vector3dBase.Vector3dBase 

371 t = B._crosserrors # XXX class attr! 

372 if raiser in (True, False): 

373 B._crosserrors = raiser 

374 return t 

375 

376 

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

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

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

380 

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

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

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

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

385 particular for name conflicts with keyword 

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

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

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

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

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

391 exception chaining (supported in Python 3+). 

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

393 ''' 

394 def _fmtuple(pairs): 

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

396 

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

398 if n > 2: 

399 s = _MODS.basics.isodd(n) 

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

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

402 t += args[-1:] 

403 elif n == 2: 

404 t = (fmt_name_value % args), 

405 elif n: # == 1 

406 t = str(args[0]), 

407 

408 if kwds: 

409 t += _fmtuple(itemsorted(kwds)) 

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

411 

412 if txt is not None: 

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

414 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_ 

415 t = C(t, x) 

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

417# x = NN # XXX or t? 

418 Error.__init__(inst, t) 

419# inst.__x_txt__ = x # hold explanation 

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

421 _error_under(inst) 

422 

423 

424def _error_under(inst): 

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

426 ''' 

427 n = inst.__class__.__name__ 

428 if n.startswith(_UNDER_): 

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

430 return inst 

431 

432 

433def exception_chaining(error=None): 

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

435 

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

437 

438 @return: If C{B{error} is None}, return C{True} if exception 

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

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

441 B{C{error}} is not C{None}, return it's error 

442 I{cause} or C{None}. 

443 

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

445 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any 

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

447 ''' 

448 return _exception_chaining if error is None else \ 

449 getattr(error, '__cause__', None) 

450 

451 

452def _incompatible(this): 

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

454 ''' 

455 return _SPACE_(_incompatible_, _with_, this) 

456 

457 

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

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

460 

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

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

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

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

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

466 

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

468 ''' 

469 return _XError(Error, **txt_name_values_cause) 

470 

471 

472def isError(obj): 

473 '''Check a (caught) exception. 

474 

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

476 

477 @return: C{True} if B{C{obj}} is a C{pygeodesy} error, 

478 C{False} if B{C{obj}} is a standard Python error 

479 of C{None} if neither. 

480 ''' 

481 return True if isinstance(obj, _XErrors) else ( 

482 False if isinstance(obj, Exception) else None) 

483 

484 

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

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

487 

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

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

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

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

492 

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

494 ''' 

495 x, Error = _xkwds_pop_(name_value_Error_cause, cause=None, Error=TypeError) 

496 n, v = _xkwds_popitem(name_value_Error_cause) if name_value_Error_cause \ 

497 else (_name_value_, MISSING) # XXX else tuple(...) 

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

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

500 return _XError(Error, n, txt=t, cause=x) 

501 

502 

503def itemsorted(adict, *args, **asorted_reverse): 

504 '''Return the items of C{B{adict}} sorted I{alphabetically, case-insensitively} 

505 and in I{ascending} order. 

506 

507 @arg args: Optional argument(s) for method C{B{adict}.items(B*{args})}. 

508 @kwarg asorted_reverse: Use keyword argument C{B{asorted}=False} for 

509 I{case-sensitive} sorting and C{B{reverse}=True} for 

510 results in C{descending} order. 

511 ''' 

512 def _un(item): 

513 return item[0].lower() 

514 

515 # see .rhumb.Rhumb and ._RhumbLine 

516 a, r = _xkwds_get_(asorted_reverse, asorted=True, reverse=False) \ 

517 if asorted_reverse else (True, False) 

518 items = adict.items(*args) if args else adict.items() 

519 return sorted(items, reverse=r, key=_un if a else None) 

520 

521 

522def limiterrors(raiser=None): 

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

524 

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

526 ignore L{LimitError} exceptions. Use 

527 C{None} to leave the setting unchanged. 

528 

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

530 ''' 

531 global _limiterrors 

532 t = _limiterrors 

533 if raiser in (True, False): 

534 _limiterrors = raiser 

535 return t 

536 

537 

538def _or(*words): 

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

540 ''' 

541 return _and_or(_or_, *words) 

542 

543 

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

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

546 

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

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

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

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

551 argument to override the default. 

552 

553 @return: Parser result. 

554 

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

556 ''' 

557 try: 

558 return parser(*args) 

559 except Exception as x: 

560 E = _xkwds_pop(name_values_Error, Error=type(x) if isError(x) else 

561 ParseError) 

562 raise _XError(E, **_xkwds(name_values_Error, cause=x)) 

563 

564 

565def rangerrors(raiser=None): 

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

567 

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

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

570 the setting unchanged. 

571 

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

573 ''' 

574 global _rangerrors 

575 t = _rangerrors 

576 if raiser in (True, False): 

577 _rangerrors = raiser 

578 return t 

579 

580 

581def _SciPyIssue(x, *extras): # PYCHOK no cover 

582 if isinstance(x, (RuntimeWarning, UserWarning)): 

583 Error = SciPyWarning 

584 else: 

585 Error = SciPyError # PYCHOK not really 

586 t = _SPACE_(str(x).strip(), *extras) 

587 return Error(t, cause=x) 

588 

589 

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

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

592 ''' 

593 if len(name_default) == 1: 

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

595 return getattr(obj, n, d) 

596 raise _xkwds_Error(_xattr, {}, name_default) 

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), _not_, repr(datum2.name)) 

608 raise _AssertionError(t) 

609 

610 

611def _xellipsoidal(**name_value): 

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

613 

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

615 

616 @raise TypeError: Not ellipsoidal B{C{value}}. 

617 ''' 

618 try: 

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

620 if v.isEllipsoidal: 

621 return v 

622 break 

623 else: 

624 n = v = MISSING 

625 except AttributeError: 

626 pass 

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

628 

629 

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

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

632 ''' 

633 try: # C{_Error} style 

634 return Error(*args, **kwds) 

635 except TypeError: # no keyword arguments 

636 pass 

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

638 E = Error(str(e)) 

639 if _exception_chaining: 

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

641 return E 

642 

643 

644_XErrors = _TypeError, _ValueError 

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

646_X2Error = {AssertionError: _AssertionError, 

647 AttributeError: _AttributeError, 

648 ImportError: _ImportError, 

649 IndexError: _IndexError, 

650 KeyError: _KeyError, 

651 NameError: _NameError, 

652 NotImplementedError: _NotImplementedError, 

653 OverflowError: _OverflowError, 

654 TypeError: _TypeError, 

655 ValueError: _ValueError, 

656 ZeroDivisionError: _ZeroDivisionError} 

657 

658 

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

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

661 

662 @arg x: The exception (usually, C{_Error}). 

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

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

665 ''' 

666 return _XError(type(x), *args, **_xkwds(kwds, cause=x)) 

667 

668 

669def _xError2(x): # in .fsums 

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

671 

672 @arg x: The exception instance (usually, C{Exception}). 

673 ''' 

674 X = type(x) 

675 E = _X2Error.get(X, X) 

676 if E is X and not isError(x): 

677 E = _NotImplementedError 

678 t = repr(x) 

679 else: 

680 t = str(x) 

681 return E, t 

682 

683 

684try: 

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

686 

687 def _xkwds(kwds, **dflts): 

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

689 ''' 

690 return (dflts | kwds) if kwds else dflts 

691 

692except AttributeError: 

693 from copy import copy as _copy 

694 

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

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

697 ''' 

698 d = dflts 

699 if kwds: 

700 d = _copy(d) 

701 d.update(kwds) 

702 return d 

703 

704 

705def _xkwds_bool(inst, **kwds): # in .frechet, .hausdorff, .heights 

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

707 ''' 

708 for n, v in kwds.items(): 

709 b = getattr(inst, n, None) 

710 if b is None: # invalid bool attr 

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

712 raise _AttributeError(t, txt=_not_('applicable')) 

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

714 setattr(inst, NN(_UNDER_, n), v) 

715 

716 

717def _xkwds_Error(where, kwds, name_txt, txt=_default_): 

718 # Helper for _xkwds_get, _xkwds_pop and _xkwds_popitem below 

719 f = _COMMASPACE_.join(_pairs(kwds) + _pairs(name_txt)) 

720 f = _MODS.streprs.Fmt.PAREN(where.__name__, f) 

721 t = _multiple_ if name_txt else _no_ 

722 t = _SPACE_(t, _EQUAL_(_name_, txt), _kwargs_) 

723 return _AssertionError(f, txt=t) 

724 

725 

726def _xkwds_get(kwds, **name_default): 

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

728 ''' 

729 if len(name_default) == 1: 

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

731 return kwds.get(n, d) 

732 raise _xkwds_Error(_xkwds_get, kwds, name_default) 

733 

734 

735def _xkwds_get_(kwds, **names_defaults): 

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

737 in I{case-insensitive, alphabetical} order. 

738 ''' 

739 for n, d in itemsorted(names_defaults): 

740 yield kwds.get(n, d) 

741 

742 

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

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

745 ''' 

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

747 

748 

749def _xkwds_pop(kwds, **name_default): 

750 '''(INTERNAL) Pop a C{kwds} value by C{name}, or the C{default}. 

751 ''' 

752 if len(name_default) == 1: 

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

754 return kwds.pop(n, d) 

755 raise _xkwds_Error(_xkwds_pop, kwds, name_default) 

756 

757 

758def _xkwds_pop_(kwds, **names_defaults): 

759 '''(INTERNAL) Pop and yield each C{kwds} value or its C{default} 

760 in I{case-insensitive, alphabetical} order. 

761 ''' 

762 for n, d in itemsorted(names_defaults): 

763 yield kwds.pop(n, d) 

764 

765 

766def _xkwds_popitem(name_value): 

767 '''(INTERNAL) Return exactly one C{(name, value)} item. 

768 ''' 

769 if len(name_value) == 1: # XXX TypeError 

770 return name_value.popitem() # XXX AttributeError 

771 raise _xkwds_Error(_xkwds_popitem, (), name_value, txt=_value_) 

772 

773 

774def _Xorder(_Coeffs, Error, **Xorder): # in .ktm, .rhumbBase 

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

776 ''' 

777 X, m = Xorder.popitem() 

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

779 return m 

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

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

782 

783 

784def _xzip(*args, **strict): # PYCHOK no cover 

785 '''(INTERNAL) Standard C{zip(..., strict=True)}. 

786 ''' 

787 s = _xkwds_get(strict, strict=True) 

788 if s: 

789 _zip = _MODS.basics._zip 

790 if _zip is zip: # < (3, 10) 

791 t = _MODS.streprs.unstr(_xzip.__name__, *args, strict=s) 

792 raise _NotImplementedError(t, txt=None) 

793 return _zip(*args) 

794 return zip(*args) 

795 

796# **) MIT License 

797# 

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

799# 

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

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

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

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

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

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

806# 

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

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

809# 

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

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

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

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

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

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

816# OTHER DEALINGS IN THE SOFTWARE.