Skip to content

Instantly share code, notes, and snippets.

@kg583
Last active May 20, 2023 18:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kg583/8d224979d8da7edd60768bd7453a7b64 to your computer and use it in GitHub Desktop.
Save kg583/8d224979d8da7edd60768bd7453a7b64 to your computer and use it in GitHub Desktop.
A simple system for adaptively loading different data types into a class
class Dock:
"""
Base class to inherit to implement the loader system
"""
loaders = {}
def load(self, data):
"""
Load some data based on the data's type
Checks for a matching method in this class's loaders attribute,
which can be set explicitly or by decorating methods with @Loader
"""
# The format for loaders is {(type1, type2, ...): loader}
for loader_types, loader in self.loaders.items():
if any(isinstance(data, loader_type) for loader_type in loader_types):
# Once a valid loader is found, no others are attempted
loader(self, data)
return
raise TypeError(f"could not find valid loader for type {type(data)}")
class Loader:
"""
Decorator class identifying methods as loaders
Specify the type(s) accepted by the loader via @Loader[type1, type2, ...]
Requires the owner class to inherit from Dock
Must be at the top of any decorator chain
"""
types = ()
def __init__(self, func):
# We need to hold onto the function we're decorating until the owner is created
# You *could* do this with a regular function, but there's hardly any point
self._func = func
def __call__(self, *args, **kwargs):
# This is only here to appease type checkers
pass
def __class_getitem__(cls, item: tuple[type, ...] | type) -> type:
# Implements the syntax @Loader[type1, type2, ...]
# IMO it looks better than the obvious @Loader(type1, type2, ...)
try:
# Loader[type1, type2, ...]
return type("Loader", (Loader,), {"types": tuple(item)})
except TypeError:
# Loader[type]
return type("Loader", (Loader,), {"types": (item,)})
def __set_name__(self, owner, name: str):
# Tell the owner class that this a loader at class creation
# Note that we build a new copy of the dictionary to prevent parents
# from having loaders that belong to their children
owner.loaders = owner.loaders | {self.types: self._func}
# Keeping the method as a Loader instance would require more magic
# So let's forget that we're a Loader
setattr(owner, name, self._func)
class ByteContainer(Dock):
def __init__(self, byte: bytes):
self.byte = byte
@Loader[int]
def load_int(integer: int):
self.byte = int.to_bytes(integer % 256, 1, 'big')
@Loader[str]
def load_string(string: str):
self.byte = string[0].encode()
@Loader[bytes, bytearray]
def load_bytes_like(bytes_like: bytes):
self.byte = bytes(bytes_like[:1])
container = ByteContainer(b'\x69')
container.load(42)
container.load("foo")
container.load(b'ar')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment