Skip to content

Commit 1d23075

Browse files
committed
Moved LockedFD and its test into the gitdb project
1 parent 06590ae commit 1d23075

File tree

3 files changed

+5
-214
lines changed

3 files changed

+5
-214
lines changed

lib/git/ext/gitdb

Submodule gitdb updated from 133988a to f50643f

lib/git/utils.py

+3-141
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
from gitdb.util import (
1313
stream_copy,
14-
make_sha
14+
make_sha,
15+
FDStreamWrapper,
16+
LockedFD
1517
)
1618

1719

@@ -271,146 +273,6 @@ def _obtain_lock(self):
271273
break
272274
# END endless loop
273275

274-
275-
class FDStreamWrapper(object):
276-
"""A simple wrapper providing the most basic functions on a file descriptor
277-
with the fileobject interface. Cannot use os.fdopen as the resulting stream
278-
takes ownership"""
279-
__slots__ = ("_fd", '_pos')
280-
def __init__(self, fd):
281-
self._fd = fd
282-
self._pos = 0
283-
284-
def write(self, data):
285-
self._pos += len(data)
286-
os.write(self._fd, data)
287-
288-
def read(self, count=0):
289-
if count == 0:
290-
count = os.path.getsize(self._filepath)
291-
# END handle read everything
292-
293-
bytes = os.read(self._fd, count)
294-
self._pos += len(bytes)
295-
return bytes
296-
297-
def fileno(self):
298-
return self._fd
299-
300-
def tell(self):
301-
return self._pos
302-
303-
304-
class LockedFD(LockFile):
305-
"""This class facilitates a safe read and write operation to a file on disk.
306-
If we write to 'file', we obtain a lock file at 'file.lock' and write to
307-
that instead. If we succeed, the lock file will be renamed to overwrite
308-
the original file.
309-
310-
When reading, we obtain a lock file, but to prevent other writers from
311-
succeeding while we are reading the file.
312-
313-
This type handles error correctly in that it will assure a consistent state
314-
on destruction.
315-
316-
:note: with this setup, parallel reading is not possible"""
317-
__slots__ = ("_filepath", '_fd', '_write')
318-
319-
def __init__(self, filepath):
320-
"""Initialize an instance with the givne filepath"""
321-
self._filepath = filepath
322-
self._fd = None
323-
self._write = None # if True, we write a file
324-
325-
def __del__(self):
326-
# will do nothing if the file descriptor is already closed
327-
if self._fd is not None:
328-
self.rollback()
329-
330-
def _lockfilepath(self):
331-
return "%s.lock" % self._filepath
332-
333-
def open(self, write=False, stream=False):
334-
"""Open the file descriptor for reading or writing, both in binary mode.
335-
:param write: if True, the file descriptor will be opened for writing. Other
336-
wise it will be opened read-only.
337-
:param stream: if True, the file descriptor will be wrapped into a simple stream
338-
object which supports only reading or writing
339-
:return: fd to read from or write to. It is still maintained by this instance
340-
and must not be closed directly
341-
:raise IOError: if the lock could not be retrieved
342-
:raise OSError: If the actual file could not be opened for reading
343-
:note: must only be called once"""
344-
if self._write is not None:
345-
raise AssertionError("Called %s multiple times" % self.open)
346-
347-
self._write = write
348-
349-
# try to open the lock file
350-
binary = getattr(os, 'O_BINARY', 0)
351-
lockmode = os.O_WRONLY | os.O_CREAT | os.O_EXCL | binary
352-
try:
353-
fd = os.open(self._lockfilepath(), lockmode)
354-
if not write:
355-
os.close(fd)
356-
else:
357-
self._fd = fd
358-
# END handle file descriptor
359-
except OSError:
360-
raise IOError("Lock at %r could not be obtained" % self._lockfilepath())
361-
# END handle lock retrieval
362-
363-
# open actual file if required
364-
if self._fd is None:
365-
# we could specify exlusive here, as we obtained the lock anyway
366-
self._fd = os.open(self._filepath, os.O_RDONLY | binary)
367-
# END open descriptor for reading
368-
369-
if stream:
370-
return FDStreamWrapper(self._fd)
371-
else:
372-
return self._fd
373-
# END handle stream
374-
375-
def commit(self):
376-
"""When done writing, call this function to commit your changes into the
377-
actual file.
378-
The file descriptor will be closed, and the lockfile handled.
379-
:note: can be called multiple times"""
380-
self._end_writing(successful=True)
381-
382-
def rollback(self):
383-
"""Abort your operation without any changes. The file descriptor will be
384-
closed, and the lock released.
385-
:note: can be called multiple times"""
386-
self._end_writing(successful=False)
387-
388-
def _end_writing(self, successful=True):
389-
"""Handle the lock according to the write mode """
390-
if self._write is None:
391-
raise AssertionError("Cannot end operation if it wasn't started yet")
392-
393-
if self._fd is None:
394-
return
395-
396-
os.close(self._fd)
397-
self._fd = None
398-
399-
lockfile = self._lockfilepath()
400-
if self._write and successful:
401-
# on windows, rename does not silently overwrite the existing one
402-
if sys.platform == "win32":
403-
if os.path.isfile(self._filepath):
404-
os.remove(self._filepath)
405-
# END remove if exists
406-
# END win32 special handling
407-
os.rename(lockfile, self._filepath)
408-
else:
409-
# just delete the file so far, we failed
410-
os.remove(lockfile)
411-
# END successful handling
412-
413-
414276

415277
class LazyMixin(object):
416278
"""

test/git/test_utils.py

+1-72
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from git.objects.utils import *
1313
from git import *
1414
from git.cmd import dashify
15+
1516
import time
1617

1718

@@ -68,78 +69,6 @@ def test_blocking_lock_file(self):
6869
elapsed = time.time() - start
6970
assert elapsed <= wait_time + 0.02 # some extra time it may cost
7071

71-
def _cmp_contents(self, file_path, data):
72-
# raise if data from file at file_path
73-
# does not match data string
74-
fp = open(file_path, "rb")
75-
try:
76-
assert fp.read() == data
77-
finally:
78-
fp.close()
79-
80-
def test_safe_operation(self):
81-
my_file = tempfile.mktemp()
82-
orig_data = "hello"
83-
new_data = "world"
84-
my_file_fp = open(my_file, "wb")
85-
my_file_fp.write(orig_data)
86-
my_file_fp.close()
87-
88-
try:
89-
lfd = LockedFD(my_file)
90-
lockfilepath = lfd._lockfilepath()
91-
92-
# cannot end before it was started
93-
self.failUnlessRaises(AssertionError, lfd.rollback)
94-
self.failUnlessRaises(AssertionError, lfd.commit)
95-
96-
# open for writing
97-
assert not os.path.isfile(lockfilepath)
98-
wfd = lfd.open(write=True)
99-
assert lfd._fd is wfd
100-
assert os.path.isfile(lockfilepath)
101-
102-
# write data and fail
103-
os.write(wfd, new_data)
104-
lfd.rollback()
105-
assert lfd._fd is None
106-
self._cmp_contents(my_file, orig_data)
107-
assert not os.path.isfile(lockfilepath)
108-
109-
# additional call doesnt fail
110-
lfd.commit()
111-
lfd.rollback()
112-
113-
# test reading
114-
lfd = LockedFD(my_file)
115-
rfd = lfd.open(write=False)
116-
assert os.read(rfd, len(orig_data)) == orig_data
117-
118-
assert os.path.isfile(lockfilepath)
119-
# deletion rolls back
120-
del(lfd)
121-
assert not os.path.isfile(lockfilepath)
122-
123-
124-
# write data - concurrently
125-
lfd = LockedFD(my_file)
126-
olfd = LockedFD(my_file)
127-
assert not os.path.isfile(lockfilepath)
128-
wfdstream = lfd.open(write=True, stream=True) # this time as stream
129-
assert os.path.isfile(lockfilepath)
130-
# another one fails
131-
self.failUnlessRaises(IOError, olfd.open)
132-
133-
wfdstream.write(new_data)
134-
lfd.commit()
135-
assert not os.path.isfile(lockfilepath)
136-
self._cmp_contents(my_file, new_data)
137-
138-
# could test automatic _end_writing on destruction
139-
finally:
140-
os.remove(my_file)
141-
# END final cleanup
142-
14372
def test_user_id(self):
14473
assert '@' in get_user_id()
14574

0 commit comments

Comments
 (0)