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
2 changes: 1 addition & 1 deletion .github/workflows/auto_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 24 additions & 5 deletions src/ducktools/classbuilder/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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%
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
17 changes: 13 additions & 4 deletions src/ducktools/classbuilder/methods.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

__lazy_modules__: list[str]

import threading
import _thread
import types
import typing

Expand Down Expand Up @@ -111,7 +111,7 @@ class _AttachedMethod:
cls: type

_generated_method: types.FunctionType
_lock: threading.Lock
_lock: _thread.LockType

def __init__(
self,
Expand Down Expand Up @@ -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: ...
Expand All @@ -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,
Expand Down