Coverage for pygeodesy/props.py: 98%
202 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-05-20 11:54 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-05-20 11:54 -0400
2# -*- coding: utf-8 -*-
4u'''Mutable, immutable and caching/memoizing properties and
5deprecation decorators.
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'''
13from pygeodesy.basics import isclass
14from pygeodesy.errors import _AssertionError, _AttributeError, \
15 _xkwds, _xkwds_get
16from 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 # _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
25from functools import wraps as _wraps
27__all__ = _ALL_LAZY.props
28__version__ = '23.04.26'
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')
39def _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
58def _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
68def _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)
76def _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.
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)
94 if attrs:
95 _update_attrs(inst, *attrs) # remove attributes from inst.__dict__
96 u -= len(d)
97 return u # updates
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
123def _update_attrs(inst, *attrs):
124 '''(INTERNAL) Zap all named C{attrs} of an instance.
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
144class _PropertyBase(property):
145 '''(INTERNAL) Base class for C{P/property/_RO}.
146 '''
147 def __init__(self, method, fget, fset, doc=NN):
149 if not callable(method):
150 self.getter(method) # PYCHOK no cover
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
158 property.__init__(self, fget, fset, self._fdel, d or _N_A_)
160 def _fdel(self, inst):
161 '''Zap the I{cached/memoized} C{property} value.
162 '''
163 self._update(inst, None) # PYCHOK no cover
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
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)
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
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
199 def deleter(self, fdel):
200 '''Throws an C{AttributeError}, always.
201 '''
202 raise self._Error(_invalid_, self.deleter, fdel)
204 def getter(self, fget):
205 '''Throws an C{AttributeError}, always.
206 '''
207 raise self._Error(_invalid_, self.getter, fget)
209 def setter(self, fset):
210 '''Throws an C{AttributeError}, always.
211 '''
212 raise self._Error(_immutable_, self.setter, fset)
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)
226class 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}.
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}).
236 @note: Like standard Python C{property} without a C{setter}, but with
237 a more descriptive error message when set.
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.
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)
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)
260class 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}.
266 @see: L{Property_RO} for more details.
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 '''
272 def setter(self, method):
273 '''Make this C{Property} I{mutable}.
275 @arg method: The callable being decorated as this C{Property}'s C{setter}.
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
283 if _FOR_DOCS: # XXX force method.__doc__ into epydoc
284 _PropertyBase.__init__(self, self.method, self.method, method)
285 else:
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
293 # class Property <https://docs.Python.org/3/howto/descriptor.html>
294 _PropertyBase.__init__(self, self.method, self._fget, _fset)
295 return self
298class property_RO(_PropertyBase):
299 # No __doc__ on purpose
300 _uname = NN
302 def __init__(self, method, doc=NN): # PYCHOK expected
303 '''New I{immutable}, standard C{property} to be used as C{decorator}.
305 @arg method: The callable being decorated as C{property}'s C{getter}.
306 @kwarg doc: Optional property documentation (C{str}).
308 @note: Like standard Python C{property} without a setter, but with
309 a more descriptive error message when set.
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>
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)
334class _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__
344def property_doc_(doc):
345 '''Decorator for a standard C{property} with basic documentation.
347 @arg doc: The property documentation (C{str}).
349 @example:
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>
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))
369 return _documented_property
372def _deprecated(call, kind, qual_d):
373 '''(INTERNAL) Decorator for DEPRECATED functions, methods, etc.
375 @see: Brett Slatkin, "Effective Python", page 105, 2nd ed,
376 Addison-Wesley, 2019.
377 '''
378 doc = _docof(call)
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)
391 return _deprecated_call
394def deprecated_class(cls_or_class):
395 '''Use inside __new__ or __init__ of a DEPRECATED class.
397 @arg cls_or_class: The class (C{cls} or C{Class}).
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__)
406def deprecated_function(call):
407 '''Decorator for a DEPRECATED function.
409 @arg call: The deprecated function (C{callable}).
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
418def deprecated_method(call):
419 '''Decorator for a DEPRECATED method.
421 @arg call: The deprecated method (C{callable}).
423 @return: The B{C{call}} DEPRECATED.
424 '''
425 return _deprecated(call, _method_, NN) if _WARNINGS_X_DEV else call
428def _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_)
435if _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)
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
451 _PropertyBase.__init__(self, method, _fget, None, doc=doc)
453 def setter(self, method):
454 '''Decorator for a DEPRECATED C{property} or C{Property} setter.
456 @arg method: The callable being decorated as this C{Property}'s C{setter}.
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
464 if _FOR_DOCS: # XXX force method.__doc__ into epydoc
465 _PropertyBase.__init__(self, self.method, self.method, method)
466 else:
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
476 # class Property <https://docs.Python.org/3/howto/descriptor.html>
477 _PropertyBase.__init__(self, self.method, self._fget, _fset)
478 return self
480else: # PYCHOK no cover
481 class deprecated_property(property): # PYCHOK expected
482 '''Decorator for a DEPRECATED C{property} or C{Property}.
483 '''
484 pass
486deprecated_Property = deprecated_property
489def deprecated_Property_RO(method):
490 '''Decorator for a DEPRECATED L{Property_RO}.
492 @arg method: The C{Property_RO.fget} method (C{callable}).
494 @return: The B{C{method}} DEPRECATED.
495 '''
496 return _deprecated_RO(method, Property_RO)
499def deprecated_property_RO(method):
500 '''Decorator for a DEPRECATED L{property_RO}.
502 @arg method: The C{property_RO.fget} method (C{callable}).
504 @return: The B{C{method}} DEPRECATED.
505 '''
506 return _deprecated_RO(method, property_RO)
509def _deprecated_RO(method, _RO):
510 '''(INTERNAL) Create a DEPRECATED C{property_RO} or C{Property_RO}.
511 '''
512 doc = _docof(method)
514 if _WARNINGS_X_DEV:
516 class _Deprecated_RO(_PropertyBase):
517 __doc__ = doc
519 def __init__(self, method):
520 _PropertyBase.__init__(self, method, self._fget, self._fset_error, doc=doc)
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)
527 return _Deprecated_RO(method)
528 else: # PYCHOK no cover
529 return _RO(method, doc=doc)
532def _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:]
543def _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
552class DeprecationWarnings(object):
553 '''(INTERNAL) Handle C{DeprecationWaring}s.
554 '''
555 _Warnings = 0
557 def __call__(self): # for backward compatibility
558 '''Have any C{DeprecationWarning}s been reported or raised?
560 @return: The number of C{DeprecationWarning}s (C{int}) so
561 far or C{None} if not enabled.
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
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
581 @Property_RO
582 def _warn(self):
583 '''Get Python's C{warnings.warn}.
584 '''
585 from warnings import warn
586 return warn
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
595DeprecationWarnings = DeprecationWarnings() # PYCHOK singleton
596_throwarning = DeprecationWarnings.throw
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.