1 2# -*- coding: utf-8 -*- 3 4u'''Im-/mutable, caching or memoizing properties and deprecation decorators. 5 6To enable C{DeprecationWarning}s from C{PyGeodesy}, set environment 7variable C{PYGEODESY_WARNINGS} to a non-empty string and run C{python} 8with command line option C{-X dev} or one or the C{-W} choices, see 9function L{DeprecationWarnings} below. 10''' 11from pygeodesy.errors import _AssertionError, _AttributeError, _xkwds 12from pygeodesy.interns import NN, _DOT_, _EQUALSPACED_, _immutable_, \ 13 _invalid_, MISSING, _N_A_, _SPACE_, _UNDER_ 14from pygeodesy.lazily import _ALL_LAZY, _getenv, _FOR_DOCS, \ 15 _PYTHON_X_DEV, _sys 16from pygeodesy.streprs import Fmt 17 18from functools import wraps as _wraps 19from warnings import warn as _warn 20 21__all__ = _ALL_LAZY.props 22__version__ = '21.09.14' 23 24_DEPRECATED_ = 'DEPRECATED' 25_dont_use_ = _DEPRECATED_ + ", don't use." 26_has_been_ = 'has been' 27_Warnings = 0 28_W_DEV = (bool(_sys.warnoptions) or _PYTHON_X_DEV) \ 29 and _getenv('PYGEODESY_WARNINGS', NN) 30 31 32def _hasProperty(inst, name, *Classes): 33 '''(INTERNAL) Check whether C{inst} has a C{P/property/_RO} by this C{name}. 34 ''' 35 ps = Classes if Classes else _PropertyBase 36 for c in inst.__class__.__mro__[:-1]: 37 p = c.__dict__.get(name, None) 38 if isinstance(p, ps) and p.name == name: 39 return True 40 return False 41 42 43def _update_all(inst, *attrs): 44 '''(INTERNAL) Zap all I{cached} L{property_RO}s, L{Property_RO}s 45 and the named C{attrs} from an instance' C{__dict__}. 46 47 @return: The number of updates (C{int}), if any. 48 ''' 49 d = inst.__dict__ 50 u = len(d) 51 52 for c in inst.__class__.__mro__[:-1]: 53 for n, p in c.__dict__.items(): 54 if isinstance(p, _PropertyBase) and p.name == n: 55 p._update(inst, c) 56 57 p = d.pop 58 for a in attrs: # PYCHOK no cover 59 if hasattr(inst, a): 60 p(a, None) 61 else: 62 from pygeodesy.named import classname 63 n = classname(inst, prefixed=True) 64 a = _DOT_(n, _SPACE_(a, _invalid_)) 65 raise _AssertionError(a, txt=repr(inst)) 66 67 return u - len(d) # of updates 68 69 70class _PropertyBase(property): 71 '''(INTERNAL) Base class for C{P/property/_RO}. 72 ''' 73 def __init__(self, method, fget, fset, doc=NN): 74 75 if not callable(method): 76 self.getter(method) # PYCHOK no cover 77 78 self.method = method 79 self.name = method.__name__ 80 d = doc or method.__doc__ 81 if _FOR_DOCS and d: 82 self.__doc__ = d # PYCHOK no cover 83 84 property.__init__(self, fget, fset, self._fdel, d or _N_A_) 85 86 def _fdel(self, inst): # deleter 87 '''Zap the I{cached/memoized} C{property} value. 88 ''' 89 self._update(inst, None) # PYCHOK no cover 90 91 def _fget(self, inst): 92 '''Get and I{cache/memoize} the C{property} value. 93 ''' 94 try: # to get the value cached in instance' __dict__ 95 return inst.__dict__[self.name] 96 except KeyError: 97 # cache the value in the instance' __dict__ 98 inst.__dict__[self.name] = val = self.method(inst) 99 return val 100 101 def _fset_error(self, inst, val): 102 '''Throws an C{AttributeError}, always. 103 ''' 104 from pygeodesy.named import classname 105 n = _DOT_(classname(inst), self.name) 106 self._immutable_error(_EQUALSPACED_(n, repr(val))) 107 108 def _immutable_error(self, name): 109 from pygeodesy.named import classname 110 e = _SPACE_(_immutable_, classname(self)) 111 raise _AttributeError(e, txt=name) 112 113 def _invalid_error(self, name): 114 from pygeodesy.named import classname 115 e = _SPACE_(_invalid_, classname(self)) 116 raise _AttributeError(e, txt=name) 117 118 def _update(self, inst, *unused): 119 '''(INTERNAL) Zap the I{cached/memoized} C{inst.__dict__[name]} item. 120 ''' 121 inst.__dict__.pop(self.name, None) # name, NOT _name 122 123 def deleter(self, fdel): 124 '''Throws an C{AttributeError}, always. 125 ''' 126 n = _DOT_(self.name, self.deleter.__name__) 127 self._invalid_error(_SPACE_(n, fdel.__name__)) 128 129 def getter(self, fget): 130 '''Throws an C{AttributeError}, always. 131 ''' 132 n = _DOT_(self.name, self.getter.__name__) 133 self._invalid_error(_SPACE_(n, fget.__name__)) 134 135 def setter(self, fset): 136 '''Throws an C{AttributeError}, always. 137 ''' 138 n = _DOT_(self.name, self.setter.__name__) 139 self._immutable_error(_SPACE_(n, fset.__name__)) 140 141 142class Property_RO(_PropertyBase): 143 # No __doc__ on purpose 144 def __init__(self, method, doc=NN): # PYCHOK expected 145 '''New I{immutable}, I{caching}, I{memoizing} C{property} I{Factory} 146 to be used as C{decorator}. 147 148 @arg method: The callable being decorated as this C{property}'s C{getter}, 149 to be invoked only once. 150 @kwarg doc: Optional property documentation (C{str}). 151 152 @note: Like standard Python C{property} without a C{setter}, but with 153 a more descriptive error message when set. 154 155 @see: Python 3's U{functools.cached_property<https://docs.Python.org/3/ 156 library/functools.html#functools.cached_property>} and U{-.cache 157 <https://Docs.Python.org/3/library/functools.html#functools.cache>} 158 to I{cache} or I{memoize} the property value. 159 160 @see: Luciano Ramalho, "Fluent Python", page 636, O'Reilly, 2016, 161 "Coding a Property Factory", especially Example 19-24 and U{class 162 Property<https://docs.Python.org/3/howto/descriptor.html>}. 163 ''' 164 _fget = method if _FOR_DOCS else self._fget # XXX force method.__doc__ to epydoc 165 _PropertyBase.__init__(self, method, _fget, self._fset_error) 166 167 def __get__(self, inst, *unused): # PYCHOK 2 vs 3 args 168 if inst is None: 169 return self 170 try: # to get the cached value immediately 171 return inst.__dict__[self.name] 172 except (AttributeError, KeyError): 173 return self._fget(inst) 174 175 176class Property(Property_RO): 177 # No __doc__ on purpose 178 __init__ = Property_RO.__init__ 179 '''New I{mutable}, I{caching}, I{memoizing} C{property} I{Factory} 180 to be used as C{decorator}. 181 182 @see: L{Property_RO} for more details. 183 184 @note: Unless and until the C{setter} is defined, this L{Property} behaves 185 like an I{immutable}, I{caching}, I{memoizing} L{Property_RO}. 186 ''' 187 188 def setter(self, method): 189 '''Make this C{Property} I{mutable}. 190 191 @arg method: The callable being decorated as this C{Property}'s C{setter}. 192 193 @note: Setting a new property value always clears the previously I{cached} 194 or I{memoized} value I{after} invoking the B{C{method}}. 195 ''' 196 if not callable(method): 197 _PropertyBase.setter(self, method) # PYCHOK no cover 198 199 if _FOR_DOCS: # XXX force method.__doc__ into epydoc 200 _PropertyBase.__init__(self, self.method, self.method, method) 201 else: 202 203 def _fset(inst, val): 204 '''Set and I{cache}, I{memoize} the C{property} value. 205 ''' 206 method(inst, val) 207 self._update(inst) # un-cache this item 208 209 # class Property <https://docs.Python.org/3/howto/descriptor.html> 210 _PropertyBase.__init__(self, self.method, self._fget, _fset) 211 return self 212 213 214class property_RO(_PropertyBase): 215 # No __doc__ on purpose 216 _uname = NN 217 218 def __init__(self, method, doc=NN): # PYCHOK expected 219 '''New I{immutable}, standard C{property} to be used as C{decorator}. 220 221 @arg method: The callable being decorated as C{property}'s C{getter}. 222 @kwarg doc: Optional property documentation (C{str}). 223 224 @note: Like standard Python C{property} without a setter, but with 225 a more descriptive error message when set. 226 227 @see: L{Property_RO}. 228 ''' 229 _PropertyBase.__init__(self, method, method, self._fset_error, doc=doc) 230 self._uname = NN(_UNDER_, self.name) # actual attr UNDER<name> 231 232 def _update(self, inst, *Clas): # PYCHOK signature 233 '''(INTERNAL) Zap the I{cached} C{B{inst}.__dict__[_name]} item. 234 ''' 235 uname = self._uname 236 if uname in inst.__dict__: 237 if Clas: # overrides inst.__class__ 238 d = Clas[0].__dict__.get(uname, MISSING) 239 else: 240 d = getattr(inst.__class__, uname, MISSING) 241# if d is MISSING: # XXX superfluous 242# for c in inst.__class__.__mro__[:-1]: 243# if uname in c.__dict__: 244# d = c.__dict__[uname] 245# break 246 if d is None: # remove inst value 247 inst.__dict__.pop(uname) 248 249 250def property_doc_(doc): 251 '''Decorator for a standard C{property} with basic documentation. 252 253 @arg doc: The property documentation (C{str}). 254 255 @example: 256 257 >>> @property_doc_("documentation text.") 258 >>> def name(self): 259 >>> ... 260 >>> 261 >>> @name.setter 262 >>> def name(self, value): 263 >>> ... 264 ''' 265 # See Luciano Ramalho, "Fluent Python", page 212ff, O'Reilly, 2016, 266 # "Parameterized Decorators", especially Example 7-23. Also, see 267 # <https://Python-3-Patterns-Idioms-Test.ReadTheDocs.io/en/latest/PythonDecorators.html> 268 269 def _documented_property(method): 270 '''(INTERNAL) Return the documented C{property}. 271 ''' 272 t = 'get and set' if doc.startswith(_SPACE_) else NN 273 return property(method, None, None, NN('Property to ', t, doc)) 274 275 return _documented_property 276 277 278def _deprecated(call, kind, qual_d): 279 '''(INTERNAL) Decorator for DEPRECATED functions, methods, etc. 280 281 @see: Brett Slatkin, "Effective Python", page 105, 2nd ed, 282 Addison-Wesley, 2019. 283 ''' 284 doc = _docof(call) 285 286 @_wraps(call) # PYCHOK self? 287 def _deprecated_call(*args, **kwds): 288 if qual_d: # function 289 q = qual_d 290 elif args: # method 291 q = _qualified(args[0], call.__name__) 292 else: # PYCHOK no cover 293 q = call.__name__ 294 _throwarning(kind, q, doc) 295 return call(*args, **kwds) 296 297 return _deprecated_call 298 299 300def deprecated_class(cls_or_class): 301 '''Use inside __new__ or __init__ of a DEPRECATED class. 302 303 @arg cls_or_class: The class (C{cls} or C{Class}). 304 305 @note: NOT a decorator! 306 ''' 307 if _W_DEV: 308 q = _DOT_(cls_or_class.__module__, cls_or_class.__name__) 309 _throwarning('class', q, cls_or_class.__doc__) 310 311 312def deprecated_function(call): 313 '''Decorator for a DEPRECATED function. 314 315 @arg call: The deprecated function (C{callable}). 316 317 @return: The B{C{call}} DEPRECATED. 318 ''' 319 return _deprecated(call, 'function', _DOT_( 320 call.__module__, call.__name__)) if \ 321 _W_DEV else call 322 323 324def deprecated_method(call): 325 '''Decorator for a DEPRECATED method. 326 327 @arg call: The deprecated method (C{callable}). 328 329 @return: The B{C{call}} DEPRECATED. 330 ''' 331 return _deprecated(call, 'method', NN) if _W_DEV else call 332 333 334def _deprecated_module(name): # PYCHOK no cover 335 '''(INTERNAL) Callable within a DEPRECATED module. 336 ''' 337 if _W_DEV: 338 _throwarning('module', name, _dont_use_) 339 340 341def deprecated_Property_RO(method): 342 '''Decorator for a DEPRECATED L{Property_RO}. 343 344 @arg method: The C{Property_RO.fget} method (C{callable}). 345 346 @return: The B{C{method}} DEPRECATED. 347 ''' 348 return _deprecated_RO(method, Property_RO) 349 350 351def deprecated_property_RO(method): 352 '''Decorator for a DEPRECATED L{property_RO}. 353 354 @arg method: The C{property_RO.fget} method (C{callable}). 355 356 @return: The B{C{method}} DEPRECATED. 357 ''' 358 return _deprecated_RO(method, property_RO) 359 360 361def _deprecated_RO(method, _RO): 362 '''(INTERNAL) Create a DEPRECATED C{property_RO} or C{Property_RO}. 363 ''' 364 doc = _docof(method) 365 366 if _W_DEV: 367 368 class _Deprecated_RO(_PropertyBase): 369 __doc__ = doc 370 371 def __init__(self, method): 372 _PropertyBase.__init__(self, method, self._fget, self._fset_error, doc=doc) 373 374 def _fget(self, inst): # PYCHOK no cover 375 q = _qualified(inst, self.name) 376 _throwarning(_RO.__name__, q, doc) 377 return self.method(inst) 378 379 return _Deprecated_RO(method) 380 else: # PYCHOK no cover 381 return _RO(method, doc=doc) 382 383 384def DeprecationWarnings(): 385 '''Get the C{DeprecationWarning}s reported or raised. 386 387 @return: The number of C{DeprecationWarning}s (C{int}) or 388 C{None} if not enabled. 389 390 @note: To get C{DeprecationWarning}s if any, run C{python} 391 with environment variable C{PYGEODESY_WARNINGS} set 392 to a non-empty string AND use C{python} command line 393 option C{-X dev}, C{-W always}, or C{-W error}, etc. 394 ''' 395 return _Warnings if _W_DEV else None 396 397 398def _docof(obj): 399 '''(INTERNAL) Get uniform DEPRECATED __doc__ string. 400 ''' 401 try: 402 d = obj.__doc__.strip() 403 i = d.find(_DEPRECATED_) 404 except AttributeError: 405 i = -1 406 return _DOT_(_DEPRECATED_, NN) if i < 0 else d[i:] 407 408 409def _qualified(inst, name): 410 '''(INTERNAL) Fully qualify a name. 411 ''' 412 # _DOT_(inst.classname, name), not _DOT_(inst.named4, name) 413 c = inst.__class__ 414 q = _DOT_(c.__module__, c.__name__, name) 415 return q 416 417 418def _throwarning(kind, name, doc, **stacklevel): # stacklevel=3 419 '''(INTERNAL) Report or raise a C{DeprecationWarning}. 420 ''' 421 line = doc.split('\n\n', 1)[0].split() 422 text = _SPACE_(kind, Fmt.CURLY(L=name), _has_been_, *line) 423 kwds = _xkwds(stacklevel, stacklevel=3) 424 # XXX _warn or raise DeprecationWarning(text) 425 _warn(text, category=DeprecationWarning, **kwds) 426 427 global _Warnings 428 _Warnings += 1 429 430# **) MIT License 431# 432# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved. 433# 434# Permission is hereby granted, free of charge, to any person obtaining a 435# copy of this software and associated documentation files (the "Software"), 436# to deal in the Software without restriction, including without limitation 437# the rights to use, copy, modify, merge, publish, distribute, sublicense, 438# and/or sell copies of the Software, and to permit persons to whom the 439# Software is furnished to do so, subject to the following conditions: 440# 441# The above copyright notice and this permission notice shall be included 442# in all copies or substantial portions of the Software. 443# 444# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 445# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 446# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 447# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 448# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 449# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 450# OTHER DEALINGS IN THE SOFTWARE. 451