Here's what I'd like to do in the form of working code, since it's difficult for me to explain otherwise:
from typing import Callable, Generic, TypeVar
from typing_extensions import Self
# The type checker (Pylance / mypy) whines about an incorrect
# type for the first argument to the method (what should be
# `self`). This TypeVar helps it not do that.
t_CallbackSelf = TypeVar("t_CallbackSelf", bound="HasCallbacks")
class Callback(Generic[t_CallbackSelf]):
""" Decorates another class's methods, and once that
class is instantiated, that class collects all instances
of this class that have been assigned to that class. """
# An identifier by which to reference the callback function
# without using its assigned name on the class.
ident: str
# The function identified by `ident`.
callback: Callable[[t_CallbackSelf], None]
def __init__(self, ident: str) -> None:
self.ident = ident
def __call__(self, func: Callable[[t_CallbackSelf], None]):
# This is the function that actually decorates a
# classmethod.
self.callback = func
# Return self so that we replace the value defined on
# the class with this instance.
return self
class HasCallbacks:
""" Collects all its instances of Callback in a dict upon
initialization. """
callbacks: dict[str, Callback[Self]]
def __init__(self):
self.callbacks = {}
for key in dir(self):
val = getattr(self, key, None)
if isinstance(val, Callback):
self.callbacks[val.ident] = val
def do(self, ident: str):
""" Calls one of the callbacks, referenced by its
identifier `ident`. If none is registered,
ignore. """
if ident in self.callbacks:
self.callbacks[ident].callback(self)
# Decorates the `ping` method to be a `Callback` that has the
# identifier "Ping".
@Callback("Ping")
def ping(self):
print("Pong")
callmeback = HasCallbacks()
callmeback.do("Ping") # -> Pong
class AlsoHasCallbacks(HasCallbacks):
def __init__(self):
super().__init__()
self.val = 0
@Callback("Inc")
def inc(self):
self.val += 1
print(f"Val is now at {self.val}")
callmebacktoo = AlsoHasCallbacks()
callmebacktoo.do("Ping") # -> Pong
callmebacktoo.do("Inc") # -> Val is now at 1
callmebacktoo.do("Inc") # -> Val is now at 2
What bugs me about this implementation:
Callback
andHasCallbacks
are tied directly to each other, which to me means they should simply be combined into the same class.- It feels wrong to be replacing the methods themselves with completely different objects (
Callback
s) on the class itself. - All of the
Callback
s are collected every time an instance is made, when it should really only need to happen once and be collected into a class variable.
I feel like there are better ways to do this. I've done a lot of studying on decorators, __init_subclass__
and metaclasses, and I've gone through a couple of iterations on ways to do what I'm doing in the code above (and the above attempt is still the most clean version).
I'd like to know if this is the only way to do what I'd like to do, or I'd like to know the name of the thing I'm trying to do (if there's a single name for it) so that I can look that up and figure out how to do it.