55Version 2: Automatic state attachment using wrapt.bind_state_to_wrapper
66Version 3: With optional decorator arguments via static method
77Version 4: Singleton tracker with per-function stats
8+ Version 5: Custom FunctionWrapper/BoundFunctionWrapper with decorator(proxy=...)
89"""
910
1011import 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+
211285class 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+
323444if __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 ("\n All tests passed!" )
0 commit comments