1

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

3

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

5

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

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

8

9 By default, I{exception chaining} is turned I{off}. To enable I{exception

10 chaining}, use command line option C{python -X dev} I{OR} set env variable

11 C{PYTHONDEVMODE=1} or to any non-empty string I{OR} set env variable

12 C{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

17 from 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

21 from 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

25 from copy import copy as _copy

26

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

28__version__ = '24.05.06'

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

37 try:

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

67 except AttributeError: # Python 2+

68

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

70 return inst # no-op

71

72

73 class _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

80 class _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

87 class _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

94 class _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

101 class _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

108 class _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

115 class _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

122 class _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

129 class _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

136 def _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

145 class _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

152 class _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

159 class AuxError(_ValueError):

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

161 '''

162 pass

163

164

165 class 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

185 class 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

192 class 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

199 class 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

211 class 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

234 class 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

245 class MGRSError(_ValueError):

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

247 '''

248 pass

249

250

251 class NumPyError(_ValueError):

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

253 '''

254 pass

255

256

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

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

259 '''

260 pass

261

262

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

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

265 '''

266 pass

267

268

269 class 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

281 class 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

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

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

290 '''

291 pass

292

293

294 class SciPyError(PointsError):

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

296 '''

297 pass

298

299

300 class 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

314 class 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

321 class 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

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

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

330 '''

331 pass

332

333

334 def _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

342 def _and(*words):

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

344 '''

345 return _and_or(_and_, *words)

346

347

348 def _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

360 def 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

378 def _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 t = _fmtuple(zip(args[0::2], args[1::2]))

401 s = _MODS.basics.isodd(n)

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 if kwds:

409 t += _fmtuple(_MODS.basics.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

424 def _error_under(inst):

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

426 '''

427 n = inst.__class__.__name__ # _tailof?

428 if n.startswith(_UNDER_):

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

430 return inst

431

432

433 def exception_chaining(exc=None):

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

435

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

437

438 @return: If C{B{exc} 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{exc}} is not C{None}, return it's error I{cause}

442 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 exc is None else \

449 getattr(exc, '__cause__', None)

450

451

452 def _incompatible(this):

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

454 '''

455 return _SPACE_(_incompatible_, _with_, this)

456

457

458 def _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

472 def isError(exc):

473 '''Check a (caught) exception.

474

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

476

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

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

479 of C{None} if neither.

480 '''

481 def _X(exc):

482 X = type(exc)

483 m = X.__module__

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

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

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

487

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

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

490

491

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

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

494

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

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

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

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

499

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

501 '''

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

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

504

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

506

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

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

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

510

511

512 def limiterrors(raiser=None):

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

514

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

516 ignore L{LimitError} exceptions. Use

517 C{None} to leave the setting unchanged.

518

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

520 '''

521 global _limiterrors

522 t = _limiterrors

523 if raiser in (True, False):

524 _limiterrors = raiser

525 return t

526

527

528 def _or(*words):

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

530 '''

531 return _and_or(_or_, *words)

532

533

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

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

536

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

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

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

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

541 argument to override the default.

542

543 @return: Parser result.

544

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

546 '''

547 try:

548 return parser(*args)

549 except Exception as x:

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

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

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

553

554

555 def rangerrors(raiser=None):

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

557

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

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

560 the setting unchanged.

561

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

563 '''

564 global _rangerrors

565 t = _rangerrors

566 if raiser in (True, False):

567 _rangerrors = raiser

568 return t

569

570

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

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

573 E = SciPyWarning

574 else:

575 E = SciPyError # PYCHOK not really

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

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

578

579

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

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

582 '''

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

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

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

586

587

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

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

590 '''

591 if len(name_default) == 1:

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

593 return getattr(obj, n, d)

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

595

596

597 def _xcallable(**names_callables):

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

599 '''

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

601 if not callable(c):

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

603

604

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

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

607 '''

608 if Error:

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

610 if e1 != e2:

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

612 elif datum1 != datum2:

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

614 _not_, repr(datum2.name))

615 raise _AssertionError(t)

616

617

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

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

620 '''

621 if len(name_value) == 1:

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

623 try:

624 if v.isEllipsoidal:

625 return v

626 except AttributeError:

627 pass

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

629 raise _xAssertionError(_xellipsoidal, name_value)

630

631

632 def _xellipsoidall(point): # see _xellipsoidal

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

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

635 '''

636 m = _MODS.ellipsoidalBase

637 ll = isinstance(point, m.LatLonEllipsoidalBase)

638 if not ll:

639 b = _MODS.basics

640 b._xinstanceof(m.CartesianEllipsoidalBase,

641 m.LatLonEllipsoidalBase, point=point)

642 return ll

643

644

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

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

647 '''

648 try: # C{_Error} style

649 return Error(*args, **kwds)

650 except TypeError: # no keyword arguments

651 pass

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

653 E = Error(str(e))

654 if _exception_chaining:

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

656 return E

657

658

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

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

661

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

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

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

665 '''

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

667

668

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

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

671

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

673 '''

674 x = isError(exc)

675 if x:

676 E = type(exc)

677 elif x is None:

678 E = _AssertionError

679 else: # get _Error from Error

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

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

682 x = E is not _NotImplementedError

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

684

685

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

687 _TypeError, _ValueError, _ZeroDivisionError)

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

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

690# ZeroDivisionError: _ZeroDivisionError}

691

692 try:

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

694

695 def _xkwds(kwds, **dflts):

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

697 '''

698 return (dflts | kwds) if kwds else dflts

699

700 except AttributeError:

701

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

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

704 '''

705 d = dflts

706 if kwds:

707 d = _copy(d)

708 d.update(kwds)

709 return d

710

711

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

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

714# '''

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

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

717# if b is None: # invalid bool attr

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

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

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

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

722

723

724 def _xkwds_get(kwds, **name_default):

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

726 C{default} if not present.

727 '''

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

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

730 return kwds.get(n, d)

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

732

733

734 def _xkwds_get_(kwds, **names_defaults):

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

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

737 '''

738 if not isinstance(kwds, dict):

739 raise _xAssertionError(_xkwds_get_, kwds)

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

741 yield kwds.get(n, d)

742

743

744 def _xkwds_item2(kwds):

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

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

747 '''

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

749 for item in kwds.items():

750 return item

751 raise _xAssertionError(_xkwds_item2, kwds)

752

753

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

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

756 '''

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

758

759

760 def _xkwds_pop2(kwds, **name_default):

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

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

763 '''

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

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

766 if n in kwds:

767 kwds = _copy(kwds)

768 d = kwds.pop(n, d)

769 return d, kwds

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

771

772

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

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

775 '''

776 X, m = _xkwds_item2(Xorder)

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

778 return m

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

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

781

782# **) MIT License

783#

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

785#

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

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

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

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

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

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

792#

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

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

795#

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

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

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

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

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

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

802# OTHER DEALINGS IN THE SOFTWARE.