Coverage for pygeodesy/props.py: 98%
212 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-10 14:08 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-10 14:08 -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 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
25from functools import wraps as _wraps
27__all__ = _ALL_LAZY.props
28__version__ = '24.05.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, **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
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
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)
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)
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.
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)
102 if attrs:
103 _update_attrs(inst, *attrs) # remove attributes from inst.__dict__
104 u -= len(d)
105 return u # updates
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
131def _update_attrs(inst, *attrs):
132 '''(INTERNAL) Zap all named C{attrs} of an instance.
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
155class _PropertyBase(property):
156 '''(INTERNAL) Base class for C{P/property/_RO}.
157 '''
158 def __init__(self, method, fget, fset, doc=NN):
160 _xcallable(getter=method, fget=fget)
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
168 property.__init__(self, fget, fset, self._fdel, d or _N_A_)
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)
181 def _fdel(self, inst):
182 '''Zap the I{cached/memoized} C{property} value.
183 '''
184 self._update(inst, None) # PYCHOK no cover
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
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)
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
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
220 def deleter(self, fdel):
221 '''Throws an C{AttributeError}, always.
222 '''
223 raise self._Error(_invalid_, self.deleter, fdel)
225 def getter(self, fget):
226 '''Throws an C{AttributeError}, always.
227 '''
228 raise self._Error(_invalid_, self.getter, fget)
230 def setter(self, fset):
231 '''Throws an C{AttributeError}, always.
232 '''
233 raise self._Error(_immutable_, self.setter, fset)
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}.
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}).
246 @note: Like standard Python C{property} without a C{setter}, but with
247 a more descriptive error message when set.
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.
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)
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)
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}.
276 @see: L{Property_RO} for more details.
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 '''
282 def setter(self, method):
283 '''Make this C{Property} I{mutable}.
285 @arg method: The callable being decorated as this C{Property}'s C{setter}.
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
296 return self._setters(method, _fset)
298 def setter_(self, method):
299 '''Make this C{Property} I{mutable}.
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
311 return self._setters(method, _fset)
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
322class property_RO(_PropertyBase):
323 # No __doc__ on purpose
324 _uname = NN
326 def __init__(self, method, doc=NN): # PYCHOK expected
327 '''New I{immutable}, standard C{property} to be used as C{decorator}.
329 @arg method: The callable being decorated as C{property}'s C{getter}.
330 @kwarg doc: Optional property documentation (C{str}).
332 @note: Like standard Python C{property} without a setter, but with
333 a more descriptive error message when set.
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>
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)
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__
368def property_doc_(doc):
369 '''Decorator for a standard C{property} with basic documentation.
371 @arg doc: The property documentation (C{str}).
373 @example:
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>
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))
393 return _documented_property
396def _deprecated(call, kind, qual_d):
397 '''(INTERNAL) Decorator for DEPRECATED functions, methods, etc.
399 @see: Brett Slatkin, "Effective Python", page 105, 2nd ed,
400 Addison-Wesley, 2019.
401 '''
402 doc = _docof(call)
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)
415 return _deprecated_call
418def deprecated_class(cls_or_class):
419 '''Use inside __new__ or __init__ of a DEPRECATED class.
421 @arg cls_or_class: The class (C{cls} or C{Class}).
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__)
430def deprecated_function(call):
431 '''Decorator for a DEPRECATED function.
433 @arg call: The deprecated function (C{callable}).
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
442def deprecated_method(call):
443 '''Decorator for a DEPRECATED method.
445 @arg call: The deprecated method (C{callable}).
447 @return: The B{C{call}} DEPRECATED.
448 '''
449 return _deprecated(call, _method_, NN) if _WARNINGS_X_DEV else call
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_)
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)
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
475 _PropertyBase.__init__(self, method, _fget, None, doc=doc)
477 def setter(self, method):
478 '''Decorator for a DEPRECATED C{property} or C{Property} setter.
480 @arg method: The callable being decorated as this C{Property}'s C{setter}.
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
488 if _FOR_DOCS: # XXX force method.__doc__ into epydoc
489 _PropertyBase.__init__(self, self.method, self.method, method)
490 else:
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
500 # class Property <https://docs.Python.org/3/howto/descriptor.html>
501 _PropertyBase.__init__(self, self.method, self._fget, _fset)
502 return self
504else: # PYCHOK no cover
505 class deprecated_property(property): # PYCHOK expected
506 '''Decorator for a DEPRECATED C{property} or C{Property}.
507 '''
508 pass
510deprecated_Property = deprecated_property
513def deprecated_Property_RO(method):
514 '''Decorator for a DEPRECATED L{Property_RO}.
516 @arg method: The C{Property_RO.fget} method (C{callable}).
518 @return: The B{C{method}} DEPRECATED.
519 '''
520 return _deprecated_RO(method, Property_RO)
523def deprecated_property_RO(method):
524 '''Decorator for a DEPRECATED L{property_RO}.
526 @arg method: The C{property_RO.fget} method (C{callable}).
528 @return: The B{C{method}} DEPRECATED.
529 '''
530 return _deprecated_RO(method, property_RO)
533def _deprecated_RO(method, _RO):
534 '''(INTERNAL) Create a DEPRECATED C{property_RO} or C{Property_RO}.
535 '''
536 doc = _docof(method)
538 if _WARNINGS_X_DEV:
540 class _Deprecated_RO(_PropertyBase):
541 __doc__ = doc
543 def __init__(self, method):
544 _PropertyBase.__init__(self, method, self._fget, self._fset_error, doc=doc)
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)
551 return _Deprecated_RO(method)
552 else: # PYCHOK no cover
553 return _RO(method, doc=doc)
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:]
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
576class DeprecationWarnings(object):
577 '''(INTERNAL) Handle C{DeprecationWaring}s.
578 '''
579 _Warnings = 0
581 def __call__(self): # for backward compatibility
582 '''Have any C{DeprecationWarning}s been reported or raised?
584 @return: The number of C{DeprecationWarning}s (C{int}) so
585 far or C{None} if not enabled.
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
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
605 @Property_RO
606 def _warn(self):
607 '''Get Python's C{warnings.warn}.
608 '''
609 from warnings import warn
610 return warn
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
619DeprecationWarnings = DeprecationWarnings() # PYCHOK singleton
620_throwarning = DeprecationWarnings.throw
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.