Coverage for pygeodesy/internals.py: 94%
213 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-27 20:21 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-27 20:21 -0400
1# -*- coding: utf-8 -*-
3u'''Mostly INTERNAL functions, except L{machine}, L{print_} and L{printf}.
4'''
5# from pygeodesy.basics import isiterablen # _MODS
6# from pygeodesy.errors import _AttributeError, _error_init, _UnexpectedError, _xError2 # _MODS
7from pygeodesy.interns import NN, _COLON_, _DOT_, _ELLIPSIS_, _EQUALSPACED_, \
8 _immutable_, _NL_, _pygeodesy_, _PyPy__, _python_, \
9 _QUOTE1_, _QUOTE2_, _s_, _SPACE_, _sys, _UNDER_, _utf_8_
10from pygeodesy.interns import _COMMA_, _Python_ # PYCHOK used!
11# from pygeodesy.streprs import anstr, pairs, unstr # _MODS
13import os as _os # in .lazily, ...
14import os.path as _os_path
15# import sys as _sys # from .interns
17_0_0 = 0.0 # PYCHOK in .basics, .constants
18_arm64_ = 'arm64'
19_iOS_ = 'iOS'
20_macOS_ = 'macOS'
21_Windows_ = 'Windows'
24def _dunder_nameof(inst, *dflt):
25 '''(INTERNAL) Get the double_underscore __name__ attr.
26 '''
27 try:
28 return inst.__name__
29 except AttributeError:
30 pass
31 return dflt[0] if dflt else inst.__class__.__name__
34def _dunder_nameof_(*names__): # in .errors._IsnotError
35 '''(INTERNAL) Yield the _dunder_nameof or name.
36 '''
37 return map(_dunder_nameof, names__, names__)
40def _Property_RO(method):
41 '''(INTERNAL) Can't I{recursively} import L{props.property_RO}.
42 '''
43 name = _dunder_nameof(method)
45 def _del(inst, attr): # PYCHOK no cover
46 delattr(inst, attr) # force error
48 def _get(inst, **unused): # PYCHOK 2 vs 3 args
49 try: # to get the cached value immediately
50 v = inst.__dict__[name]
51 except (AttributeError, KeyError):
52 # cache the value in the instance' __dict__
53 inst.__dict__[name] = v = method(inst)
54 return v
56 def _set(inst, val): # PYCHOK no cover
57 setattr(inst, name, val) # force error
59 return property(_get, _set, _del)
62class _MODS_Base(object):
63 '''(INTERNAL) Base-class for C{lazily._ALL_MODS}.
64 '''
65 def __delattr__(self, attr): # PYCHOK no cover
66 self.__dict__.pop(attr, None)
68 def __setattr__(self, attr, value): # PYCHOK no cover
69 m = _MODS.errors
70 t = _EQUALSPACED_(self._DOT_(attr), repr(value))
71 raise m._AttributeError(_immutable_, txt=t)
73 @_Property_RO
74 def bits_machine2(self):
75 '''Get platform 2-list C{[bits, machine]}, I{once}.
76 '''
77 import platform as p
79 m = p.machine() # ARM64, arm64, x86_64, iPhone13,2, etc.
80 m = m.replace(_COMMA_, _UNDER_)
81 if m.lower() == 'x86_64': # PYCHOK on Intel or Rosetta2 ...
82 v = p.mac_ver()[0] # ... and only on macOS ...
83 if v and _version2(v) > (10, 15): # ... 11+ aka 10.16
84 # <https://Developer.Apple.com/forums/thread/659846>
85 # _sysctl_uint('hw.optional.arm64') and \
86 if _sysctl_uint('sysctl.proc_translated'):
87 m = _UNDER_(_arm64_, m) # Apple Si emulating Intel x86-64
88 return [p.architecture()[0], # bits
89 m] # arm64, arm64_x86_64, x86_64, etc.
91 @_Property_RO
92 def ctypes3(self):
93 '''Get 3-tuple C{(ctypes.CDLL, ._dlopen, .util.findlibrary)}, I{once}.
94 '''
95 if _ismacOS():
96 from ctypes import CDLL, DEFAULT_MODE, _dlopen
98 def dlopen(name):
99 return _dlopen(name, DEFAULT_MODE)
100 else: # PYCHOK no cover
101 from ctypes import CDLL
102 dlopen = _passarg
104 from ctypes.util import find_library
105 return CDLL, dlopen, find_library
107 @_Property_RO
108 def ctypes5(self):
109 '''Get 5-tuple C{(ctypes.byref, .c_char_p, .c_size_t, .c_uint, .sizeof)}, I{once}.
110 '''
111 from ctypes import byref, c_char_p, c_size_t, c_uint, sizeof # get_errno
112 return byref, c_char_p, c_size_t, c_uint, sizeof
114 def _DOT_(self, name): # PYCHOK no cover
115 return _DOT_(self.name, name)
117 @_Property_RO
118 def errors(self):
119 '''Get module C{pygeodesy.errors}, I{once}.
120 '''
121 from pygeodesy import errors # DON'T _lazy_import2
122 return errors
124 def ios_ver(self):
125 '''Mimick C{platform.xxx_ver} for C{iOS}.
126 '''
127 try: # Pythonista only
128 from platform import iOS_ver
129 return iOS_ver()
130 except (AttributeError, ImportError):
131 return NN, (NN, NN, NN), NN
133 @_Property_RO
134 def libc(self):
135 '''Load C{libc.dll|dylib}, I{once}.
136 '''
137 return _load_lib('libc')
139 @_Property_RO
140 def name(self):
141 '''Get this name (C{str}).
142 '''
143 return _dunder_nameof(self.__class__)
145 @_Property_RO
146 def nix2(self): # PYCHOK no cover
147 '''Get Linux 2-list C{[distro, version]}, I{once}.
148 '''
149 import platform as p
151 n, v = p.uname()[0], NN
152 if n.lower() == 'linux':
153 try: # use distro only for Linux, not macOS, etc.
154 import distro # <https://PyPI.org/project/distro>
155 _a = _MODS.streprs.anstr
156 v = _a(distro.version()) # first
157 n = _a(distro.id()) # .name()?
158 except (AttributeError, ImportError):
159 pass # v = str(_0_0)
160 n = n.capitalize()
161 return n, v
163 def nix_ver(self): # PYCHOK no cover
164 '''Mimick C{platform.xxx_ver} for C{*nix}.
165 '''
166 _, v = _MODS.nix2
167 t = _version2(v, n=3) if v else (NN, NN, NN)
168 return v, t, machine()
170 @_Property_RO
171 def osversion2(self):
172 '''Get 2-list C{[OS, release]}, I{once}.
173 '''
174 import platform as p
176 _Nix, _ = _MODS.nix2
177 # - mac_ver() returns ('10.12.5', ..., 'x86_64') on
178 # macOS and ('10.3.3', ..., 'iPad4,2') on iOS
179 # - win32_ver is ('XP', ..., 'SP3', ...) on Windows XP SP3
180 # - platform() returns 'Darwin-16.6.0-x86_64-i386-64bit'
181 # on macOS and 'Darwin-16.6.0-iPad4,2-64bit' on iOS
182 # - sys.platform is 'darwin' on macOS, 'ios' on iOS,
183 # 'win32' on Windows and 'cygwin' on Windows/Gygwin
184 # - distro.id() and .name() return 'Darwin' on macOS
185 for n, v in ((_iOS_, _MODS.ios_ver),
186 (_macOS_, p.mac_ver),
187 (_Windows_, p.win32_ver),
188 (_Nix, _MODS.nix_ver),
189 ('Java', p.java_ver),
190 ('uname', p.uname)):
191 v = v()[0]
192 if v and n:
193 break
194 else:
195 n = v = NN # XXX AssertioError?
196 return [n, v]
198 @_Property_RO
199 def Pythonarchine(self):
200 '''Get 3- or 4-list C{[PyPy, Python, bits, machine]}, I{once}.
201 '''
202 v = _sys.version
203 l3 = [_Python_(v)] + self.bits_machine2
204 pypy = _PyPy__(v)
205 if pypy: # PYCHOK no cover
206 l3.insert(0, pypy)
207 return l3
209 @_Property_RO
210 def streprs(self):
211 '''Get module C{pygeodesy.streprs}, I{once}.
212 '''
213 from pygeodesy import streprs # DON'T _lazy_import2
214 return streprs
216_MODS = _MODS_Base() # PYCHOK overwritten by .lazily
219def _caller3(up): # in .lazily, .named
220 '''(INTERNAL) Get 3-tuple C{(caller name, file name, line number)}
221 for the caller B{C{up}} stack frames in the Python call stack.
222 '''
223 # sys._getframe(1) ... 'importlib._bootstrap' line 1032,
224 # may throw a ValueError('call stack not deep enough')
225 f = _sys._getframe(up + 1)
226 c = f.f_code
227 return (c.co_name, # caller name
228 _os_path.basename(c.co_filename), # file name .py
229 f.f_lineno) # line number
232def _dunder_ismain(name):
233 '''(INTERNAL) Return C{name == '__main__'}.
234 '''
235 return name == '__main__'
238def _enquote(strs, quote=_QUOTE2_, white=NN): # in .basics, .solveBase
239 '''(INTERNAL) Enquote a string containing whitespace or replace
240 whitespace by C{white} if specified.
241 '''
242 if strs:
243 t = strs.split()
244 if len(t) > 1:
245 strs = white.join(t if white else (quote, strs, quote))
246 return strs
249def _headof(name):
250 '''(INTERNAL) Get the head name of qualified C{name} or the C{name}.
251 '''
252 i = name.find(_DOT_)
253 return name if i < 0 else name[:i]
256# def _is(a, b): # PYCHOK no cover
257# '''(INTERNAL) C{a is b}? in C{PyPy}
258# '''
259# return (a == b) if _isPyPy() else (a is b)
262def _isAppleM():
263 '''(INTERNAL) Is this C{Apple Silicon}? (C{bool})
264 '''
265 return _ismacOS() and machine().startswith(_arm64_)
268def _isiOS(): # in test/bases.py
269 '''(INTERNAL) Is this C{iOS}? (C{bool})
270 '''
271 return _MODS.osversion2[0] is _iOS_
274def _ismacOS(): # in test/bases.py
275 '''(INTERNAL) Is this C{macOS}? (C{bool})
276 '''
277 return _sys.platform[:6] == 'darwin' and \
278 _MODS.osversion2[0] is _macOS_ # and os.name == 'posix'
281def _isNix(): # in test/bases.py
282 '''(INTERNAL) Is this a C{Linux} distro? (C{str} or L{NN})
283 '''
284 return _MODS.nix2[0]
287def _isPyPy(): # in test/bases.py
288 '''(INTERNAL) Is this C{PyPy}? (C{bool})
289 '''
290 # platform.python_implementation() == 'PyPy'
291 return _MODS.Pythonarchine[0].startswith(_PyPy__)
294def _isWindows(): # in test/bases.py
295 '''(INTERNAL) Is this C{Windows}? (C{bool})
296 '''
297 return _sys.platform[:3] == 'win' and \
298 _MODS.osversion2[0] is _Windows_
301def _load_lib(name):
302 '''(INTERNAL) Load a C{dylib}, B{C{name}} must startwith('lib').
303 '''
304 # macOS 11+ (aka 10.16) no longer provides direct loading of
305 # system libraries. As a result, C{ctypes.util.find_library}
306 # will not find any library, unless previously installed by a
307 # low-level dlopen(name) call (with the library base C{name}).
308 CDLL, dlopen, find_lib = _MODS.ctypes3
310 ns = find_lib(name), name
311 if dlopen is not _passarg: # _ismacOS()
312 ns += (_DOT_(name, 'dylib'),
313 _DOT_(name, 'framework'), _os_path.join(
314 _DOT_(name, 'framework'), name))
315 for n in ns:
316 try:
317 if n and dlopen(n): # pre-load handle
318 lib = CDLL(n) # == ctypes.cdll.LoadLibrary(n)
319 if lib._name: # has a qualified name
320 return lib
321 except (AttributeError, OSError):
322 pass
324 return None # raise OSError
327def machine():
328 '''Return standard C{platform.machine}, but distinguishing Intel I{native}
329 from Intel I{emulation} on Apple Silicon (on macOS only).
331 @return: Machine C{'arm64'} for Apple Silicon I{native}, C{'x86_64'}
332 for Intel I{native}, C{"arm64_x86_64"} for Intel I{emulation},
333 etc. (C{str} with C{comma}s replaced by C{underscore}s).
334 '''
335 return _MODS.bits_machine2[1]
338def _name_version(pkg):
339 '''(INTERNAL) Return C{pskg.__name__ + ' ' + .__version__}.
340 '''
341 return _SPACE_(pkg.__name__, pkg.__version__)
344def _osversion2(sep=NN): # in .lazily, test/bases.versions
345 '''(INTERNAL) Get the O/S name and release as C{2-list} or C{str}.
346 '''
347 l2 = _MODS.osversion2
348 return sep.join(l2) if sep else l2 # 2-list()
351def _passarg(arg):
352 '''(INTERNAL) Helper, no-op.
353 '''
354 return arg
357def _passargs(*args):
358 '''(INTERNAL) Helper, no-op.
359 '''
360 return args
363def _plural(noun, n):
364 '''(INTERNAL) Return C{noun}['s'] or C{NN}.
365 '''
366 return NN(noun, _s_) if n > 1 else (noun if n else NN)
369def print_(*args, **nl_nt_prec_prefix__end_file_flush_sep__kwds): # PYCHOK no cover
370 '''Python 3+ C{print}-like formatting and printing.
372 @arg args: Values to be converted to C{str} and joined by B{C{sep}},
373 all positional.
375 @see: Function L{printf} for further details.
376 '''
377 return printf(NN, *args, **nl_nt_prec_prefix__end_file_flush_sep__kwds)
380def printf(fmt, *args, **nl_nt_prec_prefix__end_file_flush_sep__kwds):
381 '''C{Printf-style} and Python 3+ C{print}-like formatting and printing.
383 @arg fmt: U{Printf-style<https://Docs.Python.org/3/library/stdtypes.html#
384 printf-style-string-formatting>} format specification (C{str}).
385 @arg args: Arguments to be formatted (any C{type}, all positional).
386 @kwarg nl_nt_prec_prefix__end_file_flush_sep__kwds: Optional keyword arguments
387 C{B{nl}=0} for the number of leading blank lines (C{int}), C{B{nt}=0}
388 the number of trailing blank lines (C{int}), C{B{prefix}=NN} to be
389 inserted before the formatted text (C{str}) and Python 3+ C{print}
390 keyword arguments C{B{end}}, C{B{sep}}, C{B{file}} and C{B{flush}}.
391 Any remaining C{B{kwds}} are C{printf-style} name-value pairs to be
392 formatted, I{iff no B{C{args}} are present} using C{B{prec}=6} for
393 the number of decimal digits (C{int}).
395 @return: Number of bytes written.
396 '''
397 b, e, f, fl, p, s, kwds = _print7(**nl_nt_prec_prefix__end_file_flush_sep__kwds)
398 try:
399 if args:
400 t = (fmt % args) if fmt else s.join(map(str, args))
401 elif kwds:
402 t = (fmt % kwds) if fmt else s.join(
403 _MODS.streprs.pairs(kwds, prec=p))
404 else:
405 t = fmt
406 except Exception as x:
407 _E, s = _MODS.errors._xError2(x)
408 unstr = _MODS.streprs.unstr
409 t = unstr(printf, fmt, *args, **nl_nt_prec_prefix__end_file_flush_sep__kwds)
410 raise _E(s, txt=t, cause=x)
411 try:
412 n = f.write(NN(b, t, e))
413 except UnicodeEncodeError: # XXX only Windows
414 t = t.replace('\u2032', _QUOTE1_).replace('\u2033', _QUOTE2_)
415 n = f.write(NN(b, t, e))
416 if fl: # PYCHOK no cover
417 f.flush()
418 return n
421def _print7(nl=0, nt=0, prec=6, prefix=NN, sep=_SPACE_, file=_sys.stdout,
422 end=_NL_, flush=False, **kwds):
423 '''(INTERNAL) Unravel the C{printf} and remaining keyword arguments.
424 '''
425 if nl > 0:
426 prefix = NN(_NL_ * nl, prefix)
427 if nt > 0:
428 end = NN(end, _NL_ * nt)
429 return prefix, end, file, flush, prec, sep, kwds
432def _Pythonarchine(sep=NN): # in .lazily, test/bases.py versions
433 '''(INTERNAL) Get PyPy and Python versions, bit and machine as C{3- or 4-list} or C{str}.
434 '''
435 l3 = _MODS.Pythonarchine
436 return sep.join(l3) if sep else l3 # 3- or 4-list
439def _sizeof(obj):
440 '''(INTERNAL) Recursively size an C{obj}ect.
442 @return: The C{obj} size in bytes (C{int}),
443 ignoring class attributes and
444 counting duplicates only once or
445 C{None}.
447 @note: With C{PyPy}, the size is always C{None}.
448 '''
449 try:
450 _zB = _sys.getsizeof
451 _zD = _zB(None) # some default
452 except TypeError: # PyPy3.10
453 return None
455 _isiterablen = _MODS.basics.isiterablen
457 def _zR(s, iterable):
458 z, _s = 0, s.add
459 for o in iterable:
460 i = id(o)
461 if i not in s:
462 _s(i)
463 z += _zB(o, _zD)
464 if isinstance(o, dict):
465 z += _zR(s, o.keys())
466 z += _zR(s, o.values())
467 elif _isiterablen(o): # not map, ...
468 z += _zR(s, o)
469 else:
470 try: # size instance' attr values only
471 z += _zR(s, o.__dict__.values())
472 except AttributeError: # None, int, etc.
473 pass
474 return z
476 return _zR(set(), (obj,))
479def _sysctl_uint(name):
480 '''(INTERNAL) Get an unsigned int sysctl item by name, use on macOS ONLY!
481 '''
482 libc = _MODS.libc
483 if libc: # <https://StackOverflow.com/questions/759892/python-ctypes-and-sysctl>
484 byref, char_p, size_t, uint, sizeof = _MODS.ctypes5
485 n = name if str is bytes else bytes(name, _utf_8_) # PYCHOK isPython2 = str is bytes
486 u = uint(0)
487 z = size_t(sizeof(u))
488 r = libc.sysctlbyname(char_p(n), byref(u), byref(z), None, size_t(0))
489 else: # could find or load 'libc'
490 r = -2
491 return int(r if r else u.value) # -1 ENOENT error, -2 no libc
494def _tailof(name):
495 '''(INTERNAL) Get the base name of qualified C{name} or the C{name}.
496 '''
497 i = name.rfind(_DOT_) + 1
498 return name[i:] if i > 0 else name
501def _under(name): # PYCHOK in .datums, .auxilats, .ups, .utm, .utmupsBase, ...
502 '''(INTERNAL) Prefix C{name} with an I{underscore}.
503 '''
504 return name if name.startswith(_UNDER_) else NN(_UNDER_, name)
507def _usage(file_py, *args): # in .etm
508 '''(INTERNAL) Build "usage: python -m ..." cmd line for module B{C{file_py}}.
509 '''
510 m = _os_path.dirname(file_py).replace(_os.getcwd(), _ELLIPSIS_) \
511 .replace(_os.sep, _DOT_).strip()
512 b, x = _os_path.splitext(_os_path.basename(file_py))
513 if x == '.py' and not _dunder_ismain(b):
514 m = _DOT_(m or _pygeodesy_, b)
515 p = NN(_python_, _sys.version_info[0])
516 u = _COLON_(_dunder_nameof(_usage)[1:], NN)
517 return _SPACE_(u, p, '-m', _enquote(m), *args)
520def _version2(version, n=2):
521 '''(INTERNAL) Split C{B{version} str} into a C{1-, 2- or 3-tuple} of C{int}s.
522 '''
523 t = _version_ints(version.split(_DOT_, 2))
524 if len(t) < n:
525 t += (0,) * n
526 return t[:n]
529def _version_info(package): # in .Base.karney, .basics
530 '''(INTERNAL) Get the C{package.__version_info__} as a 2- or
531 3-tuple C{(major, minor, revision)} if C{int}s.
532 '''
533 try:
534 return _version_ints(package.__version_info__)
535 except AttributeError:
536 return _version2(package.__version__.strip(), n=3)
539def _version_ints(vs):
540 # helper for _version2 and _version_info above
542 def _ints(vs):
543 for v in vs:
544 try:
545 yield int(v.strip())
546 except (TypeError, ValueError):
547 pass
549 return tuple(_ints(vs))
552__all__ = tuple(map(_dunder_nameof, (machine, print_, printf)))
553__version__ = '24.06.05'
555if _dunder_ismain(__name__): # PYCHOK no cover
557 from pygeodesy import _isfrozen, isLazy, version as vs
559 print_(_pygeodesy_, vs, *(_Pythonarchine() + _osversion2()
560 + ['_isfrozen', _isfrozen,
561 'isLazy', isLazy]))
563# **) MIT License
564#
565# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
566#
567# Permission is hereby granted, free of charge, to any person obtaining a
568# copy of this software and associated documentation files (the "Software"),
569# to deal in the Software without restriction, including without limitation
570# the rights to use, copy, modify, merge, publish, distribute, sublicense,
571# and/or sell copies of the Software, and to permit persons to whom the
572# Software is furnished to do so, subject to the following conditions:
573#
574# The above copyright notice and this permission notice shall be included
575# in all copies or substantial portions of the Software.
576#
577# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
578# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
579# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
580# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
581# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
582# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
583# OTHER DEALINGS IN THE SOFTWARE.