Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/utils/functional.py: 64%

224 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:03 -0500

1import copy 

2import itertools 

3import operator 

4from functools import total_ordering, wraps 

5 

6 

7class cached_property: 

8 """ 

9 Decorator that converts a method with a single self argument into a 

10 property cached on the instance. 

11 

12 A cached property can be made out of an existing method: 

13 (e.g. ``url = cached_property(get_absolute_url)``). 

14 """ 

15 

16 name = None 

17 

18 @staticmethod 

19 def func(instance): 

20 raise TypeError( 

21 "Cannot use cached_property instance without calling " 

22 "__set_name__() on it." 

23 ) 

24 

25 def __init__(self, func): 

26 self.real_func = func 

27 self.__doc__ = getattr(func, "__doc__") 

28 

29 def __set_name__(self, owner, name): 

30 if self.name is None: 

31 self.name = name 

32 self.func = self.real_func 

33 elif name != self.name: 

34 raise TypeError( 

35 "Cannot assign the same cached_property to two different names " 

36 f"({self.name!r} and {name!r})." 

37 ) 

38 

39 def __get__(self, instance, cls=None): 

40 """ 

41 Call the function and put the return value in instance.__dict__ so that 

42 subsequent attribute access on the instance returns the cached value 

43 instead of calling cached_property.__get__(). 

44 """ 

45 if instance is None: 

46 return self 

47 res = instance.__dict__[self.name] = self.func(instance) 

48 return res 

49 

50 

51class classproperty: 

52 """ 

53 Decorator that converts a method with a single cls argument into a property 

54 that can be accessed directly from the class. 

55 """ 

56 

57 def __init__(self, method=None): 

58 self.fget = method 

59 

60 def __get__(self, instance, cls=None): 

61 return self.fget(cls) 

62 

63 def getter(self, method): 

64 self.fget = method 

65 return self 

66 

67 

68class Promise: 

69 """ 

70 Base class for the proxy class created in the closure of the lazy function. 

71 It's used to recognize promises in code. 

72 """ 

73 

74 pass 

75 

76 

77def lazy(func, *resultclasses): 

78 """ 

79 Turn any callable into a lazy evaluated callable. result classes or types 

80 is required -- at least one is needed so that the automatic forcing of 

81 the lazy evaluation code is triggered. Results are not memoized; the 

82 function is evaluated on every access. 

83 """ 

84 

85 @total_ordering 

86 class __proxy__(Promise): 

87 """ 

88 Encapsulate a function call and act as a proxy for methods that are 

89 called on the result of that function. The function is not evaluated 

90 until one of the methods on the result is called. 

91 """ 

92 

93 __prepared = False 

94 

95 def __init__(self, args, kw): 

96 self.__args = args 

97 self.__kw = kw 

98 if not self.__prepared: 

99 self.__prepare_class__() 

100 self.__class__.__prepared = True 

101 

102 def __reduce__(self): 

103 return ( 

104 _lazy_proxy_unpickle, 

105 (func, self.__args, self.__kw) + resultclasses, 

106 ) 

107 

108 def __repr__(self): 

109 return repr(self.__cast()) 

110 

111 @classmethod 

112 def __prepare_class__(cls): 

113 for resultclass in resultclasses: 

114 for type_ in resultclass.mro(): 

115 for method_name in type_.__dict__: 

116 # All __promise__ return the same wrapper method, they 

117 # look up the correct implementation when called. 

118 if hasattr(cls, method_name): 

119 continue 

120 meth = cls.__promise__(method_name) 

121 setattr(cls, method_name, meth) 

122 cls._delegate_bytes = bytes in resultclasses 

123 cls._delegate_text = str in resultclasses 

124 if cls._delegate_bytes and cls._delegate_text: 

125 raise ValueError( 

126 "Cannot call lazy() with both bytes and text return types." 

127 ) 

128 if cls._delegate_text: 

129 cls.__str__ = cls.__text_cast 

130 elif cls._delegate_bytes: 

131 cls.__bytes__ = cls.__bytes_cast 

132 

133 @classmethod 

134 def __promise__(cls, method_name): 

135 # Builds a wrapper around some magic method 

136 def __wrapper__(self, *args, **kw): 

137 # Automatically triggers the evaluation of a lazy value and 

138 # applies the given magic method of the result type. 

139 res = func(*self.__args, **self.__kw) 

140 return getattr(res, method_name)(*args, **kw) 

141 

142 return __wrapper__ 

143 

144 def __text_cast(self): 

145 return func(*self.__args, **self.__kw) 

146 

147 def __bytes_cast(self): 

148 return bytes(func(*self.__args, **self.__kw)) 

149 

150 def __bytes_cast_encoded(self): 

151 return func(*self.__args, **self.__kw).encode() 

152 

153 def __cast(self): 

154 if self._delegate_bytes: 

155 return self.__bytes_cast() 

156 elif self._delegate_text: 

157 return self.__text_cast() 

158 else: 

159 return func(*self.__args, **self.__kw) 

160 

161 def __str__(self): 

162 # object defines __str__(), so __prepare_class__() won't overload 

163 # a __str__() method from the proxied class. 

164 return str(self.__cast()) 

165 

166 def __eq__(self, other): 

167 if isinstance(other, Promise): 

168 other = other.__cast() 

169 return self.__cast() == other 

170 

171 def __lt__(self, other): 

172 if isinstance(other, Promise): 

173 other = other.__cast() 

174 return self.__cast() < other 

175 

176 def __hash__(self): 

177 return hash(self.__cast()) 

178 

179 def __mod__(self, rhs): 

180 if self._delegate_text: 

181 return str(self) % rhs 

182 return self.__cast() % rhs 

183 

184 def __add__(self, other): 

185 return self.__cast() + other 

186 

187 def __radd__(self, other): 

188 return other + self.__cast() 

189 

190 def __deepcopy__(self, memo): 

191 # Instances of this class are effectively immutable. It's just a 

192 # collection of functions. So we don't need to do anything 

193 # complicated for copying. 

194 memo[id(self)] = self 

195 return self 

196 

197 @wraps(func) 

198 def __wrapper__(*args, **kw): 

199 # Creates the proxy object, instead of the actual value. 

200 return __proxy__(args, kw) 

201 

202 return __wrapper__ 

203 

204 

205def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses): 

206 return lazy(func, *resultclasses)(*args, **kwargs) 

207 

208 

209def lazystr(text): 

210 """ 

211 Shortcut for the common case of a lazy callable that returns str. 

212 """ 

213 return lazy(str, str)(text) 

214 

215 

216def keep_lazy(*resultclasses): 

217 """ 

218 A decorator that allows a function to be called with one or more lazy 

219 arguments. If none of the args are lazy, the function is evaluated 

220 immediately, otherwise a __proxy__ is returned that will evaluate the 

221 function when needed. 

222 """ 

223 if not resultclasses: 

224 raise TypeError("You must pass at least one argument to keep_lazy().") 

225 

226 def decorator(func): 

227 lazy_func = lazy(func, *resultclasses) 

228 

229 @wraps(func) 

230 def wrapper(*args, **kwargs): 

231 if any( 

232 isinstance(arg, Promise) 

233 for arg in itertools.chain(args, kwargs.values()) 

234 ): 

235 return lazy_func(*args, **kwargs) 

236 return func(*args, **kwargs) 

237 

238 return wrapper 

239 

240 return decorator 

241 

242 

243def keep_lazy_text(func): 

244 """ 

245 A decorator for functions that accept lazy arguments and return text. 

246 """ 

247 return keep_lazy(str)(func) 

248 

249 

250empty = object() 

251 

252 

253def new_method_proxy(func): 

254 def inner(self, *args): 

255 if (_wrapped := self._wrapped) is empty: 

256 self._setup() 

257 _wrapped = self._wrapped 

258 return func(_wrapped, *args) 

259 

260 inner._mask_wrapped = False 

261 return inner 

262 

263 

264class LazyObject: 

265 """ 

266 A wrapper for another class that can be used to delay instantiation of the 

267 wrapped class. 

268 

269 By subclassing, you have the opportunity to intercept and alter the 

270 instantiation. If you don't need to do that, use SimpleLazyObject. 

271 """ 

272 

273 # Avoid infinite recursion when tracing __init__ (#19456). 

274 _wrapped = None 

275 

276 def __init__(self): 

277 # Note: if a subclass overrides __init__(), it will likely need to 

278 # override __copy__() and __deepcopy__() as well. 

279 self._wrapped = empty 

280 

281 def __getattribute__(self, name): 

282 if name == "_wrapped": 

283 # Avoid recursion when getting wrapped object. 

284 return super().__getattribute__(name) 

285 value = super().__getattribute__(name) 

286 # If attribute is a proxy method, raise an AttributeError to call 

287 # __getattr__() and use the wrapped object method. 

288 if not getattr(value, "_mask_wrapped", True): 

289 raise AttributeError 

290 return value 

291 

292 __getattr__ = new_method_proxy(getattr) 

293 

294 def __setattr__(self, name, value): 

295 if name == "_wrapped": 

296 # Assign to __dict__ to avoid infinite __setattr__ loops. 

297 self.__dict__["_wrapped"] = value 

298 else: 

299 if self._wrapped is empty: 

300 self._setup() 

301 setattr(self._wrapped, name, value) 

302 

303 def __delattr__(self, name): 

304 if name == "_wrapped": 

305 raise TypeError("can't delete _wrapped.") 

306 if self._wrapped is empty: 

307 self._setup() 

308 delattr(self._wrapped, name) 

309 

310 def _setup(self): 

311 """ 

312 Must be implemented by subclasses to initialize the wrapped object. 

313 """ 

314 raise NotImplementedError( 

315 "subclasses of LazyObject must provide a _setup() method" 

316 ) 

317 

318 # Because we have messed with __class__ below, we confuse pickle as to what 

319 # class we are pickling. We're going to have to initialize the wrapped 

320 # object to successfully pickle it, so we might as well just pickle the 

321 # wrapped object since they're supposed to act the same way. 

322 # 

323 # Unfortunately, if we try to simply act like the wrapped object, the ruse 

324 # will break down when pickle gets our id(). Thus we end up with pickle 

325 # thinking, in effect, that we are a distinct object from the wrapped 

326 # object, but with the same __dict__. This can cause problems (see #25389). 

327 # 

328 # So instead, we define our own __reduce__ method and custom unpickler. We 

329 # pickle the wrapped object as the unpickler's argument, so that pickle 

330 # will pickle it normally, and then the unpickler simply returns its 

331 # argument. 

332 def __reduce__(self): 

333 if self._wrapped is empty: 

334 self._setup() 

335 return (unpickle_lazyobject, (self._wrapped,)) 

336 

337 def __copy__(self): 

338 if self._wrapped is empty: 

339 # If uninitialized, copy the wrapper. Use type(self), not 

340 # self.__class__, because the latter is proxied. 

341 return type(self)() 

342 else: 

343 # If initialized, return a copy of the wrapped object. 

344 return copy.copy(self._wrapped) 

345 

346 def __deepcopy__(self, memo): 

347 if self._wrapped is empty: 

348 # We have to use type(self), not self.__class__, because the 

349 # latter is proxied. 

350 result = type(self)() 

351 memo[id(self)] = result 

352 return result 

353 return copy.deepcopy(self._wrapped, memo) 

354 

355 __bytes__ = new_method_proxy(bytes) 

356 __str__ = new_method_proxy(str) 

357 __bool__ = new_method_proxy(bool) 

358 

359 # Introspection support 

360 __dir__ = new_method_proxy(dir) 

361 

362 # Need to pretend to be the wrapped class, for the sake of objects that 

363 # care about this (especially in equality tests) 

364 __class__ = property(new_method_proxy(operator.attrgetter("__class__"))) 

365 __eq__ = new_method_proxy(operator.eq) 

366 __lt__ = new_method_proxy(operator.lt) 

367 __gt__ = new_method_proxy(operator.gt) 

368 __ne__ = new_method_proxy(operator.ne) 

369 __hash__ = new_method_proxy(hash) 

370 

371 # List/Tuple/Dictionary methods support 

372 __getitem__ = new_method_proxy(operator.getitem) 

373 __setitem__ = new_method_proxy(operator.setitem) 

374 __delitem__ = new_method_proxy(operator.delitem) 

375 __iter__ = new_method_proxy(iter) 

376 __len__ = new_method_proxy(len) 

377 __contains__ = new_method_proxy(operator.contains) 

378 

379 

380def unpickle_lazyobject(wrapped): 

381 """ 

382 Used to unpickle lazy objects. Just return its argument, which will be the 

383 wrapped object. 

384 """ 

385 return wrapped 

386 

387 

388class SimpleLazyObject(LazyObject): 

389 """ 

390 A lazy object initialized from any function. 

391 

392 Designed for compound objects of unknown type. For builtins or objects of 

393 known type, use plain.utils.functional.lazy. 

394 """ 

395 

396 def __init__(self, func): 

397 """ 

398 Pass in a callable that returns the object to be wrapped. 

399 

400 If copies are made of the resulting SimpleLazyObject, which can happen 

401 in various circumstances within Plain, then you must ensure that the 

402 callable can be safely run more than once and will return the same 

403 value. 

404 """ 

405 self.__dict__["_setupfunc"] = func 

406 super().__init__() 

407 

408 def _setup(self): 

409 self._wrapped = self._setupfunc() 

410 

411 # Return a meaningful representation of the lazy object for debugging 

412 # without evaluating the wrapped object. 

413 def __repr__(self): 

414 if self._wrapped is empty: 

415 repr_attr = self._setupfunc 

416 else: 

417 repr_attr = self._wrapped 

418 return f"<{type(self).__name__}: {repr_attr!r}>" 

419 

420 def __copy__(self): 

421 if self._wrapped is empty: 

422 # If uninitialized, copy the wrapper. Use SimpleLazyObject, not 

423 # self.__class__, because the latter is proxied. 

424 return SimpleLazyObject(self._setupfunc) 

425 else: 

426 # If initialized, return a copy of the wrapped object. 

427 return copy.copy(self._wrapped) 

428 

429 def __deepcopy__(self, memo): 

430 if self._wrapped is empty: 

431 # We have to use SimpleLazyObject, not self.__class__, because the 

432 # latter is proxied. 

433 result = SimpleLazyObject(self._setupfunc) 

434 memo[id(self)] = result 

435 return result 

436 return copy.deepcopy(self._wrapped, memo) 

437 

438 __add__ = new_method_proxy(operator.add) 

439 

440 @new_method_proxy 

441 def __radd__(self, other): 

442 return other + self 

443 

444 

445def partition(predicate, values): 

446 """ 

447 Split the values into two sets, based on the return value of the function 

448 (True/False). e.g.: 

449 

450 >>> partition(lambda x: x > 3, range(5)) 

451 [0, 1, 2, 3], [4] 

452 """ 

453 results = ([], []) 

454 for item in values: 

455 results[predicate(item)].append(item) 

456 return results