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_, \

16 _no_, _exceeds_, _f_, _F_, _g_, _limit_, \

17 _tolerance_ # PYCHOK used!

18 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

19

20 from math import fabs, log10 as _log10

21

22__all__ = _ALL_LAZY.streprs

23__version__ = '23.05.06'

24

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

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

27_eps_ = 'eps' # PYCHOK used!

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

29_PAREN_g = '(%g)' # PYCHOK used!

30_threshold_ = 'threshold' # PYCHOK used!

31

32

33 class _Fmt(str): # in .streprs

34 '''(INTERNAL) Callable formatting.

35 '''

36 name = NN

37

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

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

40 or just a single C{value}.

41 '''

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

43 break

44 else:

45 if len(name_value_) > 1:

46 n, v = name_value_[:2]

47 elif name_value_:

48 n, v = NN, name_value_[0]

49 else:

50 n, v = NN, MISSING

51 t = str.__mod__(self, v)

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

53

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

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

56# '''

57# return str.__mod__(self, arg)

58

59

60 class Fstr(str):

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

62 '''

63 name = NN

64

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

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

67 '''

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

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

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

71 return t

72

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

74 '''Regular C{%} operator.

75

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

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

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

79

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

81

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

83 '''

84 def _error(arg):

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

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

87

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

89 if islistuple(arg):

90 n = len(arg)

91 if n == 1:

92 arg = arg[0]

93 elif n == 2:

94 prec, arg = arg

95 else:

96 raise _ValueError(_error(arg))

97

98 if not isscalar(arg):

99 raise _TypeError(_error(arg))

100 return self(arg, prec=prec)

101

102

103 class _Sub(str):

104 '''(INTERNAL) Class list formatter.

105 '''

106 # see .ellipsoidalNvector.LatLon.deltaTo

107 def __call__(self, *Classes):

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

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

110

111

112 class Fmt(object):

113 '''Formatting options.

114 '''

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

116 COLON = _Fmt(':%s')

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

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

119 convergence = _Fmt(_convergence_(_PAREN_g))

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

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

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

123 e = Fstr(_e_)

124 E = Fstr(_E_)

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

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

127 exceeds_eps = _Fmt(_exceeds_(_eps_, _PAREN_g))

128 exceeds_limit = _Fmt(_exceeds_(_limit_, _PAREN_g))

129 f = Fstr(_f_)

130 F = Fstr(_F_)

131 g = Fstr(_g_)

132 G = Fstr('G')

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

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

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

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

137 PAREN_g = _Fmt(_PAREN_g)

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

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

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

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

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

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

144 TAG = ANGLE

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

146 tolerance = _Fmt(_tolerance_(_PAREN_g))

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

148

149 def __init__(self):

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

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

152 setattr(a, _name_, n)

153

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

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

156 '''

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

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

159

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

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

162 if tol:

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

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

165 t = t.replace(_tolerance_, _threshold_)

166 return _no_(t)

167

168 Fmt = Fmt() # PYCHOK singleton

169 Fmt.__name__ = Fmt.__class__.__name__

170

171_DOTSTAR_ = Fmt.DOT(_STAR_)

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

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

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

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

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

177

178

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

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

181

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

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

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

185

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

187

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

189 intermediate whitespace characters are coalesced and

190 substituted.

191 '''

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

193 for c in n:

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

195 s = s.replace(c, _SPACE_)

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

197

198

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

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

201 formatted by function L{fstr}.

202

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

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

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

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

207

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

209 of C{str}s.

210 '''

211 def _items(inst, names, Nones):

212 for n in names:

213 v = getattr(inst, n, None)

214 if Nones or v is not None:

215 yield n, v

216

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

218 return Nones, kwds

219

220 Nones, kwds = _Nones_kwds(**Nones_True__pairs_kwds)

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

222

223

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

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

226

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

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

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

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

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

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

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

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

235

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

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

238

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

240

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

242 '''

243 t = extras

244 try: # like .dms.compassPoint

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

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

247 if w > 0:

248 f = 10**p # truncate

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

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

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

252 else: # prec <= -_EN_WIDE

253 t += (NN, NN)

254 except (TypeError, ValueError) as x:

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

256 return t

257

258 if enstr2.__doc__: # PYCHOK expected

259 enstr2.__doc__ %= (_EN_WIDE,)

260

261

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

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

264 '''

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

266 if _DOT_ in s:

267 m = 1 # meter

268 else:

269 s += _0_ * wide

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

271 return float(s), m

272

273 e, m = _s2m2(estr, 0)

274 n, m = _s2m2(nstr, m)

275 if not m:

276 p = max(len(estr), len(nstr)) # 2 = Km, 5 = m, 7 = cm

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

278 return e, n, m

279

280

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

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

283

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

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

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

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

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

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

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

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

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

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

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

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

296

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

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

299 '''

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

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

302 else:

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

304

305

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

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

308 t = inst.easting, inst.northing

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

310 T = _E_, _N_

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

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

313 T += _H_,

314 return t, T

315

316

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

318 # (INTERNAL) For C{_AlbersBase.}, C{_AzimuthalBase.} and C{CassiniSoldner.}

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

320 if toRepr:

321 n = inst.name

322 if n:

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

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

325 return t

326

327

328 def fstrzs(efstr, ap1z=False):

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

330

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

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

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

334

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

336 '''

337 s = efstr.find(_DOT_)

338 if s >= 0:

339 e = efstr.rfind(Fmt.e)

340 if e < 0:

341 e = efstr.rfind(Fmt.E)

342 if e < 0:

343 e = len(efstr)

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

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

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

347

348 elif ap1z:

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

350 # point and all trailing zeros, ...

351 if efstr.isdigit():

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

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

354 e = efstr.rfind(Fmt.e)

355 if e < 0:

356 e = efstr.rfind(Fmt.E)

357 if e > 0:

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

359

360 return efstr

361

362

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

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

365

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

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

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

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

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

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

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

373 '''

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

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

376

377

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

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

380

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

382 @arg args: Optional positional arguments.

383 @kwarg kwds: Optional keyword arguments.

384

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

386 '''

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

388

389

390 def lrstrip(txt, lrpairs=_LR_PAIRS):

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

392

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

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

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

396

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

398 '''

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

400 while _n(txt) > 2:

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

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

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

404 break # restart

405 else:

406 return txt

407

408

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

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

411

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

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

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

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

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

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

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

419

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

421 '''

422 try:

423 if isinstance(items, dict):

424 items = itemsorted(items)

425 elif not islistuple(items):

426 items = tuple(items)

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

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

429 except (TypeError, ValueError):

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

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

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

433

434

435 def _pct(fmt):

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

437 '''

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

439

440

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

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

443

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

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

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

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

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

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

450

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

452 '''

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

454

455

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

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

458 '''

459 try:

460 r = int(_log10(resolution))

461 if _EN_WIDE < r or r < -_EN_PREC:

462 raise ValueError

463 except (ValueError, TypeError):

464 raise Error(resolution=resolution)

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

466

467

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

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

470 '''

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

472 if fmt in _FfEeGg:

473 fGg = fmt in _Gg

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

475

476 elif fmt.startswith(_PERCENT_):

477 fGg = False

478 try: # to make sure fmt is valid

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

480 _ = f % (_0_0,)

481 except (TypeError, ValueError):

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

483 fmt = f

484

485 else:

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

487

488 for i, o in enumerate(objs):

489 if force or isinstance(o, float):

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

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

492 _0_).endswith(_DOT_):

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

494 elif prec > 1:

495 t = fstrzs(t, ap1z=fGg)

496 elif strepr:

497 t = strepr(o)

498 else:

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

500 raise TypeError(_SPACE_(t, _not_scalar_))

501 yield t

502

503

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

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

506

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

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

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

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

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

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

513

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

515 '''

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

517

518

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

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

521

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

523 @arg args: Optional positional arguments.

524 @kwarg kwds: Optional keyword arguments, except

525 C{B{_ELLIPSIS}=False}.

526

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

528 '''

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

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

531 t += _ELLIPSIS_,

532 if kwds:

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

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

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

536

537

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

539 '''(INTERNAL) Int formatter'.

540 '''

541 return '%0*d' % w_i

542

543

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

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

546 '''

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

548 if dot:

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

550 return s

551

552

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

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

555 '''

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

557

558

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

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

561

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

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

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

565

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

567

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

569 or is not settable.

570 '''

571 def _getattr(o, a):

572 if hasattr(o, a):

573 return getattr(o, a)

574 try:

575 n = o._DOT_(a)

576 except AttributeError:

577 n = Fmt.DOT(a)

578 raise _AttributeError(o, name=n)

579

580 for a in attrs:

581 s = _getattr(other, a)

582 g = _getattr(insto, a)

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

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

585 return insto

586

587

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

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

590 '''

591 try:

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

593 except Exception as x:

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

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

596

597# **) MIT License

598#

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

600#

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

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

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

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

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

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

607#

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

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

610#

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

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

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

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

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

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

617# OTHER DEALINGS IN THE SOFTWARE.