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.internals import _plural, _tailof

18 from 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_

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

26 from copy import copy as _copy

27

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

29__version__ = '24.05.29'

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

40 try:

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

70 except AttributeError: # Python 2+

71

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

73 return inst # no-op

74

75

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

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

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

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

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

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

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

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

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

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

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

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

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

177 class AuxError(_ValueError):

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

179 '''

180 pass

181

182

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

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

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

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

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

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

263 class MGRSError(_ValueError):

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

265 '''

266 pass

267

268

269 class NumPyError(_ValueError):

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

271 '''

272 pass

273

274

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

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

277 '''

278 pass

279

280

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

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

283 '''

284 pass

285

286

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

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

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

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

308 '''

309 pass

310

311

312 class SciPyError(PointsError):

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

314 '''

315 pass

316

317

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

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

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

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

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

348 '''

349 pass

350

351

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

360 def _and(*words):

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

362 '''

363 return _and_or(_and_, *words)

364

365

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

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

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

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

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

474 def _incompatible(this):

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

476 '''

477 return _SPACE_(_incompatible_, _with_, this)

478

479

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

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

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

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

550 def _or(*words):

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

552 '''

553 return _and_or(_or_, *words)

554

555

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

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

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

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

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

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

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

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

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

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

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

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

714 try:

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

722 except 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

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

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

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

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

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

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

805 def _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.