1

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

3

4 u'''Mutable, immutable and caching/memoizing properties and

5 deprecation decorators.

6

7 To enable C{DeprecationWarning}s from C{PyGeodesy}, set env var

8 C{PYGEODESY_WARNINGS} to a non-empty string I{AND} run C{python}

9 with command line option C{-X dev} or with one of the C{-W}

10 choices, see callable L{DeprecationWarnings} below.

11'''

12

13 from pygeodesy.basics import isclass as _isclass

14 from pygeodesy.errors import _AssertionError, _AttributeError, \

15 _xcallable, _xkwds, _xkwds_get

16 from pygeodesy.interns import MISSING, NN, _an_, _COMMASPACE_, \

17 _DEPRECATED_, _DOT_, _EQUALSPACED_, \

18 _immutable_, _invalid_, _module_, _N_A_, \

19 _not_, _SPACE_, _UNDER_, _DNL_ # PYCHOK used!

20# from pygeodesy.named import callname # _MODS, avoid circular

21 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, \

22 _FOR_DOCS, _WARNINGS_X_DEV

23# from pygeodesy.streprs import Fmt # _MODS

24

25 from functools import wraps as _wraps

26

27__all__ = _ALL_LAZY.props

28__version__ = '24.07.23'

29

30_class_ = 'class'

31_dont_use_ = _DEPRECATED_ + ", don't use."

32_function_ = 'function'

33_get_and_set_ = 'get and set'

34_has_been_ = 'has been' # PYCHOK used!

35_method_ = 'method'

36_not_an_inst_ = _not_(_an_, 'instance')

37

38

39 def _allPropertiesOf(Clas_or_inst, *Bases, **excls):

40 '''(INTERNAL) Yield all C{R/property/_RO}s at C{Clas_or_inst}

41 as specified in the C{Bases} arguments, except C{excls}.

42 '''

43 if _isclass(Clas_or_inst):

44 S = Clas_or_inst, # just this Clas

45 else: # class and super-classes of inst

46 try:

47 S = Clas_or_inst.__class__.__mro__[:-1] # not object

48 except AttributeError:

49 raise

50 S = () # not an inst

51 B = Bases or _PropertyBase

52 P = _property_RO___

53 for C in S:

54 for n, p in C.__dict__.items():

55 if isinstance(p, B) and p.name == n and not (

56 isinstance(p, P) or n in excls):

57 yield p

58

59

60 def _allPropertiesOf_n(n, Clas_or_inst, *Bases, **excls):

61 '''(INTERNAL) Assert the number of C{R/property/_RO}s at C{Clas_or_inst}.

62 '''

63 t = tuple(p.name for p in _allPropertiesOf(Clas_or_inst, *Bases, **excls))

64 if len(t) != n:

65 raise _AssertionError(_COMMASPACE_.join(t), Clas_or_inst,

66 txt=_COMMASPACE_(len(t), _not_(n)))

67 return t

68

69

70 def _hasProperty(inst, name, *Classes): # in .named._NamedBase._update

71 '''(INTERNAL) Check whether C{inst} has a C{P/property/_RO} by this C{name}.

72 '''

73 p = getattr(inst.__class__, name, None) # walks __class__.__mro__

74 return bool(p and isinstance(p, Classes or _PropertyBase)

75 and p.name == name)

76

77

78# def _isclass(obj):

79# '''(INTERNAL) Get and overwrite C{_isclass}.

80# '''

81# _MODS.getmodule(__name__)._isclass = f = _MODS.basics.isclass

82# return f(obj)

83

84

85 def _update_all(inst, *attrs, **Base_needed):

86 '''(INTERNAL) Zap all I{cached} L{property_RO}s, L{Property}s,

87 L{Property_RO}s and the named C{attrs} of an instance.

88

89 @return: The number of updates (C{int}), if any.

90 '''

91 if _isclass(inst):

92 raise _AssertionError(inst, txt=_not_an_inst_)

93 try:

94 d = inst.__dict__

95 except AttributeError:

96 return 0

97 u = len(d)

98 if u > _xkwds_get(Base_needed, needed=0):

99 B = _xkwds_get(Base_needed, Base=_PropertyBase)

100 for p in _allPropertiesOf(inst, B):

101 p._update(inst) # d.pop(p.name, None)

102

103 if attrs:

104 _update_attrs(inst, *attrs) # remove attributes from inst.__dict__

105 u -= len(d)

106 return u # updates

107

108

109# def _update_all_from(inst, other, **Base):

110# '''(INTERNAL) Update all I{cached} L{Property}s and

111# L{Property_RO}s of instance C{inst} from C{other}.

112#

113# @return: The number of updates (C{int}), if any.

114# '''

115# if _isclass(inst):

116# raise _AssertionError(inst, txt=_not_an_inst_)

117# try:

118# d = inst.__dict__

119# f = other.__dict__

120# except AttributeError:

121# return 0

122# u = len(f)

123# if u:

124# u = len(d)

125# B = _xkwds_get(Base, Base=_PropertyBase)

126# for p in _allPropertiesOf(inst, B):

127# p._update_from(inst, other)

128# u -= len(d)

129# return u # number of updates

130

131

132 def _update_attrs(inst, *attrs):

133 '''(INTERNAL) Zap all named C{attrs} of an instance.

134

135 @return: The number of updates (C{int}), if any.

136 '''

137 try:

138 d = inst.__dict__

139 except AttributeError:

140 return 0

141 u = len(d)

142 if u: # zap attrs from inst.__dict__

143 _p = d.pop

144 for a in attrs:

145 _ = _p(a, MISSING)

146# if _ is MISSING and not hasattr(inst, a):

147# n = _MODS.named.classname(inst, prefixed=True)

148# a = _DOT_(n, _SPACE_(a, _invalid_))

149# raise _AssertionError(a, txt=repr(inst))

150# _ = _p(a, None) # redo: hasattr side effect

151 u -= len(d)

152 # assert u >= 0

153 return u # number of named C{attrs} zapped

154

155

156 class _PropertyBase(property):

157 '''(INTERNAL) Base class for C{P/property/_RO}.

158 '''

159 def __init__(self, method, fget, fset, doc=NN):

160

161 _xcallable(getter=method, fget=fget)

162

163 self.method = method

164 self.name = method.__name__

165 d = doc or method.__doc__

166 if _FOR_DOCS and d:

167 self.__doc__ = d # PYCHOK no cover

168

169 property.__init__(self, fget, fset, self._fdel, d or _N_A_)

170

171 def _Error(self, kind, nameter, farg):

172 '''(INTERNAL) Return an C{AttributeError} instance.

173 '''

174 if farg:

175 n = _DOT_(self.name, nameter.__name__)

176 n = _SPACE_(n, farg.__name__)

177 else:

178 n = nameter

179 e = _SPACE_(kind, _MODS.named.classname(self))

180 return _AttributeError(e, txt=n)

181

182 def _fdel(self, inst):

183 '''Zap the I{cached/memoized} C{property} value.

184 '''

185 self._update(inst, None) # PYCHOK no cover

186

187 def _fget(self, inst):

188 '''Get and I{cache/memoize} the C{property} value.

189 '''

190 try: # to get the value cached in instance' __dict__

191 return inst.__dict__[self.name]

192 except KeyError:

193 # cache the value in the instance' __dict__

194 inst.__dict__[self.name] = val = self.method(inst)

195 return val

196

197 def _fset_error(self, inst, val):

198 '''Throws an C{AttributeError}, always.

199 '''

200 n = _MODS.named.classname(inst)

201 n = _DOT_(n, self.name)

202 n = _EQUALSPACED_(n, repr(val))

203 raise self._Error(_immutable_, n, None)

204

205 def _update(self, inst, *unused):

206 '''(INTERNAL) Zap the I{cached/memoized} C{inst.__dict__[name]} item.

207 '''

208 inst.__dict__.pop(self.name, None) # name, NOT _name

209

210 def _update_from(self, inst, other):

211 '''(INTERNAL) Copy a I{cached/memoized} C{inst.__dict__[name]} item

212 from C{other.__dict__[name]} if present, otherwise zap it.

213 '''

214 n = self.name # name, NOT _name

215 v = other.__dict__.get(n, MISSING)

216 if v is MISSING:

217 inst.__dict__.pop(n, None)

218 else:

219 inst.__dict__[n] = v

220

221 def deleter(self, fdel):

222 '''Throws an C{AttributeError}, always.

223 '''

224 raise self._Error(_invalid_, self.deleter, fdel)

225

226 def getter(self, fget):

227 '''Throws an C{AttributeError}, always.

228 '''

229 raise self._Error(_invalid_, self.getter, fget)

230

231 def setter(self, fset):

232 '''Throws an C{AttributeError}, always.

233 '''

234 raise self._Error(_immutable_, self.setter, fset)

235

236

237 class Property_RO(_PropertyBase):

238 # No __doc__ on purpose

239 def __init__(self, method, doc=NN): # PYCHOK expected

240 '''New I{immutable}, I{caching}, I{memoizing} C{property} I{Factory}

241 to be used as C{decorator}.

242

243 @arg method: The callable being decorated as this C{property}'s C{getter},

244 to be invoked only once.

245 @kwarg doc: Optional property documentation (C{str}).

246

247 @note: Like standard Python C{property} without a C{setter}, but with

248 a more descriptive error message when set.

249

250 @see: Python 3's U{functools.cached_property<https://docs.Python.org/3/

251 library/functools.html#functools.cached_property>} and U{-.cache

252 <https://Docs.Python.org/3/library/functools.html#functools.cache>}

253 to I{cache} or I{memoize} the property value.

254

255 @see: Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 636

256 Example 19-24 or 2022 p. 870 Example 22-28 and U{class

257 Property<https://docs.Python.org/3/howto/descriptor.html>}.

258 '''

259 _fget = method if _FOR_DOCS else self._fget # XXX force method.__doc__ to epydoc

260 _PropertyBase.__init__(self, method, _fget, self._fset_error)

261

262 def __get__(self, inst, *unused): # PYCHOK 2 vs 3 args

263 if inst is None:

264 return self

265 try: # to get the cached value immediately

266 return inst.__dict__[self.name]

267 except (AttributeError, KeyError):

268 return self._fget(inst)

269

270

271 class Property(Property_RO):

272 # No __doc__ on purpose

273 __init__ = Property_RO.__init__

274 '''New I{mutable}, I{caching}, I{memoizing} C{property} I{Factory}

275 to be used as C{decorator}.

276

277 @see: L{Property_RO} for more details.

278

279 @note: Unless and until the C{setter} is defined, this L{Property} behaves

280 like an I{immutable}, I{caching}, I{memoizing} L{Property_RO}.

281 '''

282

283 def setter(self, method):

284 '''Make this C{Property} I{mutable}.

285

286 @arg method: The callable being decorated as this C{Property}'s C{setter}.

287

288 @note: Setting a new property value always clears the previously I{cached}

289 or I{memoized} value I{after} invoking the B{C{method}}.

290 '''

291 def _fset(inst, val):

292 '''Set and I{cache}, I{memoize} the C{property} value.

293 '''

294 _ = method(inst, val)

295 self._update(inst) # un-cache this item

296

297 return self._setters(method, _fset)

298

299 def setter_(self, method):

300 '''Make this C{Property} I{mutable}.

301

302 @arg method: The callable being decorated as this C{Property}'s C{setter}

303 and returning the new property value to be I{cached} or

304 I{memoized}.

305 '''

306 def _fset(inst, val):

307 '''Set and I{cache}, I{memoize} the C{property} value.

308 '''

309 val = method(inst, val)

310 inst.__dict__[self.name] = val

311

312 return self._setters(method, _fset)

313

314 def _setters(self, method, _fset):

315 _xcallable(setter=method, fset=_fset)

316 if _FOR_DOCS: # XXX force method.__doc__ into epydoc

317 _PropertyBase.__init__(self, self.method, self.method, method)

318 else: # class Property <https://docs.Python.org/3/howto/descriptor.html>

319 _PropertyBase.__init__(self, self.method, self._fget, _fset)

320 return self

321

322

323 class property_RO(_PropertyBase):

324 # No __doc__ on purpose

325 _uname = NN

326

327 def __init__(self, method, doc=NN): # PYCHOK expected

328 '''New I{immutable}, standard C{property} to be used as C{decorator}.

329

330 @arg method: The callable being decorated as C{property}'s C{getter}.

331 @kwarg doc: Optional property documentation (C{str}).

332

333 @note: Like standard Python C{property} without a setter, but with

334 a more descriptive error message when set.

335

336 @see: L{Property_RO}.

337 '''

338 _PropertyBase.__init__(self, method, method, self._fset_error, doc=doc)

339 self._uname = NN(_UNDER_, self.name) # actual attr UNDER<name>

340

341 def _update(self, inst, *Clas): # PYCHOK signature

342 '''(INTERNAL) Zap the I{cached} C{B{inst}.__dict__[_name]} item.

343 '''

344 uname = self._uname

345 if uname in inst.__dict__:

346 if Clas: # overrides inst.__class__

347 d = Clas[0].__dict__.get(uname, MISSING)

348 else:

349 d = getattr(inst.__class__, uname, MISSING)

350# if d is MISSING: # XXX superfluous

351# for c in inst.__class__.__mro__[:-1]:

352# if uname in c.__dict__:

353# d = c.__dict__[uname]

354# break

355 if d is None: # remove inst value

356 inst.__dict__.pop(uname)

357

358

359 class _property_RO___(_PropertyBase):

360 # No __doc__ on purpose

361

362 def __init__(self, method, doc=NN): # PYCHOK expected

363 '''New C{property_ROnce} or C{property_ROver}, holding a singleton value as

364 class attribute for all instances of that class and overwriting C{self},

365 the C{property_ROver} instance in the 1st invokation.

366

367 @see: L{property_RO} for further details.

368 '''

369 _PropertyBase.__init__(self, method, self._fget, self._fset_error, doc=doc)

370

371 def _fdel(self, unused): # PYCHOK no cover

372 '''Silently ignored, always.

373 '''

374 pass

375

376 def _update(self, *unused): # PYCHOK signature

377 '''(INTERNAL) No-op, ignore updates.

378 '''

379 pass

380

381

382 class property_ROnce(_property_RO___):

383 # No __doc__ on purpose

384

385 def _fget(self, inst):

386 '''Get the C{property} value, only I{once} and memoize/cache it.

387 '''

388 try:

389 v = self._val

390 except AttributeError:

391 v = self._val = self.method(inst)

392 return v

393

394

395 class property_ROver(_property_RO___):

396 # No __doc__ on purpose

397

398 def _fget(self, inst):

399 '''Get the C{property} value I{once} and overwrite C{self}, this C{property} instance.

400 '''

401 v = self.method(inst)

402 n = self.name

403 C = inst.__class__

404 for c in C.__mro__: # [:-1]

405 if getattr(c, n, None) is self:

406 setattr(c, n, v) # overwrite property_ROver

407 break

408 else:

409 n = _DOT_(C.__name__, n)

410 raise _AssertionError(_EQUALSPACED_(n, v))

411 return v

412

413

414 class _NamedProperty(property):

415 '''Class C{property} with retrievable name.

416 '''

417 @Property_RO

418 def name(self):

419 '''Get the name of this C{property} (C{str}).

420 '''

421 return self.fget.__name__

422

423

424 def property_doc_(doc):

425 '''Decorator for a standard C{property} with basic documentation.

426

427 @arg doc: The property documentation (C{str}).

428

429 @example:

430

431 >>>class Clas(object):

432 >>>

433 >>> @property_doc_("documentation text.")

434 >>> def name(self):

435 >>> ...

436 >>>

437 >>> @name.setter

438 >>> def name(self, value):

439 >>> ...

440 '''

441 # See Luciano Ramalho, "Fluent Python", O'Reilly, 2016 p. 212+

442 # Example 7-23 or 2022 p. 331+ Example 9-22 and <https://

443 # Python-3-Patterns-Idioms-Test.ReadTheDocs.io/en/latest/PythonDecorators.html>

444

445 def _documented_property(method):

446 '''(INTERNAL) Return the documented C{property}.

447 '''

448 t = _get_and_set_ if doc.startswith(_SPACE_) else NN

449 return _NamedProperty(method, None, None, NN('Property to ', t, doc))

450

451 return _documented_property

452

453

454 def _deprecated(call, kind, qual_d):

455 '''(INTERNAL) Decorator for DEPRECATED functions, methods, etc.

456

457 @see: Brett Slatkin, "Effective Python", 2019 page 105, 2nd

458 ed, Addison-Wesley.

459 '''

460 doc = _docof(call)

461

462 @_wraps(call) # PYCHOK self?

463 def _deprecated_call(*args, **kwds):

464 if qual_d: # function

465 q = qual_d

466 elif args: # method

467 q = _qualified(args[0], call.__name__)

468 else: # PYCHOK no cover

469 q = call.__name__

470 _throwarning(kind, q, doc)

471 return call(*args, **kwds)

472

473 return _deprecated_call

474

475

476 def deprecated_class(cls_or_class):

477 '''Use inside __new__ or __init__ of a DEPRECATED class.

478

479 @arg cls_or_class: The class (C{cls} or C{Class}).

480

481 @note: NOT a decorator!

482 '''

483 if _WARNINGS_X_DEV:

484 q = _DOT_(cls_or_class.__module__, cls_or_class.__name__)

485 _throwarning(_class_, q, cls_or_class.__doc__)

486

487

488 def deprecated_function(call):

489 '''Decorator for a DEPRECATED function.

490

491 @arg call: The deprecated function (C{callable}).

492

493 @return: The B{C{call}} DEPRECATED.

494 '''

495 return _deprecated(call, _function_, _DOT_(

496 call.__module__, call.__name__)) if \

497 _WARNINGS_X_DEV else call

498

499

500 def deprecated_method(call):

501 '''Decorator for a DEPRECATED method.

502

503 @arg call: The deprecated method (C{callable}).

504

505 @return: The B{C{call}} DEPRECATED.

506 '''

507 return _deprecated(call, _method_, NN) if _WARNINGS_X_DEV else call

508

509

510 def _deprecated_module(name): # PYCHOK no cover

511 '''(INTERNAL) Callable within a DEPRECATED module.

512 '''

513 if _WARNINGS_X_DEV:

514 _throwarning(_module_, name, _dont_use_)

515

516

517 if _WARNINGS_X_DEV:

518 class deprecated_property(_PropertyBase):

519 '''Decorator for a DEPRECATED C{property} or C{Property}.

520 '''

521 def __init__(self, method):

522 '''Decorator for a DEPRECATED C{property} or C{Property} getter.

523 '''

524 doc = _docof(method)

525

526 def _fget(inst): # PYCHOK no cover

527 '''Get the C{property} or C{Property} value.

528 '''

529 q = _qualified(inst, self.name)

530 _throwarning(property.__name__, q, doc)

531 return self.method(inst) # == method

532

533 _PropertyBase.__init__(self, method, _fget, None, doc=doc)

534

535 def setter(self, method):

536 '''Decorator for a DEPRECATED C{property} or C{Property} setter.

537

538 @arg method: The callable being decorated as this C{Property}'s C{setter}.

539

540 @note: Setting a new property value always clears the previously I{cached}

541 or I{memoized} value I{after} invoking the B{C{method}}.

542 '''

543 if not callable(method):

544 _PropertyBase.setter(self, method) # PYCHOK no cover

545

546 if _FOR_DOCS: # XXX force method.__doc__ into epydoc

547 _PropertyBase.__init__(self, self.method, self.method, method)

548 else:

549

550 def _fset(inst, val):

551 '''Set the C{property} or C{Property} value.

552 '''

553 q = _qualified(inst, self.name)

554 _throwarning(property.__name__, q, _docof(method))

555 method(inst, val)

556 # self._update(inst) # un-cache this item

557

558 # class Property <https://docs.Python.org/3/howto/descriptor.html>

559 _PropertyBase.__init__(self, self.method, self._fget, _fset)

560 return self

561

562 else: # PYCHOK no cover

563 class deprecated_property(property): # PYCHOK expected

564 '''Decorator for a DEPRECATED C{property} or C{Property}.

565 '''

566 pass

567

568 deprecated_Property = deprecated_property

569

570

571 def deprecated_Property_RO(method):

572 '''Decorator for a DEPRECATED L{Property_RO}.

573

574 @arg method: The C{Property_RO.fget} method (C{callable}).

575

576 @return: The B{C{method}} DEPRECATED.

577 '''

578 return _deprecated_RO(method, Property_RO)

579

580

581 def deprecated_property_RO(method):

582 '''Decorator for a DEPRECATED L{property_RO}.

583

584 @arg method: The C{property_RO.fget} method (C{callable}).

585

586 @return: The B{C{method}} DEPRECATED.

587 '''

588 return _deprecated_RO(method, property_RO)

589

590

591 def _deprecated_RO(method, _RO):

592 '''(INTERNAL) Create a DEPRECATED C{property_RO} or C{Property_RO}.

593 '''

594 doc = _docof(method)

595

596 if _WARNINGS_X_DEV:

597

598 class _Deprecated_RO(_PropertyBase):

599 __doc__ = doc

600

601 def __init__(self, method):

602 _PropertyBase.__init__(self, method, self._fget, self._fset_error, doc=doc)

603

604 def _fget(self, inst): # PYCHOK no cover

605 q = _qualified(inst, self.name)

606 _throwarning(_RO.__name__, q, doc)

607 return self.method(inst)

608

609 return _Deprecated_RO(method)

610 else: # PYCHOK no cover

611 return _RO(method, doc=doc)

612

613

614 def _docof(obj):

615 '''(INTERNAL) Get uniform DEPRECATED __doc__ string.

616 '''

617 try:

618 d = obj.__doc__.strip()

619 i = d.find(_DEPRECATED_)

620 except AttributeError:

621 i = -1

622 return _DOT_(_DEPRECATED_, NN) if i < 0 else d[i:]

623

624

625 def _qualified(inst, name):

626 '''(INTERNAL) Fully qualify a name.

627 '''

628 # _DOT_(inst.classname, name), not _DOT_(inst.named4, name)

629 c = inst.__class__

630 q = _DOT_(c.__module__, c.__name__, name)

631 return q

632

633

634 class DeprecationWarnings(object):

635 '''(INTERNAL) Handle C{DeprecationWaring}s.

636 '''

637 _Warnings = 0

638

639 def __call__(self): # for backward compatibility

640 '''Have any C{DeprecationWarning}s been reported or raised?

641

642 @return: The number of C{DeprecationWarning}s (C{int}) so

643 far or C{None} if not enabled.

644

645 @note: To get C{DeprecationWarning}s if any, run C{python}

646 with env var C{PYGEODESY_WARNINGS} set to a non-empty

647 string I{AND} use C{python[3]} command line option

648 C{-X dev}, C{-W always} or C{-W error}, etc.

649 '''

650 return self.Warnings

651

652 def throw(self, kind, name, doc, **stacklevel): # stacklevel=3

653 '''Report or raise a C{DeprecationWarning}.

654 '''

655 line = doc.split(_DNL_, 1)[0].strip()

656 name = _MODS.streprs.Fmt.CURLY(L=name)

657 text = _SPACE_(kind, name, _has_been_, *line.split())

658 kwds = _xkwds(stacklevel, stacklevel=3)

659 # XXX invoke warn or raise DeprecationWarning(text)

660 self._warn(text, category=DeprecationWarning, **kwds)

661 self._Warnings += 1

662

663 @Property_RO

664 def _warn(self):

665 '''Get Python's C{warnings.warn}.

666 '''

667 from warnings import warn

668 return warn

669

670 @property_RO

671 def Warnings(self):

672 '''Get the number of C{DeprecationWarning}s (C{int}) so

673 far or C{None} if not enabled.

674 '''

675 return self._Warnings if _WARNINGS_X_DEV else None

676

677 DeprecationWarnings = DeprecationWarnings() # PYCHOK singleton

678_throwarning = DeprecationWarnings.throw

679

680# **) MIT License

681#

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

683#

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

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

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

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

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

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

690#

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

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

693#

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

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

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

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

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

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

700# OTHER DEALINGS IN THE SOFTWARE.