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

14 from pygeodesy.errors import _AssertionError, _AttributeError, \

15 _xkwds, _xkwds_get

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

17 _DEPRECATED_, _DOT_, _EQUALSPACED_, \

18 _immutable_, _invalid_, _N_A_, _not_, \

19 _SPACE_, _UNDER_, _DNL_ # PYCHOK used!

20# from pygeodesy.named import callname # from _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 # from _MODS

24

25 from functools import wraps as _wraps

26

27__all__ = _ALL_LAZY.props

28__version__ = '22.09.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):

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

41 as specified in the C{Bases} arguments.

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 for C in S:

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

54 if isinstance(p, B) and p.name == n:

55 yield p

56

57

58 def _allPropertiesOf_n(n, Clas_or_inst, *Bases):

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

60 '''

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

62 if len(t) != n:

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

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

65 return t

66

67

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

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

70 '''

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

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

73 and p.name == name)

74

75

76 def _update_all(inst, *attrs, **Base):

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

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

79

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

81 '''

82 if isclass(inst):

83 raise _AssertionError(inst, txt=_not_an_inst_)

84 try:

85 d = inst.__dict__

86 except AttributeError:

87 return 0

88 u = len(d)

89 if u:

90 B = _xkwds_get(Base, Base=_PropertyBase)

91 for p in _allPropertiesOf(inst, B):

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

93

94 if attrs:

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

96 u -= len(d)

97 return u # updates

98

99

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

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

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

103#

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

105# '''

106# if isclass(inst):

107# raise _AssertionError(inst, txt=_not_an_inst_)

108# try:

109# d = inst.__dict__

110# f = other.__dict__

111# except AttributeError:

112# return 0

113# u = len(f)

114# if u:

115# u = len(d)

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

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

118# p._update_from(inst, other)

119# u -= len(d)

120# return u # updates

121

122

123 def _update_attrs(inst, *attrs):

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

125

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

127 '''

128 try:

129 d = inst.__dict__

130 except AttributeError:

131 return 0

132 u = len(d)

133 if u:

134 _p = d.pop # zap attrs from inst.__dict__

135 for a in attrs: # PYCHOK no cover

136 if _p(a, MISSING) is MISSING and not hasattr(inst, a):

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

138 a = _DOT_(n, _SPACE_(a, _invalid_))

139 raise _AssertionError(a, txt=repr(inst))

140 u -= len(d)

141 return u # updates

142

143

144 class _PropertyBase(property):

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

146 '''

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

148

149 if not callable(method):

150 self.getter(method) # PYCHOK no cover

151

152 self.method = method

153 self.name = method.__name__

154 d = doc or method.__doc__

155 if _FOR_DOCS and d:

156 self.__doc__ = d # PYCHOK no cover

157

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

159

160 def _fdel(self, inst):

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

162 '''

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

164

165 def _fget(self, inst):

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

167 '''

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

169 return inst.__dict__[self.name]

170 except KeyError:

171 # cache the value in the instance' __dict__

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

173 return val

174

175 def _fset_error(self, inst, val):

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

177 '''

178 n = _MODS.named.classname(inst)

179 n = _DOT_(n, self.name)

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

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

182

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

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

185 '''

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

187

188 def _update_from(self, inst, other):

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

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

191 '''

192 n = self.name # name, NOT _name

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

194 if v is MISSING:

195 inst.__dict__.pop(n, None)

196 else:

197 inst.__dict__[n] = v

198

199 def deleter(self, fdel):

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

201 '''

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

203

204 def getter(self, fget):

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

206 '''

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

208

209 def setter(self, fset):

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

211 '''

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

213

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

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

216 '''

217 if farg:

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

219 n = _SPACE_(n, farg.__name__)

220 else:

221 n = nameter

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

223 return _AttributeError(e, txt=n)

224

225

226 class Property_RO(_PropertyBase):

227 # No __doc__ on purpose

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

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

230 to be used as C{decorator}.

231

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

233 to be invoked only once.

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

235

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

237 a more descriptive error message when set.

238

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

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

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

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

243

244 @see: Luciano Ramalho, "Fluent Python", page 636, O'Reilly, 2016,

245 "Coding a Property Factory", especially Example 19-24 and U{class

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

247 '''

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

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

250

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

252 if inst is None:

253 return self

254 try: # to get the cached value immediately

255 return inst.__dict__[self.name]

256 except (AttributeError, KeyError):

257 return self._fget(inst)

258

259

260 class Property(Property_RO):

261 # No __doc__ on purpose

262 __init__ = Property_RO.__init__

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

264 to be used as C{decorator}.

265

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

267

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

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

270 '''

271

272 def setter(self, method):

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

274

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

276

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

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

279 '''

280 if not callable(method):

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

282

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

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

285 else:

286

287 def _fset(inst, val):

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

289 '''

290 method(inst, val)

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

292

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

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

295 return self

296

297

298 class property_RO(_PropertyBase):

299 # No __doc__ on purpose

300 _uname = NN

301

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

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

304

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

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

307

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

309 a more descriptive error message when set.

310

311 @see: L{Property_RO}.

312 '''

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

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

315

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

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

318 '''

319 uname = self._uname

320 if uname in inst.__dict__:

321 if Clas: # overrides inst.__class__

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

323 else:

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

325# if d is MISSING: # XXX superfluous

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

327# if uname in c.__dict__:

328# d = c.__dict__[uname]

329# break

330 if d is None: # remove inst value

331 inst.__dict__.pop(uname)

332

333

334 class _NamedProperty(property):

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

336 '''

337 @Property_RO

338 def name(self):

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

340 '''

341 return self.fget.__name__

342

343

344 def property_doc_(doc):

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

346

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

348

349 @example:

350

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

352 >>> def name(self):

353 >>> ...

354 >>>

355 >>> @name.setter

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

357 >>> ...

358 '''

359 # See Luciano Ramalho, "Fluent Python", page 212ff, O'Reilly, 2016,

360 # "Parameterized Decorators", especially Example 7-23. Also, see

361 # <https://Python-3-Patterns-Idioms-Test.ReadTheDocs.io/en/latest/PythonDecorators.html>

362

363 def _documented_property(method):

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

365 '''

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

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

368

369 return _documented_property

370

371

372 def _deprecated(call, kind, qual_d):

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

374

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

376 Addison-Wesley, 2019.

377 '''

378 doc = _docof(call)

379

380 @_wraps(call) # PYCHOK self?

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

382 if qual_d: # function

383 q = qual_d

384 elif args: # method

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

386 else: # PYCHOK no cover

387 q = call.__name__

388 _throwarning(kind, q, doc)

389 return call(*args, **kwds)

390

391 return _deprecated_call

392

393

394 def deprecated_class(cls_or_class):

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

396

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

398

399 @note: NOT a decorator!

400 '''

401 if _WARNINGS_X_DEV:

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

403 _throwarning(_class_, q, cls_or_class.__doc__)

404

405

406 def deprecated_function(call):

407 '''Decorator for a DEPRECATED function.

408

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

410

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

412 '''

413 return _deprecated(call, _function_, _DOT_(

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

415 _WARNINGS_X_DEV else call

416

417

418 def deprecated_method(call):

419 '''Decorator for a DEPRECATED method.

420

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

422

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

424 '''

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

426

427

428 def _deprecated_module(name): # PYCHOK no cover

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

430 '''

431 if _WARNINGS_X_DEV:

432 _throwarning('module', name, _dont_use_)

433

434

435 if _WARNINGS_X_DEV:

436 class deprecated_property(_PropertyBase):

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

438 '''

439 def __init__(self, method):

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

441 '''

442 doc = _docof(method)

443

444 def _fget(inst): # PYCHOK no cover

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

446 '''

447 q = _qualified(inst, self.name)

448 _throwarning(property.__name__, q, doc)

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

450

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

452

453 def setter(self, method):

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

455

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

457

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

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

460 '''

461 if not callable(method):

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

463

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

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

466 else:

467

468 def _fset(inst, val):

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

470 '''

471 q = _qualified(inst, self.name)

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

473 method(inst, val)

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

475

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

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

478 return self

479

480 else: # PYCHOK no cover

481 class deprecated_property(property): # PYCHOK expected

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

483 '''

484 pass

485

486 deprecated_Property = deprecated_property

487

488

489 def deprecated_Property_RO(method):

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

491

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

493

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

495 '''

496 return _deprecated_RO(method, Property_RO)

497

498

499 def deprecated_property_RO(method):

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

501

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

503

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

505 '''

506 return _deprecated_RO(method, property_RO)

507

508

509 def _deprecated_RO(method, _RO):

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

511 '''

512 doc = _docof(method)

513

514 if _WARNINGS_X_DEV:

515

516 class _Deprecated_RO(_PropertyBase):

517 __doc__ = doc

518

519 def __init__(self, method):

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

521

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

523 q = _qualified(inst, self.name)

524 _throwarning(_RO.__name__, q, doc)

525 return self.method(inst)

526

527 return _Deprecated_RO(method)

528 else: # PYCHOK no cover

529 return _RO(method, doc=doc)

530

531

532 def _docof(obj):

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

534 '''

535 try:

536 d = obj.__doc__.strip()

537 i = d.find(_DEPRECATED_)

538 except AttributeError:

539 i = -1

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

541

542

543 def _qualified(inst, name):

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

545 '''

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

547 c = inst.__class__

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

549 return q

550

551

552 class DeprecationWarnings(object):

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

554 '''

555 _Warnings = 0

556

557 def __call__(self): # for backward compatibility

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

559

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

561 far or C{None} if not enabled.

562

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

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

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

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

567 '''

568 return self.Warnings

569

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

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

572 '''

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

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

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

576 kwds = _xkwds(stacklevel, stacklevel=3)

577 # XXX invoke warn or raise DeprecationWarning(text)

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

579 self._Warnings += 1

580

581 @Property_RO

582 def _warn(self):

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

584 '''

585 from warnings import warn

586 return warn

587

588 @property_RO

589 def Warnings(self):

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

591 far or C{None} if not enabled.

592 '''

593 return self._Warnings if _WARNINGS_X_DEV else None

594

595 DeprecationWarnings = DeprecationWarnings() # PYCHOK singleton

596_throwarning = DeprecationWarnings.throw

597

598# **) MIT License

599#

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

601#

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

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

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

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

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

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

608#

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

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

611#

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

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

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

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

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

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

618# OTHER DEALINGS IN THE SOFTWARE.