6
6
import time
7
7
import warnings
8
8
from abc import ABC , abstractmethod
9
+ from dataclasses import dataclass
9
10
from threading import local
10
11
from types import TracebackType
11
12
from typing import Any
@@ -36,14 +37,46 @@ def __exit__(
36
37
self .lock .release ()
37
38
38
39
39
- class BaseFileLock (ABC , contextlib .ContextDecorator , local ):
40
+ @dataclass
41
+ class FileLockContext :
42
+ """
43
+ A dataclass which holds the context for a ``BaseFileLock`` object.
44
+ """
45
+
46
+ # The context is held in a separate class to allow optional use of thread local storage via the
47
+ # ThreadLocalFileContext class.
48
+
49
+ #: The path to the lock file.
50
+ lock_file : str
51
+
52
+ #: The default timeout value.
53
+ timeout : float
54
+
55
+ #: The mode for the lock files
56
+ mode : int
57
+
58
+ #: The file descriptor for the *_lock_file* as it is returned by the os.open() function, not None when lock held
59
+ lock_file_fd : int | None = None
60
+
61
+ #: The lock counter is used for implementing the nested locking mechanism.
62
+ lock_counter : int = 0 # When the lock is acquired is increased and the lock is only released, when this value is 0
63
+
64
+
65
+ class ThreadLocalFileContext (FileLockContext , local ):
66
+ """
67
+ A thread local version of the ``FileLockContext`` class.
68
+ """
69
+
70
+
71
+ class BaseFileLock (ABC , contextlib .ContextDecorator ):
40
72
"""Abstract base class for a file lock object."""
41
73
42
74
def __init__ (
43
75
self ,
44
76
lock_file : str | os .PathLike [Any ],
45
77
timeout : float = - 1 ,
46
78
mode : int = 0o644 ,
79
+ thread_local : bool = True ,
47
80
) -> None :
48
81
"""
49
82
Create a new lock object.
@@ -52,29 +85,29 @@ def __init__(
52
85
:param timeout: default timeout when acquiring the lock, in seconds. It will be used as fallback value in
53
86
the acquire method, if no timeout value (``None``) is given. If you want to disable the timeout, set it
54
87
to a negative value. A timeout of 0 means, that there is exactly one attempt to acquire the file lock.
55
- : param mode: file permissions for the lockfile.
88
+ :param mode: file permissions for the lockfile.
89
+ :param thread_local: Whether this object's internal context should be thread local or not.
90
+ If this is set to ``False`` then the lock will be reentrant across threads.
56
91
"""
57
- # The path to the lock file.
58
- self ._lock_file : str = os .fspath (lock_file )
59
-
60
- # The file descriptor for the *_lock_file* as it is returned by the os.open() function.
61
- # This file lock is only NOT None, if the object currently holds the lock.
62
- self ._lock_file_fd : int | None = None
63
-
64
- # The default timeout value.
65
- self ._timeout : float = timeout
92
+ self ._is_thread_local = thread_local
66
93
67
- # The mode for the lock files
68
- self ._mode : int = mode
94
+ # Create the context. Note that external code should not work with the context directly and should instead use
95
+ # properties of this class.
96
+ kwargs : dict [str , Any ] = {
97
+ "lock_file" : os .fspath (lock_file ),
98
+ "timeout" : timeout ,
99
+ "mode" : mode ,
100
+ }
101
+ self ._context : FileLockContext = (ThreadLocalFileContext if thread_local else FileLockContext )(** kwargs )
69
102
70
- # The lock counter is used for implementing the nested locking mechanism. Whenever the lock is acquired, the
71
- # counter is increased and the lock is only released, when this value is 0 again.
72
- self ._lock_counter : int = 0
103
+ def is_thread_local ( self ) -> bool :
104
+ """:return: a flag indicating if this lock is thread local or not"""
105
+ return self ._is_thread_local
73
106
74
107
@property
75
108
def lock_file (self ) -> str :
76
109
""":return: path to the lock file"""
77
- return self ._lock_file
110
+ return self ._context . lock_file
78
111
79
112
@property
80
113
def timeout (self ) -> float :
@@ -83,7 +116,7 @@ def timeout(self) -> float:
83
116
84
117
.. versionadded:: 2.0.0
85
118
"""
86
- return self ._timeout
119
+ return self ._context . timeout
87
120
88
121
@timeout .setter
89
122
def timeout (self , value : float | str ) -> None :
@@ -92,16 +125,16 @@ def timeout(self, value: float | str) -> None:
92
125
93
126
:param value: the new value, in seconds
94
127
"""
95
- self ._timeout = float (value )
128
+ self ._context . timeout = float (value )
96
129
97
130
@abstractmethod
98
131
def _acquire (self ) -> None :
99
- """If the file lock could be acquired, self._lock_file_fd holds the file descriptor of the lock file."""
132
+ """If the file lock could be acquired, self._context.lock_file_fd holds the file descriptor of the lock file."""
100
133
raise NotImplementedError
101
134
102
135
@abstractmethod
103
136
def _release (self ) -> None :
104
- """Releases the lock and sets self._lock_file_fd to None."""
137
+ """Releases the lock and sets self._context.lock_file_fd to None."""
105
138
raise NotImplementedError
106
139
107
140
@property
@@ -114,7 +147,14 @@ def is_locked(self) -> bool:
114
147
115
148
This was previously a method and is now a property.
116
149
"""
117
- return self ._lock_file_fd is not None
150
+ return self ._context .lock_file_fd is not None
151
+
152
+ @property
153
+ def lock_counter (self ) -> int :
154
+ """
155
+ :return: The number of times this lock has been acquired (but not yet released).
156
+ """
157
+ return self ._context .lock_counter
118
158
119
159
def acquire (
120
160
self ,
@@ -132,7 +172,7 @@ def acquire(
132
172
:param poll_interval: interval of trying to acquire the lock file
133
173
:param poll_intervall: deprecated, kept for backwards compatibility, use ``poll_interval`` instead
134
174
:param blocking: defaults to True. If False, function will return immediately if it cannot obtain a lock on the
135
- first attempt. Otherwise this method will block until the timeout expires or the lock is acquired.
175
+ first attempt. Otherwise, this method will block until the timeout expires or the lock is acquired.
136
176
:raises Timeout: if fails to acquire lock within the timeout period
137
177
:return: a context object that will unlock the file when the context is exited
138
178
@@ -157,18 +197,18 @@ def acquire(
157
197
"""
158
198
# Use the default timeout, if no timeout is provided.
159
199
if timeout is None :
160
- timeout = self .timeout
200
+ timeout = self ._context . timeout
161
201
162
202
if poll_intervall is not None :
163
203
msg = "use poll_interval instead of poll_intervall"
164
204
warnings .warn (msg , DeprecationWarning , stacklevel = 2 )
165
205
poll_interval = poll_intervall
166
206
167
207
# Increment the number right at the beginning. We can still undo it, if something fails.
168
- self ._lock_counter += 1
208
+ self ._context . lock_counter += 1
169
209
170
210
lock_id = id (self )
171
- lock_filename = self ._lock_file
211
+ lock_filename = self .lock_file
172
212
start_time = time .perf_counter ()
173
213
try :
174
214
while True :
@@ -180,16 +220,16 @@ def acquire(
180
220
break
181
221
elif blocking is False :
182
222
_LOGGER .debug ("Failed to immediately acquire lock %s on %s" , lock_id , lock_filename )
183
- raise Timeout (self . _lock_file )
223
+ raise Timeout (lock_filename )
184
224
elif 0 <= timeout < time .perf_counter () - start_time :
185
225
_LOGGER .debug ("Timeout on acquiring lock %s on %s" , lock_id , lock_filename )
186
- raise Timeout (self . _lock_file )
226
+ raise Timeout (lock_filename )
187
227
else :
188
228
msg = "Lock %s not acquired on %s, waiting %s seconds ..."
189
229
_LOGGER .debug (msg , lock_id , lock_filename , poll_interval )
190
230
time .sleep (poll_interval )
191
231
except BaseException : # Something did go wrong, so decrement the counter.
192
- self ._lock_counter = max (0 , self ._lock_counter - 1 )
232
+ self ._context . lock_counter = max (0 , self ._context . lock_counter - 1 )
193
233
raise
194
234
return AcquireReturnProxy (lock = self )
195
235
@@ -201,14 +241,14 @@ def release(self, force: bool = False) -> None:
201
241
:param force: If true, the lock counter is ignored and the lock is released in every case/
202
242
"""
203
243
if self .is_locked :
204
- self ._lock_counter -= 1
244
+ self ._context . lock_counter -= 1
205
245
206
- if self ._lock_counter == 0 or force :
207
- lock_id , lock_filename = id (self ), self ._lock_file
246
+ if self ._context . lock_counter == 0 or force :
247
+ lock_id , lock_filename = id (self ), self .lock_file
208
248
209
249
_LOGGER .debug ("Attempting to release lock %s on %s" , lock_id , lock_filename )
210
250
self ._release ()
211
- self ._lock_counter = 0
251
+ self ._context . lock_counter = 0
212
252
_LOGGER .debug ("Lock %s released on %s" , lock_id , lock_filename )
213
253
214
254
def __enter__ (self ) -> BaseFileLock :
0 commit comments