diff --git a/src/filelock/_api.py b/src/filelock/_api.py index 49eaf1e..7bec3ce 100644 --- a/src/filelock/_api.py +++ b/src/filelock/_api.py @@ -8,7 +8,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from threading import local -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, Any from weakref import WeakValueDictionary from ._error import Timeout @@ -77,7 +77,7 @@ class ThreadLocalFileContext(FileLockContext, local): class BaseFileLock(ABC, contextlib.ContextDecorator): """Abstract base class for a file lock object.""" - _instances: ClassVar[WeakValueDictionary[str, BaseFileLock]] = WeakValueDictionary() + _instances: WeakValueDictionary[str, BaseFileLock] def __new__( # noqa: PLR0913 cls, @@ -100,6 +100,11 @@ def __new__( # noqa: PLR0913 return instance # type: ignore[return-value] # https://github.com/python/mypy/issues/15322 + def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None: + """Setup unique state for lock subclasses.""" + super().__init_subclass__(**kwargs) + cls._instances = WeakValueDictionary() + def __init__( # noqa: PLR0913 self, lock_file: str | os.PathLike[str], diff --git a/tests/test_filelock.py b/tests/test_filelock.py index 674d81a..f9a4e5f 100644 --- a/tests/test_filelock.py +++ b/tests/test_filelock.py @@ -14,6 +14,7 @@ from types import TracebackType from typing import TYPE_CHECKING, Any, Callable, Iterator, Tuple, Type, Union from uuid import uuid4 +from weakref import WeakValueDictionary import pytest @@ -687,3 +688,17 @@ def test_singleton_locks_are_deleted_when_no_external_references_exist( assert lock_type._instances == {str(lock_path): lock} # noqa: SLF001 del lock assert lock_type._instances == {} # noqa: SLF001 + + +@pytest.mark.skipif(hasattr(sys, "pypy_version_info"), reason="del() does not trigger GC in PyPy") +@pytest.mark.parametrize("lock_type", [FileLock, SoftFileLock]) +def test_singleton_instance_tracking_is_unique_per_subclass(lock_type: type[BaseFileLock]) -> None: + class Lock1(lock_type): # type: ignore[valid-type, misc] + pass + + class Lock2(lock_type): # type: ignore[valid-type, misc] + pass + + assert isinstance(Lock1._instances, WeakValueDictionary) # noqa: SLF001 + assert isinstance(Lock2._instances, WeakValueDictionary) # noqa: SLF001 + assert Lock1._instances is not Lock2._instances # noqa: SLF001