1
+ from __future__ import annotations
2
+
3
+ import logging
1
4
import os
2
5
import sqlite3
3
6
import threading
4
- import logging
5
- from _error import Timeout
6
- from filelock ._api import AcquireReturnProxy , BaseFileLock
7
- from typing import Literal , Any
8
7
from contextlib import contextmanager
8
+ from typing import Any , Literal
9
9
from weakref import WeakValueDictionary
10
10
11
+ from _error import Timeout
12
+
13
+ from filelock ._api import AcquireReturnProxy
14
+
11
15
_LOGGER = logging .getLogger ("filelock" )
12
16
13
17
# PRAGMA busy_timeout=N delegates to https://www.sqlite.org/c3ref/busy_timeout.html,
14
18
# which accepts an int argument, which has the maximum value of 2_147_483_647 on 32-bit
15
19
# systems. Use even a lower value to be safe. This 2 bln milliseconds is about 23 days.
16
20
_MAX_SQLITE_TIMEOUT_MS = 2_000_000_000 - 1
17
21
22
+
18
23
def timeout_for_sqlite (timeout : float = - 1 , blocking : bool = True ) -> int :
19
24
if blocking is False :
20
25
return 0
21
26
if timeout == - 1 :
22
27
return _MAX_SQLITE_TIMEOUT_MS
23
28
if timeout < 0 :
24
- raise ValueError ("timeout must be a non-negative number or -1" )
25
-
29
+ msg = "timeout must be a non-negative number or -1"
30
+ raise ValueError (msg )
31
+
26
32
assert timeout >= 0
27
33
timeout_ms = int (timeout * 1000 )
28
34
if timeout_ms > _MAX_SQLITE_TIMEOUT_MS or timeout_ms < 0 :
@@ -33,9 +39,16 @@ def timeout_for_sqlite(timeout: float = -1, blocking: bool = True) -> int:
33
39
34
40
class _ReadWriteLockMeta (type ):
35
41
"""Metaclass that redirects instance creation to get_lock() when is_singleton=True."""
36
- def __call__ (cls , lock_file : str | os .PathLike [str ],
37
- timeout : float = - 1 , blocking : bool = True ,
38
- is_singleton : bool = True , * args : Any , ** kwargs : Any ) -> "ReadWriteLock" :
42
+
43
+ def __call__ (
44
+ cls ,
45
+ lock_file : str | os .PathLike [str ],
46
+ timeout : float = - 1 ,
47
+ blocking : bool = True ,
48
+ is_singleton : bool = True ,
49
+ * args : Any ,
50
+ ** kwargs : Any ,
51
+ ) -> ReadWriteLock :
39
52
if is_singleton :
40
53
return cls .get_lock (lock_file , timeout , blocking )
41
54
return super ().__call__ (lock_file , timeout , blocking , is_singleton , * args , ** kwargs )
@@ -47,16 +60,22 @@ class ReadWriteLock(metaclass=_ReadWriteLockMeta):
47
60
_instances_lock = threading .Lock ()
48
61
49
62
@classmethod
50
- def get_lock (cls , lock_file : str | os .PathLike [str ],
51
- timeout : float = - 1 , blocking : bool = True ) -> "ReadWriteLock" :
63
+ def get_lock (cls , lock_file : str | os .PathLike [str ], timeout : float = - 1 , blocking : bool = True ) -> ReadWriteLock :
52
64
"""Return the one-and-only ReadWriteLock for a given file."""
53
65
normalized = os .path .abspath (lock_file )
54
66
with cls ._instances_lock :
55
67
if normalized not in cls ._instances :
56
68
cls ._instances [normalized ] = cls (lock_file , timeout , blocking )
57
69
instance = cls ._instances [normalized ]
58
70
if instance .timeout != timeout or instance .blocking != blocking :
59
- raise ValueError ("Singleton lock created with timeout=%s, blocking=%s, cannot be changed to timeout=%s, blocking=%s" , instance .timeout , instance .blocking , timeout , blocking )
71
+ msg = "Singleton lock created with timeout=%s, blocking=%s, cannot be changed to timeout=%s, blocking=%s"
72
+ raise ValueError (
73
+ msg ,
74
+ instance .timeout ,
75
+ instance .blocking ,
76
+ timeout ,
77
+ blocking ,
78
+ )
60
79
return instance
61
80
62
81
def __init__ (
@@ -76,7 +95,7 @@ def __init__(
76
95
self ._internal_lock = threading .Lock ()
77
96
self ._lock_level = 0 # Reentrance counter.
78
97
# _current_mode holds the active lock mode ("read" or "write") or None if no lock is held.
79
- self ._current_mode : Literal ["read" , "write" , None ] = None
98
+ self ._current_mode : Literal ["read" , "write" ] | None = None
80
99
# _lock_level is the reentrance counter.
81
100
self ._lock_level = 0
82
101
self .con = sqlite3 .connect (self .lock_file , check_same_thread = False )
@@ -92,16 +111,19 @@ def __init__(
92
111
# acquire, so crashes cannot adversely affect the DB. Even journal_mode=OFF would probably
93
112
# be fine, too, but the SQLite documentation says that ROLLBACK becomes *undefined behaviour*
94
113
# with journal_mode=OFF which sounds scarier.
95
- self .con .execute (' PRAGMA journal_mode=MEMORY;' )
114
+ self .con .execute (" PRAGMA journal_mode=MEMORY;" )
96
115
97
116
def acquire_read (self , timeout : float = - 1 , blocking : bool = True ) -> AcquireReturnProxy :
98
- """Acquire a read lock. If a lock is already held, it must be a read lock.
99
- Upgrading from read to write is prohibited."""
117
+ """
118
+ Acquire a read lock. If a lock is already held, it must be a read lock.
119
+ Upgrading from read to write is prohibited.
120
+ """
100
121
with self ._internal_lock :
101
122
if self ._lock_level > 0 :
102
123
# Must already be in read mode.
103
124
if self ._current_mode != "read" :
104
- raise RuntimeError ("Cannot acquire read lock when a write lock is held (no upgrade allowed)" )
125
+ msg = "Cannot acquire read lock when a write lock is held (no upgrade allowed)"
126
+ raise RuntimeError (msg )
105
127
self ._lock_level += 1
106
128
return AcquireReturnProxy (lock = self )
107
129
@@ -117,36 +139,40 @@ def acquire_read(self, timeout: float = -1, blocking: bool = True) -> AcquireRet
117
139
if self ._lock_level > 0 :
118
140
# Must already be in read mode.
119
141
if self ._current_mode != "read" :
120
- raise RuntimeError ("Cannot acquire read lock when a write lock is held (no upgrade allowed)" )
142
+ msg = "Cannot acquire read lock when a write lock is held (no upgrade allowed)"
143
+ raise RuntimeError (msg )
121
144
self ._lock_level += 1
122
145
return AcquireReturnProxy (lock = self )
123
-
124
- self .con .execute (' PRAGMA busy_timeout=?;' , (timeout_ms ,))
125
- self .con .execute (' BEGIN TRANSACTION;' )
146
+
147
+ self .con .execute (" PRAGMA busy_timeout=?;" , (timeout_ms ,))
148
+ self .con .execute (" BEGIN TRANSACTION;" )
126
149
# Need to make SELECT to compel SQLite to actually acquire a SHARED db lock.
127
150
# See https://www.sqlite.org/lockingv3.html#transaction_control
128
- self .con .execute (' SELECT name from sqlite_schema LIMIT 1;' )
151
+ self .con .execute (" SELECT name from sqlite_schema LIMIT 1;" )
129
152
130
153
with self ._internal_lock :
131
154
self ._current_mode = "read"
132
155
self ._lock_level = 1
133
-
156
+
134
157
return AcquireReturnProxy (lock = self )
135
158
136
159
except sqlite3 .OperationalError as e :
137
- if ' database is locked' not in str (e ):
160
+ if " database is locked" not in str (e ):
138
161
raise # Re-raise unexpected errors.
139
162
raise Timeout (self .lock_file )
140
163
finally :
141
164
self ._transaction_lock .release ()
142
165
143
166
def acquire_write (self , timeout : float = - 1 , blocking : bool = True ) -> AcquireReturnProxy :
144
- """Acquire a write lock. If a lock is already held, it must be a write lock.
145
- Upgrading from read to write is prohibited."""
167
+ """
168
+ Acquire a write lock. If a lock is already held, it must be a write lock.
169
+ Upgrading from read to write is prohibited.
170
+ """
146
171
with self ._internal_lock :
147
172
if self ._lock_level > 0 :
148
173
if self ._current_mode != "write" :
149
- raise RuntimeError ("Cannot acquire write lock: already holding a read lock (no upgrade allowed)" )
174
+ msg = "Cannot acquire write lock: already holding a read lock (no upgrade allowed)"
175
+ raise RuntimeError (msg )
150
176
self ._lock_level += 1
151
177
return AcquireReturnProxy (lock = self )
152
178
@@ -158,21 +184,24 @@ def acquire_write(self, timeout: float = -1, blocking: bool = True) -> AcquireRe
158
184
with self ._internal_lock :
159
185
if self ._lock_level > 0 :
160
186
if self ._current_mode != "write" :
161
- raise RuntimeError ("Cannot acquire write lock: already holding a read lock (no upgrade allowed)" )
187
+ msg = "Cannot acquire write lock: already holding a read lock (no upgrade allowed)"
188
+ raise RuntimeError (
189
+ msg
190
+ )
162
191
self ._lock_level += 1
163
192
return AcquireReturnProxy (lock = self )
164
-
165
- self .con .execute (' PRAGMA busy_timeout=?;' , (timeout_ms ,))
166
- self .con .execute (' BEGIN EXCLUSIVE TRANSACTION;' )
193
+
194
+ self .con .execute (" PRAGMA busy_timeout=?;" , (timeout_ms ,))
195
+ self .con .execute (" BEGIN EXCLUSIVE TRANSACTION;" )
167
196
168
197
with self ._internal_lock :
169
198
self ._current_mode = "write"
170
199
self ._lock_level = 1
171
-
200
+
172
201
return AcquireReturnProxy (lock = self )
173
202
174
203
except sqlite3 .OperationalError as e :
175
- if ' database is locked' not in str (e ):
204
+ if " database is locked" not in str (e ):
176
205
raise # Re-raise if it is an unexpected error.
177
206
raise Timeout (self .lock_file )
178
207
finally :
@@ -183,7 +212,8 @@ def release(self, force: bool = False) -> None:
183
212
if self ._lock_level == 0 :
184
213
if force :
185
214
return
186
- raise RuntimeError ("Cannot release a lock that is not held" )
215
+ msg = "Cannot release a lock that is not held"
216
+ raise RuntimeError (msg )
187
217
if force :
188
218
self ._lock_level = 0
189
219
else :
@@ -200,10 +230,11 @@ def release(self, force: bool = False) -> None:
200
230
# (We provide two context managers as helpers.)
201
231
202
232
@contextmanager
203
- def read_lock (self , timeout : float | None = None ,
204
- blocking : bool | None = None ):
205
- """Context manager for acquiring a read lock.
206
- Attempts to upgrade to write lock are disallowed."""
233
+ def read_lock (self , timeout : float | None = None , blocking : bool | None = None ):
234
+ """
235
+ Context manager for acquiring a read lock.
236
+ Attempts to upgrade to write lock are disallowed.
237
+ """
207
238
if timeout is None :
208
239
timeout = self .timeout
209
240
if blocking is None :
@@ -215,10 +246,11 @@ def read_lock(self, timeout: float | None = None,
215
246
self .release ()
216
247
217
248
@contextmanager
218
- def write_lock (self , timeout : float | None = None ,
219
- blocking : bool | None = None ):
220
- """Context manager for acquiring a write lock.
221
- Acquiring read locks on the same file while helding a write lock is prohibited."""
249
+ def write_lock (self , timeout : float | None = None , blocking : bool | None = None ):
250
+ """
251
+ Context manager for acquiring a write lock.
252
+ Acquiring read locks on the same file while helding a write lock is prohibited.
253
+ """
222
254
if timeout is None :
223
255
timeout = self .timeout
224
256
if blocking is None :
@@ -228,9 +260,7 @@ def write_lock(self, timeout: float | None = None,
228
260
yield
229
261
finally :
230
262
self .release ()
231
-
263
+
232
264
def __del__ (self ) -> None :
233
265
"""Called when the lock object is deleted."""
234
266
self .release (force = True )
235
-
236
-
0 commit comments