Skip to content
This repository was archived by the owner on Jul 16, 2022. It is now read-only.

Commit b428419

Browse files
committed
feat: extract safeatomic wrapper module
1 parent 4183999 commit b428419

6 files changed

Lines changed: 50 additions & 227 deletions

File tree

README.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22
python-atomicwrites
33
===================
44

5+
This distribution is now only a thin compatibility wrapper around
6+
`safeatomic <https://github.com/atomic-libs/safeatomic>`_. New code
7+
should depend on ``safeatomic`` directly. The dependency is resolved
8+
from its GitHub repository, for example::
9+
10+
pip install git+https://github.com/atomic-libs/safeatomic.git
11+
512
Unmaintained
613
============
714

atomicwrites/__init__.py

Lines changed: 10 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -1,229 +1,14 @@
1-
import contextlib
2-
import io
3-
import os
4-
import sys
5-
import tempfile
1+
"""Compatibility wrapper around the :mod:`safeatomic` package."""
62

7-
try:
8-
import fcntl
9-
except ImportError:
10-
fcntl = None
3+
from __future__ import annotations
114

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+
)
1711

18-
__version__ = '1.4.1'
12+
__all__ = ["AtomicWriter", "atomic_write", "move_atomic", "replace_atomic"]
1913

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"

atomicwrites/safeatomic_wrapper.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""Internal adapter around the :mod:`safeatomic` package."""
2+
3+
from __future__ import annotations
4+
5+
__all__ = ["AtomicWriter", "atomic_write", "move_atomic", "replace_atomic"]
6+
7+
try: # pragma: no cover - handled in tests
8+
from safeatomic import (
9+
AtomicWriter,
10+
atomic_write,
11+
move_atomic,
12+
replace_atomic,
13+
)
14+
except Exception as exc: # pragma: no cover - makes dependency explicit
15+
raise ImportError(
16+
"python-atomicwrites now depends on the 'safeatomic' package."
17+
" Please install safeatomic to use this compatibility wrapper."
18+
) from exc

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build-system]
2+
requires = ["setuptools", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[tool.setuptools.extras_require]
6+
dev = ["pytest"]

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
long_description=open('README.rst').read(),
2525
packages=find_packages(exclude=['tests.*', 'tests']),
2626
include_package_data=True,
27+
install_requires=['safeatomic @ git+https://github.com/atomic-libs/safeatomic.git'],
2728
classifiers=[
2829
'License :: OSI Approved :: MIT License',
2930
'Programming Language :: Python :: 2',

tests/test_atomicwrites.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import errno
22
import os
33

4-
from atomicwrites import atomic_write
5-
64
import pytest
75

6+
safeatomic = pytest.importorskip("safeatomic")
7+
8+
from atomicwrites import atomic_write
9+
810

911
def test_atomic_write(tmpdir):
1012
fname = tmpdir.join('ha')
@@ -89,3 +91,7 @@ def test_atomic_write_in_pwd(tmpdir):
8991
assert len(tmpdir.listdir()) == 1
9092
finally:
9193
os.chdir(orig_curdir)
94+
95+
96+
def test_wrapper_exposes_safeatomic():
97+
assert atomic_write is safeatomic.atomic_write

0 commit comments

Comments
 (0)