Coverage for pygeodesy/internals.py: 94%

213 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-27 20:21 -0400

1# -*- coding: utf-8 -*- 

2 

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 

12 

13import os as _os # in .lazily, ... 

14import os.path as _os_path 

15# import sys as _sys # from .interns 

16 

17_0_0 = 0.0 # PYCHOK in .basics, .constants 

18_arm64_ = 'arm64' 

19_iOS_ = 'iOS' 

20_macOS_ = 'macOS' 

21_Windows_ = 'Windows' 

22 

23 

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__ 

32 

33 

34def _dunder_nameof_(*names__): # in .errors._IsnotError 

35 '''(INTERNAL) Yield the _dunder_nameof or name. 

36 ''' 

37 return map(_dunder_nameof, names__, names__) 

38 

39 

40def _Property_RO(method): 

41 '''(INTERNAL) Can't I{recursively} import L{props.property_RO}. 

42 ''' 

43 name = _dunder_nameof(method) 

44 

45 def _del(inst, attr): # PYCHOK no cover 

46 delattr(inst, attr) # force error 

47 

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 

55 

56 def _set(inst, val): # PYCHOK no cover 

57 setattr(inst, name, val) # force error 

58 

59 return property(_get, _set, _del) 

60 

61 

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) 

67 

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) 

72 

73 @_Property_RO 

74 def bits_machine2(self): 

75 '''Get platform 2-list C{[bits, machine]}, I{once}. 

76 ''' 

77 import platform as p 

78 

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. 

90 

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 

97 

98 def dlopen(name): 

99 return _dlopen(name, DEFAULT_MODE) 

100 else: # PYCHOK no cover 

101 from ctypes import CDLL 

102 dlopen = _passarg 

103 

104 from ctypes.util import find_library 

105 return CDLL, dlopen, find_library 

106 

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 

113 

114 def _DOT_(self, name): # PYCHOK no cover 

115 return _DOT_(self.name, name) 

116 

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 

123 

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 

132 

133 @_Property_RO 

134 def libc(self): 

135 '''Load C{libc.dll|dylib}, I{once}. 

136 ''' 

137 return _load_lib('libc') 

138 

139 @_Property_RO 

140 def name(self): 

141 '''Get this name (C{str}). 

142 ''' 

143 return _dunder_nameof(self.__class__) 

144 

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 

150 

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 

162 

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() 

169 

170 @_Property_RO 

171 def osversion2(self): 

172 '''Get 2-list C{[OS, release]}, I{once}. 

173 ''' 

174 import platform as p 

175 

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] 

197 

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 

208 

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 

215 

216_MODS = _MODS_Base() # PYCHOK overwritten by .lazily 

217 

218 

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 

230 

231 

232def _dunder_ismain(name): 

233 '''(INTERNAL) Return C{name == '__main__'}. 

234 ''' 

235 return name == '__main__' 

236 

237 

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 

247 

248 

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] 

254 

255 

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) 

260 

261 

262def _isAppleM(): 

263 '''(INTERNAL) Is this C{Apple Silicon}? (C{bool}) 

264 ''' 

265 return _ismacOS() and machine().startswith(_arm64_) 

266 

267 

268def _isiOS(): # in test/bases.py 

269 '''(INTERNAL) Is this C{iOS}? (C{bool}) 

270 ''' 

271 return _MODS.osversion2[0] is _iOS_ 

272 

273 

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' 

279 

280 

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] 

285 

286 

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__) 

292 

293 

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_ 

299 

300 

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 

309 

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 

323 

324 return None # raise OSError 

325 

326 

327def machine(): 

328 '''Return standard C{platform.machine}, but distinguishing Intel I{native} 

329 from Intel I{emulation} on Apple Silicon (on macOS only). 

330 

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] 

336 

337 

338def _name_version(pkg): 

339 '''(INTERNAL) Return C{pskg.__name__ + ' ' + .__version__}. 

340 ''' 

341 return _SPACE_(pkg.__name__, pkg.__version__) 

342 

343 

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() 

349 

350 

351def _passarg(arg): 

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

353 ''' 

354 return arg 

355 

356 

357def _passargs(*args): 

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

359 ''' 

360 return args 

361 

362 

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) 

367 

368 

369def print_(*args, **nl_nt_prec_prefix__end_file_flush_sep__kwds): # PYCHOK no cover 

370 '''Python 3+ C{print}-like formatting and printing. 

371 

372 @arg args: Values to be converted to C{str} and joined by B{C{sep}}, 

373 all positional. 

374 

375 @see: Function L{printf} for further details. 

376 ''' 

377 return printf(NN, *args, **nl_nt_prec_prefix__end_file_flush_sep__kwds) 

378 

379 

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. 

382 

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}). 

394 

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 

419 

420 

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 

430 

431 

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 

437 

438 

439def _sizeof(obj): 

440 '''(INTERNAL) Recursively size an C{obj}ect. 

441 

442 @return: The C{obj} size in bytes (C{int}), 

443 ignoring class attributes and 

444 counting duplicates only once or 

445 C{None}. 

446 

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 

454 

455 _isiterablen = _MODS.basics.isiterablen 

456 

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 

475 

476 return _zR(set(), (obj,)) 

477 

478 

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 

492 

493 

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 

499 

500 

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) 

505 

506 

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) 

518 

519 

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] 

527 

528 

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) 

537 

538 

539def _version_ints(vs): 

540 # helper for _version2 and _version_info above 

541 

542 def _ints(vs): 

543 for v in vs: 

544 try: 

545 yield int(v.strip()) 

546 except (TypeError, ValueError): 

547 pass 

548 

549 return tuple(_ints(vs)) 

550 

551 

552__all__ = tuple(map(_dunder_nameof, (machine, print_, printf))) 

553__version__ = '24.06.05' 

554 

555if _dunder_ismain(__name__): # PYCHOK no cover 

556 

557 from pygeodesy import _isfrozen, isLazy, version as vs 

558 

559 print_(_pygeodesy_, vs, *(_Pythonarchine() + _osversion2() 

560 + ['_isfrozen', _isfrozen, 

561 'isLazy', isLazy])) 

562 

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.