1717from typing import Tuple
1818from typing import Type
1919
20- import wrapt
21-
2220from ddtrace .internal .datadog .profiling import ddup
2321from ddtrace .profiling import _threading
2422from ddtrace .profiling import collector
@@ -34,22 +32,24 @@ def _current_thread() -> Tuple[int, str]:
3432 return thread_id , _threading .get_thread_name (thread_id )
3533
3634
37- # We need to know if wrapt is compiled in C or not. If it's not using the C module, then the wrappers function will
38- # appear in the stack trace and we need to hide it.
39- WRAPT_C_EXT : bool
40- if os .environ .get ("WRAPT_DISABLE_EXTENSIONS" ):
41- WRAPT_C_EXT = False
42- else :
43- try :
44- import wrapt ._wrappers as _w # noqa: F401
45- except ImportError :
46- WRAPT_C_EXT = False
47- else :
48- WRAPT_C_EXT = True
49- del _w
35+ class _ProfiledLock :
36+ """Lightweight lock wrapper that profiles lock acquire/release operations.
37+
38+ This is a simple delegating wrapper that intercepts lock methods without
39+ the overhead of a full proxy object.
40+ """
5041
42+ __slots__ = (
43+ "__wrapped__" ,
44+ "_self_tracer" ,
45+ "_self_max_nframes" ,
46+ "_self_capture_sampler" ,
47+ "_self_endpoint_collection_enabled" ,
48+ "_self_init_loc" ,
49+ "_self_acquired_at" ,
50+ "_self_name" ,
51+ )
5152
52- class _ProfiledLock (wrapt .ObjectProxy ):
5353 def __init__ (
5454 self ,
5555 wrapped : Any ,
@@ -58,12 +58,13 @@ def __init__(
5858 capture_sampler : collector .CaptureSampler ,
5959 endpoint_collection_enabled : bool ,
6060 ) -> None :
61- wrapt . ObjectProxy . __init__ ( self , wrapped )
61+ self . __wrapped__ : Any = wrapped
6262 self ._self_tracer : Optional [Tracer ] = tracer
6363 self ._self_max_nframes : int = max_nframes
6464 self ._self_capture_sampler : collector .CaptureSampler = capture_sampler
6565 self ._self_endpoint_collection_enabled : bool = endpoint_collection_enabled
66- frame : FrameType = sys ._getframe (2 if WRAPT_C_EXT else 3 )
66+ # Frame depth: 0=__init__, 1=_profiled_allocate_lock, 2=_LockAllocatorWrapper.__call__, 3=caller
67+ frame : FrameType = sys ._getframe (3 )
6768 code : CodeType = frame .f_code
6869 self ._self_init_loc : str = "%s:%d" % (os .path .basename (code .co_filename ), frame .f_lineno )
6970 self ._self_acquired_at : int = 0
@@ -134,11 +135,6 @@ def acquire(self, *args: Any, **kwargs: Any) -> Any:
134135 return self ._acquire (self .__wrapped__ .acquire , * args , ** kwargs )
135136
136137 def _release (self , inner_func : Callable [..., Any ], * args : Any , ** kwargs : Any ) -> None :
137- # The underlying threading.Lock class is implemented using C code, and
138- # it doesn't have the __dict__ attribute. So we can't do
139- # self.__dict__.pop("_self_acquired_at", None) to remove the attribute.
140- # Instead, we need to use the following workaround to retrieve and
141- # remove the attribute.
142138 start : Optional [int ] = getattr (self , "_self_acquired_at" , None )
143139 try :
144140 # Though it should generally be avoided to call release() from
@@ -213,9 +209,14 @@ def _find_self_name(self, var_dict: Dict[str, Any]) -> Optional[str]:
213209 return name
214210 if config .lock .name_inspect_dir :
215211 for attribute in dir (value ):
216- if not attribute .startswith ("__" ) and getattr (value , attribute ) is self :
217- self ._self_name = attribute
218- return attribute
212+ try :
213+ if not attribute .startswith ("__" ) and getattr (value , attribute ) is self :
214+ self ._self_name = attribute
215+ return attribute
216+ except AttributeError :
217+ # With __slots__, accessing unset attributes raises AttributeError
218+ # (e.g., _self_acquired_at after it's been deleted in _release)
219+ continue
219220 return None
220221
221222 # Get lock acquire/release call location and variable name the lock is assigned to
@@ -251,12 +252,38 @@ def _maybe_update_self_name(self) -> None:
251252 if not self ._self_name :
252253 self ._self_name = ""
253254
255+ # Delegate remaining lock methods to the wrapped lock
256+ def locked (self ) -> bool :
257+ """Return True if lock is currently held."""
258+ return self .__wrapped__ .locked ()
259+
260+ def __repr__ (self ) -> str :
261+ return f"<_ProfiledLock({ self .__wrapped__ !r} ) at { self ._self_init_loc } >"
262+
263+ # Support for being used in with statements
264+ def __bool__ (self ) -> bool :
265+ return True
266+
267+
268+ class _LockAllocatorWrapper :
269+ """Wrapper for lock allocator functions that prevents method binding.
270+
271+ When a function is stored as a class attribute and accessed via an instance,
272+ Python's descriptor protocol normally binds it as a method. This wrapper
273+ prevents that behavior by implementing __get__ to always return self,
274+ similar to how staticmethod works, but as a callable object.
275+ """
276+
277+ __slots__ = ("_func" ,)
278+
279+ def __init__ (self , func : Callable [..., Any ]) -> None :
280+ self ._func : Callable [..., Any ] = func
281+
282+ def __call__ (self , * args : Any , ** kwargs : Any ) -> Any :
283+ return self ._func (* args , ** kwargs )
254284
255- class FunctionWrapper (wrapt .FunctionWrapper ):
256- # Override the __get__ method: whatever happens, _allocate_lock is always considered by Python like a "static"
257- # method, even when used as a class attribute. Python never tried to "bind" it to a method, because it sees it is a
258- # builtin function. Override default wrapt behavior here that tries to detect bound method.
259- def __get__ (self , instance : Any , owner : Optional [Type ] = None ) -> FunctionWrapper : # type: ignore
285+ def __get__ (self , instance : Any , owner : Optional [Type ] = None ) -> _LockAllocatorWrapper :
286+ # Always return self, never bind as a method
260287 return self
261288
262289
@@ -303,9 +330,9 @@ def patch(self) -> None:
303330 # Nobody should use locks from `_thread`; if they do so, then it's deliberate and we don't profile.
304331 self ._original = self ._get_patch_target ()
305332
306- # TODO: `instance` is unused
307- def _allocate_lock ( wrapped : Any , instance : Any , args : Any , kwargs : Any ) -> _ProfiledLock :
308- lock : Any = wrapped (* args , ** kwargs )
333+ # Create a simple wrapper function that returns profiled locks
334+ def _profiled_allocate_lock ( * args : Any , ** kwargs : Any ) -> _ProfiledLock :
335+ lock : Any = self . _original (* args , ** kwargs )
309336 return self .PROFILED_LOCK_CLASS (
310337 lock ,
311338 self .tracer ,
@@ -314,7 +341,9 @@ def _allocate_lock(wrapped: Any, instance: Any, args: Any, kwargs: Any) -> _Prof
314341 self .endpoint_collection_enabled ,
315342 )
316343
317- self ._set_patch_target (FunctionWrapper (self ._original , _allocate_lock ))
344+ # Wrap the function to prevent it from being bound as a method when
345+ # accessed as a class attribute (e.g., Foo.lock_class = threading.Lock)
346+ self ._set_patch_target (_LockAllocatorWrapper (_profiled_allocate_lock ))
318347
319348 def unpatch (self ) -> None :
320349 """Unpatch the threading module for tracking lock allocation."""
0 commit comments