cdxcore 0.1.6__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cdxcore might be problematic. Click here for more details.

cdxcore/pretty.py ADDED
@@ -0,0 +1,468 @@
1
+ """
2
+ Overview
3
+ --------
4
+
5
+ A simple :class:`cdxcore.pretty.PrettyObject` class which mimics directory access to its members.
6
+ The purpose is a functional-programming style pattern for generating complex objects::
7
+
8
+ from cdxbasics.prettydict import PrettyObject
9
+ pdct = PrettyObject(z=1)
10
+
11
+ pdct.num_samples = 1000
12
+ pdct.num_batches = 100
13
+ pdct.method = "signature"
14
+
15
+ The object allows accessing members via ``[]``:
16
+
17
+ print( pdct['num_samples'] ) # -> 1000
18
+ print( pdct['num_batches'] ) # -> 100
19
+
20
+ Features
21
+ ^^^^^^^^
22
+
23
+ :class:`cdxcore.pretty.PrettyObject` implements all relevant dictionary protocols, so objects of type :class:`cdxcore.pretty.PrettyObject` can
24
+ (nearly always) be passed where dictionaries are expected:
25
+
26
+ * A :class:`cdxcore.pretty.PrettyObject` object supports standard dictionary semantics in addition to member attribute
27
+ access.
28
+ That means you can use ``pdct['num_samples']`` as well as ``pdc.num_samples``.
29
+ You can mix standard dictionary notation with member attribute notation::
30
+
31
+ print(pdct["num_samples"]) # -> prints "1000"
32
+ pdct["test"] = 1 # sets pdct.test to 1
33
+
34
+ * Iterations work just like for dictionaries; for example::
35
+
36
+ for k,v in pdct.items():
37
+ print( k, v)
38
+
39
+ * Applying ``str`` and ``repr`` to objects of type :class:`cdxcore.pretty.PrettyObject` will return dictionary-type
40
+ results, so for example ``print(pdct)`` of the above will return ``{'z': 1, 'num_samples': 1000, 'num_batches': 100, 'method': 'signature'}``.
41
+
42
+ The :attr:`cdxcore.pretty.PrettyObject.at_pos` attribute allows accessing elements of the ordered dictionary
43
+ by positon:
44
+
45
+ * ``cdxcore.pretty.PrettyObject.at_pos[i]`` returns the `i` th element.
46
+
47
+ * ``cdxcore.pretty.PrettyObject.at_pos.keys[i]`` returns the `i` th key.
48
+
49
+ * ``cdxcore.pretty.PrettyObject.at_pos.items[i]`` returns the `i` th item.
50
+
51
+ For example::
52
+
53
+ print(pdct.at_pos[3]) # -> prints "signature"
54
+ print(pdct.at_pos.keys[3]) # -> prints "method"
55
+
56
+ You can also assign member functions to a :class:`cdxcore.pretty.PrettyObject`.
57
+ The following works as expected::
58
+
59
+ pdct.f = lambda self, y: return self.y*x
60
+
61
+ (to assign a static function which does not refer to ``self``, use ``pdct['g'] = lambda z : return z``).
62
+
63
+ Dataclasses
64
+ ^^^^^^^^^^^
65
+
66
+ :mod:`dataclasses` rely on default values of any member being "frozen" objects, which most user-defined objects and
67
+ :class:`cdxcore.pretty.PrettyObject` objects are not.
68
+ This limitation applies as well to `flax <https://flax-linen.readthedocs.io/en/latest/api_reference/flax.linen/module.html>`__ modules.
69
+ To use non-frozen default values, use the
70
+ :meth:`cdxcore.pretty.PrettyObject.as_field` function::
71
+
72
+ from cdxbasics.prettydict import PrettyObject
73
+ from dataclasses import dataclass
74
+
75
+ @dataclass
76
+ class Data:
77
+ data : PrettyObject = PrettyObject(x=2).as_field()
78
+
79
+ def f(self):
80
+ return self.data.x
81
+
82
+ d = Data() # default constructor used.
83
+ d.f() # -> returns 2
84
+
85
+ Import
86
+ ------
87
+ .. code-block:: python
88
+
89
+ from cdxcore.pretty import PrettyObject as pdct
90
+
91
+ Documentation
92
+ -------------
93
+ """
94
+
95
+ from collections import OrderedDict
96
+ import dataclasses as dataclasses
97
+ from dataclasses import Field
98
+ import types as types
99
+ from collections.abc import Mapping, MutableMapping, Sequence
100
+
101
+ class __No_Default_dummy():
102
+ pass
103
+ no_default = __No_Default_dummy()
104
+
105
+ class PrettyObject(MutableMapping):
106
+ """
107
+ Class mimicing an ordered dictionary.
108
+
109
+ Example::
110
+
111
+ from cdxcore.pretty import PrettyObject
112
+ pdct = PrettyObject()
113
+ pdct.x = 1
114
+ pdct['y'] = 2
115
+ print( pdct['x'], pdct.y ) # -> prints 1 2
116
+
117
+ The object mimics a dictionary::
118
+
119
+ print(pdct) # -> '{'x': 1, 'y': 2}'
120
+
121
+ u = dict( pdct )
122
+ print(u) # -> {'x': 1, 'y': 2}
123
+
124
+ u = { k: 2*v for k,v in pdct.items() }
125
+ print(u) # -> {'x': 2, 'y': 4}
126
+
127
+ l = list( pdct )
128
+ print(l) # -> ['x', 'y']
129
+
130
+ *Important:*
131
+ attributes starting with '__' cannot be accessed with item ``[]`` notation.
132
+ In other words::
133
+
134
+ pdct = PrettyObject()
135
+ pdct.__x = 1 # fine
136
+ _ = pdct['__x'] # <- throws an exception
137
+
138
+ **Access by Index Position**
139
+
140
+ :class:`cdxcore.pretty.PrettyObject` retains order of construction. To access its members
141
+ by index position, use the :attr:`cdxcore.pretty.PrettyObject.at_pos` attribute::
142
+
143
+ print(pdct.at_pos[1]) # -> prints "2"
144
+ print(pdct.at_pos.keys[1]) # -> prints "y"
145
+ print(list(pdct.at_pos.items[2])) # -> prints "[('x', 1), ('y', 2)]"
146
+
147
+ **Assigning Member Functions**
148
+
149
+ ``PrettyObject`` objects also allow assigning bona fide member functions by a simple semantic of the form::
150
+
151
+ pdct = PrettyObject(b=2)
152
+ pdct.mult_b = lambda self, x: self.b*x
153
+ pdct.mult_b(3) # -> 6
154
+
155
+ Calling ``pdct.mult_b(3)`` with above ``pdct`` will return `6` as expected.
156
+ To assign static member functions, use the ``[]`` operator.
157
+ The reason for this is as follows: consider::
158
+
159
+ def mult( a, b ):
160
+ return a*b
161
+ pdct = PrettyObject()
162
+ pdct.mult = mult
163
+ pdct.mult(3,4) --> produces am error as three arguments must be passed: self, 3, and 4
164
+
165
+ In this case, use::
166
+
167
+ pdct = PrettyObject()
168
+ pdct['mult'] = mult
169
+ pdct.mult(3,4) --> 12
170
+
171
+ You can also pass member functions to the constructor::
172
+
173
+ p = PrettyObject( f=lambda self, x: self.y*x, y=2)
174
+ p.f(3) # -> 6
175
+
176
+ **Operators**
177
+
178
+ Objects of type :class:`cdxcore.pretty.PrettyObject` support the following operators:
179
+
180
+ * Comparison operator ``==`` and ``!=`` test for equality of keys and values. Unlike for dictionaries
181
+ comparisons are performed in *in order*. That means ``PrettyObject(x=1,y=2)`` and ``PrettyObject(y=2,x=1)``
182
+ are *not* equal.
183
+
184
+ * Super/subset operators ``>=`` and ``<=`` test for a super/sup set relationship, respectively.
185
+
186
+ * The ``a | b`` returns the union of two :class:`cdxcore.pretty.PrettyObject`. Elements of the ``b`` overwrite any elements of ``a``, if they
187
+ are present in both. The order of the new dictionary is determined by the order of appearance of keys in first ``a`` and then ``b``, that
188
+ means in all but trivial cases ``a|b != b|a``.
189
+
190
+ The ``|=`` operator is a short-cut for :meth:`cdxcore.pretty.PrettyObject.update`.
191
+
192
+ Parameters
193
+ ----------
194
+ copy : Mapping, optional
195
+ If present, assign elements of ``copy`` to ``self``.
196
+
197
+ ** kwargs:
198
+ Key/value pairs to be added to ``self``.
199
+ """
200
+ def __init__(self, copy : Mapping = None, **kwargs):
201
+ """
202
+ :meta private:
203
+ """
204
+ if not copy is None:
205
+ self.update(copy)
206
+ for k, v in kwargs.items():
207
+ setattr(self, k, v)
208
+
209
+ def __getitem__(self, key):
210
+ try:
211
+ return getattr( self, key )
212
+ except AttributeError as e:
213
+ raise KeyError(key,*e.args)
214
+
215
+ def __setitem__(self,key,value):
216
+ """
217
+ Route ``self[key] = value`` to the base class ``__setattr__`` method.
218
+ This way you can assign static functions using ``[]`` which assinging
219
+ functions using ``.`` will assign member functions.
220
+ """
221
+ try:
222
+ super().__setattr__(key, value)
223
+ return self[key]
224
+ except AttributeError as e:
225
+ raise KeyError(key,*e.args)
226
+
227
+ def __delitem__(self,key):
228
+ try:
229
+ delattr(self, key)
230
+ except AttributeError as e:
231
+ raise KeyError(key,*e.args)
232
+ def __iter__(self):
233
+ return self.__dict__.__iter__()
234
+ def __reversed__(self):
235
+ return self.__dict__.__reversed__()
236
+ def __sizeof__(self):
237
+ return self.__dict__.__sizeof__()
238
+ def __contains__(self, key):
239
+ return self.__dict__.__contains__(key)
240
+ def __len__(self):
241
+ return self.__dict__.__len__()
242
+
243
+ # allow assigning functions with ``self``
244
+ def __setattr__(self, key, value):
245
+ """
246
+ ``__setattr__`` converts function assignments to member functions
247
+ """
248
+ if key[:2] == "__":
249
+ super().__setattr__(key, value)
250
+ if isinstance(value,types.FunctionType):
251
+ # bind function to this object
252
+ value = types.MethodType(value,self)
253
+ elif isinstance(value,types.MethodType):
254
+ # re-point the method to the current instance
255
+ value = types.MethodType(value.__func__,self)
256
+ super().__setattr__(key, value)
257
+
258
+ # dictionary
259
+ def copy(self, **kwargs):
260
+ """ Return a shallow copy; optionally add further key/value pairs. """
261
+ return PrettyObject(self,**kwargs)
262
+ def clear(self):
263
+ """ Delete all elements. """
264
+ self.__dict__.clear()
265
+
266
+ def get(self, key, default = no_default ):
267
+ """ Equivalent to :meth:`dict.get`. """
268
+ try:
269
+ return getattr(self, key) if default == no_default else getattr(self, key, default)
270
+ except AttributeError as e:
271
+ raise KeyError(key,*e.args)
272
+
273
+ def pop(self, key, default = no_default ):
274
+ """ Equivalent to :meth:`dict.pop`. """
275
+ try:
276
+ v = getattr(self, key) if default == no_default else getattr(self, key, default)
277
+ delattr(self,key)
278
+ return v
279
+ except AttributeError as e:
280
+ raise KeyError(key,*e.args)
281
+ def setdefault( self, key, default=None ):
282
+ """ Equivalent to :meth:`dict.setdefault`. """
283
+ #return self.__dict__.setdefault(key,default)
284
+ if not hasattr(self, key):
285
+ self.__setattr__(key, default)
286
+ return getattr(self,key)
287
+
288
+ def update(self, other : Mapping = None, **kwargs):
289
+ """ Equivalent to :meth:`dict.update`. """
290
+ if not other is None:
291
+ for k, v in other.items():
292
+ setattr(self, k, v)
293
+ for k, v in kwargs.items():
294
+ setattr(self, k, v)
295
+ return self
296
+
297
+ # behave like a dictionary
298
+ def keys(self):
299
+ """ Equivalent to :meth:`dict.keys` """
300
+ return self.__dict__.keys()
301
+ def items(self):
302
+ """ Equivalent to :meth:`dict.items` """
303
+ return self.__dict__.items()
304
+ def values(self):
305
+ """ Equivalent to :meth:`dict.values` """
306
+ return self.__dict__.values()
307
+
308
+ # update
309
+ def __ior__(self, other):
310
+ return self.update(other)
311
+ def __or__(self, other):
312
+ copy = self.copy()
313
+ copy.update(other)
314
+ return copy
315
+ def __ror__(self, other):
316
+ copy = self.copy()
317
+ copy.update(other)
318
+ return copy
319
+
320
+ # dictionary comparison
321
+ def __eq__(self, other):
322
+ """
323
+ Comparison operator. Unlike dictionary comparison, this comparision operator
324
+ preservers order.
325
+ """
326
+ if len(self) != len(other):
327
+ return False
328
+ for k1, k2 in zip( self, other ):
329
+ if not k1==k2:
330
+ return False
331
+ for v1, v2 in zip( self.values(), other.values() ):
332
+ if not v1==v2:
333
+ return False
334
+ return True
335
+ def __le__(self, other):
336
+ """
337
+ Subset operator i.e. if ``self`` is contained in ``other``, including values.
338
+ """
339
+ for k, v in self.items():
340
+ if not k in other:
341
+ return False
342
+ if not v == other[k]:
343
+ return False
344
+ return True
345
+ def __ge__(self, other):
346
+ """
347
+ Superset operator i.e. if ``self`` is a superset of ``other``, including values.
348
+ """
349
+ return other <= self
350
+
351
+ def __neq__(self, other):
352
+ """
353
+ Comparison operator. Unlike dictionary comparison, this comparision operator
354
+ preservers order.
355
+ """
356
+ return not self == other
357
+
358
+ # print representation
359
+ def __repr__(self):
360
+ return f"PrettyObject({self.__dict__.__repr__()})"
361
+ def __str__(self):
362
+ return self.__dict__.__str__()
363
+
364
+ # data classes
365
+ def as_field(self) -> Field:
366
+ """
367
+ This function provides support for :class:`dataclasses.dataclass` fields
368
+ with ``PrettyObject`` default values.
369
+
370
+ When adding
371
+ a `field <https://docs.python.org/3/library/dataclasses.html#dataclasses.field>`__
372
+ with a non-frozen default value to a ``@dataclass`` class,
373
+ a ``default_factory`` has to be provided.
374
+ The function ``as_field`` returns the corresponding :class:`dataclasses.Field`
375
+ element by returning simply::
376
+
377
+ def factory():
378
+ return self
379
+ return dataclasses.field( default_factory=factory )
380
+
381
+ Usage is as follows::
382
+
383
+ from dataclasses import dataclass
384
+ @dataclass
385
+ class A:
386
+ data : PrettyDict = PrettyDict(x=2).as_field()
387
+
388
+ a = A()
389
+ print(a.data.x) # -> "2"
390
+ a = A(data=PrettyDict(x=3))
391
+ print(a.data.x) # -> "3"
392
+ """
393
+ def factory():
394
+ return self
395
+ return dataclasses.field( default_factory=factory )
396
+
397
+ @property
398
+ def at_pos(self):
399
+ """
400
+ Elementary access to the data contained in ``self`` by ordinal position.
401
+ The ordinal
402
+ position of an element is determined by the order of addition to the dictionary.
403
+
404
+ * ``at_pos[position]`` returns an element or elements at an ordinal position:
405
+
406
+ * It returns a single element if ``position`` refers to only one field.
407
+ * If ``position`` is a slice then the respecitve list of fields is returned.
408
+
409
+ * ``at_pos.keys[position]`` returns the key or keys at ``position``.
410
+
411
+ * ``at_pos.items[position]`` returns the tuple ``(key, element)`` or a list thereof for ``position``.
412
+
413
+ You can also write data using the `attribute` notation:
414
+
415
+ * ``at_pos[position] = item`` assigns an item to an ordinal position:
416
+
417
+ * If ``position`` refers to a single element, ``item`` must be the value to be assigned to this element.
418
+ * If ``position`` is a slice then '``item`` must resolve to a list (or generator) of the required size.
419
+ """
420
+
421
+ class Access(Sequence):
422
+ """
423
+ Wrapper object to allow index access for at_pos
424
+ """
425
+ def __init__(self):
426
+ self.__keys = None
427
+
428
+ def __getitem__(_, position):
429
+ key = _.keys[position]
430
+ return self[key] if not isinstance(key,list) else [ self[k] for k in key ]
431
+ def __setitem__(_, position, item ):
432
+ key = _.keys[position]
433
+ if not isinstance(key,list):
434
+ self[key] = item
435
+ else:
436
+ for k, i in zip(key, item):
437
+ self[k] = i
438
+ def __len__(_):
439
+ return len(self)
440
+ def __iter__(_):
441
+ for key in self:
442
+ yield self[key]
443
+
444
+ @property
445
+ def keys(_) -> list:
446
+ """ Returns the list of keys in construction order """
447
+ return list(self.keys())
448
+ @property
449
+ def values(_) -> list:
450
+ """ Returns the list of values in construction order """
451
+ return list(self.values())
452
+ @property
453
+ def items(_) -> Sequence:
454
+ """ Returns the sequence of key, value pairs of the original dictionary """
455
+ class ItemAccess(Sequence):
456
+ def __init__(_x):
457
+ _x.keys = list(self.keys())
458
+ def __getitem__(_x, position):
459
+ key = _x.keys[position]
460
+ return (key, self[key]) if not isinstance(key,(list,types.GeneratorType)) else [ (k,self[k]) for k in key ]
461
+ def __len__(_x):
462
+ return len(_x.keys)
463
+ def __iter__(_x):
464
+ for key in _x.keys:
465
+ yield key, self[key]
466
+ return ItemAccess()
467
+
468
+ return Access()