improvements to rewrite cache invalidation
- stat the source path before it is read. - Validate the source size in addition to mtime.
This commit is contained in:
parent
068548f7a9
commit
d4cd1aad8e
|
@ -1,6 +1,8 @@
|
||||||
NEXT
|
NEXT
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
- Improve assertion rewriting cache invalidation precision.
|
||||||
|
|
||||||
- fixed issue561: adapt autouse fixture example for python3.
|
- fixed issue561: adapt autouse fixture example for python3.
|
||||||
|
|
||||||
- fixed issue453: assertion rewriting issue with __repr__ containing
|
- fixed issue453: assertion rewriting issue with __repr__ containing
|
||||||
|
|
|
@ -134,12 +134,12 @@ class AssertionRewritingHook(object):
|
||||||
co = _read_pyc(fn_pypath, pyc, state.trace)
|
co = _read_pyc(fn_pypath, pyc, state.trace)
|
||||||
if co is None:
|
if co is None:
|
||||||
state.trace("rewriting %r" % (fn,))
|
state.trace("rewriting %r" % (fn,))
|
||||||
co = _rewrite_test(state, fn_pypath)
|
source_stat, co = _rewrite_test(state, fn_pypath)
|
||||||
if co is None:
|
if co is None:
|
||||||
# Probably a SyntaxError in the test.
|
# Probably a SyntaxError in the test.
|
||||||
return None
|
return None
|
||||||
if write:
|
if write:
|
||||||
_make_rewritten_pyc(state, fn_pypath, pyc, co)
|
_make_rewritten_pyc(state, source_stat, pyc, co)
|
||||||
else:
|
else:
|
||||||
state.trace("found cached rewritten pyc for %r" % (fn,))
|
state.trace("found cached rewritten pyc for %r" % (fn,))
|
||||||
self.modules[name] = co, pyc
|
self.modules[name] = co, pyc
|
||||||
|
@ -192,13 +192,12 @@ class AssertionRewritingHook(object):
|
||||||
pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider)
|
pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider)
|
||||||
|
|
||||||
|
|
||||||
def _write_pyc(state, co, source_path, pyc):
|
def _write_pyc(state, co, source_stat, pyc):
|
||||||
# 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, and I hope
|
# import. However, there's little reason deviate, and I hope
|
||||||
# sometime to be able to use imp.load_compiled to load them. (See
|
# sometime to be able to use imp.load_compiled to load them. (See
|
||||||
# the comment in load_module above.)
|
# the comment in load_module above.)
|
||||||
mtime = int(source_path.mtime())
|
|
||||||
try:
|
try:
|
||||||
fp = open(pyc, "wb")
|
fp = open(pyc, "wb")
|
||||||
except IOError:
|
except IOError:
|
||||||
|
@ -210,7 +209,9 @@ def _write_pyc(state, co, source_path, pyc):
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
fp.write(imp.get_magic())
|
fp.write(imp.get_magic())
|
||||||
fp.write(struct.pack("<l", mtime))
|
mtime = int(source_stat.mtime)
|
||||||
|
size = source_stat.size & 0xFFFFFFFF
|
||||||
|
fp.write(struct.pack("<ll", mtime, size))
|
||||||
marshal.dump(co, fp)
|
marshal.dump(co, fp)
|
||||||
finally:
|
finally:
|
||||||
fp.close()
|
fp.close()
|
||||||
|
@ -225,9 +226,10 @@ BOM_UTF8 = '\xef\xbb\xbf'
|
||||||
def _rewrite_test(state, fn):
|
def _rewrite_test(state, fn):
|
||||||
"""Try to read and rewrite *fn* and return the code object."""
|
"""Try to read and rewrite *fn* and return the code object."""
|
||||||
try:
|
try:
|
||||||
|
stat = fn.stat()
|
||||||
source = fn.read("rb")
|
source = fn.read("rb")
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
return None
|
return None, None
|
||||||
if ASCII_IS_DEFAULT_ENCODING:
|
if ASCII_IS_DEFAULT_ENCODING:
|
||||||
# ASCII is the default encoding in Python 2. Without a coding
|
# ASCII is the default encoding in Python 2. Without a coding
|
||||||
# declaration, Python 2 will complain about any bytes in the file
|
# declaration, Python 2 will complain about any bytes in the file
|
||||||
|
@ -246,14 +248,15 @@ def _rewrite_test(state, fn):
|
||||||
cookie_re.match(source[0:end1]) is None and
|
cookie_re.match(source[0:end1]) is None and
|
||||||
cookie_re.match(source[end1 + 1:end2]) is None):
|
cookie_re.match(source[end1 + 1:end2]) is None):
|
||||||
if hasattr(state, "_indecode"):
|
if hasattr(state, "_indecode"):
|
||||||
return None # encodings imported us again, we don't rewrite
|
# encodings imported us again, so don't rewrite.
|
||||||
|
return None, None
|
||||||
state._indecode = True
|
state._indecode = True
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
source.decode("ascii")
|
source.decode("ascii")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
# Let it fail in real import.
|
# Let it fail in real import.
|
||||||
return None
|
return None, None
|
||||||
finally:
|
finally:
|
||||||
del state._indecode
|
del state._indecode
|
||||||
# On Python versions which are not 2.7 and less than or equal to 3.1, the
|
# On Python versions which are not 2.7 and less than or equal to 3.1, the
|
||||||
|
@ -265,7 +268,7 @@ def _rewrite_test(state, fn):
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
# Let this pop up again in the real import.
|
# Let this pop up again in the real import.
|
||||||
state.trace("failed to parse: %r" % (fn,))
|
state.trace("failed to parse: %r" % (fn,))
|
||||||
return None
|
return None, None
|
||||||
rewrite_asserts(tree)
|
rewrite_asserts(tree)
|
||||||
try:
|
try:
|
||||||
co = compile(tree, fn.strpath, "exec")
|
co = compile(tree, fn.strpath, "exec")
|
||||||
|
@ -273,20 +276,20 @@ def _rewrite_test(state, fn):
|
||||||
# It's possible that this error is from some bug in the
|
# It's possible that this error is from some bug in the
|
||||||
# assertion rewriting, but I don't know of a fast way to tell.
|
# assertion rewriting, but I don't know of a fast way to tell.
|
||||||
state.trace("failed to compile: %r" % (fn,))
|
state.trace("failed to compile: %r" % (fn,))
|
||||||
return None
|
return None, None
|
||||||
return co
|
return stat, co
|
||||||
|
|
||||||
def _make_rewritten_pyc(state, fn, pyc, co):
|
def _make_rewritten_pyc(state, source_stat, pyc, co):
|
||||||
"""Try to dump rewritten code to *pyc*."""
|
"""Try to dump rewritten code to *pyc*."""
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
# Windows grants exclusive access to open files and doesn't have atomic
|
# Windows grants exclusive access to open files and doesn't have atomic
|
||||||
# rename, so just write into the final file.
|
# rename, so just write into the final file.
|
||||||
_write_pyc(state, co, fn, pyc)
|
_write_pyc(state, co, source_stat, pyc)
|
||||||
else:
|
else:
|
||||||
# When not on windows, assume rename is atomic. Dump the code object
|
# When not on windows, assume rename is atomic. Dump the code object
|
||||||
# into a file specific to this process and atomically replace it.
|
# into a file specific to this process and atomically replace it.
|
||||||
proc_pyc = pyc + "." + str(os.getpid())
|
proc_pyc = pyc + "." + str(os.getpid())
|
||||||
if _write_pyc(state, co, fn, proc_pyc):
|
if _write_pyc(state, co, source_stat, proc_pyc):
|
||||||
os.rename(proc_pyc, pyc)
|
os.rename(proc_pyc, pyc)
|
||||||
|
|
||||||
def _read_pyc(source, pyc, trace=lambda x: None):
|
def _read_pyc(source, pyc, trace=lambda x: None):
|
||||||
|
@ -301,13 +304,14 @@ def _read_pyc(source, pyc, trace=lambda x: None):
|
||||||
with fp:
|
with fp:
|
||||||
try:
|
try:
|
||||||
mtime = int(source.mtime())
|
mtime = int(source.mtime())
|
||||||
data = fp.read(8)
|
size = source.size()
|
||||||
|
data = fp.read(12)
|
||||||
except EnvironmentError as e:
|
except EnvironmentError as e:
|
||||||
trace('_read_pyc(%s): EnvironmentError %s' % (source, e))
|
trace('_read_pyc(%s): EnvironmentError %s' % (source, e))
|
||||||
return None
|
return None
|
||||||
# Check for invalid or out of date pyc file.
|
# Check for invalid or out of date pyc file.
|
||||||
if (len(data) != 8 or data[:4] != imp.get_magic() or
|
if (len(data) != 12 or data[:4] != imp.get_magic() or
|
||||||
struct.unpack("<l", data[4:])[0] != mtime):
|
struct.unpack("<ll", data[4:]) != (mtime, size)):
|
||||||
trace('_read_pyc(%s): invalid or out of date pyc' % source)
|
trace('_read_pyc(%s): invalid or out of date pyc' % source)
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
|
@ -318,6 +322,7 @@ def _read_pyc(source, pyc, trace=lambda x: None):
|
||||||
if not isinstance(co, types.CodeType):
|
if not isinstance(co, types.CodeType):
|
||||||
trace('_read_pyc(%s): not a code object' % source)
|
trace('_read_pyc(%s): not a code object' % source)
|
||||||
return None
|
return None
|
||||||
|
open("/tmp/goop", "wb").write(b"hi")
|
||||||
return co
|
return co
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -511,13 +511,13 @@ class TestAssertionRewriteHookDetails(object):
|
||||||
state = AssertionState(config, "rewrite")
|
state = AssertionState(config, "rewrite")
|
||||||
source_path = tmpdir.ensure("source.py")
|
source_path = tmpdir.ensure("source.py")
|
||||||
pycpath = tmpdir.join("pyc").strpath
|
pycpath = tmpdir.join("pyc").strpath
|
||||||
assert _write_pyc(state, [1], source_path, pycpath)
|
assert _write_pyc(state, [1], source_path.stat(), pycpath)
|
||||||
def open(*args):
|
def open(*args):
|
||||||
e = IOError()
|
e = IOError()
|
||||||
e.errno = 10
|
e.errno = 10
|
||||||
raise e
|
raise e
|
||||||
monkeypatch.setattr(b, "open", open)
|
monkeypatch.setattr(b, "open", open)
|
||||||
assert not _write_pyc(state, [1], source_path, pycpath)
|
assert not _write_pyc(state, [1], source_path.stat(), pycpath)
|
||||||
|
|
||||||
def test_resources_provider_for_loader(self, testdir):
|
def test_resources_provider_for_loader(self, testdir):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue