Skip to content

Commit 07de076

Browse files
fix(profiling): enable sub-classing wrapped lock types by custom lock types
1 parent db5d2d3 commit 07de076

File tree

3 files changed

+85
-0
lines changed

3 files changed

+85
-0
lines changed

ddtrace/profiling/collector/_lock.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,17 @@ def __getattr__(self, name: str) -> Any:
314314
return getattr(original_class, name)
315315
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
316316

317+
def __mro_entries__(self, bases: Tuple[Any, ...]) -> Tuple[Type[Any], ...]:
318+
"""Support subclassing the wrapped lock type (PEP 560).
319+
320+
When custom lock types inherit from a wrapped lock
321+
(e.g. neo4j's AsyncRLock that inherits from asyncio.Lock), program error with:
322+
> TypeError: _LockAllocatorWrapper.__init__() takes 2 positional arguments but 4 were given
323+
324+
This method returns the actual object type to be used as the base class.
325+
"""
326+
return (self._original_class,) # type: ignore[return-value]
327+
317328

318329
class LockCollector(collector.CaptureSamplerCollector):
319330
"""Record lock usage."""

tests/profiling/collector/test_asyncio.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,48 @@ def teardown_method(self):
5151
except Exception as e:
5252
print("Error while deleting file: ", e)
5353

54+
def test_subclassing_wrapped_lock(self):
55+
"""Test that subclassing asyncio.Lock works when profiling is active.
56+
57+
Regression test for a bug reported by neo4j users where their library defines:
58+
class AsyncRLock(asyncio.Lock): ...
59+
60+
With profiling enabled, this would fail with:
61+
TypeError: _LockAllocatorWrapper.__init__() takes 2 positional arguments but 4 were given
62+
63+
The fix implements __mro_entries__ on _LockAllocatorWrapper (PEP 560).
64+
"""
65+
from ddtrace.profiling.collector._lock import _LockAllocatorWrapper
66+
67+
with collector_asyncio.AsyncioLockCollector(capture_pct=100):
68+
# Verify the wrapper is in place
69+
assert isinstance(asyncio.Lock, _LockAllocatorWrapper)
70+
71+
# This should NOT raise TypeError
72+
class AsyncRLock(asyncio.Lock):
73+
"""A custom lock that extends asyncio.Lock (like neo4j does)."""
74+
75+
def __init__(self) -> None:
76+
super().__init__()
77+
self._owner = None
78+
self._count = 0
79+
80+
# Verify the subclass can be instantiated
81+
custom_lock = AsyncRLock()
82+
assert hasattr(custom_lock, "_owner")
83+
assert hasattr(custom_lock, "_count")
84+
assert custom_lock._owner is None
85+
assert custom_lock._count == 0
86+
87+
# Verify async lock functionality still works
88+
async def test_async_lock():
89+
await custom_lock.acquire()
90+
assert custom_lock.locked()
91+
custom_lock.release()
92+
assert not custom_lock.locked()
93+
94+
asyncio.get_event_loop().run_until_complete(test_async_lock())
95+
5496
async def test_asyncio_lock_events(self):
5597
with collector_asyncio.AsyncioLockCollector(capture_pct=100):
5698
lock = asyncio.Lock() # !CREATE! test_asyncio_lock_events

tests/profiling/collector/test_threading.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,38 @@ def __init__(self, lock_class: LockTypeClass) -> None:
564564
# Try this way too
565565
Foobar(self.lock_class)
566566

567+
def test_subclassing_wrapped_lock(self) -> None:
568+
"""Test that subclassing a wrapped lock type works correctly.
569+
570+
Regression test for a bug where third-party libraries (e.g. neo4j) that subclass
571+
asyncio.Lock would fail with:
572+
TypeError: _LockAllocatorWrapper.__init__() takes 2 positional arguments but 4 were given
573+
574+
The fix implements __mro_entries__ on _LockAllocatorWrapper (PEP 560).
575+
"""
576+
from ddtrace.profiling.collector._lock import _LockAllocatorWrapper
577+
578+
with self.collector_class():
579+
# Verify the wrapper is in place
580+
assert isinstance(self.lock_class, _LockAllocatorWrapper)
581+
582+
# This should NOT raise TypeError
583+
class CustomLock(self.lock_class): # type: ignore[misc]
584+
"""A custom lock that extends the wrapped lock type."""
585+
586+
def __init__(self) -> None:
587+
super().__init__()
588+
self.custom_attr = "test"
589+
590+
# Verify the subclass works correctly
591+
custom_lock = CustomLock()
592+
assert hasattr(custom_lock, "custom_attr")
593+
assert custom_lock.custom_attr == "test"
594+
595+
# Verify lock functionality still works
596+
assert custom_lock.acquire()
597+
custom_lock.release()
598+
567599
def test_lock_events(self) -> None:
568600
# The first argument is the recorder.Recorder which is used for the
569601
# v1 exporter. We don't need it for the v2 exporter.

0 commit comments

Comments
 (0)