Skip to content

Commit 0be78a1

Browse files
committed
handle simple generics like list[str].
Fix inheritance bugs.
1 parent 9ac07b8 commit 0be78a1

File tree

5 files changed

+289
-195
lines changed

5 files changed

+289
-195
lines changed

README.rst

Lines changed: 102 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,18 @@ The ``register()`` attribute returns the undecorated function which enables deco
4242
... print("Half of your number:", end=" ")
4343
... print(arg / 2)
4444

45-
The ``register()`` attribute only works inside a class statement, relying on ``SingleDispatch.__init_subclass__``
46-
to create the actual dispatch table. This also means that (unlike functools.singledispatch) two methods
45+
The ``register()`` method relys on ``SingleDispatch.__init_subclass__``
46+
to create the actual dispatch table rather than adding the function directly.
47+
This also means that (unlike functools.singledispatchmethod) two methods
4748
with the same name cannot be registered as only the last one will be in the class dictionary.
4849

49-
Functions not defined in the class can be registered using the ``add_overload`` attribute.
50+
Functions not defined in the class can be registered with a generic method using the ``add_overload`` method.
5051

5152
>>> def nothing(obj, arg, verbose=False):
5253
... print('Nothing.')
5354
>>> MyClass.fun.add_overload(type(None), nothing)
5455

56+
Using ``add_overload`` will affect all instances of ``MyClass`` as if it were part of the class declaration.
5557
When called, the generic function dispatches on the type of the first argument
5658

5759
>>> a = MyClass()
@@ -72,9 +74,10 @@ Nothing.
7274
>>> a.fun(1.23)
7375
0.615
7476

75-
Where there is no registered implementation for a specific type, its method resolution order is used to find a more generic implementation. The original function decorated with ``@singledispatch`` is registered for the base ``object`` type, which means it is used if no better implementation is found.
77+
Where there is no registered implementation for a specific type, its method resolution order is used to find a more generic implementation.
78+
The original function decorated with ``@singledispatch`` is registered for the base ``object`` type, which means it is used if no better implementation is found.
7679

77-
To check which implementation will the generic function choose for a given type, use the ``dispatch()`` attribute
80+
To check which implementation will the generic function choose for a given type, use the ``dispatch()`` method
7881

7982
>>> a.fun.dispatch(float)
8083
<function MyClass.fun_num at 0x1035a2840>
@@ -84,55 +87,90 @@ To check which implementation will the generic function choose for a given type,
8487
To access all registered implementations, use the read-only ``registry`` attribute
8588

8689
>>> a.fun.registry.keys()
87-
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
88-
<class 'decimal.Decimal'>, <class 'list'>,
89-
<class 'float'>])
90+
dict_keys([<class 'object'>, <class 'int'>, <class 'list'>, <class 'decimal.Decimal'>, <class 'float'>, <class 'NoneType'>])
9091
>>> a.fun.registry[float]
9192
<function MyClass.fun_num at 0x1035a2840>
9293
>>> a.fun.registry[object]
9394
<function MyClass.fun at 0x103fe0000>
9495

96+
Extending the dispatch table.
97+
-----------------------------
98+
9599
Subclasses can extend the type registry of the function on the base class with their own overrides.
96100
The ``SingleDispatch`` mixin class ensures that each subclass has it's own independant copy of the dispatch registry
97101

98102
>>> class SubClass(MyClass):
99103
... @MyClass.fun.register(str)
100104
... def fun_str(self, arg, verbose=False):
101-
... print('str')
105+
... print('subclass')
102106
...
103107
>>> s = SubClass()
104108
>>> s.fun('hello')
105-
str
109+
subclass
106110
>>> b = MyClass()
107111
>>> b.fun('hello')
108112
hello
109113

110-
Method overrides do not need to provide the ``register`` decorator again to be used in the dispatch of ``fun``
114+
Overriding the dispatch table
115+
-----------------------------
116+
There are two ways to override the dispatch function for a given type.
117+
One way is to override a base-class method that is in the base class dispatch table.
118+
Method overrides do not need to provide the ``register`` decorator again to be used in the dispatch of ``fun``, you can
119+
simply override the specific dispatch function you want to modify.
111120

112-
>>> class SubClass2(MyClass):
121+
>>> class Mixin1(MyClass):
113122
... def fun_int(self, arg, verbose=False):
114123
... print('subclass int')
115124
...
116-
>>> s = SubClass2()
125+
>>> s = Mixin1()
117126
>>> s.fun(1)
118127
subclass int
119128

120-
However, providing the register decorator with the same type will also work.
121-
Decorating a method override with a different type (not a good idea) will register the different type and leave the base-class handler in place for the orginal type.
129+
The other way is to register a method with the same type using the `register` method.
130+
131+
>>> class SubClass3(MyClass):
132+
... @MyClass.fun.register(int)
133+
... def fun_int_override(self, arg, verbose=False):
134+
... print('subclass3 int')
135+
...
136+
>>> s = SubClass3()
137+
>>> s.fun(1)
138+
subclass3 int
139+
140+
Note that the decorator takes precedence over the method name, so if you do something like this:
141+
142+
>>> class SubClass4(MyClass):
143+
... @MyClass.fun.register(str)
144+
... def fun_int(self, arg, verbose=False):
145+
... print('silly mistake')
146+
147+
then SubClass4.fun_int is used for string arguments.
148+
149+
>>> s = SubClass4()
150+
>>> s.fun(1)
151+
1
152+
>>> s.fun('a string')
153+
silly mistake
154+
155+
Instance overrides
156+
------------------
122157

123158
Method overrides can be specified on individual instances if necessary
124159

125160
>>> def fun_str(obj, arg, verbose=False):
126-
... print('str')
161+
... print('instance str')
127162
>>> b = MyClass()
128163
>>> b.fun.register(str, fun_str)
129164
<function fun_str at 0x000002376A3D32F0>
130165
>>> b.fun('hello')
131-
str
166+
instance str
132167
>>> b2 = MyClass()
133168
>>> b2.fun('hello')
134169
hello
135170

171+
Integration with type hints
172+
---------------------------
173+
136174
For functions annotated with types, the decorator will infer the type of the first argument automatically as shown below
137175

138176
>>> class MyClassAnno(SingleDispatch):
@@ -148,65 +186,77 @@ For functions annotated with types, the decorator will infer the type of the fir
148186
... @MyClassAnno.fun.register
149187
... def fun_float(self, arg: float):
150188
... print('float')
189+
...
190+
... @MyClassAnno.fun.register
191+
... def fun_list(self, arg: typing.List[str]):
192+
... print('list')
193+
194+
Note that methoddispatch ignores type specialization in annotations as only the class is used for dispatching.
195+
This means that ``fun_list`` will be called for all list instances regardless of what is in the list.
151196

152-
Finally, accessing the method ``fun`` via a class will use the dispatch registry for that class
197+
Accessing the method ``fun`` via a class will use the dispatch registry for that class
153198

154-
>>> SubClass2.fun(s, 1)
199+
>>> Mixin1.fun(s, 1)
155200
subclass int
156201
>>> MyClass.fun(s, 1)
157202
1
158203

159-
You can use Mixin or multiple inheritance to add a generic method to a class, like this:
204+
``super()`` also works as expected using the dispatch table of the super class.
205+
206+
>>> super(Mixin1, s).fun(1)
207+
1
208+
209+
The usual method resolution order applies to mixin or multiple inheritance. For example:
160210

161211
>>> class BaseClassForMixin(SingleDispatch):
162-
...
163212
... def __init__(self):
164-
... self.mixin_base = 0
213+
... self.dispatched_by = ''
165214
...
166215
... @singledispatch
167216
... def foo(self, bar):
168-
... self.mixin_base += 1
217+
... print('BaseClass')
169218
... return 'default'
170219
...
171-
>>> class SubClass2Mixin(BaseClassForMixin):
172-
... def __init__(self):
173-
... self.mixin_2 = 1
174-
... super().__init__()
220+
... @foo.register(float)
221+
... def foo_float(self, bar):
222+
... print('BaseClass')
223+
... return 'float'
224+
...
225+
>>> class Mixin1(BaseClassForMixin):
175226
...
176227
... @BaseClassForMixin.foo.register(int)
177228
... def foo_int(self, bar):
178-
... self.mixin_2 += 1
229+
... print('Mixin1')
179230
... return 'int'
180231
...
181-
>>> class SubClass3Mixin(BaseClassForMixin):
182-
... def __init__(self):
183-
... self.mixin_3 = 1
184-
... super().__init__()
185-
...
186232
... @BaseClassForMixin.foo.register(str)
187233
... def foo_str(self, bar):
188-
... self.mixin_3 += 2
189-
... return 'str'
190-
...
191-
>>> class SubClass4Mixin(BaseClassForMixin):
192-
... @BaseClassForMixin.foo.register(set)
193-
... def foo_set(self, bar):
194-
... return 'set'
234+
... print('Mixin1')
235+
... return 'str2'
195236
...
196-
>>> class SubClassWithMixin(SubClass4Mixin, SubClass3Mixin, SubClass2Mixin, BaseClassForMixin):
197-
... def __init__(self):
198-
... self.master = 10
199-
... super().__init__()
237+
>>> class Mixin2(BaseClassForMixin):
238+
... @BaseClassForMixin.foo.register(str)
239+
... def foo_str2(self, bar):
240+
... print('Mixin2')
241+
... return 'str3'
200242
...
201-
... @BaseClassForMixin.foo.register(float)
243+
>>> class SubClassWithMixins(Mixin2, Mixin1):
202244
... def foo_float(self, bar):
203-
... self.master += 1
245+
... print('SubClassWithMixins')
204246
... return 'float'
205247

206-
Then you can use it like this:
248+
Note that even though ``Mixin2`` has method ``foo_str2`` it will still override ``Mixin1.foo_str`` in
249+
the dispatch of ``foo()`` because they are both handlers for the ``str`` type and Mixin2 comes before
250+
Mixin1 in the bases list.
251+
207252

208-
>>> b = SubClassWithMixin()
209-
>>> b.foo('text')
210-
'str'
211-
>>> b.mixin_3
212-
2
253+
>>> s = SubClassWithMixins()
254+
>>> s.foo('text')
255+
Mixin2
256+
'str3'
257+
>>> s.foo(1)
258+
Mixin1
259+
'int'
260+
>>> s.foo(3.2)
261+
SubClassWithMixins
262+
'float'

0 commit comments

Comments
 (0)