1

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

3

4 u'''Some, basic definitions, functions and dependencies.

5

6 Use env variable C{PYGEODESY_XPACKAGES} to avoid import of dependencies

7 C{geographiclib}, C{numpy} and/or C{scipy}. Set C{PYGEODESY_XPACKAGES}

8 to a comma-separated list of package names to be excluded from import.

9'''

10# make sure int/int division yields float quotient

11 from __future__ import division

12 division = 1 / 2 # .albers, .azimuthal, .constants, etc., .utily

13 if not division:

14 raise ImportError('%s 1/2 == %s' % ('division', division))

15 del division

16

17 from pygeodesy.errors import _AssertionError, _AttributeError, _ImportError, \

18 _TypeError, _TypesError, _ValueError, _xkwds_get, \

19 _xAssertionError

20 from pygeodesy.interns import MISSING, NN, _1_, _by_, _COMMA_, _DOT_, _DEPRECATED_, \

21 _ELLIPSIS4_, _enquote, _EQUAL_, _in_, _invalid_, _N_A_, \

22 _SPACE_, _UNDER_, _version_ # _utf_8_

23 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _FOR_DOCS, \

24 _getenv, LazyImportError, _sys, _sys_version_info2

25

26 from copy import copy as _copy, deepcopy as _deepcopy

27 from math import copysign as _copysign

28 import inspect as _inspect

29

30__all__ = _ALL_LAZY.basics

31__version__ = '24.01.02'

32

33_0_0 = 0.0 # in .constants

34_below_ = 'below'

35_can_t_ = "can't"

36_list_tuple_types = (list, tuple)

37_list_tuple_set_types = (list, tuple, set)

38_odd_ = 'odd'

39_required_ = 'required'

40_PYGEODESY_XPACKAGES_ = 'PYGEODESY_XPACKAGES'

41

42 try: # Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 395, 2022 p. 577+

43 from numbers import Integral as _Ints, Real as _Scalars # .units

44 except ImportError:

45 try:

46 _Ints = int, long # int objects (C{tuple})

47 except NameError: # Python 3+

48 _Ints = int, # int objects (C{tuple})

49 _Scalars = _Ints + (float,)

50

51 try:

52 try: # use C{from collections.abc import ...} in Python 3.9+

53 from collections.abc import Sequence as _Sequence # in .points

54 except ImportError: # no .abc in Python 3.8- and 2.7-

55 from collections import Sequence as _Sequence # in .points

56 if isinstance([], _Sequence) and isinstance((), _Sequence):

57 # and isinstance(range(1), _Sequence):

58 _Seqs = _Sequence

59 else:

60 raise ImportError # _AssertionError

61 except ImportError:

62 _Sequence = tuple # immutable for .points._Basequence

63 _Seqs = list, _Sequence # , range for function len2 below

64

65

66 def _passarg(arg): # in .auxilats.auxLat

67 '''(INTERNAL) Helper, no-op.

68 '''

69 return arg

70

71

72 def _passargs(*args): # in .utily

73 '''(INTERNAL) Helper, no-op.

74 '''

75 return args

76

77

78 try:

79 _Bytes = unicode, bytearray # PYCHOK expected

80 _Strs = basestring, str # XXX , bytes

81 str2ub = ub2str = _passarg # avoids UnicodeDecodeError

82

83 def _Xstr(exc): # PYCHOK no cover

84 '''I{Invoke only with caught ImportError} B{C{exc}}.

85

86 C{... "can't import name _distributor_init" ...}

87

88 only for C{numpy}, C{scipy} import errors occurring

89 on arm64 Apple Silicon running macOS' Python 2.7.16?

90 '''

91 t = str(exc)

92 if '_distributor_init' in t:

93 from sys import exc_info

94 from traceback import extract_tb

95 tb = exc_info()[2] # 3-tuple (type, value, traceback)

96 t4 = extract_tb(tb, 1)[0] # 4-tuple (file, line, name, 'import ...')

97 t = _SPACE_(_can_t_, t4[3] or _N_A_)

98 del tb, t4

99 return t

100

101 except NameError: # Python 3+

102 from pygeodesy.interns import _utf_8_

103

104 _Bytes = bytes, bytearray

105 _Strs = str, # tuple

106 _Xstr = str

107

108 def str2ub(sb):

109 '''Convert C{str} to C{unicode bytes}.

110 '''

111 if isinstance(sb, _Strs):

112 sb = sb.encode(_utf_8_)

113 return sb

114

115 def ub2str(ub):

116 '''Convert C{unicode bytes} to C{str}.

117 '''

118 if isinstance(ub, _Bytes):

119 ub = str(ub.decode(_utf_8_))

120 return ub

121

122

123 def clips(sb, limit=50, white=NN):

124 '''Clip a string to the given length limit.

125

126 @arg sb: String (C{str} or C{bytes}).

127 @kwarg limit: Length limit (C{int}).

128 @kwarg white: Optionally, replace all whitespace (C{str}).

129

130 @return: The clipped or unclipped B{C{sb}}.

131 '''

132 T = type(sb)

133 if len(sb) > limit > 8:

134 h = limit // 2

135 sb = T(_ELLIPSIS4_).join((sb[:h], sb[-h:]))

136 if white: # replace whitespace

137 sb = T(white).join(sb.split())

138 return sb

139

140

141 def copysign0(x, y):

142 '''Like C{math.copysign(x, y)} except C{zero}, I{unsigned}.

143

144 @return: C{math.copysign(B{x}, B{y})} if B{C{x}} else

145 C{type(B{x})(0)}.

146 '''

147 return _copysign(x, (y if y else 0)) if x else copytype(0, x)

148

149

150 def copytype(x, y):

151 '''Return the value of B{x} as C{type} of C{y}.

152

153 @return: C{type(B{y})(B{x})}.

154 '''

155 return type(y)(x if x else _0_0)

156

157

158 def halfs2(str2):

159 '''Split a string in 2 halfs.

160

161 @arg str2: String to split (C{str}).

162

163 @return: 2-Tuple C{(_1st, _2nd)} half (C{str}).

164

165 @raise ValueError: Zero or odd C{len(B{str2})}.

166 '''

167 h, r = divmod(len(str2), 2)

168 if r or not h:

169 raise _ValueError(str2=str2, txt=_odd_)

170 return str2[:h], str2[h:]

171

172

173 def int1s(x):

174 '''Count the number of 1-bits in an C{int}, I{unsigned}.

175

176 @note: C{int1s(-B{x}) == int1s(abs(B{x}))}.

177 '''

178 try:

179 return x.bit_count() # Python 3.10+

180 except AttributeError:

181 # bin(-x) = '-' + bin(abs(x))

182 return bin(x).count(_1_)

183

184

185 def isbool(obj):

186 '''Check whether an object is C{bool}ean.

187

188 @arg obj: The object (any C{type}).

189

190 @return: C{True} if B{C{obj}} is C{bool}ean,

191 C{False} otherwise.

192 '''

193 return isinstance(obj, bool) # and (obj is False

194# or obj is True)

195

196 if isbool(1) or isbool(0): # PYCHOK assert

197 raise _AssertionError(isbool=1)

198

199 if _FOR_DOCS: # XXX avoid epidoc Python 2.7 error

200

201 def isclass(obj):

202 '''Return C{True} if B{C{obj}} is a C{class} or C{type}.

203

204 @see: Python's C{inspect.isclass}.

205 '''

206 return _inspect.isclass(obj)

207 else:

208 isclass = _inspect.isclass

209

210

211 def isCartesian(obj, ellipsoidal=None):

212 '''Is B{C{obj}} some C{Cartesian}?

213

214 @arg obj: The object (any C{type}).

215 @kwarg ellipsoidal: If C{None}, return the type of any C{Cartesian},

216 if C{True}, only an ellipsoidal C{Cartesian type}

217 or if C{False}, only a spherical C{Cartesian type}.

218

219 @return: C{type(B{obj}} if B{C{obj}} is a C{Cartesian} instance of

220 the required type, C{False} if a C{Cartesian} of an other

221 type or C{None} otherwise.

222 '''

223 if ellipsoidal is not None:

224 try:

225 return obj.ellipsoidalCartesian if ellipsoidal else obj.sphericalCartesian

226 except AttributeError:

227 return None

228 return isinstanceof(obj, _MODS.cartesianBase.CartesianBase)

229

230

231 def iscomplex(obj):

232 '''Check whether an object is a C{complex} or complex C{str}.

233

234 @arg obj: The object (any C{type}).

235

236 @return: C{True} if B{C{obj}} is C{complex}, otherwise

237 C{False}.

238 '''

239 try: # hasattr('conjugate'), hasattr('real') and hasattr('imag')

240 return isinstance(obj, complex) or (isstr(obj)

241 and isinstance(complex(obj), complex)) # numbers.Complex?

242 except (TypeError, ValueError):

243 return False

244

245

246 def isDEPRECATED(obj):

247 '''Return C{True} if C{B{obj}} is a C{DEPRECATED} class, method

248 or function, C{False} if not or C{None} if undetermined.

249 '''

250 try: # XXX inspect.getdoc(obj)

251 return bool(obj.__doc__.lstrip().startswith(_DEPRECATED_))

252 except AttributeError:

253 return None

254

255

256 def isfloat(obj):

257 '''Check whether an object is a C{float} or float C{str}.

258

259 @arg obj: The object (any C{type}).

260

261 @return: C{True} if B{C{obj}} is a C{float}, otherwise

262 C{False}.

263 '''

264 try:

265 return isinstance( obj, float) or (isstr(obj)

266 and isinstance(float(obj), float))

267 except (TypeError, ValueError):

268 return False

269

270

271 try:

272 isidentifier = str.isidentifier # Python 3, must be str

273 except AttributeError: # Python 2-

274

275 def isidentifier(obj):

276 '''Return C{True} if B{C{obj}} is a Python identifier.

277 '''

278 return bool(obj and isstr(obj)

279 and obj.replace(_UNDER_, NN).isalnum()

280 and not obj[:1].isdigit())

281

282

283 def isinstanceof(obj, *classes):

284 '''Is B{C{ob}} an intance of one of the C{classes}?

285

286 @arg obj: The instance (any C{type}).

287 @arg classes: One or more classes (C{class}).

288

289 @return: C{type(B{obj}} if B{C{obj}} is an instance

290 of the B{C{classes}}, C{None} otherwise.

291 '''

292 return type(obj) if isinstance(obj, classes) else None

293

294

295 def isint(obj, both=False):

296 '''Check for C{int} type or an integer C{float} value.

297

298 @arg obj: The object (any C{type}).

299 @kwarg both: If C{true}, check C{float} and L{Fsum}

300 type and value (C{bool}).

301

302 @return: C{True} if B{C{obj}} is C{int} or I{integer}

303 C{float} or L{Fsum}, C{False} otherwise.

304

305 @note: Both C{isint(True)} and C{isint(False)} return

306 C{False} (and no longer C{True}).

307 '''

308 if isinstance(obj, _Ints) and not isbool(obj):

309 return True

310 elif both: # and isinstance(obj, (float, Fsum))

311 try: # NOT , _Scalars) to include Fsum!

312 return obj.is_integer()

313 except AttributeError:

314 pass # XXX float(int(obj)) == obj?

315 return False

316

317

318 try:

319 from keyword import iskeyword # Python 2.7+

320 except ImportError:

321

322 def iskeyword(unused):

323 '''Not Implemented, C{False} always.

324 '''

325 return False

326

327

328 def isLatLon(obj, ellipsoidal=None):

329 '''Is B{C{obj}} some C{LatLon}?

330

331 @arg obj: The object (any C{type}).

332 @kwarg ellipsoidal: If C{None}, return the type of any C{LatLon},

333 if C{True}, only an ellipsoidal C{LatLon type}

334 or if C{False}, only a spherical C{LatLon type}.

335

336 @return: C{type(B{obj}} if B{C{obj}} is a C{LatLon} instance of

337 the required type, C{False} if a C{LatLon} of an other

338 type or {None} otherwise.

339 '''

340 if ellipsoidal is not None:

341 try:

342 return obj.ellipsoidalLatLon if ellipsoidal else obj.sphericalLatLon

343 except AttributeError:

344 return None

345 return isinstanceof(obj, _MODS.latlonBase.LatLonBase)

346

347

348 def islistuple(obj, minum=0):

349 '''Check for list or tuple C{type} with a minumal length.

350

351 @arg obj: The object (any C{type}).

352 @kwarg minum: Minimal C{len} required C({int}).

353

354 @return: C{True} if B{C{obj}} is C{list} or C{tuple} with

355 C{len} at least B{C{minum}}, C{False} otherwise.

356 '''

357 return isinstance(obj, _list_tuple_types) and len(obj) >= minum

358

359

360 def isNvector(obj, ellipsoidal=None):

361 '''Is B{C{obj}} some C{Nvector}?

362

363 @arg obj: The object (any C{type}).

364 @kwarg ellipsoidal: If C{None}, return the type of any C{Nvector},

365 if C{True}, only an ellipsoidal C{Nvector type}

366 or if C{False}, only a spherical C{Nvector type}.

367

368 @return: C{type(B{obj}} if B{C{obj}} is an C{Nvector} instance of

369 the required type, C{False} if an C{Nvector} of an other

370 type or {None} otherwise.

371 '''

372 if ellipsoidal is not None:

373 try:

374 return obj.ellipsoidalNvector if ellipsoidal else obj.sphericalNvector

375 except AttributeError:

376 return None

377 return isinstanceof(obj, _MODS.nvectorBase.NvectorBase)

378

379

380 def isodd(x):

381 '''Is B{C{x}} odd?

382

383 @arg x: Value (C{scalar}).

384

385 @return: C{True} if B{C{x}} is odd,

386 C{False} otherwise.

387 '''

388 return bool(int(x) & 1) # == bool(int(x) % 2)

389

390

391 def isscalar(obj):

392 '''Check for scalar types.

393

394 @arg obj: The object (any C{type}).

395

396 @return: C{True} if B{C{obj}} is C{scalar}, C{False} otherwise.

397 '''

398 return isinstance(obj, _Scalars) and not isbool(obj)

399

400

401 def issequence(obj, *excls):

402 '''Check for sequence types.

403

404 @arg obj: The object (any C{type}).

405 @arg excls: Classes to exclude (C{type}), all positional.

406

407 @note: Excluding C{tuple} implies excluding C{namedtuple}.

408

409 @return: C{True} if B{C{obj}} is a sequence, C{False} otherwise.

410 '''

411 return isinstance(obj, _Seqs) and not (excls and isinstance(obj, excls))

412

413

414 def isstr(obj):

415 '''Check for string types.

416

417 @arg obj: The object (any C{type}).

418

419 @return: C{True} if B{C{obj}} is C{str}, C{False} otherwise.

420 '''

421 return isinstance(obj, _Strs)

422

423

424 def issubclassof(Sub, *Supers):

425 '''Check whether a class is a sub-class of some other class(es).

426

427 @arg Sub: The sub-class (C{class}).

428 @arg Supers: One or more C(super) classes (C{class}).

429

430 @return: C{True} if B{C{Sub}} is a sub-class of any B{C{Supers}},

431 C{False} if not (C{bool}) or C{None} if B{C{Sub}} is not

432 a class or if no B{C{Supers}} are given or none of those

433 are a class.

434 '''

435 if isclass(Sub):

436 t = tuple(S for S in Supers if isclass(S))

437 if t:

438 return bool(issubclass(Sub, t))

439 return None

440

441

442 def len2(items):

443 '''Make built-in function L{len} work for generators, iterators,

444 etc. since those can only be started exactly once.

445

446 @arg items: Generator, iterator, list, range, tuple, etc.

447

448 @return: 2-Tuple C{(n, items)} of the number of items (C{int})

449 and the items (C{list} or C{tuple}).

450 '''

451 if not isinstance(items, _Seqs): # NOT hasattr(items, '__len__'):

452 items = list(items)

453 return len(items), items

454

455

456 def map1(fun1, *xs): # XXX map_

457 '''Apply each B{C{xs}} to a single-argument function and

458 return a C{tuple} of results.

459

460 @arg fun1: 1-Arg function to apply (C{callable}).

461 @arg xs: Arguments to apply (C{any positional}).

462

463 @return: Function results (C{tuple}).

464 '''

465 return tuple(map(fun1, xs))

466

467

468 def map2(func, *xs):

469 '''Apply arguments to a function and return a C{tuple} of results.

470

471 Unlike Python 2's built-in L{map}, Python 3+ L{map} returns a

472 L{map} object, an iterator-like object which generates the

473 results only once. Converting the L{map} object to a tuple

474 maintains the Python 2 behavior.

475

476 @arg func: Function to apply (C{callable}).

477 @arg xs: Arguments to apply (C{list, tuple, ...}).

478

479 @return: Function results (C{tuple}).

480 '''

481 return tuple(map(func, *xs))

482

483

484 def neg(x, neg0=None):

485 '''Negate C{x} and optionally, negate C{0.0} amd C{-0.0}.

486

487 @kwarg neg0: Defines the return value for zero C{B{x}}: if C{None}

488 return C{0.0}, if C{True} return C{NEG0 if B{x}=0.0}

489 and C{0.0 if B{x}=NEG0} or if C{False} return C{B{x}}

490 I{as-is} (C{bool} or C{None}).

491

492 @return: C{-B{x} if B{x} else 0.0, NEG0 or B{x}}.

493 '''

494 return (-x) if x else (_0_0 if neg0 is None else (x if not neg0 else

495 (_0_0 if signBit(x) else _MODS.constants.NEG0)))

496

497

498 def neg_(*xs):

499 '''Negate all C{xs} with L{neg}.

500

501 @return: A C{map(neg, B{xs})}.

502 '''

503 return map(neg, xs)

504

505

506 def _reverange(n, stop=-1, step=-1):

507 '''(INTERNAL) Reversed range yielding C{n-1, n-1-step, ..., stop+1}.

508 '''

509 return range(n - 1, stop, step)

510

511

512 def signBit(x):

513 '''Return C{signbit(B{x})}, like C++.

514

515 @return: C{True} if C{B{x} < 0} or C{NEG0} (C{bool}).

516 '''

517 return x < 0 or _MODS.constants.isneg0(x)

518

519

520 def _signOf(x, ref): # in .fsums

521 '''(INTERNAL) Return the sign of B{C{x}} versus B{C{ref}}.

522 '''

523 return +1 if x > ref else (-1 if x < ref else 0)

524

525

526 def signOf(x):

527 '''Return sign of C{x} as C{int}.

528

529 @return: -1, 0 or +1 (C{int}).

530 '''

531 try:

532 s = x.signOf() # Fsum instance?

533 except AttributeError:

534 s = _signOf(x, 0)

535 return s

536

537

538 def _sizeof(inst):

539 '''(INTERNAL) Recursively size an C{inst}ance.

540

541 @return: Instance' size in bytes (C{int}),

542 ignoring class attributes and

543 counting duplicates only once or

544 C{None}.

545

546 @note: With C{PyPy}, the size is always C{None}.

547 '''

548 try:

549 _zB = _sys.getsizeof

550 _zD = _zB(None) # get some default

551 except TypeError: # PyPy3.10

552 return None

553

554 def _zR(s, iterable):

555 z, _s = 0, s.add

556 for o in iterable:

557 i = id(o)

558 if i not in s:

559 _s(i)

560 z += _zB(o, _zD)

561 if isinstance(o, dict):

562 z += _zR(s, o.keys())

563 z += _zR(s, o.values())

564 elif isinstance(o, _list_tuple_set_types):

565 z += _zR(s, o)

566 else:

567 try: # size instance' attr values only

568 z += _zR(s, o.__dict__.values())

569 except AttributeError: # None, int, etc.

570 pass

571 return z

572

573 return _zR(set(), (inst,))

574

575

576 def splice(iterable, n=2, **fill):

577 '''Split an iterable into C{n} slices.

578

579 @arg iterable: Items to be spliced (C{list}, C{tuple}, ...).

580 @kwarg n: Number of slices to generate (C{int}).

581 @kwarg fill: Optional fill value for missing items.

582

583 @return: A generator for each of B{C{n}} slices,

584 M{iterable[i::n] for i=0..n}.

585

586 @raise TypeError: Invalid B{C{n}}.

587

588 @note: Each generated slice is a C{tuple} or a C{list},

589 the latter only if the B{C{iterable}} is a C{list}.

590

591 @example:

592

593 >>> from pygeodesy import splice

594

595 >>> a, b = splice(range(10))

596 >>> a, b

597 ((0, 2, 4, 6, 8), (1, 3, 5, 7, 9))

598

599 >>> a, b, c = splice(range(10), n=3)

600 >>> a, b, c

601 ((0, 3, 6, 9), (1, 4, 7), (2, 5, 8))

602

603 >>> a, b, c = splice(range(10), n=3, fill=-1)

604 >>> a, b, c

605 ((0, 3, 6, 9), (1, 4, 7, -1), (2, 5, 8, -1))

606

607 >>> tuple(splice(list(range(9)), n=5))

608 ([0, 5], [1, 6], [2, 7], [3, 8], [4])

609

610 >>> splice(range(9), n=1)

611 <generator object splice at 0x0...>

612 '''

613 if not isint(n):

614 raise _TypeError(n=n)

615

616 t = iterable

617 if not isinstance(t, _list_tuple_types):

618 t = tuple(t) # force tuple, also for PyPy3

619

620 if n > 1:

621 if fill:

622 fill = _xkwds_get(fill, fill=MISSING)

623 if fill is not MISSING:

624 m = len(t) % n

625 if m > 0: # same type fill

626 t += type(t)((fill,) * (n - m))

627 for i in range(n):

628 # XXX t[i::n] chokes PyChecker

629 yield t[slice(i, None, n)]

630 else:

631 yield t

632

633

634 def _splituple(strs, *sep_splits): # in .mgrs, .osgr, .webmercator

635 '''(INTERNAL) Split a C{comma}- or C{whitespace}-separated

636 string into a C{tuple} of stripped strings.

637 '''

638 t = (strs.split(*sep_splits) if sep_splits else

639 strs.replace(_COMMA_, _SPACE_).split()) if strs else ()

640 return tuple(s.strip() for s in t if s)

641

642

643_XPACKAGES = _splituple(_getenv(_PYGEODESY_XPACKAGES_, NN))

644

645

646 def unsigned0(x):

647 '''Unsign if C{0.0}.

648

649 @return: C{B{x}} if B{C{x}} else C{0.0}.

650 '''

651 return x if x else _0_0

652

653

654 def _xargs_kwds_names(func):

655 '''(INTERNAL) Get a C{func}'s args and kwds names, including

656 C{self} for methods.

657

658 @note: Python 2 does I{not} include the C{*args} nor the

659 C{**kwds} names.

660 '''

661 try:

662 args_kwds = _inspect.signature(func).parameters.keys()

663 except AttributeError: # .signature new Python 3+

664 args_kwds = _inspect.getargspec(func).args

665 return tuple(args_kwds)

666

667

668 def _xcopy(inst, deep=False):

669 '''(INTERNAL) Copy an object, shallow or deep.

670

671 @arg inst: The object to copy (any C{type}).

672 @kwarg deep: If C{True} make a deep, otherwise

673 a shallow copy (C{bool}).

674

675 @return: The copy of B{C{inst}}.

676 '''

677 return _deepcopy(inst) if deep else _copy(inst)

678

679

680 def _xdup(inst, deep=False, **items):

681 '''(INTERNAL) Duplicate an object, replacing some attributes.

682

683 @arg inst: The object to copy (any C{type}).

684 @kwarg deep: If C{True} copy deep, otherwise shallow.

685 @kwarg items: Attributes to be changed (C{any}).

686

687 @return: A duplicate of B{C{inst}} with modified

688 attributes, if any B{C{items}}.

689

690 @raise AttributeError: Some B{C{items}} invalid.

691 '''

692 d = _xcopy(inst, deep=deep)

693 for n, v in items.items():

694 if getattr(d, n, v) != v:

695 setattr(d, n, v)

696 elif not hasattr(d, n):

697 t = _MODS.named.classname(inst)

698 t = _SPACE_(_DOT_(t, n), _invalid_)

699 raise _AttributeError(txt=t, this=inst, **items)

700 return d

701

702

703 def _xgeographiclib(where, *required):

704 '''(INTERNAL) Import C{geographiclib} and check required version.

705 '''

706 try:

707 _xpackage(_xgeographiclib)

708 import geographiclib

709 except ImportError as x:

710 raise _xImportError(x, where, Error=LazyImportError)

711 return _xversion(geographiclib, where, *required)

712

713

714 def _xImportError(x, where, Error=_ImportError, **name):

715 '''(INTERNAL) Embellish an C{Lazy/ImportError}.

716 '''

717 t = _SPACE_(_required_, _by_, _xwhere(where, **name))

718 return Error(_Xstr(x), txt=t, cause=x)

719

720

721 def _xinstanceof(*Types, **names_values):

722 '''(INTERNAL) Check C{Types} of all C{name=value} pairs.

723

724 @arg Types: One or more classes or types (C{class}),

725 all positional.

726 @kwarg names_values: One or more C{B{name}=value} pairs

727 with the C{value} to be checked.

728

729 @raise TypeError: One B{C{names_values}} pair is not an

730 instance of any of the B{C{Types}}.

731 '''

732 if not (Types and names_values):

733 raise _xAssertionError(_xinstanceof, *Types, **names_values)

734

735 for n, v in names_values.items():

736 if not isinstance(v, Types):

737 raise _TypesError(n, v, *Types)

738

739

740 def _xnumpy(where, *required):

741 '''(INTERNAL) Import C{numpy} and check required version.

742 '''

743 try:

744 _xpackage(_xnumpy)

745 import numpy

746 except ImportError as x:

747 raise _xImportError(x, where)

748 return _xversion(numpy, where, *required)

749

750

751 def _xpackage(_xpkg):

752 '''(INTERNAL) Check dependency to be excluded.

753 '''

754 n = _xpkg.__name__[2:]

755 if n in _XPACKAGES:

756 x = _SPACE_(n, _in_, _PYGEODESY_XPACKAGES_)

757 e = _enquote(_getenv(_PYGEODESY_XPACKAGES_, NN))

758 raise ImportError(_EQUAL_(x, e))

759

760

761 def _xor(x, *xs):

762 '''(INTERNAL) Exclusive-or C{x} and C{xs}.

763 '''

764 for x_ in xs:

765 x ^= x_

766 return x

767

768

769 def _xscipy(where, *required):

770 '''(INTERNAL) Import C{scipy} and check required version.

771 '''

772 try:

773 _xpackage(_xscipy)

774 import scipy

775 except ImportError as x:

776 raise _xImportError(x, where)

777 return _xversion(scipy, where, *required)

778

779

780 def _xsubclassof(*Classes, **names_values):

781 '''(INTERNAL) Check (super) class of all C{name=value} pairs.

782

783 @arg Classes: One or more classes or types (C{class}), all

784 positional.

785 @kwarg names_values: One or more C{B{name}=value} pairs

786 with the C{value} to be checked.

787

788 @raise TypeError: One B{C{names_values}} pair is not a

789 (sub-)class of any of the B{C{Classes}}.

790 '''

791 if not (Classes and names_values):

792 raise _xAssertionError(_xsubclassof, *Classes, **names_values)

793

794 for n, v in names_values.items():

795 if not issubclassof(v, *Classes):

796 raise _TypesError(n, v, *Classes)

797

798

799 def _xversion(package, where, *required, **name):

800 '''(INTERNAL) Check the C{package} version vs B{C{required}}.

801 '''

802 n = len(required)

803 if n:

804 t = _xversion_info(package)

805 if t[:n] < required:

806 t = _SPACE_(package.__name__, _version_, _DOT_(*t),

807 _below_, _DOT_(*required),

808 _required_, _by_, _xwhere(where, **name))

809 raise ImportError(t)

810 return package

811

812

813 def _xversion_info(package): # in .karney

814 '''(INTERNAL) Get the C{package.__version_info__} as a 2- or

815 3-tuple C{(major, minor, revision)} if C{int}s.

816 '''

817 try:

818 t = package.__version_info__

819 except AttributeError:

820 t = package.__version__.strip()

821 t = t.replace(_DOT_, _SPACE_).split()[:3]

822 return map2(int, t)

823

824

825 def _xwhere(where, **name):

826 '''(INTERNAL) Get the fully qualified name.

827 '''

828 m = _MODS.named.modulename(where, prefixed=True)

829 if name:

830 n = _xkwds_get(name, name=NN)

831 if n:

832 m = _DOT_(m, n)

833 return m

834

835

836 if _sys_version_info2 < (3, 10): # see .errors

837 _zip = zip # PYCHOK exported

838 else: # Python 3.10+

839

840 def _zip(*args):

841 return zip(*args, strict=True)

842

843# **) MIT License

844#

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

846#

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

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

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

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

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

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

853#

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

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

856#

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

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

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

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

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

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

863# OTHER DEALINGS IN THE SOFTWARE.