Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

 

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

 

u'''Im-/mutable, caching or memoizing properties and decorators. 

''' 

from pygeodesy.errors import _AssertionError, _AttributeError 

from pygeodesy.interns import NN, _DOT_, _EQUALSPACED_, \ 

_immutable_, _invalid_, _N_A_, \ 

_SPACE_, _UNDER_ 

from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS 

 

__all__ = _ALL_LAZY.props 

__version__ = '21.01.26' 

 

 

def _hasProperty(inst, name, *Classes): 

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

''' 

ps = Classes if Classes else _PropertyBase 

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

p = c.__dict__.get(name, None) 

if isinstance(p, ps) and p.name == name: 

return True 

return False 

 

 

def _update_all(inst, *attrs): 

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

and the named C{attrs} from an instance' C{__dict__}. 

 

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

''' 

d = inst.__dict__ 

u = len(d) 

 

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

for n, p in c.__dict__.items(): 

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

p._update(inst, c) 

 

for a in attrs: 

if hasattr(inst, a): 

d.pop(a, None) 

else: # PYCHOK no cover 

from pygeodesy.named import classname 

n = classname(inst, prefixed=True) 

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

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

 

return u - len(d) 

 

 

class _PropertyBase(property): 

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

''' 

def __init__(self, method, fget, fset): 

 

if not callable(method): 

self.getter(method) # PYCHOK no cover 

 

self.method = method 

self.name = method.__name__ 

if _FOR_DOCS and method.__doc__: 

self.__doc__ = method.__doc__ # PYCHOK no cover 

 

property.__init__(self, fget, fset, self._fdel, method.__doc__ or _N_A_) 

 

def _fdel(self, inst): # deleter 

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

 

def _fget(self, inst): 

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

return inst.__dict__[self.name] 

except KeyError: 

# cache the value in the instance' __dict__ 

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

return val 

 

def _fset_error(self, inst, val): 

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

''' 

from pygeodesy.named import classname 

n = _DOT_(classname(inst), self.name) 

self._immutable_error(_EQUALSPACED_(n, repr(val))) 

 

def _immutable_error(self, name): 

from pygeodesy.named import classname 

e = _SPACE_(_immutable_, classname(self)) 

raise _AttributeError(e, txt=name) 

 

def _invalid_error(self, name): 

from pygeodesy.named import classname 

e = _SPACE_(_invalid_, classname(self)) 

raise _AttributeError(e, txt=name) 

 

def _update(self, inst, *unused): 

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

''' 

inst.__dict__.pop(self.name, None) 

 

def deleter(self, fdel): 

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

''' 

n = _DOT_(self.name, self.deleter.__name__) 

self._invalid_error(_SPACE_(n, fdel.__name__)) 

 

def getter(self, fget): 

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

''' 

n = _DOT_(self.name, self.getter.__name__) 

self._invalid_error(_SPACE_(n, fget.__name__)) 

 

def setter(self, fset): 

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

''' 

n = _DOT_(self.name, self.setter.__name__) 

self._immutable_error(_SPACE_(n, fset.__name__)) 

 

 

class Property_RO(_PropertyBase): 

# No __doc__ on purpose 

def __init__(self, method): # PYCHOK expected 

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

to be used as C{decorator}. 

 

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

to be invoked only once. 

 

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

a more descriptive error message when set. 

 

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

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

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

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

 

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

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

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

''' 

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

 

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

if inst is None: 

return self 

try: # to get the cached value immediately 

return inst.__dict__[self.name] 

except (AttributeError, KeyError): 

return self._fget(inst) 

 

 

class Property(Property_RO): 

# No __doc__ on purpose 

__init__ = Property_RO.__init__ 

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

to be used as C{decorator}. 

 

@see: L{Property_RO} for more details. 

 

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

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

''' 

 

def setter(self, method): 

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

 

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

 

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

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

''' 

if not callable(method): 

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

 

def _fset(inst, val): 

method(inst, val) 

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

 

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

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

return self 

 

 

class property_RO(_PropertyBase): 

# No __doc__ on purpose 

def __init__(self, method): # PYCHOK expected 

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

 

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

 

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

a more descriptive error message when set. 

 

@see: L{Property_RO}. 

''' 

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

 

def _update(self, inst, clas=None): # PYCHOK signature 

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

''' 

c = clas or inst.__class__ 

if c: 

_n = NN(_UNDER_, self.name) 

if c.__dict__.get(_n, False) is None: # in (None, ()): 

inst.__dict__.pop(_n, None) 

 

 

def property_doc_(doc): 

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

 

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

 

@example: 

 

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

>>> def name(self): 

>>> ... 

>>> 

>>> @name.setter 

>>> def name(self, value): 

>>> ... 

''' 

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

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

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

 

def _documented_property(method): 

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

''' 

t = 'get and set' if doc.startswith(_SPACE_) else NN 

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

 

return _documented_property 

 

# **) MIT License 

# 

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

# 

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

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

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

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

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

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

# 

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

# in all copies or substantial portions of the Software. 

# 

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

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

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

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

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

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

# OTHER DEALINGS IN THE SOFTWARE.