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

2

3 u'''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

7 from pygeodesy.interns import NN, _COLON_, _DOT_, _ELLIPSIS_, _EQUALSPACED_, \

8 _immutable_, _NL_, _pygeodesy_, _PyPy__, _python_, \

9 _QUOTE1_, _QUOTE2_, _s_, _SPACE_, _sys, _UNDER_, _utf_8_

10 from pygeodesy.interns import _COMMA_, _Python_ # PYCHOK used!

11# from pygeodesy.streprs import anstr, pairs, unstr # _MODS

12

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

14 import 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

24 def _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

34 def _Property_RO(method):

35 '''(INTERNAL) Can't Irecursively import L{props.property_RO}.

36 '''

37 name = _dunder_nameof(method)

38

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

40 delattr(inst, attr) # force error

41

42 def _get(inst, **unused): # PYCHOK 2 vs 3 args

43 try: # to get the cached value immediately

44 v = inst.__dict__[name]

45 except (AttributeError, KeyError):

46 # cache the value in the instance' __dict__

47 inst.__dict__[name] = v = method(inst)

48 return v

49

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

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

52

53 return property(_get, _set, _del)

54

55

56 class _ALL_MODS_Base(object):

57 '''(INTERNAL) Base-class for C{lazily._ALL_MODS}.

58 '''

59 def __delattr__(self, attr): # PYCHOK no cover

60 self.__dict__.pop(attr, None)

61

62 def __setattr__(self, attr, value): # PYCHOK no cover

63 m = _MODS.errors

64 t = _EQUALSPACED_(self._DOT_(attr), repr(value))

65 raise m._AttributeError(_immutable_, txt=t)

66

67 @_Property_RO

68 def bits_machine2(self):

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

70 '''

71 import platform as p

72

73 m = p.machine() # ARM64, arm64, x86_64, iPhone13,2, etc.

74 m = m.replace(_COMMA_, _UNDER_)

75 if m.lower() == 'x86_64': # PYCHOK on Intel or Rosetta2 ...

76 v = p.mac_ver()[0] # ... and only on macOS ...

77 if v and _version2(v) > (10, 15): # ... 11+ aka 10.16

78 # <https://Developer.Apple.com/forums/thread/659846>

79 # _sysctl_uint('hw.optional.arm64') and \

80 if _sysctl_uint('sysctl.proc_translated'):

81 m = _UNDER_(_arm64_, m) # Apple Si emulating Intel x86-64

82 return [p.architecture()[0], # bits

83 m] # arm64, arm64_x86_64, x86_64, etc.

84

85 @_Property_RO

86 def ctypes3(self):

87 '''Get 3-tuple C{(ctypes.CDLL, ._dlopen, .util.findlibrary)}, I{once}.

88 '''

89 if _ismacOS():

90 from ctypes import CDLL, DEFAULT_MODE, _dlopen

91

92 def dlopen(name):

93 return _dlopen(name, DEFAULT_MODE)

94 else: # PYCHOK no cover

95 from ctypes import CDLL

96 dlopen = _passarg

97

98 from ctypes.util import find_library

99 return CDLL, dlopen, find_library

100

101 @_Property_RO

102 def ctypes5(self):

103 '''Get 5-tuple C{(ctypes.byref, .c_char_p, .c_size_t, .c_uint, .sizeof)}, I{once}.

104 '''

105 from ctypes import byref, c_char_p, c_size_t, c_uint, sizeof # get_errno

106 return byref, c_char_p, c_size_t, c_uint, sizeof

107

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

109 return _DOT_(self.name, name)

110

111 @_Property_RO

112 def errors(self):

113 '''Get module C{pygeodesy.errors}, I{once}.

114 '''

115 from pygeodesy import errors # DON'T _lazy_import2

116 return errors

117

118 def ios_ver(self):

119 '''Mimick C{platform.xxx_ver} for C{iOS}.

120 '''

121 try: # Pythonista only

122 from platform import iOS_ver

123 return iOS_ver()

124 except (AttributeError, ImportError):

125 return NN, (NN, NN, NN), NN

126

127 @_Property_RO

128 def libc(self):

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

130 '''

131 return _load_lib('libc')

132

133 @_Property_RO

134 def name(self):

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

136 '''

137 return _dunder_nameof(self.__class__)

138

139 @_Property_RO

140 def nix2(self): # PYCHOK no cover

141 '''Get Linux 2-list C{[distro, version]}, I{once}.

142 '''

143 import platform as p

144

145 n, v = p.uname()[0], NN

146 if n.lower() == 'linux':

147 try: # use distro only for Linux, not macOS, etc.

148 import distro # <https://PyPI.org/project/distro>

149 _a = _MODS.streprs.anstr

150 v = _a(distro.version()) # first

151 n = _a(distro.id()) # .name()?

152 except (AttributeError, ImportError):

153 pass # v = str(_0_0)

154 n = n.capitalize()

155 return n, v

156

157 def nix_ver(self): # PYCHOK no cover

158 '''Mimick C{platform.xxx_ver} for C{*nix}.

159 '''

160 _, v = _MODS.nix2

161 t = _version2(v, n=3) if v else (NN, NN, NN)

162 return v, t, machine()

163

164 @_Property_RO

165 def osversion2(self):

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

167 '''

168 import platform as p

169

170 _Nix, _ = _MODS.nix2

171 # - mac_ver() returns ('10.12.5', ..., 'x86_64') on

172 # macOS and ('10.3.3', ..., 'iPad4,2') on iOS

173 # - win32_ver is ('XP', ..., 'SP3', ...) on Windows XP SP3

174 # - platform() returns 'Darwin-16.6.0-x86_64-i386-64bit'

175 # on macOS and 'Darwin-16.6.0-iPad4,2-64bit' on iOS

176 # - sys.platform is 'darwin' on macOS, 'ios' on iOS,

177 # 'win32' on Windows and 'cygwin' on Windows/Gygwin

178 # - distro.id() and .name() return 'Darwin' on macOS

179 for n, v in ((_iOS_, _MODS.ios_ver),

180 (_macOS_, p.mac_ver),

181 (_Windows_, p.win32_ver),

182 (_Nix, _MODS.nix_ver),

183 ('Java', p.java_ver),

184 ('uname', p.uname)):

185 v = v()[0]

186 if v and n:

187 break

188 else:

189 n = v = NN # XXX AssertioError?

190 return [n, v]

191

192 @_Property_RO

193 def Pythonarchine(self):

194 '''Get 3- or 4-list C{[PyPy, Python, bits, machine]}, I{once}.

195 '''

196 v = _sys.version

197 l3 = [_Python_(v)] + self.bits_machine2

198 pypy = _PyPy__(v)

199 if pypy: # PYCHOK no cover

200 l3.insert(0, pypy)

201 return l3

202

203 @_Property_RO

204 def streprs(self):

205 '''Get module C{pygeodesy.streprs}, I{once}.

206 '''

207 from pygeodesy import streprs # DON'T _lazy_import2

208 return streprs

209

210_MODS = _ALL_MODS_Base() # PYCHOK overwritten by .lazily

211

212

213 def _caller3(up): # in .lazily, .named

214 '''(INTERNAL) Get 3-tuple C{(caller name, file name, line number)}

215 for the caller B{C{up}} stack frames in the Python call stack.

216 '''

217 # sys._getframe(1) ... 'importlib._bootstrap' line 1032,

218 # may throw a ValueError('call stack not deep enough')

219 f = _sys._getframe(up + 1)

220 return (f.f_code.co_name, # caller name

221 _os_path.basename(f.f_code.co_filename), # file name

222 f.f_lineno) # line number

223

224

225 def _dunder_ismain(name):

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

227 '''

228 return name == '__main__'

229

230

231 def _enquote(strs, quote=_QUOTE2_, white=NN): # in .basics, .solveBase

232 '''(INTERNAL) Enquote a string containing whitespace or replace

233 whitespace by C{white} if specified.

234 '''

235 if strs:

236 t = strs.split()

237 if len(t) > 1:

238 strs = white.join(t if white else (quote, strs, quote))

239 return strs

240

241

242 def _headof(name):

243 '''(INTERNAL) Get the head name of qualified C{name} or the C{name}.

244 '''

245 i = name.find(_DOT_)

246 return name if i < 0 else name[:i]

247

248

249# def _is(a, b): # PYCHOK no cover

250# '''(INTERNAL) C{a is b}? in C{PyPy}

251# '''

252# return (a == b) if _isPyPy() else (a is b)

253

254

255 def _isAppleM():

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

257 '''

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

259

260

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

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

263 '''

264 return _MODS.osversion2[0] is _iOS_

265

266

267 def _ismacOS(): # in test/bases.py

268 '''(INTERNAL) Is this C{macOS}? (C{bool})

269 '''

270 return _sys.platform[:6] == 'darwin' and \

271 _MODS.osversion2[0] is _macOS_ # and os.name == 'posix'

272

273

274 def _isNix(): # in test/bases.py

275 '''(INTERNAL) Is this a C{Linux} distro? (C{str} or L{NN})

276 '''

277 return _MODS.nix2[0]

278

279

280 def _isPyPy(): # in test/bases.py

281 '''(INTERNAL) Is this C{PyPy}? (C{bool})

282 '''

283 # platform.python_implementation() == 'PyPy'

284 return _MODS.Pythonarchine[0].startswith(_PyPy__)

285

286

287 def _isWindows(): # in test/bases.py

288 '''(INTERNAL) Is this C{Windows}? (C{bool})

289 '''

290 return _sys.platform[:3] == 'win' and \

291 _MODS.osversion2[0] is _Windows_

292

293

294 def _load_lib(name):

295 '''(INTERNAL) Load a C{dylib}, B{C{name}} must startwith('lib').

296 '''

297 # macOS 11+ (aka 10.16) no longer provides direct loading of

298 # system libraries. As a result, C{ctypes.util.find_library}

299 # will not find any library, unless previously installed by a

300 # low-level dlopen(name) call (with the library base C{name}).

301 CDLL, dlopen, find_lib = _MODS.ctypes3

302

303 ns = find_lib(name), name

304 if dlopen is not _passarg: # _ismacOS()

305 ns += (_DOT_(name, 'dylib'),

306 _DOT_(name, 'framework'), _os_path.join(

307 _DOT_(name, 'framework'), name))

308 for n in ns:

309 try:

310 if n and dlopen(n): # pre-load handle

311 lib = CDLL(n) # == ctypes.cdll.LoadLibrary(n)

312 if lib._name: # has a qualified name

313 return lib

314 except (AttributeError, OSError):

315 pass

316

317 return None # raise OSError

318

319

320 def machine():

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

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

323

324 @return: Machine C{'arm64'} for Apple Silicon I{native}, C{'x86_64'}

325 for Intel I{native}, C{"arm64_x86_64"} for Intel I{emulation},

326 etc. (C{str} with C{comma}s replaced by C{underscore}s).

327 '''

328 return _MODS.bits_machine2[1]

329

330

331 def _name_version(pkg):

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

333 '''

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

335

336

337 def _osversion2(sep=NN): # in .lazily, test/bases.versions

338 '''(INTERNAL) Get the O/S name and release as C{2-list} or C{str}.

339 '''

340 l2 = _MODS.osversion2

341 return sep.join(l2) if sep else l2 # 2-list()

342

343

344 def _passarg(arg):

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

346 '''

347 return arg

348

349

350 def _passargs(*args):

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

352 '''

353 return args

354

355

356 def _plural(noun, n):

357 '''(INTERNAL) Return C{noun}['s'] or C{NN}.

358 '''

359 return NN(noun, _s_) if n > 1 else (noun if n else NN)

360

361

362 def print_(*args, **nl_nt_prec_prefix__end_file_flush_sep_kwds): # PYCHOK no cover

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

364

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

366 all positional.

367

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

369 '''

370 return printf(NN, *args, **nl_nt_prec_prefix__end_file_flush_sep_kwds)

371

372

373 def printf(fmt, *args, **nl_nt_prec_prefix__end_file_flush_sep_kwds):

374 '''C{Printf-style} and Python 3+ C{print}-like formatting and printing.

375

376 @arg fmt: U{Printf-style<https://Docs.Python.org/3/library/stdtypes.html#

377 printf-style-string-formatting>} format specification (C{str}).

378 @arg args: Arguments to be formatted (any C{type}, all positional).

379 @kwarg nl_nt_prec_prefix__end_file_flush_sep_kwds: Optional keyword arguments

380 C{B{nl}=0} for the number of leading blank lines (C{int}), C{B{nt}=0}

381 the number of trailing blank lines (C{int}), C{B{prefix}=NN} to be

382 inserted before the formatted text (C{str}) and Python 3+ C{print}

383 keyword arguments C{B{end}}, C{B{sep}}, C{B{file}} and C{B{flush}}.

384 Any remaining C{B{kwds}} are C{printf-style} name-value pairs to be

385 formatted, I{iff no B{C{args}} are present} using C{B{prec}=6} for

386 the number of decimal digits (C{int}).

387

388 @return: Number of bytes written.

389 '''

390 b, e, f, fl, p, s, kwds = _print7(**nl_nt_prec_prefix__end_file_flush_sep_kwds)

391 try:

392 if args:

393 t = (fmt % args) if fmt else s.join(map(str, args))

394 elif kwds:

395 t = (fmt % kwds) if fmt else s.join(

396 _MODS.streprs.pairs(kwds, prec=p))

397 else:

398 t = fmt

399 except Exception as x:

400 _E, s = _MODS.errors._xError2(x)

401 unstr = _MODS.streprs.unstr

402 t = unstr(printf, fmt, *args, **nl_nt_prec_prefix__end_file_flush_sep_kwds)

403 raise _E(s, txt=t, cause=x)

404 try:

405 n = f.write(NN(b, t, e))

406 except UnicodeEncodeError: # XXX only Windows

407 t = t.replace('\u2032', _QUOTE1_).replace('\u2033', _QUOTE2_)

408 n = f.write(NN(b, t, e))

409 if fl: # PYCHOK no cover

410 f.flush()

411 return n

412

413

414 def _print7(nl=0, nt=0, prec=6, prefix=NN, sep=_SPACE_, file=_sys.stdout,

415 end=_NL_, flush=False, **kwds):

416 '''(INTERNAL) Unravel the C{printf} and remaining keyword arguments.

417 '''

418 if nl > 0:

419 prefix = NN(_NL_ * nl, prefix)

420 if nt > 0:

421 end = NN(end, _NL_ * nt)

422 return prefix, end, file, flush, prec, sep, kwds

423

424

425 def _Pythonarchine(sep=NN): # in .lazily, test/bases.py versions

426 '''(INTERNAL) Get PyPy and Python versions, bit and machine as C{3- or 4-list} or C{str}.

427 '''

428 l3 = _MODS.Pythonarchine

429 return sep.join(l3) if sep else l3 # 3- or 4-list

430

431

432 def _sizeof(obj):

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

434

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

436 ignoring class attributes and

437 counting duplicates only once or

438 C{None}.

439

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

441 '''

442 try:

443 _zB = _sys.getsizeof

444 _zD = _zB(None) # some default

445 except TypeError: # PyPy3.10

446 return None

447

448 _isiterablen = _MODS.basics.isiterablen

449

450 def _zR(s, iterable):

451 z, _s = 0, s.add

452 for o in iterable:

453 i = id(o)

454 if i not in s:

455 _s(i)

456 z += _zB(o, _zD)

457 if isinstance(o, dict):

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

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

460 elif _isiterablen(o): # not map, ...

461 z += _zR(s, o)

462 else:

463 try: # size instance' attr values only

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

465 except AttributeError: # None, int, etc.

466 pass

467 return z

468

469 return _zR(set(), (obj,))

470

471

472 def _sysctl_uint(name):

473 '''(INTERNAL) Get an unsigned int sysctl item by name, use on macOS ONLY!

474 '''

475 libc = _MODS.libc

476 if libc: # <https://StackOverflow.com/questions/759892/python-ctypes-and-sysctl>

477 byref, char_p, size_t, uint, sizeof = _MODS.ctypes5

478 n = name if str is bytes else bytes(name, _utf_8_) # PYCHOK isPython2 = str is bytes

479 u = uint(0)

480 z = size_t(sizeof(u))

481 r = libc.sysctlbyname(char_p(n), byref(u), byref(z), None, size_t(0))

482 else: # could find or load 'libc'

483 r = -2

484 return int(r if r else u.value) # -1 ENOENT error, -2 no libc

485

486

487 def _tailof(name):

488 '''(INTERNAL) Get the base name of qualified C{name} or the C{name}.

489 '''

490 i = name.rfind(_DOT_) + 1

491 return name[i:] if i > 0 else name

492

493

494 def _under(name): # PYCHOK in .datums, .auxilats, .ups, .utm, .utmupsBase, ...

495 '''(INTERNAL) Prefix C{name} with an I{underscore}.

496 '''

497 return name if name.startswith(_UNDER_) else NN(_UNDER_, name)

498

499

500 def _usage(file_py, *args): # in .etm

501 '''(INTERNAL) Build "usage: python -m ..." cmd line for module B{C{file_py}}.

502 '''

503 m = _os_path.dirname(file_py).replace(_os.getcwd(), _ELLIPSIS_) \

504 .replace(_os.sep, _DOT_).strip()

505 b, x = _os_path.splitext(_os_path.basename(file_py))

506 if x == '.py' and not _dunder_ismain(b):

507 m = _DOT_(m or _pygeodesy_, b)

508 p = NN(_python_, _sys.version_info[0])

509 u = _COLON_(_dunder_nameof(_usage)[1:], NN)

510 return _SPACE_(u, p, '-m', _enquote(m), *args)

511

512

513 def _version2(version, n=2):

514 '''(INTERNAL) Split C{B{version} str} into a C{1-, 2- or 3-tuple} of C{int}s.

515 '''

516 t = _version_ints(version.split(_DOT_, 2))

517 if len(t) < n:

518 t += (0,) * n

519 return t[:n]

520

521

522 def _version_info(package): # in .Base.karney, .basics

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

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

525 '''

526 try:

527 return _version_ints(package.__version_info__)

528 except AttributeError:

529 return _version2(package.__version__.strip(), n=3)

530

531

532 def _version_ints(vs):

533 # helper for _version2 and _version_info above

534

535 def _ints(vs):

536 for v in vs:

537 try:

538 yield int(v.strip())

539 except (TypeError, ValueError):

540 pass

541

542 return tuple(_ints(vs))

543

544

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

546__version__ = '24.05.25'

547

548 if _dunder_ismain(__name__): # PYCHOK no cover

549

550 from pygeodesy import _isfrozen, isLazy, version as vs

551

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

553 + ['_isfrozen', _isfrozen,

554 'isLazy', isLazy]))

555

556# **) MIT License

557#

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

559#

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

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

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

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

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

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

566#

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

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

569#

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

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

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

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

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

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

576# OTHER DEALINGS IN THE SOFTWARE.