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 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 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,