cs-obj 20250306__py2.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.
cs/obj.py ADDED
@@ -0,0 +1,550 @@
1
+ #!/usr/bin/python
2
+ #
3
+ # Random stuff for "objects". - Cameron Simpson <cs@cskk.id.au>
4
+ #
5
+
6
+ r'''
7
+ Convenience facilities for objects.
8
+ '''
9
+
10
+ from collections import defaultdict
11
+ from copy import copy as copy0
12
+ import sys
13
+ from threading import Lock
14
+ import traceback
15
+ from types import SimpleNamespace
16
+ from weakref import WeakValueDictionary
17
+
18
+ from cs.deco import OBSOLETE
19
+
20
+ __version__ = '20250306'
21
+
22
+ DISTINFO = {
23
+ 'keywords': ["python2", "python3"],
24
+ 'classifiers': [
25
+ "Programming Language :: Python",
26
+ "Programming Language :: Python :: 3",
27
+ ],
28
+ 'install_requires': ['cs.deco'],
29
+ }
30
+
31
+ T_SEQ = 'SEQUENCE'
32
+ T_MAP = 'MAPPING'
33
+ T_SCALAR = 'SCALAR'
34
+
35
+ def flavour(obj):
36
+ """ Return constants indicating the ``flavour'' of an object:
37
+ * `T_MAP`: DictType, DictionaryType, objects with an __keys__ or keys attribute.
38
+ * `T_SEQ`: TupleType, ListType, objects with an __iter__ attribute.
39
+ * `T_SCALAR`: Anything else.
40
+ """
41
+ t = type(obj)
42
+ if isinstance(t, (tuple, list)):
43
+ return T_SEQ
44
+ if isinstance(t, dict):
45
+ return T_MAP
46
+ if hasattr(obj, '__keys__') or hasattr(obj, 'keys'):
47
+ return T_MAP
48
+ if hasattr(obj, '__iter__'):
49
+ return T_SEQ
50
+ return T_SCALAR
51
+
52
+ # pylint: disable=too-few-public-methods
53
+ class O(SimpleNamespace):
54
+ ''' The `O` class is now obsolete, please subclass `types.SimpleNamespace`
55
+ or use a dataclass.
56
+ '''
57
+
58
+ callers = set()
59
+
60
+ def __init__(self, **kw):
61
+ frame = traceback.extract_stack(None, 2)[0]
62
+ caller = (frame[0], frame[1])
63
+ if caller not in self.callers:
64
+ self.callers.add(caller)
65
+ print(
66
+ "WARNING: %s:%d %s: obsolete use of cs.obj.O, please shift to types.SimpleNamespace."
67
+ % (frame[0], frame[1], frame[2]),
68
+ file=sys.stderr
69
+ )
70
+ SimpleNamespace.__init__(self, **kw)
71
+
72
+ def O_merge(o, _conflict=None, _overwrite=False, **kw):
73
+ ''' Merge key:value pairs from a mapping into an object.
74
+
75
+ Ignore keys that do not start with a letter.
76
+ New attributes or attributes whose values compare equal are
77
+ merged in. Unequal values are passed to:
78
+
79
+ _conflict(o, attr, old_value, new_value)
80
+
81
+ to resolve the conflict. If _conflict is omitted or None
82
+ then the new value overwrites the old if _overwrite is true.
83
+ '''
84
+ for attr, value in kw.items():
85
+ if attr or not attr[0].isalpha():
86
+ continue
87
+ try:
88
+ ovalue = getattr(o, attr)
89
+ except AttributeError:
90
+ # new attribute -
91
+ setattr(o, attr, value)
92
+ else:
93
+ if ovalue != value:
94
+ if _conflict is None:
95
+ if _overwrite:
96
+ setattr(o, attr, value)
97
+ else:
98
+ _conflict(o, attr, ovalue, value)
99
+
100
+ def O_attrs(o):
101
+ ''' Yield attribute names from `o` which are pertinent to `O_str`.
102
+
103
+ Note: this calls `getattr(o,attr)` to inspect it in order to
104
+ prune callables.
105
+ '''
106
+ for attr in sorted(dir(o)):
107
+ if attr[0].isalpha():
108
+ try:
109
+ value = getattr(o, attr)
110
+ except AttributeError:
111
+ continue
112
+ if not callable(value):
113
+ yield attr
114
+
115
+ def O_attritems(o):
116
+ ''' Generator yielding `(attr,value)` for relevant attributes of `o`.
117
+ '''
118
+ for attr in O_attrs(o):
119
+ try:
120
+ value = getattr(o, attr)
121
+ except AttributeError:
122
+ continue
123
+ else:
124
+ yield attr, value
125
+
126
+ def O_str(o, no_recurse=False, seen=None):
127
+ ''' Return a `str` representation of the object `o`.
128
+
129
+ Parameters:
130
+ * `o`: the object to describe.
131
+ * `no_recurse`: if true, do not recurse into the object's structure.
132
+ Default: `False`.
133
+ * `seen`: a set of previously sighted objects
134
+ to prevent recursion loops.
135
+ '''
136
+ if seen is None:
137
+ seen = set()
138
+ obj_type = type(o)
139
+ if obj_type in (str,):
140
+ return repr(o)
141
+ if obj_type in (tuple, int, float, bool, list):
142
+ return str(o)
143
+ if obj_type is dict:
144
+ o2 = {k: str(v) for k, v in o.items()}
145
+ return str(o2)
146
+ if obj_type is set:
147
+ return 'set(%s)' % (','.join(sorted([str(item) for item in o])))
148
+ seen.add(id(o))
149
+ if no_recurse:
150
+ attrdesc_strs = [
151
+ "%s=<%s>" % (pattr, type(pvalue).__name__)
152
+ for pattr, pvalue in O_attritems(o)
153
+ ]
154
+ else:
155
+ attrdesc_strs = []
156
+ for pattr, pvalue in O_attritems(o):
157
+ if id(pvalue) in seen:
158
+ desc = "<%s>" % (type(pvalue).__name__,)
159
+ else:
160
+ desc = "%s=%s" % (
161
+ pattr, O_str(pvalue, no_recurse=no_recurse, seen=seen)
162
+ )
163
+ attrdesc_strs.append(desc)
164
+ s = "<%s %s>" % (o.__class__.__name__, ",".join(attrdesc_strs))
165
+ seen.remove(id(o))
166
+ return s
167
+
168
+ def copy(obj, **kw):
169
+ ''' Convenient function to shallow copy an object with simple modifications.
170
+
171
+ Performs a shallow copy of `self` using `copy.copy`.
172
+
173
+ Treat all keyword arguments as `(attribute,value)` 2-tuples and
174
+ replace those attributes with the supplied values.
175
+ '''
176
+ obj2 = copy0(obj)
177
+ for attr, value in kw.items():
178
+ setattr(obj2, attr, value)
179
+ return obj2
180
+
181
+ def as_dict(o, selector=None):
182
+ ''' Return a dictionary with keys mapping to the values of the attributes of `o`.
183
+
184
+ Parameters:
185
+ * `o`: the object to map
186
+ * `selector`: the optional selection criterion
187
+
188
+ If `selector` is omitted or `None`, select "public" attributes,
189
+ those not commencing with an underscore.
190
+
191
+ If `selector` is a `str`, select attributes starting with `selector`.
192
+
193
+ Otherwise presume `selector` is callable
194
+ and select attributes `attr` where `selector(attr)` is true.
195
+ '''
196
+ if selector is None:
197
+ match = lambda attr: attr and not attr.startswith('_')
198
+ elif isinstance(selector, str):
199
+ match = lambda attr: attr.startswith(selector)
200
+ else:
201
+ match = selector
202
+ return {attr: getattr(o, attr) for attr in dir(o) if match(attr)}
203
+
204
+ @OBSOLETE("use cs.obj.as_dict")
205
+ def obj_as_dict(o, **kw):
206
+ ''' OBSOLETE convesion of an object to a `dict`. Please us `cs.obj.as_dict`.
207
+ '''
208
+ raise RuntimeError("please use cs.obj.as_dict")
209
+
210
+ class Proxy(object):
211
+ ''' An extremely simple proxy object
212
+ that passes all unmatched attribute accesses to the proxied object.
213
+
214
+ Note that setattr and delattr work directly on the proxy, not the proxied object.
215
+ '''
216
+
217
+ def __init__(self, other):
218
+ self._proxied = other
219
+
220
+ def __getattr__(self, attr):
221
+ _proxied = object.__getattribute__(self, '_proxied')
222
+ return getattr(_proxied, attr)
223
+
224
+ def __iter__(self):
225
+ _proxied = object.__getattribute__(self, '_proxied')
226
+ return iter(_proxied)
227
+
228
+ def __len__(self):
229
+ _proxied = object.__getattribute__(self, '_proxied')
230
+ return len(_proxied)
231
+
232
+ class TrackedClassMixin(object):
233
+ ''' A mixin to track all instances of a particular class.
234
+
235
+ This is aimed at checking the global state of objects of a
236
+ particular type, particularly states like counters. The
237
+ tracking is attached to the class itself.
238
+
239
+ The class to be tracked includes this mixin as a superclass and calls:
240
+
241
+ TrackedClassMixin.__init__(class_to_track)
242
+
243
+ from its __init__ method. Note that `class_to_track` is
244
+ typically the class name itself, not `type(self)` which would
245
+ track the specific subclass. At some relevant point one can call:
246
+
247
+ self.tcm_dump(class_to_track[, file])
248
+
249
+ `class_to_track` needs a `tcm_get_state` method to return the
250
+ salient information, such as this from cs.resources.MultiOpenMixin:
251
+
252
+ def tcm_get_state(self):
253
+ return {'opened': self.opened, 'opens': self._opens}
254
+
255
+ See cs.resources.MultiOpenMixin for example use.
256
+ '''
257
+
258
+ def __init__(self, cls):
259
+ try:
260
+ m = cls.__map
261
+ except AttributeError:
262
+ m = cls.__map = {}
263
+ m[id(self)] = self
264
+
265
+ def __state(self, cls):
266
+ return cls.tcm_get_state(self)
267
+
268
+ @staticmethod
269
+ def tcm_all_state(klass):
270
+ ''' Generator yielding tracking information
271
+ for objects of type `klass`
272
+ in the form `(o,state)`
273
+ where `o` if a tracked object
274
+ and `state` is the object's `get_tcm_state` method result.
275
+ '''
276
+ m = klass.__map
277
+ for o in m.values():
278
+ yield o, klass.__state(o, klass)
279
+
280
+ @staticmethod
281
+ def tcm_dump(klass, f=None):
282
+ ''' Dump the tracking information for `klass` to the file `f`
283
+ (default `sys.stderr`).
284
+ '''
285
+ if f is None:
286
+ f = sys.stderr
287
+ for o, state in TrackedClassMixin.tcm_all_state(klass):
288
+ print(str(type(o)), id(o), repr(state), file=f)
289
+
290
+ def singleton(registry, key, factory, fargs, fkwargs):
291
+ ''' Obtain an object for `key` via `registry` (a mapping of `key`=>object).
292
+ Return `(is_new,object)`.
293
+
294
+ If the `key` exists in the registry, return the associated object.
295
+ Otherwise create a new object by calling `factory(*fargs,**fkwargs)`
296
+ and store it as `key` in the `registry`.
297
+
298
+ The `registry` may be any mapping of `key`s to objects
299
+ but will usually be a `weakref.WeakValueDictionary`
300
+ in order that object references expire as normal,
301
+ allowing garbage collection.
302
+
303
+ *Note*: this function *is not* thread safe.
304
+ Multithreaded users should hold a mutex.
305
+
306
+ See the `SingletonMixin` class for a simple mixin to create
307
+ singleton classes,
308
+ which does provide thread safe operations.
309
+ '''
310
+ try:
311
+ instance = registry[key]
312
+ is_new = False
313
+ except KeyError:
314
+ instance = factory(*fargs, **fkwargs)
315
+ registry[key] = instance
316
+ is_new = True
317
+ return is_new, instance
318
+
319
+ # pylint: disable=too-few-public-methods
320
+ class SingletonMixin:
321
+ ''' A mixin turning a subclass into a singleton factory.
322
+
323
+ *Note*: this mixin overrides `object.__new__`
324
+ and may not play well with other classes which override `__new__`.
325
+
326
+ *Warning*: because of the mechanics of `__new__`,
327
+ the instance's `__init__` method will always be called
328
+ after `__new__`,
329
+ even when a preexisting object is returned.
330
+ Therefore that method should be sensible
331
+ even for an already initialised
332
+ and probably subsequently modified object.
333
+
334
+ My suggested approach is to access some attribute,
335
+ and preemptively return if it already exists.
336
+ Example:
337
+
338
+ def __init__(self, x, y):
339
+ if 'x' in self.__dict__:
340
+ return
341
+ self.x = x
342
+ self.y = y
343
+
344
+ *Note*: we probe `self.__dict__` above to accomodate classes
345
+ with a `__getattr__` method.
346
+
347
+ *Note*: each class registry has a lock,
348
+ which ensures that reuse of an object
349
+ in multiple threads will call the `__init__` method
350
+ in a thread safe serialised fashion.
351
+
352
+ Implementation requirements:
353
+ a subclass should:
354
+ * provide a method `_singleton_key(*args,**kwargs)`
355
+ returning a key for use in the single registry,
356
+ computed from the positional and keyword arguments
357
+ supplied on instance creation
358
+ i.e. those which `__init__` would normally receive.
359
+ This should have the same signature as `__init__`
360
+ but using `cls` instead of `self`.
361
+ * provide a normal `__init__` method
362
+ which can be safely called again
363
+ after some earlier initialisation.
364
+
365
+ This class is thread safe for the registry operations.
366
+
367
+ Example:
368
+
369
+ class Pool(SingletonMixin):
370
+
371
+ @classmethod
372
+ def _singleton_key(cls, foo, bah=3):
373
+ return foo, bah
374
+
375
+ def __init__(self, foo, bah=3):
376
+ if hasattr(self, 'foo'):
377
+ return
378
+ ... normal __init__ stuff here ...
379
+ self.foo = foo
380
+ ...
381
+ '''
382
+
383
+ # This lock is used to control setup of the per-class registry.
384
+ # It is shared across all subclasses, but that bypasses any need to call an
385
+ # __init__ for this mixin. In mitigation, the lock is only used if the class
386
+ # does not yet have a registry.
387
+ __global_lock = Lock()
388
+
389
+ @classmethod
390
+ def _singleton_get_registry(cls):
391
+ ''' Obtain the class singleton registry, creating it on first use.
392
+ '''
393
+ try:
394
+ registry = cls._singleton_registry
395
+ except AttributeError:
396
+ with cls.__global_lock:
397
+ try:
398
+ registry = cls._singleton_registry
399
+ except AttributeError:
400
+ # create the registry and give it its own mutex and multiindex
401
+ registry = cls._singleton_registry = WeakValueDictionary()
402
+ registry._singleton_lock = Lock()
403
+ registry._singleton_also_keys = defaultdict(WeakValueDictionary)
404
+ return registry
405
+
406
+ def __new__(cls, *a, **kw):
407
+ ''' Prepare a new instance of `cls` if required.
408
+ Return the instance.
409
+
410
+ This creates the class registry if missing,
411
+ prepares a key from `cls._singleton_key`,
412
+ then returns the entry from the registry is present,
413
+ or creates a new entry if not.
414
+ Note: if the key is `None` a new entry is always created
415
+ and not recorded in the registry.
416
+ '''
417
+ super_new = super().__new__
418
+
419
+ # pylint: disable=unused-argument
420
+ def factory(*fargs, **fkwargs):
421
+ ''' Prepare a new object; does not yet call `__init__`.
422
+ This accepts arguments to support use via the `singleton()` function.
423
+ '''
424
+ return super_new(cls)
425
+
426
+ okey = cls._singleton_key(*a, **kw)
427
+ if okey is None:
428
+ # if the returned key is None we always make a new instance
429
+ # and do not register in the registry
430
+ instance = factory()
431
+ else:
432
+ # normal behaviour:
433
+ # reuse an existing instance or make a new one
434
+ registry = cls._singleton_get_registry()
435
+ with registry._singleton_lock:
436
+ is_new, instance = singleton(registry, okey, factory, (), {})
437
+ if is_new:
438
+ instance._SingletonMixin_key = okey
439
+ else:
440
+ assert instance._SingletonMixin_key == okey
441
+ return instance
442
+
443
+ # default hash and equality methods
444
+ def __hash__(self):
445
+ return hash(self._SingletonMixin_key)
446
+
447
+ def __eq__(self, other):
448
+ return self is other
449
+
450
+ @classmethod
451
+ def singleton_also_by(cls, also_key, key):
452
+ ''' Obtain a singleton by a secondary key.
453
+ Return the instance or `None`.
454
+
455
+ Parameters:
456
+ * `also_key`: the name of the secondary key index
457
+ * `key`: the key for the index
458
+ '''
459
+ registry = cls._singleton_get_registry()
460
+ with registry._singleton_lock:
461
+ return registry._singleton_also_keys[also_key].get(key)
462
+
463
+ # pylint: disable=no-self-use
464
+ def _singleton_also_indexmap(self):
465
+ ''' Return a mapping of secondary key names and their matching key values.
466
+ '''
467
+ return {}
468
+
469
+ def _singleton_also_index(self):
470
+ ''' Return a mapping of secondary key names and their matching key values.
471
+ '''
472
+ registry = self._singleton_get_registry()
473
+ with registry._singleton_lock:
474
+ for also_key, key in self._singleton_also_indexmap().items():
475
+ registry._singleton_also_keys[also_key][key] = self
476
+
477
+ @classmethod
478
+ def _singleton_instances(cls):
479
+ ''' Return a list of the current class instances.
480
+ '''
481
+ try:
482
+ registry = cls._singleton_registry
483
+ except AttributeError:
484
+ return []
485
+ else:
486
+ return list(
487
+ filter(
488
+ lambda obj: obj is not None,
489
+ map(lambda ref: ref(), registry.valuerefs())
490
+ )
491
+ )
492
+
493
+ class Sentinel:
494
+ ''' A simple class for named sentinels whose `str()` is just the name
495
+ and whose `==` uses `is`.
496
+
497
+ Example:
498
+
499
+ >>> from cs.obj import Sentinel
500
+ >>> MISSING = Sentinel("MISSING")
501
+ >>> print(MISSING)
502
+ MISSING
503
+ >>> other = Sentinel("other")
504
+ >>> MISSING == other
505
+ False
506
+ >>> MISSING == MISSING
507
+ True
508
+ '''
509
+
510
+ __slots__ = 'name',
511
+
512
+ def __init__(self, name):
513
+ self.name = name
514
+
515
+ def __str__(self):
516
+ return self.name
517
+
518
+ def __repr__(self):
519
+ return "%s(%r)" % (self.__class__.__name__, self.name)
520
+
521
+ def __eq__(self, other):
522
+ return self is other
523
+
524
+ def public_subclasses(cls, extras=()):
525
+ ''' Return a set of the subclasses of `cls` which have public names.
526
+ '''
527
+ classes = set()
528
+ q = list(cls.__subclasses__())
529
+ q.extend(extras)
530
+ while q:
531
+ subcls = q.pop(0)
532
+ if subcls in classes:
533
+ # seen this one
534
+ continue
535
+ if not subcls.__name__.startswith('_'):
536
+ # public name, include it
537
+ classes.add(subcls)
538
+ # append the further subclasses for consideration
539
+ try:
540
+ subclasses = subcls.__subclasses__()
541
+ except TypeError:
542
+ # type is a subclass of object, but its __subclasses__ is just a function
543
+ pass
544
+ else:
545
+ q.extend(subclasses)
546
+ return classes
547
+
548
+ if __name__ == '__main__':
549
+ import cs.obj_tests
550
+ cs.obj_tests.selftest(sys.argv)
@@ -0,0 +1,333 @@
1
+ Metadata-Version: 2.4
2
+ Name: cs-obj
3
+ Version: 20250306
4
+ Summary: Convenience facilities for objects.
5
+ Keywords: python2,python3
6
+ Author-email: Cameron Simpson <cs@cskk.id.au>
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
15
+ Requires-Dist: cs.deco>=20250306
16
+ Project-URL: MonoRepo Commits, https://bitbucket.org/cameron_simpson/css/commits/branch/main
17
+ Project-URL: Monorepo Git Mirror, https://github.com/cameron-simpson/css
18
+ Project-URL: Monorepo Hg/Mercurial Mirror, https://hg.sr.ht/~cameron-simpson/css
19
+ Project-URL: Source, https://github.com/cameron-simpson/css/blob/main/lib/python/cs/obj.py
20
+
21
+ Convenience facilities for objects.
22
+
23
+ *Latest release 20250306*:
24
+ * Remove cs.py3 dependency, this module has not worked in python2 for quite a while.
25
+ * copy: drop use of positional parameters.
26
+
27
+ ## <a name="as_dict"></a>`as_dict(o, selector=None)`
28
+
29
+ Return a dictionary with keys mapping to the values of the attributes of `o`.
30
+
31
+ Parameters:
32
+ * `o`: the object to map
33
+ * `selector`: the optional selection criterion
34
+
35
+ If `selector` is omitted or `None`, select "public" attributes,
36
+ those not commencing with an underscore.
37
+
38
+ If `selector` is a `str`, select attributes starting with `selector`.
39
+
40
+ Otherwise presume `selector` is callable
41
+ and select attributes `attr` where `selector(attr)` is true.
42
+
43
+ ## <a name="copy"></a>`copy(obj, **kw)`
44
+
45
+ Convenient function to shallow copy an object with simple modifications.
46
+
47
+ Performs a shallow copy of `self` using `copy.copy`.
48
+
49
+ Treat all keyword arguments as `(attribute,value)` 2-tuples and
50
+ replace those attributes with the supplied values.
51
+
52
+ ## <a name="flavour"></a>`flavour(obj)`
53
+
54
+ Return constants indicating the ``flavour'' of an object:
55
+ * `T_MAP`: DictType, DictionaryType, objects with an __keys__ or keys attribute.
56
+ * `T_SEQ`: TupleType, ListType, objects with an __iter__ attribute.
57
+ * `T_SCALAR`: Anything else.
58
+
59
+ ## <a name="O"></a>Class `O(types.SimpleNamespace)`
60
+
61
+ The `O` class is now obsolete, please subclass `types.SimpleNamespace`
62
+ or use a dataclass.
63
+
64
+ ## <a name="O_attritems"></a>`O_attritems(o)`
65
+
66
+ Generator yielding `(attr,value)` for relevant attributes of `o`.
67
+
68
+ ## <a name="O_attrs"></a>`O_attrs(o)`
69
+
70
+ Yield attribute names from `o` which are pertinent to `O_str`.
71
+
72
+ Note: this calls `getattr(o,attr)` to inspect it in order to
73
+ prune callables.
74
+
75
+ ## <a name="O_merge"></a>`O_merge(o, _conflict=None, _overwrite=False, **kw)`
76
+
77
+ Merge key:value pairs from a mapping into an object.
78
+
79
+ Ignore keys that do not start with a letter.
80
+ New attributes or attributes whose values compare equal are
81
+ merged in. Unequal values are passed to:
82
+
83
+ _conflict(o, attr, old_value, new_value)
84
+
85
+ to resolve the conflict. If _conflict is omitted or None
86
+ then the new value overwrites the old if _overwrite is true.
87
+
88
+ ## <a name="O_str"></a>`O_str(o, no_recurse=False, seen=None)`
89
+
90
+ Return a `str` representation of the object `o`.
91
+
92
+ Parameters:
93
+ * `o`: the object to describe.
94
+ * `no_recurse`: if true, do not recurse into the object's structure.
95
+ Default: `False`.
96
+ * `seen`: a set of previously sighted objects
97
+ to prevent recursion loops.
98
+
99
+ ## <a name="obj_as_dict"></a>`obj_as_dict(o, **kw)`
100
+
101
+ OBSOLETE obj_as_dict
102
+
103
+ OBSOLETE convesion of an object to a `dict`. Please us `cs.obj.as_dict`.
104
+
105
+ ## <a name="Proxy"></a>Class `Proxy`
106
+
107
+ An extremely simple proxy object
108
+ that passes all unmatched attribute accesses to the proxied object.
109
+
110
+ Note that setattr and delattr work directly on the proxy, not the proxied object.
111
+
112
+ ## <a name="public_subclasses"></a>`public_subclasses(cls, extras=())`
113
+
114
+ Return a set of the subclasses of `cls` which have public names.
115
+
116
+ ## <a name="Sentinel"></a>Class `Sentinel`
117
+
118
+ A simple class for named sentinels whose `str()` is just the name
119
+ and whose `==` uses `is`.
120
+
121
+ Example:
122
+
123
+ >>> from cs.obj import Sentinel
124
+ >>> MISSING = Sentinel("MISSING")
125
+ >>> print(MISSING)
126
+ MISSING
127
+ >>> other = Sentinel("other")
128
+ >>> MISSING == other
129
+ False
130
+ >>> MISSING == MISSING
131
+ True
132
+
133
+ ## <a name="singleton"></a>`singleton(registry, key, factory, fargs, fkwargs)`
134
+
135
+ Obtain an object for `key` via `registry` (a mapping of `key`=>object).
136
+ Return `(is_new,object)`.
137
+
138
+ If the `key` exists in the registry, return the associated object.
139
+ Otherwise create a new object by calling `factory(*fargs,**fkwargs)`
140
+ and store it as `key` in the `registry`.
141
+
142
+ The `registry` may be any mapping of `key`s to objects
143
+ but will usually be a `weakref.WeakValueDictionary`
144
+ in order that object references expire as normal,
145
+ allowing garbage collection.
146
+
147
+ *Note*: this function *is not* thread safe.
148
+ Multithreaded users should hold a mutex.
149
+
150
+ See the `SingletonMixin` class for a simple mixin to create
151
+ singleton classes,
152
+ which does provide thread safe operations.
153
+
154
+ ## <a name="SingletonMixin"></a>Class `SingletonMixin`
155
+
156
+ A mixin turning a subclass into a singleton factory.
157
+
158
+ *Note*: this mixin overrides `object.__new__`
159
+ and may not play well with other classes which override `__new__`.
160
+
161
+ *Warning*: because of the mechanics of `__new__`,
162
+ the instance's `__init__` method will always be called
163
+ after `__new__`,
164
+ even when a preexisting object is returned.
165
+ Therefore that method should be sensible
166
+ even for an already initialised
167
+ and probably subsequently modified object.
168
+
169
+ My suggested approach is to access some attribute,
170
+ and preemptively return if it already exists.
171
+ Example:
172
+
173
+ def __init__(self, x, y):
174
+ if 'x' in self.__dict__:
175
+ return
176
+ self.x = x
177
+ self.y = y
178
+
179
+ *Note*: we probe `self.__dict__` above to accomodate classes
180
+ with a `__getattr__` method.
181
+
182
+ *Note*: each class registry has a lock,
183
+ which ensures that reuse of an object
184
+ in multiple threads will call the `__init__` method
185
+ in a thread safe serialised fashion.
186
+
187
+ Implementation requirements:
188
+ a subclass should:
189
+ * provide a method `_singleton_key(*args,**kwargs)`
190
+ returning a key for use in the single registry,
191
+ computed from the positional and keyword arguments
192
+ supplied on instance creation
193
+ i.e. those which `__init__` would normally receive.
194
+ This should have the same signature as `__init__`
195
+ but using `cls` instead of `self`.
196
+ * provide a normal `__init__` method
197
+ which can be safely called again
198
+ after some earlier initialisation.
199
+
200
+ This class is thread safe for the registry operations.
201
+
202
+ Example:
203
+
204
+ class Pool(SingletonMixin):
205
+
206
+ @classmethod
207
+ def _singleton_key(cls, foo, bah=3):
208
+ return foo, bah
209
+
210
+ def __init__(self, foo, bah=3):
211
+ if hasattr(self, 'foo'):
212
+ return
213
+ ... normal __init__ stuff here ...
214
+ self.foo = foo
215
+ ...
216
+
217
+ *`SingletonMixin.__hash__(self)`*:
218
+ default hash and equality methods
219
+
220
+ *`SingletonMixin.singleton_also_by(also_key, key)`*:
221
+ Obtain a singleton by a secondary key.
222
+ Return the instance or `None`.
223
+
224
+ Parameters:
225
+ * `also_key`: the name of the secondary key index
226
+ * `key`: the key for the index
227
+
228
+ ## <a name="TrackedClassMixin"></a>Class `TrackedClassMixin`
229
+
230
+ A mixin to track all instances of a particular class.
231
+
232
+ This is aimed at checking the global state of objects of a
233
+ particular type, particularly states like counters. The
234
+ tracking is attached to the class itself.
235
+
236
+ The class to be tracked includes this mixin as a superclass and calls:
237
+
238
+ TrackedClassMixin.__init__(class_to_track)
239
+
240
+ from its __init__ method. Note that `class_to_track` is
241
+ typically the class name itself, not `type(self)` which would
242
+ track the specific subclass. At some relevant point one can call:
243
+
244
+ self.tcm_dump(class_to_track[, file])
245
+
246
+ `class_to_track` needs a `tcm_get_state` method to return the
247
+ salient information, such as this from cs.resources.MultiOpenMixin:
248
+
249
+ def tcm_get_state(self):
250
+ return {'opened': self.opened, 'opens': self._opens}
251
+
252
+ See cs.resources.MultiOpenMixin for example use.
253
+
254
+ *`TrackedClassMixin.tcm_all_state(klass)`*:
255
+ Generator yielding tracking information
256
+ for objects of type `klass`
257
+ in the form `(o,state)`
258
+ where `o` if a tracked object
259
+ and `state` is the object's `get_tcm_state` method result.
260
+
261
+ *`TrackedClassMixin.tcm_dump(klass, f=None)`*:
262
+ Dump the tracking information for `klass` to the file `f`
263
+ (default `sys.stderr`).
264
+
265
+ # Release Log
266
+
267
+
268
+
269
+ *Release 20250306*:
270
+ * Remove cs.py3 dependency, this module has not worked in python2 for quite a while.
271
+ * copy: drop use of positional parameters.
272
+
273
+ *Release 20250103*:
274
+ public_subclasses: new optional extras= parameter for additional classes to scan, now returns a set.
275
+
276
+ *Release 20241009*:
277
+ public_subclasses: catch TypeError from "type.__subclasses__", which is just a function.
278
+
279
+ *Release 20241005*:
280
+ New public_subclasses(cls) returning all subclasses with public names.
281
+
282
+ *Release 20220918*:
283
+ * SingletonMixin: change example to probe self__dict__ instead of hasattr, faster and less fragile.
284
+ * New Sentinel class for named sentinel objects, equal only to their own instance.
285
+
286
+ *Release 20220530*:
287
+ SingletonMixin: add default __hash__ and __eq__ methods to support dict and set membership.
288
+
289
+ *Release 20210717*:
290
+ SingletonMixin: if cls._singleton_key returns None we always make a new instance and do not register it.
291
+
292
+ *Release 20210306*:
293
+ SingletonMixin: make singleton_also_by() a public method.
294
+
295
+ *Release 20210131*:
296
+ SingletonMixin: new _singleton_also_indexmap method to return a mapping of secondary keys to values to secondary lookup, _singleton_also_index() to update these indices, _singleton_also_by to look up a secondary index.
297
+
298
+ *Release 20210122*:
299
+ SingletonMixin: new _singleton_instances() method returning a list of the current instances.
300
+
301
+ *Release 20201227*:
302
+ SingletonMixin: correctly invoke __new__, a surprisingly fiddly task to get right.
303
+
304
+ *Release 20201021*:
305
+ * @OBSOLETE(obj_as_dict), recommend "as_dict()".
306
+ * [BREAKING] change as_dict() to accept a single optional selector instead of various mutually exclusive keywords.
307
+
308
+ *Release 20200716*:
309
+ SingletonMixin: no longer require special _singleton_init method, reuse default __init__ implicitly through __new__ mechanics.
310
+
311
+ *Release 20200517*:
312
+ Documentation improvements.
313
+
314
+ *Release 20200318*:
315
+ * Replace obsolete O class with a new subclass of SimpleNamespace which issues a warning.
316
+ * New singleton() generic factory function and SingletonMixin mixin class for making singleton classes.
317
+
318
+ *Release 20190103*:
319
+ * New mixin class TrackedClassMixin to track all instances of a particular class.
320
+ * Documentation updates.
321
+
322
+ *Release 20170904*:
323
+ Minor cleanups.
324
+
325
+ *Release 20160828*:
326
+ * Use "install_requires" instead of "requires" in DISTINFO.
327
+ * Minor tweaks.
328
+
329
+ *Release 20150118*:
330
+ move long_description into cs/README-obj.rst
331
+
332
+ *Release 20150110*:
333
+ cleaned out some old junk, readied metadata for PyPI
@@ -0,0 +1,4 @@
1
+ cs/obj.py,sha256=TrkEWW-yM5XxbCOELJwmXDQIfveszC0E50IOv8CYLdA,16579
2
+ cs_obj-20250306.dist-info/WHEEL,sha256=BXjIu84EnBiZ4HkNUBN93Hamt5EPQMQ6VkF7-VZ_Pu0,100
3
+ cs_obj-20250306.dist-info/METADATA,sha256=P3Ib6MGxxJKlA7FSNXPl9X9tEh2pwT8pXPsFzzJFkwk,10926
4
+ cs_obj-20250306.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.11.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any