1

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

3

4 u'''Floating point and other formatting utilities.

5'''

6

7 from pygeodesy.basics import _0_0, isint, islistuple, isscalar, isstr, _zip

8# from pygeodesy.constants import _0_0

9 from pygeodesy.errors import _AttributeError, _IsnotError, itemsorted, _or, \

10 _TypeError, _ValueError, _xkwds_get, _xkwds_pop

11 from pygeodesy.interns import NN, _0_, _0to9_, MISSING, _BAR_, _COMMASPACE_, \

12 _DOT_, _E_, _ELLIPSIS_, _EQUAL_, _H_, _LR_PAIRS, \

13 _N_, _name_, _not_, _not_scalar_, _PERCENT_, \

14 _SPACE_, _STAR_, _UNDER_, _dunder_nameof

15 from pygeodesy.interns import _convergence_, _distant_, _e_, _EQUALSPACED_, _no_, \

16 _exceeds_, _f_, _F_, _g_, _tolerance_ # PYCHOK used!

17 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

18

19 from math import fabs, log10 as _log10

20

21__all__ = _ALL_LAZY.streprs

22__version__ = '23.03.30'

23

24_EN_PREC = 6 # max MGRS/OSGR precision, 1 micrometer

25_EN_WIDE = 5 # number of MGRS/OSGR units, log10(_100km)

26_eps_ = 'eps' # PYCHOK used!

27_OKd_ = '._-' # acceptable name characters

28_threshold_ = 'threshold' # PYCHOK used !

29

30

31 class _Fmt(str): # in .streprs

32 '''(INTERNAL) Callable formatting.

33 '''

34 name = NN

35

36 def __call__(self, *name_value_, **name_value):

37 '''Format a C{name=value} pair or C{name, value} pair

38 or just a single C{value}.

39 '''

40 for n, v in name_value.items():

41 break

42 else:

43 if len(name_value_) > 1:

44 n, v = name_value_[:2]

45 elif name_value_:

46 n, v = NN, name_value_[0]

47 else:

48 n, v = NN, MISSING

49 t = str.__mod__(self, v)

50 return NN(n, t) if n else t

51

52# def __mod__(self, arg, **unused):

53# '''Regular C{%} operator.

54# '''

55# return str.__mod__(self, arg)

56

57

58 class Fstr(str):

59 '''(INTERNAL) C{float} format.

60 '''

61 name = NN

62

63 def __call__(self, flt, prec=None, ints=False):

64 '''Format the B{C{flt}} like function L{fstr}.

65 '''

66 # see also function C{fstr} if isscalar case below

67 t = str.__mod__(_pct(self), flt) if prec is None else next(

68 _streprs(prec, (flt,), self, ints, True, None))

69 return t

70

71 def __mod__(self, arg, **unused):

72 '''Regular C{%} operator.

73

74 @arg arg: A C{scalar} value to be formatted (either

75 the C{scalar}, or a 1-tuple C{(scalar,)},

76 or 2-tuple C{(prec, scalar)}.

77

78 @raise TypeError: Non-scalar B{C{arg}} value.

79

80 @raise ValueError: Invalid B{C{arg}}.

81 '''

82 def _error(arg):

83 n = _DOT_(Fstr.__name__, self.name or self)

84 return _SPACE_(n, _PERCENT_, repr(arg))

85

86 prec = 6 # default std %f and %F

87 if islistuple(arg):

88 n = len(arg)

89 if n == 1:

90 arg = arg[0]

91 elif n == 2:

92 prec, arg = arg

93 else:

94 raise _ValueError(_error(arg))

95

96 if not isscalar(arg):

97 raise _TypeError(_error(arg))

98 return self(arg, prec=prec)

99

100

101 class _Sub(str):

102 '''(INTERNAL) Class list formatter.

103 '''

104 # see .ellipsoidalNvector.LatLon.deltaTo

105 def __call__(self, *Classes):

106 t = _or(*(C.__name__ for C in Classes))

107 return str.__mod__(self, t or MISSING)

108

109

110 class Fmt(object):

111 '''Formatting options.

112 '''

113 ANGLE = _Fmt('<%s>')

114 COLON = _Fmt(':%s')

115# COLONSPACE = _Fmt(': %s') # == _COLONSPACE_(n, v)

116# COMMASPACE = _Fmt(', %s') # == _COMMASPACE_(n, v)

117 convergence = _Fmt(_convergence_('(%g)'))

118 CURLY = _Fmt('{%s}') # BRACES

119 distant = _Fmt(_distant_('(%.3g)'))

120 DOT = _Fmt('.%s') # == NN(_DOT_, n)

121 e = Fstr(_e_)

122 E = Fstr(_E_)

123 EQUAL = _Fmt(_EQUAL_(NN, '%s'))

124 EQUALSPACED = _Fmt(_EQUALSPACED_(NN, '%s'))

125 exceeds_eps = _Fmt(_exceeds_(_eps_, '(%g)'))

126 f = Fstr(_f_)

127 F = Fstr(_F_)

128 g = Fstr(_g_)

129 G = Fstr('G')

130 h = Fstr('%+.*f') # height, .streprs.hstr

131 limit = _Fmt(' %s limit') # .units

132 LOPEN = _Fmt('(%s]') # left-open range (L, R]

133 PAREN = _Fmt('(%s)')

134 PARENSPACED = _Fmt(' (%s)')

135 QUOTE2 = _Fmt('"%s"')

136 ROPEN = _Fmt('[%s)') # right-open range [L, R)

137# SPACE = _Fmt(' %s') # == _SPACE_(n, v)

138 SQUARE = _Fmt('[%s]') # BRACKETS

139 sub_class = _Sub('%s (sub-)class')

140 TAG = ANGLE

141 TAGEND = _Fmt('</%s>')

142 tolerance = _Fmt(_tolerance_('(%g)'))

143 zone = _Fmt('%02d') # .epsg, .mgrs, .utmupsBase

144

145 def __init__(self):

146 for n, a in self.__class__.__dict__.items():

147 if isinstance(a, (Fstr, _Fmt)):

148 setattr(a, _name_, n)

149

150 def __call__(self, obj, prec=9):

151 '''Return C{str(B{obj})} or C{repr(B{obj})}.

152 '''

153 return str(obj) if isint(obj) else next(

154 _streprs(prec, (obj,), Fmt.g, False, False, repr))

155

156 def no_convergence(self, _d, *tol, **thresh):

157 t = Fmt.convergence(fabs(_d))

158 if tol:

159 t = _COMMASPACE_(t, Fmt.tolerance(tol[0]))

160 if thresh and _xkwds_get(thresh, thresh=False):

161 t = t.replace(_tolerance_, _threshold_)

162 return _no_(t)

163

164 Fmt = Fmt() # PYCHOK singleton

165 Fmt.__name__ = Fmt.__class__.__name__

166

167_DOTSTAR_ = Fmt.DOT(_STAR_)

168# formats %G and %g drop all trailing zeros and the

169# decimal point, making the float appear as an int

170_Gg = (Fmt.G, Fmt.g)

171_FfEeGg = (Fmt.F, Fmt.f, Fmt.E, Fmt.e) + _Gg # float formats

172_Fspec_ = NN('[%[<flags>][<width>]', _DOTSTAR_, ']', _BAR_.join(_FfEeGg)) # in testStreprs

173

174

175 def anstr(name, OKd=_OKd_, sub=_UNDER_):

176 '''Make a valid name of alphanumeric and OKd characters.

177

178 @arg name: The original name (C{str}).

179 @kwarg OKd: Other acceptable characters (C{str}).

180 @kwarg sub: Substitute for invalid charactes (C{str}).

181

182 @return: The modified name (C{str}).

183

184 @note: Leading and trailing whitespace characters are removed,

185 intermediate whitespace characters are coalesced and

186 substituted.

187 '''

188 s = n = str(name).strip()

189 for c in n:

190 if not (c.isalnum() or c in OKd or c in sub):

191 s = s.replace(c, _SPACE_)

192 return sub.join(s.strip().split())

193

194

195 def attrs(inst, *names, **Nones_True__pairs_kwds): # prec=6, fmt=Fmt.F, ints=False, Nones=True, sep=_EQUAL_

196 '''Get instance attributes as I{name=value} strings, with C{float}s

197 formatted by function L{fstr}.

198

199 @arg inst: The instance (any C{type}).

200 @arg names: The attribute names, all other positional (C{str}).

201 @kwarg Nones_True__pairs_kwds: Keyword argument for function L{pairs}, except

202 C{B{Nones}=True} to in-/exclude missing or C{None}-valued attributes.

203

204 @return: A C{tuple(B{sep}.join(t) for t in zip(B{names}, reprs(values, ...)))}

205 of C{str}s.

206 '''

207 def _items(inst, names, Nones):

208 for n in names:

209 v = getattr(inst, n, None)

210 if Nones or v is not None:

211 yield n, v

212

213 def _Nones_kwds(Nones=True, **kwds):

214 return Nones, kwds

215

216 Nones, kwds = _Nones_kwds(**Nones_True__pairs_kwds)

217 return pairs(_items(inst, names, Nones), **kwds)

218

219

220 def _boolkwds(inst, **name_value_pairs): # in .frechet, .hausdorff, .heights

221 '''(INTERNAL) Set applicable C{bool} properties/attributes.

222 '''

223 for n, v in name_value_pairs.items():

224 b = getattr(inst, n, None)

225 if b is None: # invalid bool attr

226 t = _SPACE_(_EQUAL_(n, repr(v)), 'for', inst.__class__.__name__) # XXX .classname

227 raise _AttributeError(t, txt=_not_('applicable'))

228 if v in (False, True) and v != b:

229 setattr(inst, NN(_UNDER_, n), v)

230

231

232 def enstr2(easting, northing, prec, *extras, **wide_dot):

233 '''Return an MGRS/OSGR easting, northing string representations.

234

235 @arg easting: Easting from false easting (C{meter}).

236 @arg northing: Northing from from false northing (C{meter}).

237 @arg prec: Precision, the number of I{decimal} digits (C{int}) or if

238 negative, the number of I{units to drop}, like MGRS U{PRECISION

239 <https://GeographicLib.SourceForge.io/C++/doc/GeoConvert.1.html#PRECISION>}.

240 @arg extras: Optional leading items (C{str}s).

241 @kwarg wide_dot: Optional keword argument C{B{wide}=%d} for the number of I{unit digits}

242 (C{int}) and C{B{dot}=False} (C{bool}) to insert a decimal point.

243

244 @return: B{C{extras}} + 2-tuple C{(str(B{easting}), str(B{northing}))} or

245 + 2-tuple C{("", "")} for C{B{prec} <= -B{wide}}.

246

247 @raise ValueError: Invalid B{C{easting}}, B{C{northing}} or B{C{prec}}.

248

249 @note: The B{C{easting}} and B{C{northing}} values are I{truncated, not rounded}.

250 '''

251 t = extras

252 try: # like .dms.compassPoint

253 p = min(int(prec), _EN_PREC)

254 w = p + _xkwds_get(wide_dot, wide=_EN_WIDE)

255 if w > 0:

256 f = 10**p # truncate

257 d = (-p) if p > 0 and _xkwds_get(wide_dot, dot=False) else 0

258 t += (_0wdot(w, int(easting * f), d),

259 _0wdot(w, int(northing * f), d))

260 else: # prec <= -_EN_WIDE

261 t += (NN, NN)

262 except (TypeError, ValueError) as x:

263 raise _ValueError(easting=easting, northing=northing, prec=prec, cause=x)

264 return t

265

266 if enstr2.__doc__: # PYCHOK expected

267 enstr2.__doc__ %= (_EN_WIDE,)

268

269

270 def _enstr2m3(estr, nstr, wide=_EN_WIDE): # in .mgrs, .osgr

271 '''(INTERNAL) Convert east- and northing C{str}s to meter and resolution.

272 '''

273 def _s2m2(s, m): # e or n str to float meter

274 if _DOT_ in s:

275 m = 1 # meter

276 else:

277 s += _0_ * wide

278 s = _DOT_(s[:wide], s[wide:wide+_EN_PREC])

279 return float(s), m

280

281 e, m = _s2m2(estr, 0)

282 n, m = _s2m2(nstr, m)

283 if not m:

284 p = max(len(estr), len(nstr)) # 2 = km, 5 = m, 7 = cm

285 m = 10**max(-_EN_PREC, wide - p) # resolution, meter

286 return e, n, m

287

288

289 def fstr(floats, prec=6, fmt=Fmt.F, ints=False, sep=_COMMASPACE_, strepr=None):

290 '''Convert one or more floats to string, optionally stripped of trailing zero decimals.

291

292 @arg floats: Single or a list, sequence, tuple, etc. (C{scalar}s).

293 @kwarg prec: The C{float} precision, number of decimal digits (0..9).

294 Trailing zero decimals are stripped if B{C{prec}} is

295 positive, but kept for negative B{C{prec}} values. In

296 addition, trailing decimal zeros are stripped for U{alternate,

297 form '#'<https://docs.Python.org/3/library/stdtypes.html

298 #printf-style-string-formatting>}.

299 @kwarg fmt: Optional, C{float} format (C{str}).

300 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}).

301 @kwarg sep: Separator joining the B{C{floats}} (C{str}).

302 @kwarg strepr: Optional callable to format non-C{floats} (typically

303 C{repr}, C{str}) or C{None} to raise a TypeError.

304

305 @return: The C{sep.join(strs(floats, ...)} joined (C{str}) or single

306 C{strs((floats,), ...)} (C{str}) if B{C{floats}} is C{scalar}.

307 '''

308 if isscalar(floats): # see Fstr.__call__ above

309 return next(_streprs(prec, (floats,), fmt, ints, True, strepr))

310 else:

311 return sep.join(_streprs(prec, floats, fmt, ints, True, strepr))

312

313

314 def _fstrENH2(inst, prec, m): # in .css, .lcc, .utmupsBase

315 # (INTERNAL) For C{Css.} and C{Lcc.} C{toRepr} and C{toStr} and C{UtmUpsBase._toStr}.

316 t = inst.easting, inst.northing

317 t = tuple(_streprs(prec, t, Fmt.F, False, True, None))

318 T = _E_, _N_

319 if m is not None and fabs(inst.height): # fabs(self.height) > EPS

320 t += hstr(inst.height, prec=-2, m=m),

321 T += _H_,

322 return t, T

323

324

325 def _fstrLL0(inst, prec, toRepr): # in .azimuthal, .css

326 # (INTERNAL) For C{_AzimuthalBase.} and C{CassiniSoldner.} C{toStr} and C{toRepr}.

327 t = tuple(_streprs(prec, inst.latlon0, Fmt.F, False, True, None))

328 if toRepr:

329 n = inst.name

330 if n:

331 t += Fmt.EQUAL(_name_, repr(n)),

332 t = Fmt.PAREN(inst.classname, _COMMASPACE_.join(t))

333 return t

334

335

336 def fstrzs(efstr, ap1z=False):

337 '''Strip trailing zero decimals from a C{float} string.

338

339 @arg efstr: Float with or without exponent (C{str}).

340 @kwarg ap1z: Append the decimal point and one zero decimal

341 if the B{C{efstr}} is all digits (C{bool}).

342

343 @return: Float (C{str}).

344 '''

345 s = efstr.find(_DOT_)

346 if s >= 0:

347 e = efstr.rfind(Fmt.e)

348 if e < 0:

349 e = efstr.rfind(Fmt.E)

350 if e < 0:

351 e = len(efstr)

352 s += 2 # keep 1st _DOT_ + _0_

353 if s < e and efstr[e-1] == _0_:

354 efstr = NN(efstr[:s], efstr[s:e].rstrip(_0_), efstr[e:])

355

356 elif ap1z:

357 # %.G and %.g formats may drop the decimal

358 # point and all trailing zeros, ...

359 if efstr.isdigit():

360 efstr += _DOT_ + _0_ # ... append or ...

361 else: # ... insert one dot and zero

362 e = efstr.rfind(Fmt.e)

363 if e < 0:

364 e = efstr.rfind(Fmt.E)

365 if e > 0:

366 efstr = NN(efstr[:e], _DOT_, _0_, efstr[e:])

367

368 return efstr

369

370

371 def hstr(height, prec=2, fmt=Fmt.h, ints=False, m=NN):

372 '''Return a string for the height value.

373

374 @arg height: Height value (C{float}).

375 @kwarg prec: The C{float} precision, number of decimal digits (0..9).

376 Trailing zero decimals are stripped if B{C{prec}} is

377 positive, but kept for negative B{C{prec}} values.

378 @kwarg fmt: Optional, C{float} format (C{str}).

379 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}).

380 @kwarg m: Optional unit of the height (C{str}).

381 '''

382 h = next(_streprs(prec, (height,), fmt, ints, True, None))

383 return NN(h, str(m)) if m else h

384

385

386 def instr(inst, *args, **kwds):

387 '''Return the string representation of an instantiation.

388

389 @arg inst: The instance (any C{type}).

390 @arg args: Optional positional arguments.

391 @kwarg kwds: Optional keyword arguments.

392

393 @return: Representation (C{str}).

394 '''

395 return unstr(_MODS.named.classname(inst), *args, **kwds)

396

397

398 def lrstrip(txt, lrpairs=_LR_PAIRS):

399 '''Left- I{and} right-strip parentheses, brackets, etc. from a string.

400

401 @arg txt: String to be stripped (C{str}).

402 @kwarg lrpairs: Parentheses, etc. to remove (C{dict} of one or several

403 C{(Left, Right)} pairs).

404

405 @return: Stripped B{C{txt}} (C{str}).

406 '''

407 _e, _s, _n = str.endswith, str.startswith, len

408 while _n(txt) > 2:

409 for L, R in lrpairs.items():

410 if _e(txt, R) and _s(txt, L):

411 txt = txt[_n(L):-_n(R)]

412 break # restart

413 else:

414 return txt

415

416

417 def pairs(items, prec=6, fmt=Fmt.F, ints=False, sep=_EQUAL_):

418 '''Convert items to I{name=value} strings, with C{float}s handled like L{fstr}.

419

420 @arg items: Name-value pairs (C{dict} or 2-{tuple}s of any C{type}s).

421 @kwarg prec: The C{float} precision, number of decimal digits (0..9).

422 Trailing zero decimals are stripped if B{C{prec}} is

423 positive, but kept for negative B{C{prec}} values.

424 @kwarg fmt: Optional, C{float} format (C{str}).

425 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}).

426 @kwarg sep: Separator joining I{names} and I{values} (C{str}).

427

428 @return: A C{tuple(B{sep}.join(t) for t in B{items}))} of C{str}s.

429 '''

430 try:

431 if isinstance(items, dict):

432 items = itemsorted(items)

433 elif not islistuple(items):

434 items = tuple(items)

435 # can't unzip empty items tuple, list, etc.

436 n, v = _zip(*items) if items else ((), ()) # strict=True

437 except (TypeError, ValueError):

438 raise _IsnotError(dict.__name__, '2-tuples', items=items)

439 v = _streprs(prec, v, fmt, ints, False, repr)

440 return tuple(sep.join(t) for t in _zip(map(str, n), v)) # strict=True

441

442

443 def _pct(fmt):

444 '''(INTERNAL) Prefix C{%} if needed.

445 '''

446 return fmt if _PERCENT_ in fmt else NN(_PERCENT_, fmt)

447

448

449 def reprs(objs, prec=6, fmt=Fmt.F, ints=False):

450 '''Convert objects to C{repr} strings, with C{float}s handled like L{fstr}.

451

452 @arg objs: List, sequence, tuple, etc. (any C{type}s).

453 @kwarg prec: The C{float} precision, number of decimal digits (0..9).

454 Trailing zero decimals are stripped if B{C{prec}} is

455 positive, but kept for negative B{C{prec}} values.

456 @kwarg fmt: Optional, C{float} format (C{str}).

457 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}).

458

459 @return: A C{tuple(map(fstr|repr, objs))} of C{str}s.

460 '''

461 return tuple(_streprs(prec, objs, fmt, ints, False, repr)) if objs else ()

462

463

464 def _resolution10(resolution, Error=ValueError): # in .mgrs, .osgr

465 '''(INTERNAL) Validate C{resolution} in C{meter}.

466 '''

467 try:

468 r = int(_log10(resolution))

469 if _EN_WIDE < r or r < -_EN_PREC:

470 raise ValueError

471 except (ValueError, TypeError):

472 raise Error(resolution=resolution)

473 return _MODS.units.Meter(resolution=10**r)

474

475

476 def _streprs(prec, objs, fmt, ints, force, strepr):

477 '''(INTERNAL) Helper for C{fstr}, C{pairs}, C{reprs} and C{strs}

478 '''

479 # <https://docs.Python.org/3/library/stdtypes.html#printf-style-string-formatting>

480 if fmt in _FfEeGg:

481 fGg = fmt in _Gg

482 fmt = NN(_PERCENT_, _DOT_, abs(prec), fmt)

483

484 elif fmt.startswith(_PERCENT_):

485 fGg = False

486 try: # to make sure fmt is valid

487 f = fmt.replace(_DOTSTAR_, Fmt.DOT(abs(prec)))

488 _ = f % (_0_0,)

489 except (TypeError, ValueError):

490 raise _ValueError(fmt=fmt, txt=_not_(repr(_DOTSTAR_)))

491 fmt = f

492

493 else:

494 raise _ValueError(fmt=fmt, txt=_not_(repr(_Fspec_)))

495

496 for i, o in enumerate(objs):

497 if force or isinstance(o, float):

498 t = fmt % (float(o),)

499 if ints and t.rstrip(_0to9_ if isint(o, both=True) else

500 _0_).endswith(_DOT_):

501 t = t.split(_DOT_)[0]

502 elif prec > 1:

503 t = fstrzs(t, ap1z=fGg)

504 elif strepr:

505 t = strepr(o)

506 else:

507 t = Fmt.PARENSPACED(Fmt.SQUARE(objs=i), o)

508 raise TypeError(_SPACE_(t, _not_scalar_))

509 yield t

510

511

512 def strs(objs, prec=6, fmt=Fmt.F, ints=False):

513 '''Convert objects to C{str} strings, with C{float}s handled like L{fstr}.

514

515 @arg objs: List, sequence, tuple, etc. (any C{type}s).

516 @kwarg prec: The C{float} precision, number of decimal digits (0..9).

517 Trailing zero decimals are stripped if B{C{prec}} is

518 positive, but kept for negative B{C{prec}} values.

519 @kwarg fmt: Optional, C{float} format (C{str}).

520 @kwarg ints: Optionally, remove the decimal dot for C{int} values (C{bool}).

521

522 @return: A C{tuple(map(fstr|str, objs))} of C{str}s.

523 '''

524 return tuple(_streprs(prec, objs, fmt, ints, False, str)) if objs else ()

525

526

527 def unstr(where, *args, **kwds):

528 '''Return the string representation of an invokation.

529

530 @arg where: Class, function, method (C{type}) or name (C{str}).

531 @arg args: Optional positional arguments.

532 @kwarg kwds: Optional keyword arguments, except

533 C{B{_ELLIPSIS}=False}.

534

535 @return: Representation (C{str}).

536 '''

537 t = reprs(args, fmt=Fmt.g) if args else ()

538 if kwds and _xkwds_pop(kwds, _ELLIPSIS=False):

539 t += _ELLIPSIS_,

540 if kwds:

541 t += pairs(itemsorted(kwds), fmt=Fmt.g)

542 n = where if isstr(where) else _dunder_nameof(where)

543 return Fmt.PAREN(n, _COMMASPACE_.join(t))

544

545

546 def _0wd(*w_i): # in .osgr, .wgrs

547 '''(INTERNAL) Int formatter'.

548 '''

549 return '%0*d' % w_i

550

551

552 def _0wdot(w, f, dot=0):

553 '''(INTERNAL) Int and Float formatter'.

554 '''

555 s = _0wd(w, int(f))

556 if dot:

557 s = _DOT_(s[:dot], s[dot:])

558 return s

559

560

561 def _0wpF(*w_p_f): # in .dms, .osgr

562 '''(INTERNAL) Float deg, min, sec formatter'.

563 '''

564 return '%0*.*f' % w_p_f # XXX was F

565

566

567 def _xattrs(insto, other, *attrs):

568 '''(INTERNAL) Copy attribute values from B{C{other}} to B{C{insto}}.

569

570 @arg insto: Object to copy attribute values to (any C{type}).

571 @arg other: Object to copy attribute values from (any C{type}).

572 @arg attrs: One or more attribute names (C{str}s).

573

574 @return: Object B{C{insto}}, updated.

575

576 @raise AttributeError: An B{C{attrs}} doesn't exist

577 or is not settable.

578 '''

579 def _getattr(o, a):

580 if hasattr(o, a):

581 return getattr(o, a)

582 try:

583 n = o._DOT_(a)

584 except AttributeError:

585 n = Fmt.DOT(a)

586 raise _AttributeError(o, name=n)

587

588 for a in attrs:

589 s = _getattr(other, a)

590 g = _getattr(insto, a)

591 if (g is None and s is not None) or g != s:

592 setattr(insto, a, s) # not settable?

593 return insto

594

595

596 def _xzipairs(names, values, sep=_COMMASPACE_, fmt=NN, pair_fmt=Fmt.COLON):

597 '''(INTERNAL) Zip C{names} and C{values} into a C{str}, joined and bracketed.

598 '''

599 try:

600 t = sep.join(pair_fmt(*t) for t in _zip(names, values)) # strict=True

601 except Exception as x:

602 raise _ValueError(names=names, values=values, cause=x)

603 return (fmt % (t,)) if fmt else t

604

605# **) MIT License

606#

607# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.

608#

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

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

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

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

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

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

615#

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

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

618#

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

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

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

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

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

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

625# OTHER DEALINGS IN THE SOFTWARE.