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 _DUNDER_nameof_, _getPYGEODESY, _plural, _tailof

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

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

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

21 _specified_, _UNDER_, _vs_, _with_

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

23# from pygeodesy import streprs as _streprs # _MODS

24# from pygeodesy.unitsBase import Str # _MODS

25# from pygeodesy.vector3dBase import Vector3dBase # _MODS

26

27 from copy import copy as _copy

28

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

30__version__ = '24.11.02'

31

32_argument_ = 'argument'

33_box_ = 'box'

34_expected_ = 'expected'

35_limiterrors = True # in .formy

36_name_value_ = repr('name=value')

37_rangerrors = True # in .dms

38_region_ = 'region'

39_streprs = _MODS.into(streprs=__name__)

40_vs__ = _SPACE_(NN, _vs_, NN)

41

42 try:

43 _exception_chaining = None # not available

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

45

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

47 _exception_chaining = True # turned on, std

48 raise AttributeError() # allow exception chaining

49

50 _exception_chaining = False # turned off

51

52 def _error_cause(inst, cause=None):

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

54

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

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

57

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

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

60 or C{None} to avoid exception chaining.

61

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

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

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

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

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

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

68 '''

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

70 return inst

71

72 except AttributeError: # Python 2+

73

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

75 return inst # no-op

76

77

78 class _AssertionError(AssertionError):

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

80 '''

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

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

83

84

85 class _AttributeError(AttributeError):

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

87 '''

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

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

90

91

92 class _ImportError(ImportError):

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

94 '''

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

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

97

98

99 class _IndexError(IndexError):

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

101 '''

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

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

104

105

106 class _KeyError(KeyError):

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

108 '''

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

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

111

112

113 class _NameError(NameError):

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

115 '''

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

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

118

119

120 class _NotImplementedError(NotImplementedError):

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

122 '''

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

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

125

126

127 class _OverflowError(OverflowError):

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

129 '''

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

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

132

133

134 class _TypeError(TypeError):

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

136 '''

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

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

139

140

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

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

143 '''

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

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

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

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

148

149

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

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

152 '''

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

154 n = len(kwds)

155 if args:

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

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

158 else:

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

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

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

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

163

164

165 class _ValueError(ValueError):

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

167 '''

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

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

170

171

172 class _ZeroDivisionError(ZeroDivisionError):

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

174 '''

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

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

177

178

179 class AuxError(_ValueError):

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

181 '''

182 pass

183

184

185 class ClipError(_ValueError):

186 '''Clip box or clip region issue.

187 '''

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

189 '''New L{ClipError}.

190

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

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

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

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

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

196 for exception chaining.

197 '''

198 if len(name_n_corners) == 3:

199 t, n, v = name_n_corners

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

201 name_n_corners = n, v

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

203

204

205 class CrossError(_ValueError):

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

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

208 '''

209 pass

210

211

212 class GeodesicError(_ValueError):

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

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

215 '''

216 pass

217

218

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

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

221 '''

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

223 '''New L{IntersectionError}.

224 '''

225 if args:

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

227 else:

228 _ValueError.__init__(self, **kwds)

229

230

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

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

233 '''

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

235 '''New L{LenError}.

236

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

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

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

240 (C{keyword arguments}).

241 '''

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

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

244 return ns, vs, txt, cause

245

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

247 ns = _COMMASPACE_.join(ns)

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

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

250 t = _SPACE_(t, _len_, vs)

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

252

253

254 class LimitError(_ValueError):

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

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

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

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

259

260 @see: Subclass L{UnitError}.

261 '''

262 pass

263

264

265 class MGRSError(_ValueError):

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

267 '''

268 pass

269

270

271 class NumPyError(_ValueError):

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

273 '''

274 pass

275

276

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

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

279 '''

280 pass

281

282

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

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

285 '''

286 pass

287

288

289 class RangeError(_ValueError):

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

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

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

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

294

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

296 '''

297 pass

298

299

300 class RhumbError(_ValueError):

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

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

303 '''

304 pass

305

306

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

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

309 '''

310 pass

311

312

313 class SciPyError(PointsError):

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

315 '''

316 pass

317

318

319 class SciPyWarning(PointsError):

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

321

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

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

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

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

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

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

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

329 '''

330 pass

331

332

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

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

335 conversion issue.

336 '''

337 pass

338

339

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

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

342 or C{high} limit.

343 '''

344 pass

345

346

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

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

349 '''

350 pass

351

352

353 def _an(noun):

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

355 on the pronounciation of the first letter.

356 '''

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

358 return _SPACE_(a, noun)

359

360

361 def _and(*words):

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

363 '''

364 return _and_or(_and_, *words)

365

366

367 def _and_or(last, *words):

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

369 '''

370 t, w = NN, list(words)

371 if w:

372 t = w.pop()

373 if w:

374 w = _COMMASPACE_.join(w)

375 t = _SPACE_(w, last, t)

376 return t

377

378

379 def crosserrors(raiser=None):

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

381

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

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

384 leave the setting unchanged.

385

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

387

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

389 '''

390 V = _MODS.vector3dBase.Vector3dBase

391 t = V._crosserrors # XXX class attr!

392 if raiser in (True, False):

393 V._crosserrors = raiser

394 return t

395

396

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

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

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

400

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

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

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

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

405 particular for name conflicts with keyword

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

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

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

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

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

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

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

413 exception chaining (supported in Python 3+).

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

415 '''

416 def _fmtuple(pairs):

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

418

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

420 if n > 2:

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

422 s = _MODS.basics.isodd(n)

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

424 t += args[-1:]

425 elif n == 2:

426 t = (fmt_name_value % args),

427 elif n: # == 1

428 t = str(args[0]),

429 if kwds:

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

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

432

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

434 else txt__.__name__)

435 if x is not None:

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

437 C = _COMMASPACE_ if _COLON_ in t else _COLONSPACE_

438 t = C(t, x)

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

440# x = NN # XXX or t?

441 Error.__init__(inst, t)

442# inst.__x_txt__ = x # hold explanation

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

444 _error_under(inst)

445

446

447 def _error_under(inst):

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

449 '''

450 n = inst.__class__.__name__ # _tailof?

451 if n.startswith(_UNDER_):

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

453 return inst

454

455

456 def exception_chaining(exc=None):

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

458

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

460

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

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

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

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

465 or C{None} if there is none.

466

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

468 set env var C{PYGEODESY_EXCEPTION_CHAINING} to any

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

470 '''

471 return _exception_chaining if exc is None else \

472 getattr(exc, '__cause__', None)

473

474

475 def _incompatible(this):

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

477 '''

478 return _SPACE_(_incompatible_, _with_, this)

479

480

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

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

483

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

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

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

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

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

489

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

491 '''

492 return _XError(Error, **txt_name_values_cause)

493

494

495 def isError(exc):

496 '''Check a (caught) exception.

497

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

499

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

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

502 of C{None} if neither.

503 '''

504 def _X(exc):

505 X = type(exc)

506 m = X.__module__

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

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

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

510

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

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

513

514

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

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

517

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

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

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

521 for exception chaining.

522

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

524 '''

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

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

527 n, v = _xkwds_item2(kwds)

528

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

530 t = _not_(_an(_or(*_DUNDER_nameof_(*types__))) if types__ else _specified_)

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

532

533

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, **Error_name_values): # name=value[, ..., Error=ParseError]

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

558

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

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

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

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

563

564 @return: Parser result.

565

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

567 '''

568 try:

569 return parser(*args)

570 except Exception as x:

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

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

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

574

575

576 def rangerrors(raiser=None):

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

578

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

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

581 the setting unchanged.

582

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

584 '''

585 global _rangerrors

586 t = _rangerrors

587 if raiser in (True, False):

588 _rangerrors = raiser

589 return t

590

591

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

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

594 E = SciPyWarning

595 else:

596 E = SciPyError # PYCHOK not really

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

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

599

600

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

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

603 '''

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

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

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

607

608

609 def _xattr(obj, **name_default):

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

611 '''

612 if len(name_default) == 1:

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

614 return getattr(obj, n, d)

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

616

617

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

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

620

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

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

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

624

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

626

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

628 '''

629 def _getattr(o, a):

630 if hasattr(o, a):

631 return getattr(o, a)

632 try:

633 n = o._DOT_(a)

634 except AttributeError:

635 n = _streprs.Fmt.DOT(a)

636 raise _AttributeError(o, name=n)

637

638 for a in attrs:

639 s = _getattr(other, a)

640 g = _getattr(inst, a)

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

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

643 return inst

644

645

646 def _xcallable(**names_callables):

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

648 '''

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

650 if not callable(c):

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

652

653

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

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

656 '''

657 if Error:

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

659 if e1 != e2:

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

661 elif datum1 != datum2:

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

663 _not_, repr(datum2.name))

664 raise _AssertionError(t)

665

666

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

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

669 '''

670 if len(name_value) == 1:

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

672 try:

673 if v.isEllipsoidal:

674 return v

675 except AttributeError:

676 pass

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

678 raise _xAssertionError(_xellipsoidal, name_value)

679

680

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

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

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

684 '''

685 m = _MODS.ellipsoidalBase

686 ll = isinstance(point, m.LatLonEllipsoidalBase)

687 if not ll:

688 b = _MODS.basics

689 b._xinstanceof(m.CartesianEllipsoidalBase,

690 m.LatLonEllipsoidalBase, point=point)

691 return ll

692

693

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

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

696 '''

697 if E2 != E1:

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

699 return E1

700

701

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

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

704 '''

705 try: # C{_Error} style

706 return Error(*args, **kwds)

707 except TypeError: # no keyword arguments

708 pass

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

710 E = Error(str(e))

711 if _exception_chaining:

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

713 return E

714

715

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

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

718

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

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

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

722 '''

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

724

725

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

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

728

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

730 '''

731 x = isError(exc)

732 if x:

733 E = type(exc)

734 elif x is None:

735 E = _AssertionError

736 else: # get _Error from Error

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

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

739 x = E is not _NotImplementedError

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

741

742

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

744 _TypeError, _ValueError, _ZeroDivisionError)

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

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

747# ZeroDivisionError: _ZeroDivisionError}

748

749

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

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

752 '''

753 if G1.ellipsoid != G2.ellipsoid:

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

755 return G1

756

757

758 try:

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

760

761 def _xkwds(kwds, **dflts):

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

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

764 '''

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

766

767 except AttributeError:

768

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

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

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

772 '''

773 d = dflts

774 if kwds:

775 d = _copy(d)

776 d.update(kwds)

777 return d

778

779

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

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

782# '''

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

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

785# if b is None: # invalid bool attr

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

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

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

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

790

791

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

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

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

795# '''

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

797# for n, m in items:

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

799# t = orig[n]

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

801# yield n, t

802#

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

804

805

806 def _xkwds_get(kwds, **name_default):

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

808 C{default} if not present.

809 '''

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

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

812 return kwds.get(n, v)

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

814

815

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

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

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

819# '''

820# if not isinstance(kwds, dict):

821# raise _xAssertionError(_xkwds_get_, kwds)

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

823# yield kwds.get(n, v)

824

825

826 def _xkwds_get1(kwds, **name_default):

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

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

829 with any remaining keyword arguments.

830 '''

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

832 if kwds:

833 raise _UnexpectedError(**kwds)

834 return v

835

836

837 def _xkwds_item2(kwds):

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

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

840 '''

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

842 for item in kwds.items():

843 return item

844 raise _xAssertionError(_xkwds_item2, kwds)

845

846

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

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

849 '''

850 if not isinstance(kwds, dict):

851 raise _xAssertionError(_xkwds_kwds, kwds)

852 _g = kwds.get

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

854

855

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

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

858 '''

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

860

861

862 def _xkwds_pop(kwds, **name_default):

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

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

865 '''

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

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

868 return kwds.pop(n, v)

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

870

871

872 def _xkwds_pop2(kwds, **name_default):

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

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

875 '''

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

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

878 if n in kwds:

879 kwds = _copy(kwds)

880 v = kwds.pop(n, v)

881 return v, kwds

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

883

884

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

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

887 '''

888 X, m = _xkwds_item2(Xorder)

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

890 return m

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

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

893

894

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

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

897 '''

898 def _xs(*xs):

899 if len(xs) > 4:

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

901 return xs

902

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

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

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

906

907

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

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

910 '''

911 S = _MODS.unitsBase.Str

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

913 return _IsnotError(*r, **name_value_Error)

914

915# **) MIT License

916#

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

918#

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

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

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

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

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

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

925#

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

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

928#

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

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

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

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

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

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

935# OTHER DEALINGS IN THE SOFTWARE.