3825b9efc5be94c3097daf8142465f48053f9e78
[mdis.git] / src / mdis / dispatcher.py
1 __all__ = [
2 "Dispatcher",
3 "DispatcherMeta",
4 "Hook",
5 ]
6
7 import collections
8 import inspect
9 import types
10
11
12 class Hook(object):
13 def __init__(self, *typeids):
14 for typeid in typeids:
15 if not callable(typeid):
16 raise ValueError(typeid)
17 self.__typeids = typeids
18 return super().__init__()
19
20 def __iter__(self):
21 yield from self.__typeids
22
23 def __repr__(self):
24 names = []
25 for typeid in self.__typeids:
26 name = typeid.__qualname__
27 module = typeid.__module__
28 if module not in ("builtins",):
29 name = f"{module}.{name}"
30 names.append(name)
31 return f"<{', '.join(names)}>"
32
33 def __call__(self, call):
34 class ConcreteHook(Hook):
35 def __call__(self, dispatcher, instance):
36 return call(self=dispatcher, instance=instance)
37
38 return ConcreteHook(*tuple(self))
39
40
41 class DispatcherMeta(type):
42 __hooks__ = {}
43
44 def __new__(metacls, name, bases, ns):
45 hooks = {}
46 ishook = lambda member: isinstance(member, Hook)
47
48 for basecls in reversed(bases):
49 members = inspect.getmembers(basecls, predicate=ishook)
50 for (_, hook) in members:
51 hooks.update(dict.fromkeys(hook, hook))
52
53 conflicts = collections.defaultdict(list)
54 for (key, value) in tuple(ns.items()):
55 if not ishook(value):
56 continue
57 hook = value
58 for typeid in hook:
59 hooks[typeid] = hook
60 conflicts[typeid].append(key)
61 ns[key] = hook
62
63 for (typeid, keys) in conflicts.items():
64 if len(keys) > 1:
65 raise ValueError(f"dispatch conflict: {keys!r}")
66
67 ns["__hooks__"] = types.MappingProxyType(hooks)
68
69 return super().__new__(metacls, name, bases, ns)
70
71 def dispatch(cls, typeid=object):
72 hook = cls.__hooks__.get(typeid)
73 if hook is not None:
74 return hook
75 for (checker, hook) in cls.__hooks__.items():
76 if not isinstance(checker, type) and checker(typeid):
77 return hook
78 return None
79
80
81 class Dispatcher(metaclass=DispatcherMeta):
82 def __call__(self, instance):
83 for typeid in instance.__class__.__mro__:
84 hook = self.__class__.dispatch(typeid=typeid)
85 if hook is not None:
86 break
87 if hook is None:
88 hook = self.__class__.dispatch()
89 return hook(dispatcher=self, instance=instance)
90
91 @Hook(object)
92 def dispatch_object(self, instance):
93 raise NotImplementedError()