Skip to content

Commit 12ef667

Browse files
committed
Untested Windows support, API rewrite
1 parent b0fb95f commit 12ef667

File tree

3 files changed

+78
-39
lines changed

3 files changed

+78
-39
lines changed

atomicwrites/__init__.py

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,75 @@
1-
import codecs
21
import contextlib
32
import os
3+
import sys
44
import tempfile
55

66
__version__ = '0.1.1'
77

8+
if sys.platform != 'win32':
9+
def replace_atomic(src, dst):
10+
os.rename(src, dst)
811

9-
class AtomicWriterBase(object):
12+
def move_atomic(src, dst):
13+
os.link(src, dst)
14+
os.unlink(src)
15+
else:
16+
import win32api
17+
import win32con
18+
19+
_windows_default_flags = win32con.WRITE_THROUGH
20+
21+
def replace_atomic(src, dst):
22+
win32api.MoveFileEx(
23+
src, dst,
24+
win32con.MOVEFILE_REPLACE_EXISTING | _windows_default_flags
25+
)
26+
27+
def move_atomic(src, dst):
28+
win32api.MoveFileEx(
29+
src, dst,
30+
_windows_default_flags
31+
)
32+
33+
34+
replace_atomic.__doc__ = \
1035
'''
11-
A helper class for performing atomic writes. Not fully implemented, you
12-
should use a subclass of it instead. Usage::
36+
Move ``src`` to ``dst``. If ``dst`` exists, it will be silently
37+
overwritten.
38+
39+
Both paths must reside on the same filesystem for the operation to be
40+
atomic.
41+
''' + (replace_atomic.__doc__ or '')
42+
43+
44+
move_atomic.__doc__ = \
45+
'''
46+
Move ``src`` to ``dst``. There might a timewindow where both filesystem
47+
entries exist. If ``dst`` already exists, an error will be raised.
48+
49+
Both paths must reside on the same filesystem for the operation to be
50+
atomic.
51+
''' + (move_atomic.__doc__ or '')
1352

14-
with MyWriter(path).open() as f:
53+
54+
class AtomicWriter(object):
55+
'''
56+
A helper class for performing atomic writes. Usage::
57+
58+
with AtomicWriter(path).open() as f:
1559
f.write(...)
1660
61+
62+
It uses a temporary file in the same directory as the given path. This
63+
ensures that the temporary file resides on the same filesystem.
64+
65+
The temporary file will then be atomically moved to the target location: On
66+
POSIX, it will use ``rename`` if files should be overwritten, otherwise a
67+
combination of ``link`` and ``unlink``. On Windows, it uses ``MoveFileEx``
68+
(see MSDN_) with the appropriate flags.
69+
70+
.. _MSDN::
71+
https://msdn.microsoft.com/en-us/library/windows/desktop/aa365240%28v=vs.85%29.aspx
72+
1773
:param path: The destination filepath. May or may not exist.
1874
:param overwrite: If set to false, an error is raised if ``path`` exists.
1975
Either way, the operation is atomic.
@@ -24,16 +80,10 @@ def __init__(self, path, overwrite=False):
2480
self._overwrite = overwrite
2581

2682
def open(self, mode='w'):
27-
'''Prepare the temporary file object and return it.'''
28-
raise NotImplementedError()
29-
30-
def commit(self):
31-
'''Move the temporary file to the target location.'''
32-
raise NotImplementedError()
33-
34-
def rollback(self):
35-
'''Clean up all temporary resources.'''
36-
raise NotImplementedError()
83+
'''
84+
Open the temporary file.
85+
'''
86+
return self._open(lambda: self.get_fileobject(mode=mode))
3787

3888
@contextlib.contextmanager
3989
def _open(self, get_fileobject):
@@ -46,39 +96,25 @@ def _open(self, get_fileobject):
4696
else:
4797
self.commit()
4898

49-
50-
class PosixAtomicWriter(AtomicWriterBase):
51-
'''
52-
An implementation of :py:class:`AtomicWriterBase` for POSIX-compliant
53-
filesystems. It uses temporary files in the same directory.
54-
'''
55-
5699
def get_fileobject(self, mode):
57100
'''Return the temporary path to use.'''
58101
tmpdir = os.path.dirname(self._path)
59102
f = tempfile.NamedTemporaryFile(mode=mode, dir=tmpdir, delete=False)
60103
self._tmppath = f.name
61104
return f
62105

63-
def open(self, mode='w', **open_kwargs):
64-
'''
65-
Open the temporary file. Any arguments will be passed on to the builtin
66-
``open`` function.
67-
'''
68-
return self._open(lambda: self.get_fileobject(mode=mode))
69-
70106
def commit(self):
107+
'''Move the temporary file to the target location.'''
71108
if self._overwrite:
72109
os.rename(self._tmppath, self._path) # atomic
73110
else:
74111
os.link(self._tmppath, self._path) # atomic, fails if file exists
75112
os.unlink(self._tmppath)
76113

77114
def rollback(self):
115+
'''Clean up all temporary resources.'''
78116
os.unlink(self._tmppath)
79117

80-
AtomicWriter = PosixAtomicWriter # for current OS
81-
82118

83119
def atomic_write(path, mode='w', overwrite=False, writer_cls=AtomicWriter,
84120
**open_kwargs):

docs/index.rst

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@ API
1010
Low-level API
1111
-------------
1212

13-
.. autoclass:: AtomicWriterBase
14-
:members: open
13+
.. autofunction:: replace_atomic
1514

16-
.. autoclass:: PosixAtomicWriter
17-
:members: open
15+
.. autofunction:: move_atomic
1816

19-
.. class:: AtomicWriter
20-
21-
Automatically selected writer for the current OS.
17+
.. autoclass:: AtomicWriter
18+
:members:
2219

2320
License
2421
-------

setup.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import ast
44
import re
5+
import sys
56

67
from setuptools import find_packages, setup
78

@@ -14,6 +15,10 @@
1415
f.read().decode('utf-8')).group(1)))
1516

1617

18+
deps = []
19+
if sys.platform == 'win32':
20+
deps.append('pywin32')
21+
1722
setup(
1823
name='atomicwrites',
1924
version=version,
@@ -24,5 +29,6 @@
2429
license='MIT',
2530
long_description=open('README.rst').read(),
2631
packages=find_packages(exclude=['tests.*', 'tests']),
27-
include_package_data=True
32+
include_package_data=True,
33+
install_requires=deps
2834
)

0 commit comments

Comments
 (0)