Last active
October 2, 2020 12:33
-
-
Save polyvertex/5c306faa7c691697e57aef6b9835aa42 to your computer and use it in GitHub Desktop.
__slots__ based class pretty much like a mutable collections.namedtuple
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import itertools | |
import threading | |
_datastruct_cache_lock = threading.Lock() | |
_datastruct_cache = {} | |
class DataStruct(object): | |
""" | |
A ``__slots__`` based class that works pretty much like a mutable | |
`collections.namedtuple` | |
Values can optionally be passed at construction time to ``__init__`` using | |
either *args* or *kwargs*. | |
This class subclasses `object` so to deny the creation of a ``__dict__``. | |
Usage example:: | |
class ConfigData(DataStruct): | |
__slots__ = ("url", "user", "proxy") | |
cfg = ConfigData() # every member defaults to None | |
# members values can be passed via __init__ | |
cfg = ConfigData("http://example.com", proxy="socks://127.0.0.1") | |
print(cfg.url) # prints "http://example.com" | |
print(cfg.user) # prints "None" | |
print(cfg.proxy) # prints "socks://127.0.0.1" | |
# it is also possible to derive from your ConfigData class | |
class ExtraConfigData(ConfigData): | |
__slots__ = ("timeout", "password", "expected_http_status") | |
cfg = ExtraConfigData( | |
"http://example.com", # url | |
"foo", # user | |
"socks://127.0.0.1", # proxy | |
60, # timeout | |
expected_http_status=200) | |
print(cfg.password) # prints "None" | |
""" | |
__slots__ = () | |
def __init__(self, *args, **kwargs): | |
global _datastruct_cache | |
with _datastruct_cache_lock: | |
try: | |
klasses, all_slots = _datastruct_cache[self.__class__] | |
except KeyError: | |
klasses = [] | |
slots_defs = [] | |
for klass in self.__class__.mro(): | |
if klass is DataStruct: | |
break | |
if not klass.__slots__: | |
# raise TypeError( | |
# f"{klass.__name__}.__slots__ not defined or empty") | |
continue | |
klasses.append(klass) | |
slots_defs.append(klass.__slots__) | |
# note: reversed() because we want base-to-derived order | |
klasses = tuple(reversed(klasses)) | |
all_slots = tuple([*itertools.chain(*reversed(slots_defs))]) | |
del slots_defs | |
if len(all_slots) != len(set(all_slots)): | |
clsname = self.__class__.__name__ | |
raise ValueError( | |
f"redundant __slots__ member(s) found in {clsname} " | |
f"class or any of its bases") | |
_datastruct_cache[self.__class__] = (klasses, all_slots) | |
if len(args) > len(all_slots): | |
raise ValueError("too many positional args passed") | |
# apply args | |
for name, value in zip(all_slots, args): | |
setattr(self, name, value) | |
# apply kwargs | |
for name, value in kwargs.items(): | |
if __debug__: | |
if args and name in all_slots[0:len(args)]: | |
clsname = self.__class__.__name__ | |
raise ValueError( | |
f"{clsname}.{name} already init by a positional arg") | |
try: | |
setattr(self, name, value) | |
except AttributeError: | |
clsname = self.__class__.__name__ | |
raise ValueError( | |
f"{name} member not found in {clsname}.__slots__, or " | |
f"any of its sub-classes if any") | |
def __getattr__(self, name): | |
with _datastruct_cache_lock: | |
try: | |
klasses, all_slots = _datastruct_cache[self.__class__] | |
except KeyError: | |
clsname = self.__class__.__name__ | |
raise RuntimeError( | |
f"no cache for DataStruct-derived class {clsname}") | |
if name in all_slots: | |
return None # default value | |
else: | |
raise AttributeError(name) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment