Skip to content

Commit 4dca5a4

Browse files
committed
Attempt to solve race-condition which corrupts .pyc files on Windows
This uses of the `atomicwrites` library. This is very hard to create a reliable test for. Fix #3008
1 parent 372bcdb commit 4dca5a4

File tree

5 files changed

+23
-33
lines changed

5 files changed

+23
-33
lines changed

_pytest/assertion/rewrite.py

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import sys
1313
import types
1414

15+
import atomicwrites
1516
import py
17+
1618
from _pytest.assertion import util
1719

1820

@@ -140,7 +142,7 @@ def find_module(self, name, path=None):
140142
# Probably a SyntaxError in the test.
141143
return None
142144
if write:
143-
_make_rewritten_pyc(state, source_stat, pyc, co)
145+
_write_pyc(state, co, source_stat, pyc)
144146
else:
145147
state.trace("found cached rewritten pyc for %r" % (fn,))
146148
self.modules[name] = co, pyc
@@ -258,22 +260,21 @@ def _write_pyc(state, co, source_stat, pyc):
258260
# sometime to be able to use imp.load_compiled to load them. (See
259261
# the comment in load_module above.)
260262
try:
261-
fp = open(pyc, "wb")
262-
except IOError:
263-
err = sys.exc_info()[1].errno
264-
state.trace("error writing pyc file at %s: errno=%s" % (pyc, err))
263+
with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp:
264+
fp.write(imp.get_magic())
265+
mtime = int(source_stat.mtime)
266+
size = source_stat.size & 0xFFFFFFFF
267+
fp.write(struct.pack("<ll", mtime, size))
268+
if six.PY2:
269+
marshal.dump(co, fp.file)
270+
else:
271+
marshal.dump(co, fp)
272+
except EnvironmentError as e:
273+
state.trace("error writing pyc file at %s: errno=%s" % (pyc, e.errno))
265274
# we ignore any failure to write the cache file
266275
# there are many reasons, permission-denied, __pycache__ being a
267276
# file etc.
268277
return False
269-
try:
270-
fp.write(imp.get_magic())
271-
mtime = int(source_stat.mtime)
272-
size = source_stat.size & 0xFFFFFFFF
273-
fp.write(struct.pack("<ll", mtime, size))
274-
marshal.dump(co, fp)
275-
finally:
276-
fp.close()
277278
return True
278279

279280

@@ -338,20 +339,6 @@ def _rewrite_test(config, fn):
338339
return stat, co
339340

340341

341-
def _make_rewritten_pyc(state, source_stat, pyc, co):
342-
"""Try to dump rewritten code to *pyc*."""
343-
if sys.platform.startswith("win"):
344-
# Windows grants exclusive access to open files and doesn't have atomic
345-
# rename, so just write into the final file.
346-
_write_pyc(state, co, source_stat, pyc)
347-
else:
348-
# When not on windows, assume rename is atomic. Dump the code object
349-
# into a file specific to this process and atomically replace it.
350-
proc_pyc = pyc + "." + str(os.getpid())
351-
if _write_pyc(state, co, source_stat, proc_pyc):
352-
os.rename(proc_pyc, pyc)
353-
354-
355342
def _read_pyc(source, pyc, trace=lambda x: None):
356343
"""Possibly read a pytest pyc containing rewritten code.
357344

changelog/3008.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A rare race-condition which might result in corrupted ``.pyc`` files on Windows has been hopefully solved.

changelog/3008.trivial.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``pytest`` now depends on the `python-atomicwrites <https://github.com/untitaker/python-atomicwrites>`_ library.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def main():
6161
'setuptools',
6262
'attrs>=17.4.0',
6363
'more_itertools>=4.0.0',
64+
'atomicwrites>=1.0',
6465
]
6566
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
6667
# used by tox.ini to test with pluggy master

testing/test_assertrewrite.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -839,22 +839,22 @@ def test_meta_path():
839839
def test_write_pyc(self, testdir, tmpdir, monkeypatch):
840840
from _pytest.assertion.rewrite import _write_pyc
841841
from _pytest.assertion import AssertionState
842-
try:
843-
import __builtin__ as b
844-
except ImportError:
845-
import builtins as b
842+
import atomicwrites
843+
from contextlib import contextmanager
846844
config = testdir.parseconfig([])
847845
state = AssertionState(config, "rewrite")
848846
source_path = tmpdir.ensure("source.py")
849847
pycpath = tmpdir.join("pyc").strpath
850848
assert _write_pyc(state, [1], source_path.stat(), pycpath)
851849

852-
def open(*args):
850+
@contextmanager
851+
def atomic_write_failed(fn, mode='r', overwrite=False):
853852
e = IOError()
854853
e.errno = 10
855854
raise e
855+
yield # noqa
856856

857-
monkeypatch.setattr(b, "open", open)
857+
monkeypatch.setattr(atomicwrites, "atomic_write", atomic_write_failed)
858858
assert not _write_pyc(state, [1], source_path.stat(), pycpath)
859859

860860
def test_resources_provider_for_loader(self, testdir):

0 commit comments

Comments
 (0)