Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/ducktools/classbuilder/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import builtins
import reprlib
import _thread
try:
from _types import ( # type: ignore
FunctionType as _FunctionType,
Expand Down Expand Up @@ -176,11 +177,15 @@ class _AttachedMethod:
"""
Descriptor for attaching a method maker to a class.
"""
__slots__ = ("maker", "cls")
__slots__ = ("maker", "cls", "_generated_method", "_lock")
def __init__(self, maker, cls):
self.maker = maker
self.cls = cls

# Internals
self._generated_method = None
self._lock = _thread.allocate_lock() # in 3.12 _thread.lock doesn't exist

def __repr__(self):
return f"<_AttachedMethod for {self.maker.funcname!r} method on {self.cls.__qualname__!r}>"

Expand All @@ -193,13 +198,17 @@ def __eq__(self, other):
)

def __get__(self, inst, cls=None):
method = self.maker.generate(self.cls)
# Replace this descriptor on the class with the generated function
setattr(self.cls, self.maker.funcname, method)
with self._lock:
# Check again in case something held the lock
if self._generated_method is None:
self._generated_method = self.maker.generate(self.cls)

# Replace this descriptor on the class with the generated function
setattr(self.cls, self.maker.funcname, self._generated_method)

# Use 'get' to return the generated function as a bound method
# instead of as a regular function for first usage.
return method.__get__(inst, cls)
return self._generated_method.__get__(inst, cls)


# Argument getters for the generic cached methods
Expand Down
File renamed without changes.
34 changes: 34 additions & 0 deletions tests/method_tests/test_threading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

from ducktools.classbuilder.prefab import prefab
from ducktools.classbuilder.methods import class_init_generator, MethodMaker


def test_multithreaded_generator():
def slow_init_generator(cls: type, funcname: str = "__init__"):
# A generator with a delay so multiple threads can attempt
# to generate
time.sleep(0.01)
return class_init_generator(cls, funcname=funcname)

slow_init_maker = MethodMaker("__init__", slow_init_generator)

@prefab(init=False, eq=False, repr=False)
class Example:
a: int
b: int

slow_init_maker.attach(Example)

get_init = lambda cls: cls.__init__

with ThreadPoolExecutor() as pool:
futures = [pool.submit(get_init, Example) for _ in range(50)]
results = set()
for future in as_completed(futures):
results.add(future.result())

# Assert generation has only occured once as there is only
# one unique function in results
assert len(results) == 1