From 5818522df77156527afe69e544db9935477fd78d Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Sat, 27 Jun 2026 11:20:30 +0100 Subject: [PATCH 1/3] Add free-threaded build to tests on 3.15 --- .github/workflows/auto_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto_test.yml b/.github/workflows/auto_test.yml index b1cceb1..dd48fca 100644 --- a/.github/workflows/auto_test.yml +++ b/.github/workflows/auto_test.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python-version: ["3.15-dev", "3.14", "3.13", "3.12"] + python-version: ["3.15-dev", "3.15t-dev", "3.14", "3.13", "3.12"] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 From 0035a5e9fe4d69570625af9748f90e2bf99da212 Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Sat, 27 Jun 2026 11:46:15 +0100 Subject: [PATCH 2/3] Add locks around stat increments. --- src/ducktools/classbuilder/methods.py | 29 ++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/ducktools/classbuilder/methods.py b/src/ducktools/classbuilder/methods.py index ce5616e..347568f 100644 --- a/src/ducktools/classbuilder/methods.py +++ b/src/ducktools/classbuilder/methods.py @@ -386,13 +386,32 @@ def get_counter_field_names(argcount): # Classes to handle cached methods class _CacheStats: - __slots__ = ("hits", "misses", "skips") + __slots__ = ( + "hits", "misses", "skips", + "_hitlock", "_misslock", "_skiplock", + ) def __init__(self): self.hits = 0 self.misses = 0 self.skips = 0 + self._hitlock = _thread.allocate_lock() + self._misslock = _thread.allocate_lock() + self._skiplock = _thread.allocate_lock() + + def add_hit(self): + with self._hitlock: + self.hits += 1 + + def add_miss(self): + with self._misslock: + self.misses += 1 + + def add_skip(self): + with self._skiplock: + self.skips += 1 + @property def hit_percent(self): # If there are no cache hits, return 100% @@ -437,17 +456,17 @@ def clear(self, new_cache=None): def __call__(self, *args, **kwargs): try: result = self._internal_cache[args] - self._stats.hits += 1 + self._stats.add_hit() except KeyError: lock = self._lock_cache.setdefault(args, _thread.allocate_lock()) with lock: try: result = self._internal_cache[args] - self._stats.hits += 1 + self._stats.add_hit() except KeyError: result = self._func(*args, **kwargs) self._internal_cache[args] = result - self._stats.misses += 1 + self._stats.add_miss() return result @@ -482,7 +501,7 @@ def method_generator(cls, funcname): if args is None: # If the argument getter returns None # the method is not cacheable - source_exec.stats.skips += 1 # Add one to skip count + source_exec.stats.add_skip() # Add one to skip count return None # The first argument should always be a tuple of fields From d567807d3d5c156f7de2b81bee2e4c694bd89f84 Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Sat, 27 Jun 2026 11:58:41 +0100 Subject: [PATCH 3/3] Fix stub with additional methods and locks --- src/ducktools/classbuilder/methods.pyi | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ducktools/classbuilder/methods.pyi b/src/ducktools/classbuilder/methods.pyi index e55e4ef..bb75c41 100644 --- a/src/ducktools/classbuilder/methods.pyi +++ b/src/ducktools/classbuilder/methods.pyi @@ -22,7 +22,7 @@ __lazy_modules__: list[str] -import threading +import _thread import types import typing @@ -111,7 +111,7 @@ class _AttachedMethod: cls: type _generated_method: types.FunctionType - _lock: threading.Lock + _lock: _thread.LockType def __init__( self, @@ -163,10 +163,19 @@ def get_init_parameters(cls: type) -> _FunctionParameterType: ... def get_counter_field_names(argcount: int) -> list[str]: ... class _CacheStats: - __slots__: tuple[str, str, str] + __slots__: tuple[str, ...] hits: int misses: int skips: int + + _hitlock: _thread.LockType + _misslock: _thread.LockType + _skiplock: _thread.LockType + + def add_hit(self) -> None: ... + def add_miss(self) -> None: ... + def add_skip(self) -> None: ... + @property def hit_percent(self) -> float: ... def __init__(self) -> None: ... @@ -177,7 +186,7 @@ class _SimpleCache: _func: Callable[..., types.FunctionType] _internal_cache: dict[tuple, types.FunctionType] _stats: _CacheStats - _lock_cache: dict[tuple, threading.Lock] + _lock_cache: dict[tuple, _thread.LockType] def __init__( self, func: types.FunctionType,