88from abc import ABC , abstractmethod
99from dataclasses import dataclass
1010from threading import local
11- from typing import TYPE_CHECKING , Any
11+ from typing import TYPE_CHECKING , Any , ClassVar
12+ from weakref import WeakValueDictionary
1213
1314from ._error import Timeout
1415
@@ -76,25 +77,53 @@ class ThreadLocalFileContext(FileLockContext, local):
7677class BaseFileLock (ABC , contextlib .ContextDecorator ):
7778 """Abstract base class for a file lock object."""
7879
79- def __init__ (
80+ _instances : ClassVar [WeakValueDictionary [str , BaseFileLock ]] = WeakValueDictionary ()
81+
82+ def __new__ ( # noqa: PLR0913
83+ cls ,
84+ lock_file : str | os .PathLike [str ],
85+ timeout : float = - 1 , # noqa: ARG003
86+ mode : int = 0o644 , # noqa: ARG003
87+ thread_local : bool = True , # noqa: ARG003, FBT001, FBT002
88+ * ,
89+ is_singleton : bool = False ,
90+ ) -> Self :
91+ """Create a new lock object or if specified return the singleton instance for the lock file."""
92+ if not is_singleton :
93+ return super ().__new__ (cls )
94+
95+ instance = cls ._instances .get (str (lock_file ))
96+ if not instance :
97+ instance = super ().__new__ (cls )
98+ cls ._instances [str (lock_file )] = instance
99+
100+ return instance # type: ignore[return-value] # https://github.com/python/mypy/issues/15322
101+
102+ def __init__ ( # noqa: PLR0913
80103 self ,
81104 lock_file : str | os .PathLike [str ],
82105 timeout : float = - 1 ,
83106 mode : int = 0o644 ,
84107 thread_local : bool = True , # noqa: FBT001, FBT002
108+ * ,
109+ is_singleton : bool = False ,
85110 ) -> None :
86111 """
87112 Create a new lock object.
88113
89114 :param lock_file: path to the file
90- :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in
91- the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it
92- to a negative value. A timeout of 0 means, that there is exactly one attempt to acquire the file lock.
93- :param mode: file permissions for the lockfile.
94- :param thread_local: Whether this object's internal context should be thread local or not.
95- If this is set to ``False`` then the lock will be reentrant across threads.
115+ :param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in \
116+ the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it \
117+ to a negative value. A timeout of 0 means, that there is exactly one attempt to acquire the file lock.
118+ :param mode: file permissions for the lockfile
119+ :param thread_local: Whether this object's internal context should be thread local or not. If this is set to \
120+ ``False`` then the lock will be reentrant across threads.
121+ :param is_singleton: If this is set to ``True`` then only one instance of this class will be created \
122+ per lock file. This is useful if you want to use the lock object for reentrant locking without needing \
123+ to pass the same object around.
96124 """
97125 self ._is_thread_local = thread_local
126+ self ._is_singleton = is_singleton
98127
99128 # Create the context. Note that external code should not work with the context directly and should instead use
100129 # properties of this class.
@@ -109,6 +138,11 @@ def is_thread_local(self) -> bool:
109138 """:return: a flag indicating if this lock is thread local or not"""
110139 return self ._is_thread_local
111140
141+ @property
142+ def is_singleton (self ) -> bool :
143+ """:return: a flag indicating if this lock is singleton or not"""
144+ return self ._is_singleton
145+
112146 @property
113147 def lock_file (self ) -> str :
114148 """:return: path to the lock file"""
0 commit comments