1
2# -*- coding: utf-8 -*-
3
4u'''Some, basic definitions and functions.
5'''
6# make sure int/int division yields float quotient
7from __future__ import division
8division = 1 / 2  # .albers, .datums, .ellipsoidalVincenty, .ellipsoids,
9if not division:  # .elliptic, .etm, .fmath, .formy, .lcc, .osgr, .utily
10    raise ImportError('%s 1/2 == %s' % ('division', division))
11del division
12
13from pygeodesy.errors import _ImportError, _IsnotError, _TypeError, \
14                             _TypesError, _ValueError, _xkwds_get
15from pygeodesy.interns import EPS0, MISSING, NEG0, NN, _by_, _DOT_, \
16                             _N_A_, _name_, _SPACE_, _UNDER_, _utf_8_, \
17                             _version_, _0_0
18from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS
19
20from copy import copy as _copy, deepcopy as _deepcopy
21from inspect import isclass as _isclass
22from math import copysign as _copysign, isinf, isnan
23
24__all__ = _ALL_LAZY.basics
25__version__ = '21.09.14'
26
27_required_ = 'required'
28
29try:  # Luciano Ramalho, "Fluent Python", page 395, O'Reilly, 2016
30    from numbers import Integral as _Ints  # int objects
31except ImportError:
32    try:
33        _Ints = int, long  # int objects (C{tuple})
34    except NameError:  # Python 3+
35        _Ints = int,  # int objects (C{tuple})
36
37try:  # similarly ...
38    from numbers import Real as _Scalars  # scalar objects
39except ImportError:
40    try:
41        _Scalars = int, long, float  # scalar objects (C{tuple})
42    except NameError:
43        _Scalars = int, float  # scalar objects (C{tuple})
44
45try:
46    try:  # use C{from collections.abc import ...} in Python 3.9+
47        from collections.abc import Sequence as _Sequence  # imported by .points
48    except ImportError:  # no .abc in Python 2.7-
49        from collections import Sequence as _Sequence  # imported by .points
50    if isinstance([], _Sequence) and isinstance((), _Sequence):
51        #                        and isinstance(range(1), _Sequence):
52        _Seqs = _Sequence
53    else:
54        raise ImportError  # _AssertionError
55except ImportError:
56    _Sequence = tuple  # immutable for .points._Basequence
57    _Seqs     = list, _Sequence  # , range for function len2 below
58
59try:
60    _Bytes = unicode, bytearray  # PYCHOK expected
61    _Strs  = basestring, str
62
63    def _Xstr(x):  # XXX invoke only with caught import exception B{C{x}}: ...
64        t = str(x)  # 'cannot import name _distributor_init' (only for numpy, ...
65        if '_distributor_init' in t:  # ... scipy on arm64 macOS' Python 2.7.16?)
66            from sys import exc_info
67            from traceback import extract_tb
68            tb = exc_info()[2]  # 3-tuple (type, value, traceback)
69            t4 = extract_tb(tb, 1)[0]  # 4-tuple (file, line, name, 'import ...')
70            t = _SPACE_('cannot', t4[3] or _N_A_)
71            del tb, t4
72        return t
73
74except NameError:  # Python 3+
75    _Bytes = bytes, bytearray
76    _Strs  = str,
77    _Xstr  = str
78
79
80def clips(bstr, limit=50, white=NN):
81    '''Clip a string to the given length limit.
82
83       @arg bstr: String (C{bytes} or C{str}).
84       @kwarg limit: Length limit (C{int}).
85       @kwarg white: Optionally, replace all whitespace (C{str}).
86
87       @return: The clipped or unclipped B{C{bstr}}.
88    '''
89    if len(bstr) > limit > 8:
90        h = limit // 2
91        bstr = NN(bstr[:h], type(bstr)('....'), bstr[-h:])
92    if white:  # replace whitespace
93        bstr = type(bstr)(white).join(bstr.split())
94    return bstr
95
96
97def copysign0(x, y):
98    '''Like C{math.copysign(x, y)} except C{zero}, I{unsigned}.
99
100       @return: C{math.copysign(B{x}, B{y})} if B{C{x}} else C{0}.
101    '''
102    return _copysign(x, y) if x else copytype(0, x)
103
104
105def copytype(x, y):
106    '''Return the value of B{x} as C{type} of C{y}.
107
108       @return: C{type(B{y})(B{x})}.
109    '''
110    return type(y)(x)
111
112
113def halfs2(str2):
114    '''Split a string in 2 halfs.
115
116       @arg str2: String to split (C{str}).
117
118       @return: 2-Tuple (_1st, _2nd) half (C{str}).
119
120       @raise ValueError: Zero or odd C{len}(B{C{str2}}).
121    '''
122    h, r = divmod(len(str2), 2)
123    if r or not h:
124        raise _ValueError(str2=str2, txt='odd')
125    return str2[:h], str2[h:]
126
127
128def isbool(obj):
129    '''Check whether an object is C{bool}ean.
130
131       @arg obj: The object (any C{type}).
132
133       @return: C{True} if B{C{obj}} is C{bool}ean,
134                C{False} otherwise.
135    '''
136    return isinstance(obj, bool)  # and (obj is True
137#                                     or obj is False)
138
139
140if _FOR_DOCS:  # XXX avoid epidoc Python 2.7 error
141    def isclass(obj):
142        '''Return C{True} if B{C{obj}} is a C{class}.
143
144           @see: Python's C{inspect.isclass}.
145        '''
146        return _isclass(obj)
147else:
148    isclass = _isclass
149
150
151try:
152    from math import isfinite  # new in Python 3+
153except ImportError:
154
155    def isfinite(obj):
156        '''Check for C{Inf} and C{NaN} values.
157
158           @arg obj: Value (C{scalar}).
159
160           @return: C{False} if B{C{obj}} is C{INF} or C{NAN},
161                    C{True} otherwise.
162
163           @raise TypeError: Non-scalar B{C{obj}}.
164        '''
165        if not isscalar(obj):
166            raise _IsnotError(isscalar.__name__, obj=obj)
167        return not (isinf(obj) or isnan(obj))
168
169
170try:
171    isidentifier = str.isidentifier  # Python 3, must be str
172except AttributeError:  # Python 2-
173
174    def isidentifier(obj):
175        '''Return C{True} if B{C{obj}} is a valid Python identifier.
176        '''
177        return True if (obj and obj.replace(_UNDER_, NN).isalnum()
178                            and not obj[:1].isdigit()) else False
179
180# from math import isinf
181
182
183def isint(obj, both=False):
184    '''Check for C{int} type or an integer C{float} value.
185
186       @arg obj: The object (any C{type}).
187       @kwarg both: Optionally, check C{float} type and value (C{bool}).
188
189       @return: C{True} if B{C{obj}} is C{int} or an integer
190                C{float}, C{False} otherwise.
191
192       @note: C{isint(True)} or C{isint(False)} returns C{False} (and
193              no longer C{True}).
194    '''
195    if both and isinstance(obj, float):  # NOT _Scalars!
196        try:
197            return obj.is_integer()
198        except AttributeError:
199            return False  # XXX float(int(obj)) == obj?
200    return isinstance(obj, _Ints) and not isbool(obj)
201
202
203try:
204    from keyword import iskeyword  # Python 2.7+
205except ImportError:
206
207    def iskeyword(unused):
208        '''Not Implemented.  Return C{False}, always.
209        '''
210        return False
211
212# from math import isnan
213
214
215def isnear0(x, eps0=EPS0):
216    '''Is B{C{x}} near zero?
217
218       @arg x: Value (C{scalar}).
219       @kwarg eps0: Near-zero (C{EPS0}).
220
221       @return: C{True} if C{abs(B{x}) < B{eps0}},
222                C{False} otherwise.
223
224       @see: Function L{isnon0}.
225    '''
226    return eps0 > x > -eps0
227
228
229def isneg0(x):
230    '''Check for L{NEG0}, negative C{0.0}.
231
232       @arg x: Value (C{scalar}).
233
234       @return: C{True} if B{C{x}} is C{NEG0} or C{-0.0},
235                C{False} otherwise.
236    '''
237    return x in (_0_0, NEG0) and _copysign(1, x) < 0
238#                            and str(x).startswith('-')
239
240
241def isnon0(x, eps0=EPS0):
242    '''Is B{C{x}} non-zero?
243
244       @arg x: Value (C{scalar}).
245       @kwarg eps0: Near-zero (C{EPS0}).
246
247       @return: C{True} if C{abs(B{x}) > B{eps0}},
248                C{False} otherwise.
249
250       @see: Function L{isnear0}.
251    '''
252    return x > eps0 or (-x) > eps0
253
254
255def isodd(x):
256    '''Is B{C{x}} odd?
257
258       @arg x: Value (C{scalar}).
259
260       @return: C{True} if B{C{x}} is odd,
261                C{False} otherwise.
262    '''
263    return bool(int(x) & 1)
264
265
266def isscalar(obj):
267    '''Check for scalar types.
268
269       @arg obj: The object (any C{type}).
270
271       @return: C{True} if B{C{obj}} is C{scalar}, C{False} otherwise.
272    '''
273    return isinstance(obj, _Scalars)
274
275
276def issequence(obj, *excluded):
277    '''Check for sequence types.
278
279       @arg obj: The object (any C{type}).
280       @arg excluded: Optional exclusions (C{type}).
281
282       @note: Excluding C{tuple} implies excluding C{namedtuple}.
283
284       @return: C{True} if B{C{obj}} is a sequence, C{False} otherwise.
285    '''
286    return False if (excluded and isinstance(obj,  excluded)) else \
287                                  isinstance(obj, _Seqs)
288
289
290def isstr(obj):
291    '''Check for string types.
292
293       @arg obj: The object (any C{type}).
294
295       @return: C{True} if B{C{obj}} is C{str}, C{False} otherwise.
296    '''
297    return isinstance(obj, _Strs)
298
299
300def issubclassof(Sub, *Supers):
301    '''Check whether a class is a sub-class of some class(es).
302
303       @arg Sub: The sub-class (C{class}).
304       @arg Supers: One or more C(super) classes (C{class}).
305
306       @return: C{True} if B{C{Sub}} is a sub-class of any
307                B{C{Supers}}, C{False} otherwise (C{bool}).
308    '''
309    if isclass(Sub):
310        for S in Supers:  # any()
311            if isclass(S) and issubclass(Sub, S):
312                return True
313    return False
314
315
316def len2(items):
317    '''Make built-in function L{len} work for generators, iterators,
318       etc. since those can only be started exactly once.
319
320       @arg items: Generator, iterator, list, range, tuple, etc.
321
322       @return: 2-Tuple C{(n, items)} of the number of items (C{int})
323                and the items (C{list} or C{tuple}).
324    '''
325    if not isinstance(items, _Seqs):  # NOT hasattr(items, '__len__'):
326        items = list(items)
327    return len(items), items
328
329
330def map1(fun1, *xs):  # XXX map_
331    '''Apply each argument to a single-argument function and
332       return a C{tuple} of results.
333
334       @arg fun1: 1-Arg function to apply (C{callable}).
335       @arg xs: Arguments to apply (C{any positional}).
336
337       @return: Function results (C{tuple}).
338    '''
339    return tuple(map(fun1, xs))  # note xs, not *xs
340
341
342def map2(func, *xs):
343    '''Apply arguments to a function and return a C{tuple} of results.
344
345       Unlike Python 2's built-in L{map}, Python 3+ L{map} returns a
346       L{map} object, an iterator-like object which generates the
347       results only once.  Converting the L{map} object to a tuple
348       maintains Python 2 behavior.
349
350       @arg func: Function to apply (C{callable}).
351       @arg xs: Arguments to apply (C{list, tuple, ...}).
352
353       @return: Function results (C{tuple}).
354    '''
355    return tuple(map(func, *xs))  # note *xs, not xs
356
357
358def neg(x):
359    '''Negate C{x} unless C{zero} or C{NEG0}.
360
361       @return: C{-B{x}} if B{C{x}} else C{0.0}.
362    '''
363    return -x if x else _0_0
364
365
366def neg_(*xs):
367    '''Negate all of C{xs} with L{neg}.
368
369       @return: A C{tuple(neg(x) for x in B{xs})}.
370    '''
371    return tuple(map(neg, xs))  # see map1
372
373
374def signOf(x):
375    '''Return sign of C{x} as C{int}.
376
377       @return: -1, 0 or +1 (C{int}).
378    '''
379    return 1 if x > 0 else (-1 if x < 0 else 0)
380
381
382def splice(iterable, n=2, **fill):
383    '''Split an iterable into C{n} slices.
384
385       @arg iterable: Items to be spliced (C{list}, C{tuple}, ...).
386       @kwarg n: Number of slices to generate (C{int}).
387       @kwarg fill: Optional fill value for missing items.
388
389       @return: A generator for each of B{C{n}} slices,
390                M{iterable[i::n] for i=0..n}.
391
392       @raise TypeError: Invalid B{C{n}}.
393
394       @note: Each generated slice is a C{tuple} or a C{list},
395              the latter only if the B{C{iterable}} is a C{list}.
396
397       @example:
398
399        >>> from pygeodesy import splice
400
401        >>> a, b = splice(range(10))
402        >>> a, b
403        ((0, 2, 4, 6, 8), (1, 3, 5, 7, 9))
404
405        >>> a, b, c = splice(range(10), n=3)
406        >>> a, b, c
407        ((0, 3, 6, 9), (1, 4, 7), (2, 5, 8))
408
409        >>> a, b, c = splice(range(10), n=3, fill=-1)
410        >>> a, b, c
411        ((0, 3, 6, 9), (1, 4, 7, -1), (2, 5, 8, -1))
412
413        >>> tuple(splice(list(range(9)), n=5))
414        ([0, 5], [1, 6], [2, 7], [3, 8], [4])
415
416        >>> splice(range(9), n=1)
417        <generator object splice at 0x0...>
418    '''
419    if not isint(n):
420        raise _TypeError(n=n)
421
422    t = iterable
423    if not isinstance(t, (list, tuple)):
424        t = tuple(t)  # force tuple, also for PyPy3
425
426    if n > 1:
427        if fill:
428            fill = _xkwds_get(fill, fill=MISSING)
429            if fill is not MISSING:
430                m = len(t) % n
431                if m > 0:  # fill with same type
432                    t += type(t)((fill,)) * (n - m)
433        for i in range(n):
434            # XXX t[i::n] chokes PyChecker
435            yield t[slice(i, None, n)]
436    else:
437        yield t
438
439
440def ub2str(ub):
441    '''Convert C{unicode} or C{bytes} to C{str}.
442    '''
443    if isinstance(ub, _Bytes):
444        ub = str(ub.decode(_utf_8_))
445    return ub
446
447
448def unsign0(x):
449    '''Return unsigned C{0.0}.
450
451       @return: C{B{x}} if B{C{x}} else C{0.0}.
452    '''
453    return x if x else _0_0
454
455
456def _xcopy(inst, deep=False):
457    '''(INTERNAL) Copy an object, shallow or deep.
458
459       @arg inst: The object to copy (any C{type}).
460       @kwarg deep: If C{True} make a deep, otherwise
461                    a shallow copy (C{bool}).
462
463       @return: The copy of B{C{inst}}.
464    '''
465    return _deepcopy(inst) if deep else _copy(inst)
466
467
468def _xImportError(x, where, **name):
469    '''(INTERNAL) Embellish an C{ImportError}.
470    '''
471    t = _SPACE_(_required_, _by_, _xwhere(where, **name))
472    return _ImportError(_Xstr(x), txt=t)
473
474
475def _xinstanceof(*Types, **name_value_pairs):
476    '''(INTERNAL) Check C{Types} of all C{name=value} pairs.
477
478       @arg Types: One or more classes or types (C{class}).
479       @kwarg name_value_pairs: One or more C{B{name}=value} pairs
480                                with the C{value} to be checked.
481
482       @raise TypeError: At least one of the B{C{name_value_pairs}}
483                         is not any of the B{C{Types}}.
484    '''
485    for n, v in name_value_pairs.items():
486        if not isinstance(v, Types):
487            raise _TypesError(n, v, *Types)
488
489
490def _xnumpy(where, *required):
491    '''(INTERNAL) Import C{numpy} and check required version
492    '''
493    try:
494        import numpy
495    except ImportError as x:
496        raise _xImportError(x, where)
497    return _xversion(numpy, where, *required)
498
499
500def _xor(x, *xs):
501    '''(INTERNAL) Exclusive-or C{x} and C{xs}.
502    '''
503    for x_ in xs:
504        x ^= x_
505    return x
506
507
508def _xscipy(where, *required):
509    '''(INTERNAL) Import C{scipy} and check required version
510    '''
511    try:
512        import scipy
513    except ImportError as x:
514        raise _xImportError(x, where)
515    return _xversion(scipy, where, *required)
516
517
518def _xsubclassof(Class, **name_value_pairs):
519    '''(INTERNAL) Check super C{Class} of all C{name=value} pairs.
520
521       @arg Class: A class or type (C{class}).
522       @kwarg name_value_pairs: One or more C{B{name}=value} pairs
523                                with the C{value} to be checked.
524
525       @raise TypeError: At least one of the B{C{name_value_pairs}}
526                         is not a sub-class of B{C{Class}}.
527    '''
528    for n, v in name_value_pairs.items():
529        if not issubclassof(v, Class):
530            raise _TypesError(n, v, Class)
531
532
533def _xversion(package, where, *required, **name):  # in .karney
534    '''(INTERNAL) Check the C{package} version vs B{C{required}}.
535    '''
536    n = len(required)
537    if n:
538        t = map2(int, package.__version__.split(_DOT_))
539        if t[:n] < required:
540            t = _SPACE_(package.__name__, _version_, _DOT_(*t),
541                       'below', _DOT_(*required),
542                       _required_, _by_, _xwhere(where, **name))
543            raise ImportError(t)
544    return package
545
546
547def _xwhere(where, **name):
548    '''(INTERNAL) Get the fully qualified name.
549    '''
550    from pygeodesy.named import modulename
551    m = modulename(where, prefixed=True)
552    n = name.get(_name_, NN)
553    if n:
554        m = _DOT_(m, n)
555    return m
556
557
558# **) MIT License
559#
560# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved.
561#
562# Permission is hereby granted, free of charge, to any person obtaining a
563# copy of this software and associated documentation files (the "Software"),
564# to deal in the Software without restriction, including without limitation
565# the rights to use, copy, modify, merge, publish, distribute, sublicense,
566# and/or sell copies of the Software, and to permit persons to whom the
567# Software is furnished to do so, subject to the following conditions:
568#
569# The above copyright notice and this permission notice shall be included
570# in all copies or substantial portions of the Software.
571#
572# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
573# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
574# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
575# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
576# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
577# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
578# OTHER DEALINGS IN THE SOFTWARE.
579