Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain/plain/utils/functional.py: 66%

1 import copy

2 import itertools

3 import operator

4 from functools import total_ordering, wraps

5

6

7 class 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

51 class 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

68 class 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

77 def 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

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

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

207

208

209 def 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

216 def 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

243 def 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

250 empty = object()

251

252

253 def 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

264 class 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

380 def 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

388 class 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

445 def 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