Coverage for pygeodesy/props.py: 98%

212 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-01 11:43 -0400

1 

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

3 

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

5deprecation decorators. 

6 

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

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

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

10choices, see callable L{DeprecationWarnings} below. 

11''' 

12 

13from pygeodesy.basics import isclass as _isclass 

14from pygeodesy.errors import _AssertionError, _AttributeError, \ 

15 _xcallable, _xkwds, _xkwds_get 

16from 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 

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

22 _FOR_DOCS, _WARNINGS_X_DEV 

23# from pygeodesy.streprs import Fmt # _MODS 

24 

25from functools import wraps as _wraps 

26 

27__all__ = _ALL_LAZY.props 

28__version__ = '24.05.26' 

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 

39def _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 _isa = isinstance 

53 for C in S: 

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

55 if _isa(p, B) and p.name == n and n not in excls: 

56 yield p 

57 

58 

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

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

61 ''' 

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

63 if len(t) != n: 

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

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

66 return t 

67 

68 

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

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

71 ''' 

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

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

74 and p.name == name) 

75 

76 

77# def _isclass(obj): 

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

79# ''' 

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

81# return f(obj) 

82 

83 

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

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

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

87 

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

89 ''' 

90 if _isclass(inst): 

91 raise _AssertionError(inst, txt=_not_an_inst_) 

92 try: 

93 d = inst.__dict__ 

94 except AttributeError: 

95 return 0 

96 u = len(d) 

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

98 B = _xkwds_get(Base_needed, Base=_PropertyBase) 

99 for p in _allPropertiesOf(inst, B): 

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

101 

102 if attrs: 

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

104 u -= len(d) 

105 return u # updates 

106 

107 

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

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

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

111# 

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

113# ''' 

114# if _isclass(inst): 

115# raise _AssertionError(inst, txt=_not_an_inst_) 

116# try: 

117# d = inst.__dict__ 

118# f = other.__dict__ 

119# except AttributeError: 

120# return 0 

121# u = len(f) 

122# if u: 

123# u = len(d) 

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

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

126# p._update_from(inst, other) 

127# u -= len(d) 

128# return u # number of updates 

129 

130 

131def _update_attrs(inst, *attrs): 

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

133 

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

135 ''' 

136 try: 

137 d = inst.__dict__ 

138 except AttributeError: 

139 return 0 

140 u = len(d) 

141 if u: # zap attrs from inst.__dict__ 

142 _p = d.pop 

143 for a in attrs: 

144 _ = _p(a, MISSING) 

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

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

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

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

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

150 u -= len(d) 

151 # assert u >= 0 

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

153 

154 

155class _PropertyBase(property): 

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

157 ''' 

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

159 

160 _xcallable(getter=method, fget=fget) 

161 

162 self.method = method 

163 self.name = method.__name__ 

164 d = doc or method.__doc__ 

165 if _FOR_DOCS and d: 

166 self.__doc__ = d # PYCHOK no cover 

167 

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

169 

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

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

172 ''' 

173 if farg: 

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

175 n = _SPACE_(n, farg.__name__) 

176 else: 

177 n = nameter 

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

179 return _AttributeError(e, txt=n) 

180 

181 def _fdel(self, inst): 

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

183 ''' 

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

185 

186 def _fget(self, inst): 

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

188 ''' 

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

190 return inst.__dict__[self.name] 

191 except KeyError: 

192 # cache the value in the instance' __dict__ 

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

194 return val 

195 

196 def _fset_error(self, inst, val): 

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

198 ''' 

199 n = _MODS.named.classname(inst) 

200 n = _DOT_(n, self.name) 

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

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

203 

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

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

206 ''' 

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

208 

209 def _update_from(self, inst, other): 

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

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

212 ''' 

213 n = self.name # name, NOT _name 

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

215 if v is MISSING: 

216 inst.__dict__.pop(n, None) 

217 else: 

218 inst.__dict__[n] = v 

219 

220 def deleter(self, fdel): 

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

222 ''' 

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

224 

225 def getter(self, fget): 

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

227 ''' 

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

229 

230 def setter(self, fset): 

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

232 ''' 

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

234 

235 

236class Property_RO(_PropertyBase): 

237 # No __doc__ on purpose 

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

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

240 to be used as C{decorator}. 

241 

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

243 to be invoked only once. 

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

245 

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

247 a more descriptive error message when set. 

248 

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

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

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

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

253 

254 @see: Luciano Ramalho, "Fluent Python", O'Reilly, Example 19-24, 2016 

255 p. 636 or Example 22-28, 2022 p. 870 and U{class Property 

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

257 ''' 

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

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

260 

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

262 if inst is None: 

263 return self 

264 try: # to get the cached value immediately 

265 return inst.__dict__[self.name] 

266 except (AttributeError, KeyError): 

267 return self._fget(inst) 

268 

269 

270class Property(Property_RO): 

271 # No __doc__ on purpose 

272 __init__ = Property_RO.__init__ 

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

274 to be used as C{decorator}. 

275 

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

277 

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

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

280 ''' 

281 

282 def setter(self, method): 

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

284 

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

286 

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

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

289 ''' 

290 def _fset(inst, val): 

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

292 ''' 

293 _ = method(inst, val) 

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

295 

296 return self._setters(method, _fset) 

297 

298 def setter_(self, method): 

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

300 

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

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

303 I{memoized}. 

304 ''' 

305 def _fset(inst, val): 

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

307 ''' 

308 val = method(inst, val) 

309 inst.__dict__[self.name] = val 

310 

311 return self._setters(method, _fset) 

312 

313 def _setters(self, method, _fset): 

314 _xcallable(setter=method, fset=_fset) 

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

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

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

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

319 return self 

320 

321 

322class property_RO(_PropertyBase): 

323 # No __doc__ on purpose 

324 _uname = NN 

325 

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

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

328 

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

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

331 

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

333 a more descriptive error message when set. 

334 

335 @see: L{Property_RO}. 

336 ''' 

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

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

339 

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

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

342 ''' 

343 uname = self._uname 

344 if uname in inst.__dict__: 

345 if Clas: # overrides inst.__class__ 

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

347 else: 

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

349# if d is MISSING: # XXX superfluous 

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

351# if uname in c.__dict__: 

352# d = c.__dict__[uname] 

353# break 

354 if d is None: # remove inst value 

355 inst.__dict__.pop(uname) 

356 

357 

358class _NamedProperty(property): 

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

360 ''' 

361 @Property_RO 

362 def name(self): 

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

364 ''' 

365 return self.fget.__name__ 

366 

367 

368def property_doc_(doc): 

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

370 

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

372 

373 @example: 

374 

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

376 >>> def name(self): 

377 >>> ... 

378 >>> 

379 >>> @name.setter 

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

381 >>> ... 

382 ''' 

383 # See Luciano Ramalho, "Fluent Python", O'Reilly, Example 7-23, 

384 # 2016 p. 212+, 2022 p. 331+, Example 9-22 and <https:// 

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

386 

387 def _documented_property(method): 

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

389 ''' 

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

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

392 

393 return _documented_property 

394 

395 

396def _deprecated(call, kind, qual_d): 

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

398 

399 @see: Brett Slatkin, "Effective Python", page 105, 2nd ed, 

400 Addison-Wesley, 2019. 

401 ''' 

402 doc = _docof(call) 

403 

404 @_wraps(call) # PYCHOK self? 

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

406 if qual_d: # function 

407 q = qual_d 

408 elif args: # method 

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

410 else: # PYCHOK no cover 

411 q = call.__name__ 

412 _throwarning(kind, q, doc) 

413 return call(*args, **kwds) 

414 

415 return _deprecated_call 

416 

417 

418def deprecated_class(cls_or_class): 

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

420 

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

422 

423 @note: NOT a decorator! 

424 ''' 

425 if _WARNINGS_X_DEV: 

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

427 _throwarning(_class_, q, cls_or_class.__doc__) 

428 

429 

430def deprecated_function(call): 

431 '''Decorator for a DEPRECATED function. 

432 

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

434 

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

436 ''' 

437 return _deprecated(call, _function_, _DOT_( 

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

439 _WARNINGS_X_DEV else call 

440 

441 

442def deprecated_method(call): 

443 '''Decorator for a DEPRECATED method. 

444 

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

446 

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

448 ''' 

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

450 

451 

452def _deprecated_module(name): # PYCHOK no cover 

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

454 ''' 

455 if _WARNINGS_X_DEV: 

456 _throwarning(_module_, name, _dont_use_) 

457 

458 

459if _WARNINGS_X_DEV: 

460 class deprecated_property(_PropertyBase): 

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

462 ''' 

463 def __init__(self, method): 

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

465 ''' 

466 doc = _docof(method) 

467 

468 def _fget(inst): # PYCHOK no cover 

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

470 ''' 

471 q = _qualified(inst, self.name) 

472 _throwarning(property.__name__, q, doc) 

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

474 

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

476 

477 def setter(self, method): 

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

479 

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

481 

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

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

484 ''' 

485 if not callable(method): 

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

487 

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

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

490 else: 

491 

492 def _fset(inst, val): 

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

494 ''' 

495 q = _qualified(inst, self.name) 

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

497 method(inst, val) 

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

499 

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

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

502 return self 

503 

504else: # PYCHOK no cover 

505 class deprecated_property(property): # PYCHOK expected 

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

507 ''' 

508 pass 

509 

510deprecated_Property = deprecated_property 

511 

512 

513def deprecated_Property_RO(method): 

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

515 

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

517 

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

519 ''' 

520 return _deprecated_RO(method, Property_RO) 

521 

522 

523def deprecated_property_RO(method): 

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

525 

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

527 

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

529 ''' 

530 return _deprecated_RO(method, property_RO) 

531 

532 

533def _deprecated_RO(method, _RO): 

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

535 ''' 

536 doc = _docof(method) 

537 

538 if _WARNINGS_X_DEV: 

539 

540 class _Deprecated_RO(_PropertyBase): 

541 __doc__ = doc 

542 

543 def __init__(self, method): 

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

545 

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

547 q = _qualified(inst, self.name) 

548 _throwarning(_RO.__name__, q, doc) 

549 return self.method(inst) 

550 

551 return _Deprecated_RO(method) 

552 else: # PYCHOK no cover 

553 return _RO(method, doc=doc) 

554 

555 

556def _docof(obj): 

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

558 ''' 

559 try: 

560 d = obj.__doc__.strip() 

561 i = d.find(_DEPRECATED_) 

562 except AttributeError: 

563 i = -1 

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

565 

566 

567def _qualified(inst, name): 

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

569 ''' 

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

571 c = inst.__class__ 

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

573 return q 

574 

575 

576class DeprecationWarnings(object): 

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

578 ''' 

579 _Warnings = 0 

580 

581 def __call__(self): # for backward compatibility 

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

583 

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

585 far or C{None} if not enabled. 

586 

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

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

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

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

591 ''' 

592 return self.Warnings 

593 

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

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

596 ''' 

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

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

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

600 kwds = _xkwds(stacklevel, stacklevel=3) 

601 # XXX invoke warn or raise DeprecationWarning(text) 

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

603 self._Warnings += 1 

604 

605 @Property_RO 

606 def _warn(self): 

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

608 ''' 

609 from warnings import warn 

610 return warn 

611 

612 @property_RO 

613 def Warnings(self): 

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

615 far or C{None} if not enabled. 

616 ''' 

617 return self._Warnings if _WARNINGS_X_DEV else None 

618 

619DeprecationWarnings = DeprecationWarnings() # PYCHOK singleton 

620_throwarning = DeprecationWarnings.throw 

621 

622# **) MIT License 

623# 

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

625# 

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

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

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

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

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

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

632# 

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

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

635# 

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

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

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

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

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

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

642# OTHER DEALINGS IN THE SOFTWARE.