Use atomicrewrites only on Windows

Fixes https://github.com/pytest-dev/pytest/issues/6147
This commit is contained in:
Daniel Hahler 2019-11-07 14:41:26 +01:00
parent ab101658f0
commit 45c4a8fb3d
4 changed files with 74 additions and 32 deletions

View File

@ -0,0 +1 @@
``python-atomicwrites`` is only used on Windows, fixing a performance regression with assertion rewriting on Unix.

View File

@ -7,7 +7,7 @@ INSTALL_REQUIRES = [
"packaging", "packaging",
"attrs>=17.4.0", # should match oldattrs tox env. "attrs>=17.4.0", # should match oldattrs tox env.
"more-itertools>=4.0.0", "more-itertools>=4.0.0",
"atomicwrites>=1.0", 'atomicwrites>=1.0;sys_platform=="win32"',
'pathlib2>=2.2.0;python_version<"3.6"', 'pathlib2>=2.2.0;python_version<"3.6"',
'colorama;sys_platform=="win32"', 'colorama;sys_platform=="win32"',
"pluggy>=0.12,<1.0", "pluggy>=0.12,<1.0",

View File

@ -20,8 +20,6 @@ from typing import Optional
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
import atomicwrites
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest._version import version from _pytest._version import version
from _pytest.assertion import util from _pytest.assertion import util
@ -255,26 +253,59 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder):
return f.read() return f.read()
def _write_pyc(state, co, source_stat, pyc): def _write_pyc_fp(fp, source_stat, co):
# Technically, we don't have to have the same pyc format as # Technically, we don't have to have the same pyc format as
# (C)Python, since these "pycs" should never be seen by builtin # (C)Python, since these "pycs" should never be seen by builtin
# import. However, there's little reason deviate. # import. However, there's little reason deviate.
try: fp.write(importlib.util.MAGIC_NUMBER)
with atomicwrites.atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp: # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
fp.write(importlib.util.MAGIC_NUMBER) mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) size = source_stat.st_size & 0xFFFFFFFF
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF # "<LL" stands for 2 unsigned longs, little-ending
size = source_stat.st_size & 0xFFFFFFFF fp.write(struct.pack("<LL", mtime, size))
# "<LL" stands for 2 unsigned longs, little-ending fp.write(marshal.dumps(co))
fp.write(struct.pack("<LL", mtime, size))
fp.write(marshal.dumps(co))
except EnvironmentError as e: if sys.platform == "win32":
state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno)) from atomicwrites import atomic_write
# we ignore any failure to write the cache file
# there are many reasons, permission-denied, pycache dir being a def _write_pyc(state, co, source_stat, pyc):
# file etc. try:
return False with atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp:
return True _write_pyc_fp(fp, source_stat, co)
except EnvironmentError as e:
state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno))
# we ignore any failure to write the cache file
# there are many reasons, permission-denied, pycache dir being a
# file etc.
return False
return True
else:
def _write_pyc(state, co, source_stat, pyc):
proc_pyc = "{}.{}".format(pyc, os.getpid())
try:
fp = open(proc_pyc, "wb")
except EnvironmentError as e:
state.trace(
"error writing pyc file at {}: errno={}".format(proc_pyc, e.errno)
)
return False
try:
_write_pyc_fp(fp, source_stat, co)
os.rename(proc_pyc, fspath(pyc))
except BaseException as e:
state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno))
# we ignore any failure to write the cache file
# there are many reasons, permission-denied, pycache dir being a
# file etc.
return False
finally:
fp.close()
return True
def _rewrite_test(fn, config): def _rewrite_test(fn, config):

View File

@ -959,24 +959,34 @@ class TestAssertionRewriteHookDetails:
def test_write_pyc(self, testdir, tmpdir, monkeypatch): def test_write_pyc(self, testdir, tmpdir, monkeypatch):
from _pytest.assertion.rewrite import _write_pyc from _pytest.assertion.rewrite import _write_pyc
from _pytest.assertion import AssertionState from _pytest.assertion import AssertionState
import atomicwrites
from contextlib import contextmanager
config = testdir.parseconfig([]) config = testdir.parseconfig([])
state = AssertionState(config, "rewrite") state = AssertionState(config, "rewrite")
source_path = tmpdir.ensure("source.py") source_path = str(tmpdir.ensure("source.py"))
pycpath = tmpdir.join("pyc").strpath pycpath = tmpdir.join("pyc").strpath
assert _write_pyc(state, [1], os.stat(source_path.strpath), pycpath) assert _write_pyc(state, [1], os.stat(source_path), pycpath)
@contextmanager if sys.platform == "win32":
def atomic_write_failed(fn, mode="r", overwrite=False): from contextlib import contextmanager
e = IOError()
e.errno = 10
raise e
yield
monkeypatch.setattr(atomicwrites, "atomic_write", atomic_write_failed) @contextmanager
assert not _write_pyc(state, [1], source_path.stat(), pycpath) def atomic_write_failed(fn, mode="r", overwrite=False):
e = IOError()
e.errno = 10
raise e
yield
monkeypatch.setattr(
_pytest.assertion.rewrite, "atomic_write", atomic_write_failed
)
else:
def raise_ioerror(*args):
raise IOError()
monkeypatch.setattr("os.rename", raise_ioerror)
assert not _write_pyc(state, [1], os.stat(source_path), pycpath)
def test_resources_provider_for_loader(self, testdir): def test_resources_provider_for_loader(self, testdir):
""" """