|
1 | | -import contextlib |
2 | | -import io |
3 | | -import os |
4 | | -import sys |
5 | | -import tempfile |
| 1 | +"""Compatibility wrapper around the :mod:`safeatomic` package.""" |
6 | 2 |
|
7 | | -try: |
8 | | - import fcntl |
9 | | -except ImportError: |
10 | | - fcntl = None |
| 3 | +from __future__ import annotations |
11 | 4 |
|
12 | | -# `fspath` was added in Python 3.6 |
13 | | -try: |
14 | | - from os import fspath |
15 | | -except ImportError: |
16 | | - fspath = None |
| 5 | +from .safeatomic_wrapper import ( |
| 6 | + AtomicWriter, |
| 7 | + atomic_write, |
| 8 | + move_atomic, |
| 9 | + replace_atomic, |
| 10 | +) |
17 | 11 |
|
18 | | -__version__ = '1.4.1' |
| 12 | +__all__ = ["AtomicWriter", "atomic_write", "move_atomic", "replace_atomic"] |
19 | 13 |
|
20 | | - |
21 | | -PY2 = sys.version_info[0] == 2 |
22 | | - |
23 | | -text_type = unicode if PY2 else str # noqa |
24 | | - |
25 | | - |
26 | | -def _path_to_unicode(x): |
27 | | - if not isinstance(x, text_type): |
28 | | - return x.decode(sys.getfilesystemencoding()) |
29 | | - return x |
30 | | - |
31 | | - |
32 | | -DEFAULT_MODE = "wb" if PY2 else "w" |
33 | | - |
34 | | - |
35 | | -_proper_fsync = os.fsync |
36 | | - |
37 | | - |
38 | | -if sys.platform != 'win32': |
39 | | - if hasattr(fcntl, 'F_FULLFSYNC'): |
40 | | - def _proper_fsync(fd): |
41 | | - # https://lists.apple.com/archives/darwin-dev/2005/Feb/msg00072.html |
42 | | - # https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/fsync.2.html |
43 | | - # https://github.com/untitaker/python-atomicwrites/issues/6 |
44 | | - fcntl.fcntl(fd, fcntl.F_FULLFSYNC) |
45 | | - |
46 | | - def _sync_directory(directory): |
47 | | - # Ensure that filenames are written to disk |
48 | | - fd = os.open(directory, 0) |
49 | | - try: |
50 | | - _proper_fsync(fd) |
51 | | - finally: |
52 | | - os.close(fd) |
53 | | - |
54 | | - def _replace_atomic(src, dst): |
55 | | - os.rename(src, dst) |
56 | | - _sync_directory(os.path.normpath(os.path.dirname(dst))) |
57 | | - |
58 | | - def _move_atomic(src, dst): |
59 | | - os.link(src, dst) |
60 | | - os.unlink(src) |
61 | | - |
62 | | - src_dir = os.path.normpath(os.path.dirname(src)) |
63 | | - dst_dir = os.path.normpath(os.path.dirname(dst)) |
64 | | - _sync_directory(dst_dir) |
65 | | - if src_dir != dst_dir: |
66 | | - _sync_directory(src_dir) |
67 | | -else: |
68 | | - from ctypes import windll, WinError |
69 | | - |
70 | | - _MOVEFILE_REPLACE_EXISTING = 0x1 |
71 | | - _MOVEFILE_WRITE_THROUGH = 0x8 |
72 | | - _windows_default_flags = _MOVEFILE_WRITE_THROUGH |
73 | | - |
74 | | - def _handle_errors(rv): |
75 | | - if not rv: |
76 | | - raise WinError() |
77 | | - |
78 | | - def _replace_atomic(src, dst): |
79 | | - _handle_errors(windll.kernel32.MoveFileExW( |
80 | | - _path_to_unicode(src), _path_to_unicode(dst), |
81 | | - _windows_default_flags | _MOVEFILE_REPLACE_EXISTING |
82 | | - )) |
83 | | - |
84 | | - def _move_atomic(src, dst): |
85 | | - _handle_errors(windll.kernel32.MoveFileExW( |
86 | | - _path_to_unicode(src), _path_to_unicode(dst), |
87 | | - _windows_default_flags |
88 | | - )) |
89 | | - |
90 | | - |
91 | | -def replace_atomic(src, dst): |
92 | | - ''' |
93 | | - Move ``src`` to ``dst``. If ``dst`` exists, it will be silently |
94 | | - overwritten. |
95 | | -
|
96 | | - Both paths must reside on the same filesystem for the operation to be |
97 | | - atomic. |
98 | | - ''' |
99 | | - return _replace_atomic(src, dst) |
100 | | - |
101 | | - |
102 | | -def move_atomic(src, dst): |
103 | | - ''' |
104 | | - Move ``src`` to ``dst``. There might a timewindow where both filesystem |
105 | | - entries exist. If ``dst`` already exists, :py:exc:`FileExistsError` will be |
106 | | - raised. |
107 | | -
|
108 | | - Both paths must reside on the same filesystem for the operation to be |
109 | | - atomic. |
110 | | - ''' |
111 | | - return _move_atomic(src, dst) |
112 | | - |
113 | | - |
114 | | -class AtomicWriter(object): |
115 | | - ''' |
116 | | - A helper class for performing atomic writes. Usage:: |
117 | | -
|
118 | | - with AtomicWriter(path).open() as f: |
119 | | - f.write(...) |
120 | | -
|
121 | | - :param path: The destination filepath. May or may not exist. |
122 | | - :param mode: The filemode for the temporary file. This defaults to `wb` in |
123 | | - Python 2 and `w` in Python 3. |
124 | | - :param overwrite: If set to false, an error is raised if ``path`` exists. |
125 | | - Errors are only raised after the file has been written to. Either way, |
126 | | - the operation is atomic. |
127 | | - :param open_kwargs: Keyword-arguments to pass to the underlying |
128 | | - :py:func:`open` call. This can be used to set the encoding when opening |
129 | | - files in text-mode. |
130 | | -
|
131 | | - If you need further control over the exact behavior, you are encouraged to |
132 | | - subclass. |
133 | | - ''' |
134 | | - |
135 | | - def __init__(self, path, mode=DEFAULT_MODE, overwrite=False, |
136 | | - **open_kwargs): |
137 | | - if 'a' in mode: |
138 | | - raise ValueError( |
139 | | - 'Appending to an existing file is not supported, because that ' |
140 | | - 'would involve an expensive `copy`-operation to a temporary ' |
141 | | - 'file. Open the file in normal `w`-mode and copy explicitly ' |
142 | | - 'if that\'s what you\'re after.' |
143 | | - ) |
144 | | - if 'x' in mode: |
145 | | - raise ValueError('Use the `overwrite`-parameter instead.') |
146 | | - if 'w' not in mode: |
147 | | - raise ValueError('AtomicWriters can only be written to.') |
148 | | - |
149 | | - # Attempt to convert `path` to `str` or `bytes` |
150 | | - if fspath is not None: |
151 | | - path = fspath(path) |
152 | | - |
153 | | - self._path = path |
154 | | - self._mode = mode |
155 | | - self._overwrite = overwrite |
156 | | - self._open_kwargs = open_kwargs |
157 | | - |
158 | | - def open(self): |
159 | | - ''' |
160 | | - Open the temporary file. |
161 | | - ''' |
162 | | - return self._open(self.get_fileobject) |
163 | | - |
164 | | - @contextlib.contextmanager |
165 | | - def _open(self, get_fileobject): |
166 | | - f = None # make sure f exists even if get_fileobject() fails |
167 | | - try: |
168 | | - success = False |
169 | | - with get_fileobject(**self._open_kwargs) as f: |
170 | | - yield f |
171 | | - self.sync(f) |
172 | | - self.commit(f) |
173 | | - success = True |
174 | | - finally: |
175 | | - if not success: |
176 | | - try: |
177 | | - self.rollback(f) |
178 | | - except Exception: |
179 | | - pass |
180 | | - |
181 | | - def get_fileobject(self, suffix="", prefix=tempfile.gettempprefix(), |
182 | | - dir=None, **kwargs): |
183 | | - '''Return the temporary file to use.''' |
184 | | - if dir is None: |
185 | | - dir = os.path.normpath(os.path.dirname(self._path)) |
186 | | - descriptor, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, |
187 | | - dir=dir) |
188 | | - # io.open() will take either the descriptor or the name, but we need |
189 | | - # the name later for commit()/replace_atomic() and couldn't find a way |
190 | | - # to get the filename from the descriptor. |
191 | | - os.close(descriptor) |
192 | | - kwargs['mode'] = self._mode |
193 | | - kwargs['file'] = name |
194 | | - return io.open(**kwargs) |
195 | | - |
196 | | - def sync(self, f): |
197 | | - '''responsible for clearing as many file caches as possible before |
198 | | - commit''' |
199 | | - f.flush() |
200 | | - _proper_fsync(f.fileno()) |
201 | | - |
202 | | - def commit(self, f): |
203 | | - '''Move the temporary file to the target location.''' |
204 | | - if self._overwrite: |
205 | | - replace_atomic(f.name, self._path) |
206 | | - else: |
207 | | - move_atomic(f.name, self._path) |
208 | | - |
209 | | - def rollback(self, f): |
210 | | - '''Clean up all temporary resources.''' |
211 | | - os.unlink(f.name) |
212 | | - |
213 | | - |
214 | | -def atomic_write(path, writer_cls=AtomicWriter, **cls_kwargs): |
215 | | - ''' |
216 | | - Simple atomic writes. This wraps :py:class:`AtomicWriter`:: |
217 | | -
|
218 | | - with atomic_write(path) as f: |
219 | | - f.write(...) |
220 | | -
|
221 | | - :param path: The target path to write to. |
222 | | - :param writer_cls: The writer class to use. This parameter is useful if you |
223 | | - subclassed :py:class:`AtomicWriter` to change some behavior and want to |
224 | | - use that new subclass. |
225 | | -
|
226 | | - Additional keyword arguments are passed to the writer class. See |
227 | | - :py:class:`AtomicWriter`. |
228 | | - ''' |
229 | | - return writer_cls(path, **cls_kwargs).open() |
| 14 | +__version__ = "2.0.0" |
0 commit comments