Skip to content

Commit b4c6998

Browse files
Add Version 5 call tracker example using custom FunctionWrapper subclasses
Demonstrates the decorator(proxy=...) pattern where custom FunctionWrapper and BoundFunctionWrapper subclasses carry state and accessor methods directly, without needing bind_state_to_wrapper.
1 parent 7e49f65 commit b4c6998

1 file changed

Lines changed: 126 additions & 4 deletions

File tree

examples/call_tracker.py

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
Version 2: Automatic state attachment using wrapt.bind_state_to_wrapper
66
Version 3: With optional decorator arguments via static method
77
Version 4: Singleton tracker with per-function stats
8+
Version 5: Custom FunctionWrapper/BoundFunctionWrapper with decorator(proxy=...)
89
"""
910

1011
import wrapt
11-
from wrapt import bind_state_to_wrapper
1212

1313

1414
# ============================================================
@@ -44,7 +44,7 @@ class CallTracker2:
4444
def __init__(self):
4545
self.call_count = 0
4646

47-
@bind_state_to_wrapper(name="tracker")
47+
@wrapt.bind_state_to_wrapper(name="tracker")
4848
@wrapt.function_wrapper
4949
def __call__(self, wrapped, instance, args, kwargs):
5050
try:
@@ -62,7 +62,7 @@ class CallTracker3:
6262
def __init__(self, *, call_count=0):
6363
self.call_count = call_count
6464

65-
@bind_state_to_wrapper(name="tracker")
65+
@wrapt.bind_state_to_wrapper(name="tracker")
6666
@wrapt.function_wrapper
6767
def __call__(self, wrapped, instance, args, kwargs):
6868
try:
@@ -89,7 +89,7 @@ class CallTracker4:
8989
def __init__(self):
9090
self.stats = {}
9191

92-
@bind_state_to_wrapper(name="tracker")
92+
@wrapt.bind_state_to_wrapper(name="tracker")
9393
@wrapt.function_wrapper
9494
def __call__(self, wrapped, instance, args, kwargs):
9595
name = f"{wrapped.__module__}:{wrapped.__qualname__}"
@@ -112,6 +112,59 @@ def track(func=None, /):
112112
return tracker(func)
113113

114114

115+
# ============================================================
116+
# Version 5: Custom FunctionWrapper/BoundFunctionWrapper
117+
# ============================================================
118+
#
119+
# Instead of attaching state externally, this approach uses custom
120+
# subclasses of FunctionWrapper and BoundFunctionWrapper passed via
121+
# the proxy argument to @decorator. The call count is tracked
122+
# directly on the FunctionWrapper, and accessor methods like
123+
# call_count() and reset() are available on both the wrapper and
124+
# bound wrapper without needing bind_state_to_wrapper.
125+
126+
127+
class _TrackerBoundWrapper(wrapt.BoundFunctionWrapper):
128+
129+
def __call__(self, *args, **kwargs):
130+
try:
131+
return super().__call__(*args, **kwargs)
132+
finally:
133+
self._self_parent._self_call_count += 1
134+
135+
def call_count(self):
136+
return self._self_parent._self_call_count
137+
138+
def reset(self):
139+
self._self_parent._self_call_count = 0
140+
141+
142+
class _TrackerWrapper(wrapt.FunctionWrapper):
143+
144+
__bound_function_wrapper__ = _TrackerBoundWrapper
145+
146+
def __init__(self, wrapped, wrapper, **kwargs):
147+
super().__init__(wrapped, wrapper, **kwargs)
148+
self._self_call_count = 0
149+
150+
def __call__(self, *args, **kwargs):
151+
try:
152+
return super().__call__(*args, **kwargs)
153+
finally:
154+
self._self_call_count += 1
155+
156+
def call_count(self):
157+
return self._self_call_count
158+
159+
def reset(self):
160+
self._self_call_count = 0
161+
162+
163+
@wrapt.decorator(proxy=_TrackerWrapper)
164+
def track_calls_v5(wrapped, instance, args, kwargs):
165+
return wrapped(*args, **kwargs)
166+
167+
115168
# ============================================================
116169
# Test all versions
117170
# ============================================================
@@ -208,6 +261,27 @@ def static_method(x, y):
208261
return x + y
209262

210263

264+
@track_calls_v5
265+
def add_v5(x, y):
266+
return x + y
267+
268+
269+
class MyClass5:
270+
@track_calls_v5
271+
def method(self, x, y):
272+
return x + y
273+
274+
@track_calls_v5
275+
@classmethod
276+
def class_method(cls, x, y):
277+
return x + y
278+
279+
@track_calls_v5
280+
@staticmethod
281+
def static_method(x, y):
282+
return x + y
283+
284+
211285
class MyClass3b:
212286
@CallTracker3.track(call_count=10)
213287
def method(self, x, y):
@@ -320,10 +394,58 @@ def test_version4():
320394
print(" ALL PASSED")
321395

322396

397+
def test_version5():
398+
print("\n--- Version 5 (custom FunctionWrapper/BoundFunctionWrapper) ---")
399+
400+
# Normal function
401+
add_v5.reset()
402+
assert add_v5(1, 2) == 3
403+
assert add_v5(3, 4) == 7
404+
assert add_v5.call_count() == 2
405+
print(f" function: call_count={add_v5.call_count()} (expected 2)")
406+
407+
# Instance method
408+
MyClass5.method.reset()
409+
obj = MyClass5()
410+
assert obj.method(1, 2) == 3
411+
assert obj.method(3, 4) == 7
412+
assert obj.method.call_count() == 2
413+
print(f" instance method: call_count={obj.method.call_count()} (expected 2)")
414+
415+
# Shared across instances
416+
obj2 = MyClass5()
417+
obj2.method(5, 6)
418+
assert obj.method.call_count() == 3
419+
assert obj2.method.call_count() == 3
420+
print(f" shared across instances: call_count={obj.method.call_count()} (expected 3)")
421+
422+
# Class method
423+
MyClass5.class_method.reset()
424+
assert MyClass5.class_method(1, 2) == 3
425+
assert MyClass5.class_method(3, 4) == 7
426+
assert MyClass5.class_method.call_count() == 2
427+
print(f" class method: call_count={MyClass5.class_method.call_count()} (expected 2)")
428+
429+
# Static method
430+
MyClass5.static_method.reset()
431+
assert MyClass5.static_method(1, 2) == 3
432+
assert MyClass5.static_method(3, 4) == 7
433+
assert MyClass5.static_method.call_count() == 2
434+
print(f" static method: call_count={MyClass5.static_method.call_count()} (expected 2)")
435+
436+
# Reset works
437+
add_v5.reset()
438+
assert add_v5.call_count() == 0
439+
print(" reset: OK")
440+
441+
print(" ALL PASSED")
442+
443+
323444
if __name__ == "__main__":
324445
test_version("Version 1 (manual)", add_v1, MyClass1)
325446
test_version("Version 2 (bind_state_to_wrapper)", add_v2, MyClass2)
326447
test_version("Version 3a (static track, no args)", add_v3a, MyClass3a)
327448
test_version("Version 3b (static track, call_count=10)", add_v3b, MyClass3b, expected_base=10)
328449
test_version4()
450+
test_version5()
329451
print("\nAll tests passed!")

0 commit comments

Comments
 (0)