-
Notifications
You must be signed in to change notification settings - Fork 0
/
simplesuper.py
executable file
·327 lines (262 loc) · 11.5 KB
/
simplesuper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#!/usr/bin/env python
# encoding: utf-8
__all__ = ['SuperProxy']
import inspect
import re
import sys
import traceback
IS_PY2 = 2 == sys.version_info.major
try:
reversed
except NameError:
def reversed(an_iterable):
copied_iterable = list(an_iterable)
copied_iterable.reverse()
return copied_iterable
class SmartMethodCall(object):
def __init__(self, method, *vargs, **kwargs):
self._method = method
self._vargs, self._kwargs = self._arguments_for_call(vargs, kwargs)
# --- find correct arguments -----------------------------------------------
def call_with_correct_parameters(self):
return self._method(*self._vargs, **self._kwargs)
def _arguments_for_call(self, vargs, kwargs):
# always prefer explicit arguments
if self._did_specify_arguments_explicitely(vargs, kwargs):
return (vargs, kwargs)
return self._arguments_for_super_method()
def _did_specify_arguments_explicitely(self, vargs, kwargs):
if vargs or kwargs:
return True
# yes, this is extremly ugly - however in Python 2.x there is no other
# way to differentiate between self.super(*[], **{}) and self.super()
caller_source_lines = inspect.getframeinfo(sys._getframe(4))[3]
caller_source_code = caller_source_lines[0]
match = re.search('self.super\((.*?)\)', caller_source_code)
assert match is not None, repr(caller_source_code)
if re.search('\S', match.group(1)):
return True
return False
def _get_argspec(self):
if IS_PY2:
(args, varargs, varkw, defaults) = inspect.getargspec(self._method)
else:
(args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) = inspect.getfullargspec(self._method)
return (args, varargs, varkw, defaults)
def _arguments_for_super_method(self):
if not inspect.isroutine(self._method):
# special treatment of object's __init__
return ([], {})
(args, varargs, varkw, defaults) = self._get_argspec()
if len(args) == 1 and varargs is None: # just self
return ([], {})
return self._find_arguments_for_called_method()
def _find_arguments_for_called_method(self):
caller_frame = sys._getframe(3 + 2)
caller_arg_names, caller_varg_name, caller_kwarg_name, caller_arg_values = inspect.getargvalues(caller_frame)
(callee_arg_names, callee_varargs, callee_kwarg_name, callee_defaults) = self._get_argspec()
vargs = []
kwargs = {}
if len(caller_arg_names) > len(callee_arg_names):
for name in caller_arg_names[len(callee_arg_names):]:
kwargs[name] = caller_arg_values[name]
# [1:..] because we don't need self
for name in caller_arg_names[1:len(callee_arg_names)]:
vargs.append(caller_arg_values[name])
if caller_varg_name:
vargs.extend(caller_arg_values[caller_varg_name])
if caller_kwarg_name:
kwargs.update(caller_arg_values[caller_kwarg_name])
return vargs, kwargs
class SuperFinder(object):
# --- find correct super method --------------------------------------------
def super_method(self, method_name=None):
caller_self = self._find_caller_self()
code = sys._getframe(2).f_code
if method_name is None:
method_name = code.co_name
super_class = self._find_class(caller_self, code)
return getattr(super(super_class, caller_self), method_name)
def _find_caller_self(self):
arg_names, varg_name, kwarg_name, arg_values = inspect.getargvalues(sys._getframe(3))
return arg_values[arg_names[0]]
def _points_to_this_function(self, code, func):
if IS_PY2:
return self._points_to_this_function_py2x(code, func)
return self._points_to_this_function_py3k(code, func)
def _points_to_this_function_py2x(self, code, func):
# Objects special methods like __init__ are c-stuff that is only
# available to python as <slot_wrapper> which don't have im_func
# members, so I can't get the code object to find the actual
# implementation.
# However this is not neccessary, as I only want to find methods defined
# in python (the caller) so I can just skip all <slot_wrappers>
if hasattr(func, 'im_func'):
other_code = func.im_func.func_code
if id(code) == id(other_code):
return True
return False
def _points_to_this_function_py3k(self, code, func):
members = dict(inspect.getmembers(func))
if '__code__' in members:
return id(code) == id(members['__code__'])
else:
return False
# We cannot just use self.__class__, because we need the class where the
# method is defined, not where it is called on.
def _find_class(self, instance, code):
method_name = code.co_name
for klass in reversed(inspect.getmro(instance.__class__)):
if hasattr(klass, method_name):
func = getattr(klass, method_name)
if self._points_to_this_function(code, func):
return klass
assert False, 'Class not found'
class SuperProxy(object):
"This has as few methods as possible, to serve as an ideal proxy."
def __call__(self, *vargs, **kwargs):
method = SuperFinder().super_method()
call = SmartMethodCall(method, *vargs, **kwargs)
return call.call_with_correct_parameters()
def __getattr__(self, method_name):
return SuperFinder().super_method(method_name=method_name)
# ------------------------------------------------------------------------------
# test cases
import unittest
class Super(object):
super = SuperProxy()
def __init__(self):
self.did_call_super = False
def method(self, *vargs, **kwargs):
self.did_call_super = True
return self
def verify(self):
assert self.did_call_super
class SuperTests(unittest.TestCase):
def test_no_arguments(self):
class Upper(Super):
def method(self):
return self.super()
class Lower(Upper):
def method(self):
return self.super()
Lower().method().verify()
def test_positional_argument(self):
class Upper(Super):
def method(self, arg, *vargs):
assert 'fnord' == arg
assert (23, 42) == vargs
return self.super()
class Lower(Upper):
def method(self, arg, *vargs):
self.super(arg, *vargs)
return self.super()
Lower().method('fnord', 23, 42).verify()
def test_test_keyword_argument(self):
class Upper(Super):
def method(self, arg1, arg2, **kwargs):
assert 'fnord' == arg1
assert 23 == arg2
assert {'foo': 'bar'}
return self.super()
class Lower(Upper):
def method(self, arg1, arg2, **kwargs):
self.super(arg1=arg1, arg2=arg2, **kwargs)
return self.super()
Lower().method(arg1='fnord', arg2=23, foo='bar').verify()
def test_positional_variable_and_keyword_arguments(self):
class Upper(Super):
def method(self, arg, *vargs, **kwargs):
assert 'fnord' == arg
assert (23, 42) == vargs
assert {'foo': 'bar'} == kwargs
return self.super()
class Lower(Upper):
def method(self, arg, *vargs, **kwargs):
self.super(arg, *vargs, **kwargs)
return self.super()
Lower().method('fnord', 23, 42, foo='bar').verify()
def test_default_arguments(self):
class Upper(Super):
def method(self, arg):
assert 'fnord' == arg
return self.super()
class Lower(Upper):
def method(self, arg='fnord'):
self.super(arg)
return self.super()
Lower().method().verify()
def test_can_change_arguments_to_super(self):
class Upper(Super):
def method(self, arg):
assert 'fnord' == arg
return self.super()
class Lower(Upper):
def method(self, arg):
return self.super('fnord')
Lower().method('foobar').verify()
def test_super_has_fewer_arguments(self):
class Upper(Super):
def method(self, arg):
assert 23 == arg
return self.super()
class Lower(Upper):
def method(self, arg1, arg2):
return self.super(arg1)
Lower().method(23, 42).verify()
def test_can_call_arbitrary_method_on_super(self):
class Upper(Super):
def foo(self):
return self.super.method()
class Lower(Upper):
def bar(self):
return self.super.foo()
Lower().bar().verify()
def test_can_use_super_in_init(self):
# Objects special method like __init__ are special and can't be accessed like
# normal methods. This test verifies that these methods can still be called.
class Upper(object):
super = SuperProxy()
def __init__(self):
self.super()
self.did_call_super = True
class Lower(Upper):
def __init__(self):
return self.super()
self.assertEqual(True, Lower().did_call_super)
def test_do_not_pass_arguments_by_default_if_lower_doesnt_have_any(self):
# In order to have a nice API using self.super(), we need to be smart
# so we can can detect the case where no arguments should be passed
# as opposed to the case where all original arguments should be passed.
class Upper(Super):
def foo(self):
return self.super.method()
class Lower(Upper):
def foo(self, default=None, *args, **kwargs):
return self.super()
Lower().foo().verify()
def test_use_correct_default_arguments_for_super_method(self):
class Upper(Super):
def foo(self, important_key='fnord', *args, **kwargs):
assert important_key == 'fnord', repr(important_key)
return self.super.method()
class Lower(Upper):
def foo(self, some_paramter=None, *args, **kwargs):
# Actually self.super.foo(*args, **kwargs) would work but it's
# too easy to use the statement below so we have to support it.
return self.super(*args, **kwargs)
Lower().foo().verify()
def test_add_arguments_to_kwargs_if_upper_has_less_named_arguments_than_lower(self):
class Upper(Super):
def foo(self, some_parameter, **kwargs):
assert 'another_parameter' in kwargs
assert kwargs['another_parameter'] == 'fnord'
return self.super.method()
class Lower(Upper):
def foo(self, some_parameter, another_parameter='fnord', **kwargs):
return self.super()
Lower().foo(None).verify()
def test_call_wrapped_super_method(self):
pass # TODO, FIXME
if __name__ == '__main__':
unittest.main()