Skip to content

Commit f09c004

Browse files
committed
atomicwrites: remove dependency
The package has been deprecated. Replace usage with open-coded replacement. Also copy the test suite for the function. Lifted from pimutils/khal#1393 (which I also authored). Fixes: #24689
1 parent 2c100d1 commit f09c004

File tree

6 files changed

+116
-9
lines changed

6 files changed

+116
-9
lines changed

binder/environment.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ dependencies:
1111
# to requirements/main.yml, and copy it here.
1212
- aiohttp >=3.11.2
1313
- asyncssh >=2.14.0,<3.0.0
14-
- atomicwrites >=1.2.0
1514
- bcrypt >=4.3.0
1615
- chardet >=2.0.0
1716
- cloudpickle >=0.5.0

requirements/main.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ channels:
99
dependencies:
1010
- aiohttp >=3.11.2
1111
- asyncssh >=2.14.0,<3.0.0
12-
- atomicwrites >=1.2.0
1312
- bcrypt >=4.3.0
1413
- chardet >=2.0.0
1514
- cloudpickle >=0.5.0

setup.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,6 @@ def run(self):
270270
'aiohttp>=3.11.2',
271271
'applaunchservices>=0.3.0;platform_system=="Darwin"',
272272
'asyncssh>=2.14.0,<3.0.0',
273-
'atomicwrites>=1.2.0',
274273
'bcrypt>=4.3.0',
275274
'chardet>=2.0.0',
276275
'cloudpickle>=0.5.0',

spyder/dependencies.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
AIOHTTP_REQVER = '>=3.11.2'
3434
APPLAUNCHSERVICES_REQVER = '>=0.3.0'
3535
ASYNCSSH_REQVER = '>=2.14.0,<3.0.0'
36-
ATOMICWRITES_REQVER = '>=1.2.0'
3736
BCRYPT_REQVER = ">=4.3.0"
3837
CHARDET_REQVER = '>=2.0.0'
3938
CLOUDPICKLE_REQVER = '>=0.5.0'
@@ -108,10 +107,6 @@
108107
'package_name': 'asyncssh',
109108
'features': _('Connect to remote kernels through SSH'),
110109
'required_version': ASYNCSSH_REQVER},
111-
{'modname': "atomicwrites",
112-
'package_name': "atomicwrites",
113-
'features': _("Atomic file writes in the Editor"),
114-
'required_version': ATOMICWRITES_REQVER},
115110
{'modname': "bcrypt",
116111
'package_name': "bcrypt",
117112
'features': _("Decrypt passphrase of SSH key files"),

spyder/tests/test_atomic_write.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# From https://github.com/untitaker/python-atomicwrites/blob/4183999d9b7e81af85dee070d5311299bdf5164c/tests/test_atomicwrites.py
2+
3+
import errno
4+
import os
5+
6+
from spyder.utils.encoding import atomic_write
7+
8+
import pytest
9+
10+
11+
def test_atomic_write(tmpdir):
12+
fname = tmpdir.join('ha')
13+
for i in range(2):
14+
with atomic_write(str(fname), overwrite=True, dir=tmpdir, mode='w') as f:
15+
f.write('hoho')
16+
17+
with pytest.raises(OSError) as excinfo:
18+
with atomic_write(str(fname), overwrite=False, dir=tmpdir, mode='w') as f:
19+
f.write('haha')
20+
21+
assert excinfo.value.errno == errno.EEXIST
22+
23+
assert fname.read() == 'hoho'
24+
assert len(tmpdir.listdir()) == 1
25+
26+
27+
def test_teardown(tmpdir):
28+
fname = tmpdir.join('ha')
29+
with pytest.raises(AssertionError):
30+
with atomic_write(str(fname), overwrite=True, dir=tmpdir, mode='w'):
31+
assert False
32+
33+
assert not tmpdir.listdir()
34+
35+
36+
def test_replace_simultaneously_created_file(tmpdir):
37+
fname = tmpdir.join('ha')
38+
with atomic_write(str(fname), overwrite=True, dir=tmpdir, mode='w') as f:
39+
f.write('hoho')
40+
fname.write('harhar')
41+
assert fname.read() == 'harhar'
42+
assert fname.read() == 'hoho'
43+
assert len(tmpdir.listdir()) == 1
44+
45+
46+
def test_dont_remove_simultaneously_created_file(tmpdir):
47+
fname = tmpdir.join('ha')
48+
with pytest.raises(OSError) as excinfo:
49+
with atomic_write(str(fname), overwrite=False, dir=tmpdir, mode='w') as f:
50+
f.write('hoho')
51+
fname.write('harhar')
52+
assert fname.read() == 'harhar'
53+
54+
assert excinfo.value.errno == errno.EEXIST
55+
assert fname.read() == 'harhar'
56+
assert len(tmpdir.listdir()) == 1
57+
58+
59+
# Verify that nested exceptions during rollback do not overwrite the initial
60+
# exception that triggered a rollback.
61+
def test_open_reraise(tmpdir):
62+
fname = tmpdir.join('ha')
63+
with pytest.raises(AssertionError):
64+
aw = atomic_write(str(fname), overwrite=False, dir=tmpdir, mode='w')
65+
with aw:
66+
# Mess with internals, so commit will trigger a ValueError. We're
67+
# testing that the initial AssertionError triggered below is
68+
# propagated up the stack, not the second exception triggered
69+
# during commit.
70+
aw.rollback = lambda: 1 / 0
71+
# Now trigger our own exception.
72+
assert False, "Intentional failure for testing purposes"
73+
74+
75+
def test_atomic_write_in_pwd(tmpdir):
76+
orig_curdir = os.getcwd()
77+
try:
78+
os.chdir(str(tmpdir))
79+
fname = 'ha'
80+
for i in range(2):
81+
with atomic_write(str(fname), overwrite=True, dir=tmpdir, mode='w') as f:
82+
f.write('hoho')
83+
84+
with pytest.raises(OSError) as excinfo:
85+
with atomic_write(str(fname), overwrite=False, dir=tmpdir, mode='w') as f:
86+
f.write('haha')
87+
88+
assert excinfo.value.errno == errno.EEXIST
89+
90+
assert open(fname).read() == 'hoho'
91+
assert len(tmpdir.listdir()) == 1
92+
finally:
93+
os.chdir(orig_curdir)

spyder/utils/encoding.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
# Standard library imports
1515
from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF32
16+
import contextlib
1617
import tempfile
1718
import locale
1819
import re
@@ -25,7 +26,6 @@
2526

2627
# Third-party imports
2728
from chardet.universaldetector import UniversalDetector
28-
from atomicwrites import atomic_write
2929

3030
# Local imports
3131
from spyder.py3compat import (is_string, to_text_string, is_binary_string,
@@ -244,6 +244,28 @@ def to_unicode(string):
244244
return string
245245

246246

247+
@contextlib.contextmanager
248+
def atomic_write(dest, overwrite, dir, mode):
249+
"""Atomically write a file"""
250+
fd, src = tempfile.mkstemp(prefix=os.path.basename(dest), dir=dir)
251+
file = os.fdopen(fd, mode=mode)
252+
253+
try:
254+
yield file
255+
except Exception:
256+
os.unlink(src)
257+
raise
258+
else:
259+
file.flush()
260+
file.close()
261+
262+
if overwrite:
263+
os.rename(src, dest)
264+
else:
265+
os.link(src, dest)
266+
os.unlink(src)
267+
268+
247269
def write(text, filename, encoding='utf-8', mode='wb'):
248270
"""
249271
Write 'text' to file ('filename') assuming 'encoding' in an atomic way

0 commit comments

Comments
 (0)