Coverage for pygeodesy/errors.py: 91%

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 import basics as _basics # _MODS.into

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

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

17 from pygeodesy.internals import _envPYGEODESY, _plural, _tailof, typename

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

19 _COMMASPACE_, _datum_, _DOT_, _ELLIPSIS_, _ellipsoidal_, \

20 _EQUALSPACED_, _immutable_, _incompatible_, _invalid_, _keyword_, \

21 _LatLon_, _len_, _not_, _or_, _SPACE_, _specified_, _UNDER_, \

22 _vs_, _with_

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

24# from pygeodesy import streprs as _streprs # _MODS.into

25# from pygeodesy.unitsBase import Str # _MODS

26# from pygeodesy.vector3dBase import Vector3dBase # _MODS

27

28 from copy import copy as _copy

29

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

31 __version__ = '26.02.12'

32

33 _argument_ = 'argument'

34 _basics = _MODS.into(basics=__name__)

35 _box_ = 'box'

36 _del_ = 'del'

37 _expected_ = 'expected'

38 _limiterrors = True # in .formy

39 _name_value_ = repr('name=value')

40 _rangerrors = True # in .dms

41 _region_ = 'region'

42 _streprs = _MODS.into(streprs=__name__)

43 _vs__ = _SPACE_(NN, _vs_, NN)

44

45 try:

46 _exception_chaining = None # not available

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

48

49 if _PYTHON_X_DEV or _envPYGEODESY('EXCEPTION_CHAINING'): # == _std_

50 _exception_chaining = True # turned on, std

51 raise AttributeError() # allow exception chaining

52

53 _exception_chaining = False # turned off

54

55 def _error_cause(inst, cause=None):

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

57

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

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

60

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

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

63 or C{None} to avoid exception chaining.

64

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

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

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

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

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

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

71 '''

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

73 return inst

74

75 except AttributeError: # Python 2+

76

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

78 return inst # no-op

79

80

81 class _AssertionError(AssertionError):

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

83 '''

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

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

86

87

88 class _AttributeError(AttributeError):

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

90 '''

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

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

93

94

95 class _ConvergenceError(ValueError): # in .ellipses, .elliptic

96 '''(INTERNAL) Format a C{ConvergenceError}.

97 '''

98 def __init__(self, maxit, d, tol, **thresh): # PYCHOK cause=None

99 t = _streprs.Fmt.no_convergence(d, tol, **thresh)

100 _error_init(ValueError, self, (), maxit=maxit, txt=t)

101

102

103 class _ImportError(ImportError):

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

105 '''

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

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

108

109

110 class _IndexError(IndexError):

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

112 '''

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

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

115

116

117 class _KeyError(KeyError):

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

119 '''

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

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

122

123

124 class _NameError(NameError):

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

126 '''

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

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

129

130

131 class _NotImplementedError(NotImplementedError):

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

133 '''

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

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

136

137

138 class _OverflowError(OverflowError):

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

140 '''

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

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

143

144

145 class _TypeError(TypeError):

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

147 '''

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

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

150

151

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

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

154 '''

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

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

157 t = _an(_or(*map(typename, Types, Types)))

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

159

160

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

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

163 '''

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

165 n = len(kwds)

166 if args:

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

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

169 else:

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

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

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

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

174

175

176 class _ValueError(ValueError):

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

178 '''

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

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

181

182

183 class _ZeroDivisionError(ZeroDivisionError):

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

185 '''

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

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

188

189

190 class AuxError(_ValueError):

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

192 '''

193 pass

194

195

196 class ClipError(_ValueError):

197 '''Clip box or clip region issue.

198 '''

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

200 '''New L{ClipError}.

201

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

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

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

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

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

207 for exception chaining.

208 '''

209 if len(name_n_corners) == 3:

210 t, n, v = name_n_corners

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

212 name_n_corners = n, v

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

214

215

216 class CrossError(_ValueError):

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

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

219 '''

220 pass

221

222

223 class GeodesicError(_ValueError):

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

225 L{geodesicw<pygeodesy.geodesicw>}, L{geodsolve<pygeodesy.geodsolve>} or

226 L{karney<pygeodesy.karney>}.

227 '''

228 pass

229

230

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

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

233 '''

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

235 '''New L{IntersectionError}.

236 '''

237 if args:

238 t = _COMMASPACE_(*map(repr, args))

239 _ValueError.__init__(self, t, **kwds)

240 else:

241 _ValueError.__init__(self, **kwds)

242

243

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

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

246 '''

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

248 '''New L{LenError}.

249

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

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

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

253 (C{keyword arguments}).

254 '''

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

256 ns, vs = zip(*_basics.itemsorted(kwds)) # unzip

257 return ns, vs, txt, cause

258

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

260 ns = _COMMASPACE_.join(ns)

261 t = _streprs.Fmt.PAREN(typename(where), ns)

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

263 t = _SPACE_(t, _len_, vs)

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

265

266

267 class LimitError(_ValueError):

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

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

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

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

272

273 @see: Subclass L{UnitError}.

274 '''

275 pass

276

277

278 class MGRSError(_ValueError):

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

280 '''

281 pass

282

283

284 class NumPyError(_ValueError):

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

286 '''

287 pass

288

289

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

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

292 '''

293 pass

294

295

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

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

298 '''

299 pass

300

301

302 class RangeError(_ValueError):

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

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

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

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

307

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

309 '''

310 pass

311

312

313 class RhumbError(_ValueError):

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

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

316 '''

317 pass

318

319

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

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

322 '''

323 pass

324

325

326 class SciPyError(PointsError):

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

328 '''

329 pass

330

331

332 class SciPyWarning(PointsError):

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

334

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

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

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

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

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

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

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

342 '''

343 pass

344

345

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

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

348 conversion issue.

349 '''

350 pass

351

352

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

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

355 or C{high} limit.

356 '''

357 pass

358

359

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

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

362 '''

363 pass

364

365

366 def _an(noun):

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

368 on the pronounciation of the first letter.

369 '''

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

371 return _SPACE_(a, noun)

372

373

374 def _and(*words):

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

376 '''

377 return _and_or(_and_, *words)

378

379

380 def _and_or(last, *words):

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

382 '''

383 t, w = NN, list(words)

384 if w:

385 t = w.pop()

386 if w:

387 w = _COMMASPACE_.join(w)

388 t = _SPACE_(w, last, t)

389 return t

390

391

392 def crosserrors(raiser=None):

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

394

395 @kwarg raiser: Use C{True} to throw, C{False} to ignore

396 L{CrossError} exceptions or C{None} to

397 leave the setting unchanged.

398

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

400

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

402 '''

403 V = _MODS.vector3dBase.Vector3dBase

404 t = V._crosserrors # XXX class attr!

405 if raiser is not None:

406 V._crosserrors = bool(raiser)

407 return t

408

409

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

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

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

413

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

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

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

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

418 particular for name conflicts with keyword

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

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

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

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

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

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

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

426 exception chaining (supported in Python 3+).

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

428 '''

429 def _fmtuple(pairs):

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

431

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

433 if n > 2:

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

435 s = _basics.isodd(n)

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

437 t += args[-1:]

438 elif n == 2:

439 t = (fmt_name_value % args),

440 elif n: # == 1

441 t = str(args[0]),

442 if kwds:

443 t += _fmtuple(_basics.itemsorted(kwds))

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

445

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

447 else typename(txt__))

448 if x is not None:

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

450 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_

451 t = C(t, x)

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

453# x = NN # XXX or t?

454 Error.__init__(inst, t)

455# inst.__x_txt__ = x # hold explanation

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

457 _error_under(inst)

458

459

460 def _error_under(inst):

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

462 '''

463 t = type(inst)

464 n = typename(t) # _tailof?

465 if n.startswith(_UNDER_):

466 t.__name__ = n.lstrip(_UNDER_)

467 return inst

468

469

470 def exception_chaining(exc=None):

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

472

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

474

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

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

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

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

479 or C{None} if there is none.

480

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

482 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any

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

484 '''

485 return _exception_chaining if exc is None else \

486 getattr(exc, '__cause__', None) # _DCAUSE_

487

488

489 def _ImmutableError(inst, attr, value=_del_, Error=_TypeError): # PYCHOK self

490 '''(INTERNAL) Format an C{immutable _TypeError}.

491 '''

492 n = typename(inst)

493 n = _DOT_(_xattr(inst, name=n), attr)

494 t = _SPACE_(_del_, n) if value is _del_ else \

495 _EQUALSPACED_(n, repr(value))

496 return Error(_immutable_, txt=t)

497

498

499 def _incompatible(this):

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

501 '''

502 return _SPACE_(_incompatible_, _with_, this)

503

504

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

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

507

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

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

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

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

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

513

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

515 '''

516 return _XError(Error, **txt_name_values_cause)

517

518

519 def isError(exc):

520 '''Check a (caught) exception.

521

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

523

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

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

526 of C{None} if neither.

527 '''

528 def _X(exc):

529 X = type(exc)

530 m = X.__module__

531 return _basics.issubclassof(X, *_XErrors) or \

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

533 _tailof(typename(X)).startswith(_UNDER_))

534

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

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

537

538

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

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

541

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

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

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

545 for exception chaining.

546

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

548 '''

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

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

551 n, v = _xkwds_item2(kwds)

552

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

554 t = _an(_or(*map(typename, types__, types__))) if types__ else _specified_

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

556

557

558 def limiterrors(raiser=None):

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

560

561 @kwarg raiser: Use C{True} to raise, C{False} to

562 ignore L{LimitError} exceptions or

563 C{None} to leave the setting unchanged.

564

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

566 '''

567 global _limiterrors

568 t = _limiterrors

569 if raiser is not None:

570 _limiterrors = bool(raiser)

571 return t

572

573

574 def _or(*words):

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

576 '''

577 return _and_or(_or_, *words)

578

579

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

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

582

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

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

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

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

587

588 @return: Parser result.

589

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

591 '''

592 try:

593 return parser(*args)

594 except Exception as x:

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

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

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

598

599

600 def rangerrors(raiser=None):

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

602

603 @kwarg raiser: Use C{True} to raise or C{False} to ignore

604 L{RangeError} exceptions or C{None} to leave

605 the setting unchanged.

606

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

608 '''

609 global _rangerrors

610 t = _rangerrors

611 if raiser is not None:

612 _rangerrors = bool(raiser)

613 return t

614

615

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

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

618 E = SciPyWarning

619 else:

620 E = SciPyError # PYCHOK not really

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

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

623

624

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

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

627 '''

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

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

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

631

632

633 def _xattr(obj, **name_default):

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

635 '''

636 if len(name_default) == 1:

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

638 try: # obj is tuple

639 return getattr(obj, n, d)

640 except ValueError:

641 return d

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

643

644

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

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

647

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

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

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

651

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

653

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

655 '''

656 def _getattr(o, a):

657 if hasattr(o, a):

658 return getattr(o, a)

659 try:

660 n = o._DOT_(a)

661 except AttributeError:

662 n = _streprs.Fmt.DOT(a)

663 raise _AttributeError(o, name=n)

664

665 for a in attrs:

666 s = _getattr(other, a)

667 g = _getattr(inst, a)

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

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

670 return inst

671

672

673 def _xcallable(**names_callables):

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

675 '''

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

677 if not callable(c):

678 raise _TypeError(n, c, txt_not_=typename(callable)) # txt__

679

680

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

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

683 '''

684 if Error:

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

686 if e1 != e2:

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

688 elif datum1 != datum2:

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

690 _not_, repr(datum2.name))

691 raise _AssertionError(t)

692

693

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

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

696 '''

697 if len(name_value) == 1:

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

699 try:

700 if v.isEllipsoidal:

701 return v

702 except AttributeError:

703 pass

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

705 raise _xAssertionError(_xellipsoidal, name_value)

706

707

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

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

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

711 '''

712 m = _MODS.ellipsoidalBase

713 ll = isinstance(point, m.LatLonEllipsoidalBase)

714 if not ll:

715 _basics._xinstanceof(m.CartesianEllipsoidalBase,

716 m.LatLonEllipsoidalBase, point=point)

717 return ll

718

719

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

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

722 '''

723 if E2 != E1:

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

725 return E1

726

727

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

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

730 '''

731 try: # C{_Error} style

732 return Error(*args, **kwds)

733 except TypeError: # no keyword arguments

734 pass

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

736 E = Error(str(e))

737 if _exception_chaining:

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

739 return E

740

741

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

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

744

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

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

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

748 '''

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

750

751

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

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

754

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

756 '''

757 x = isError(exc)

758 if x:

759 E = type(exc)

760 elif x is None:

761 E = _AssertionError

762 else: # get _Error from Error

763 n = NN(_UNDER_, _tailof(typename(type(exc))))

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

765 x = E is not _NotImplementedError

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

767

768

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

770 _TypeError, _ValueError, _ZeroDivisionError)

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

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

773# ZeroDivisionError: _ZeroDivisionError}

774

775

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

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

778 '''

779 if G1.ellipsoid != G2.ellipsoid:

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

781 return G1

782

783

784 try:

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

786

787 def _xkwds(kwds, **dflts):

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

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

790 '''

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

792

793 except AttributeError:

794

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

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

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

798 '''

799 d = dflts

800 if kwds:

801 d = _copy(d)

802 d.update(kwds)

803 return d

804

805

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

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

808# '''

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

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

811# if b is None: # invalid bool attr

812# t = _SPACE_(_EQUAL_(n, repr(v)), 'for', typename(type(inst)) # XXX .classname

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

814# if _basics.isbool(v) and v != b:

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

816

817

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

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

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

821# '''

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

823# for n, m in items:

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

825# t = orig[n]

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

827# yield n, t

828#

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

830

831

832 def _xkwds_get(kwds, **name_default):

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

834 C{default} if not present.

835 '''

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

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

838 return kwds.get(n, v)

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

840

841

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

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

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

845# '''

846# if not isinstance(kwds, dict):

847# raise _xAssertionError(_xkwds_get_, kwds)

848# for n, v in _basics.itemsorted(names_defaults):

849# yield kwds.get(n, v)

850

851

852 def _xkwds_get1(kwds, **name_default):

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

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

855 with any remaining keyword arguments.

856 '''

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

858 if kwds:

859 raise _UnexpectedError(**kwds)

860 return v

861

862

863 def _xkwds_item2(kwds):

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

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

866 '''

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

868 for item in kwds.items():

869 return item

870 raise _xAssertionError(_xkwds_item2, kwds)

871

872

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

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

875 '''

876 if not isinstance(kwds, dict):

877 raise _xAssertionError(_xkwds_kwds, kwds)

878 _g = kwds.get

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

880

881

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

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

884 '''

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

886

887

888 def _xkwds_pop(kwds, **name_default):

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

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

891 '''

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

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

894 return kwds.pop(n, v)

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

896

897

898 def _xkwds_pop2(kwds, **name_default):

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

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

901 '''

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

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

904 if n in kwds:

905 kwds = _copy(kwds)

906 v = kwds.pop(n, v)

907 return v, kwds

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

909

910

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

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

913 '''

914 X, m = _xkwds_item2(Xorder)

915 if m in _Coeffs and _basics.isint(m):

916 return m

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

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

919

920

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

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

923 '''

924 def _xs(*xs):

925 if len(xs) > 4:

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

927 return xs

928

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

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

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

932

933

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

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

936 '''

937 s = typename(_MODS.unitsBase.Str)

938 t = tuple(map(typename, Refs)) + (s, _LatLon_, 'LatLon*Tuple')

939 return _IsnotError(*t, **name_value_Error)

940

941# **) MIT License

942#

943# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.

944#

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

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

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

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

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

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

951#

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

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

954#

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

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

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

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

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

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

961# OTHER DEALINGS IN THE SOFTWARE.