Skip to content

Commit da241c6

Browse files
authored
Split main module (#94)
Signed-off-by: Bernát Gábor <[email protected]>
1 parent 6079e6d commit da241c6

File tree

8 files changed

+372
-455
lines changed

8 files changed

+372
-455
lines changed

src/filelock/__init__.py

Lines changed: 23 additions & 428 deletions
Large diffs are not rendered by default.

src/filelock/_api.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import logging
2+
import time
3+
from threading import Lock
4+
5+
from ._error import Timeout
6+
7+
_LOGGER = logging.getLogger(__name__)
8+
9+
10+
# This is a helper class which is returned by :meth:`BaseFileLock.acquire` and wraps the lock to make sure __enter__
11+
# is not called twice when entering the with statement. If we would simply return *self*, the lock would be acquired
12+
# again in the *__enter__* method of the BaseFileLock, but not released again automatically. issue #37 (memory leak)
13+
class AcquireReturnProxy(object):
14+
def __init__(self, lock):
15+
self.lock = lock
16+
17+
def __enter__(self):
18+
return self.lock
19+
20+
def __exit__(self, exc_type, exc_value, traceback):
21+
self.lock.release()
22+
23+
24+
class BaseFileLock(object):
25+
"""Implements the base class of a file lock."""
26+
27+
def __init__(self, lock_file, timeout=-1):
28+
""" """
29+
# The path to the lock file.
30+
self._lock_file = lock_file
31+
32+
# The file descriptor for the *_lock_file* as it is returned by the os.open() function.
33+
# This file lock is only NOT None, if the object currently holds the lock.
34+
self._lock_file_fd = None
35+
36+
# The default timeout value.
37+
self.timeout = timeout
38+
39+
# We use this lock primarily for the lock counter.
40+
self._thread_lock = Lock()
41+
42+
# The lock counter is used for implementing the nested locking mechanism. Whenever the lock is acquired, the
43+
# counter is increased and the lock is only released, when this value is 0 again.
44+
self._lock_counter = 0
45+
46+
@property
47+
def lock_file(self):
48+
"""The path to the lock file."""
49+
return self._lock_file
50+
51+
@property
52+
def timeout(self):
53+
"""
54+
You can set a default timeout for the filelock. It will be used as fallback value in the acquire method, if no
55+
timeout value (*None*) is given. If you want to disable the timeout, set it to a negative value. A timeout of
56+
0 means, that there is exactly one attempt to acquire the file lock.
57+
58+
.. versionadded:: 2.0.0
59+
"""
60+
return self._timeout
61+
62+
@timeout.setter
63+
def timeout(self, value):
64+
"""change the timeout parameter"""
65+
self._timeout = float(value)
66+
67+
def _acquire(self):
68+
"""If the file lock could be acquired, self._lock_file_fd holds the file descriptor of the lock file."""
69+
raise NotImplementedError
70+
71+
def _release(self):
72+
"""Releases the lock and sets self._lock_file_fd to None."""
73+
raise NotImplementedError
74+
75+
@property
76+
def is_locked(self):
77+
"""True, if the object holds the file lock.
78+
79+
.. versionchanged:: 2.0.0
80+
81+
This was previously a method and is now a property.
82+
"""
83+
return self._lock_file_fd is not None
84+
85+
def acquire(self, timeout=None, poll_intervall=0.05):
86+
"""
87+
Acquires the file lock or fails with a :exc:`Timeout` error.
88+
89+
.. code-block:: python
90+
91+
# You can use this method in the context manager (recommended)
92+
with lock.acquire():
93+
pass
94+
95+
# Or use an equivalent try-finally construct:
96+
lock.acquire()
97+
try:
98+
pass
99+
finally:
100+
lock.release()
101+
102+
:arg float timeout:
103+
The maximum time waited for the file lock.
104+
If ``timeout < 0``, there is no timeout and this method will
105+
block until the lock could be acquired.
106+
If ``timeout`` is None, the default :attr:`~timeout` is used.
107+
108+
:arg float poll_intervall:
109+
We check once in *poll_intervall* seconds if we can acquire the
110+
file lock.
111+
112+
:raises Timeout:
113+
if the lock could not be acquired in *timeout* seconds.
114+
115+
.. versionchanged:: 2.0.0
116+
117+
This method returns now a *proxy* object instead of *self*,
118+
so that it can be used in a with statement without side effects.
119+
"""
120+
# Use the default timeout, if no timeout is provided.
121+
if timeout is None:
122+
timeout = self.timeout
123+
124+
# Increment the number right at the beginning. We can still undo it, if something fails.
125+
with self._thread_lock:
126+
self._lock_counter += 1
127+
128+
lock_id = id(self)
129+
lock_filename = self._lock_file
130+
start_time = time.time()
131+
try:
132+
while True:
133+
with self._thread_lock:
134+
if not self.is_locked:
135+
_LOGGER.debug("Attempting to acquire lock %s on %s", lock_id, lock_filename)
136+
self._acquire()
137+
138+
if self.is_locked:
139+
_LOGGER.info("Lock %s acquired on %s", lock_id, lock_filename)
140+
break
141+
elif 0 <= timeout < time.time() - start_time:
142+
_LOGGER.debug("Timeout on acquiring lock %s on %s", lock_id, lock_filename)
143+
raise Timeout(self._lock_file)
144+
else:
145+
msg = "Lock %s not acquired on %s, waiting %s seconds ..."
146+
_LOGGER.debug(msg, lock_id, lock_filename, poll_intervall)
147+
time.sleep(poll_intervall)
148+
except BaseException: # Something did go wrong, so decrement the counter.
149+
with self._thread_lock:
150+
self._lock_counter = max(0, self._lock_counter - 1)
151+
raise
152+
return AcquireReturnProxy(lock=self)
153+
154+
def release(self, force=False):
155+
"""
156+
Releases the file lock.
157+
158+
Please note, that the lock is only completly released, if the lock
159+
counter is 0.
160+
161+
Also note, that the lock file itself is not automatically deleted.
162+
163+
:arg bool force:
164+
If true, the lock counter is ignored and the lock is released in
165+
every case.
166+
"""
167+
with self._thread_lock:
168+
169+
if self.is_locked:
170+
self._lock_counter -= 1
171+
172+
if self._lock_counter == 0 or force:
173+
lock_id, lock_filename = id(self), self._lock_file
174+
175+
_LOGGER.debug("Attempting to release lock %s on %s", lock_id, lock_filename)
176+
self._release()
177+
self._lock_counter = 0
178+
_LOGGER.info("Lock %s released on %s", lock_id, lock_filename)
179+
180+
def __enter__(self):
181+
self.acquire()
182+
return self
183+
184+
def __exit__(self, exc_type, exc_value, traceback):
185+
self.release()
186+
187+
def __del__(self):
188+
self.release(force=True)
189+
190+
191+
__all__ = [
192+
"BaseFileLock",
193+
"AcquireReturnProxy",
194+
]

src/filelock/_error.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import sys
2+
3+
if sys.version[0] == 3:
4+
TimeoutError = TimeoutError
5+
else:
6+
TimeoutError = OSError
7+
8+
9+
class Timeout(TimeoutError):
10+
"""Raised when the lock could not be acquired in *timeout* seconds."""
11+
12+
def __init__(self, lock_file):
13+
#: The path of the file lock.
14+
self.lock_file = lock_file
15+
16+
def __str__(self):
17+
return "The file lock '{}' could not be acquired.".format(self.lock_file)
18+
19+
20+
__all__ = [
21+
"Timeout",
22+
]

src/filelock/_soft.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import os
2+
3+
from ._api import BaseFileLock
4+
5+
6+
class SoftFileLock(BaseFileLock):
7+
"""Simply watches the existence of the lock file."""
8+
9+
def _acquire(self):
10+
open_mode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_TRUNC
11+
try:
12+
fd = os.open(self._lock_file, open_mode)
13+
except OSError:
14+
pass
15+
else:
16+
self._lock_file_fd = fd
17+
18+
def _release(self):
19+
os.close(self._lock_file_fd)
20+
self._lock_file_fd = None
21+
try:
22+
os.remove(self._lock_file)
23+
# The file is already deleted and that's what we want.
24+
except OSError:
25+
pass
26+
27+
28+
__all__ = [
29+
"SoftFileLock",
30+
]

src/filelock/_unix.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import os
2+
3+
from ._api import BaseFileLock
4+
5+
try:
6+
import fcntl
7+
except ImportError:
8+
fcntl = None
9+
10+
#: a flag to indicate if the fcntl API is available
11+
has_fcntl = fcntl is not None
12+
13+
14+
class UnixFileLock(BaseFileLock):
15+
"""Uses the :func:`fcntl.flock` to hard lock the lock file on unix systems."""
16+
17+
def _acquire(self):
18+
open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
19+
fd = os.open(self._lock_file, open_mode)
20+
try:
21+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
22+
except (OSError, IOError):
23+
os.close(fd)
24+
else:
25+
self._lock_file_fd = fd
26+
27+
def _release(self):
28+
# Do not remove the lockfile:
29+
# https://github.com/tox-dev/py-filelock/issues/31
30+
# https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition
31+
fd = self._lock_file_fd
32+
self._lock_file_fd = None
33+
fcntl.flock(fd, fcntl.LOCK_UN)
34+
os.close(fd)
35+
36+
37+
__all__ = [
38+
"has_fcntl",
39+
"UnixFileLock",
40+
]

src/filelock/_windows.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
3+
from ._api import BaseFileLock
4+
5+
try:
6+
import msvcrt
7+
except ImportError:
8+
msvcrt = None
9+
10+
11+
class WindowsFileLock(BaseFileLock):
12+
"""Uses the :func:`msvcrt.locking` function to hard lock the lock file on windows systems."""
13+
14+
def _acquire(self):
15+
open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC
16+
try:
17+
fd = os.open(self._lock_file, open_mode)
18+
except OSError:
19+
pass
20+
else:
21+
try:
22+
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
23+
except (OSError, IOError):
24+
os.close(fd)
25+
else:
26+
self._lock_file_fd = fd
27+
28+
def _release(self):
29+
fd = self._lock_file_fd
30+
self._lock_file_fd = None
31+
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
32+
os.close(fd)
33+
34+
try:
35+
os.remove(self._lock_file)
36+
# Probably another instance of the application hat acquired the file lock.
37+
except OSError:
38+
pass
39+
40+
41+
__all__ = [
42+
"WindowsFileLock",
43+
]

0 commit comments

Comments
 (0)