Skip to content

Commit 1d532da

Browse files
committed
assertion/rewrite: write pyc's according to PEP-552 on Python>=3.7
Python 3.7 changes the pyc format by adding a flags byte. Even though it is not necessary for us to match it, it is nice to be able to read pyc files we emit for debugging the rewriter. Update our custom pyc files to use that format. We write flags==0 meaning we still use the mtime+size format rather the newer hash format.
1 parent e986d84 commit 1d532da

File tree

3 files changed

+77
-10
lines changed

3 files changed

+77
-10
lines changed

changelog/8014.trivial.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
`.pyc` files created by pytest's assertion rewriting now conform to the newer PEP-552 format on Python>=3.7.
2+
(These files are internal and only interpreted by pytest itself.)

src/_pytest/assertion/rewrite.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -281,12 +281,16 @@ def _write_pyc_fp(
281281
) -> None:
282282
# Technically, we don't have to have the same pyc format as
283283
# (C)Python, since these "pycs" should never be seen by builtin
284-
# import. However, there's little reason deviate.
284+
# import. However, there's little reason to deviate.
285285
fp.write(importlib.util.MAGIC_NUMBER)
286+
# https://www.python.org/dev/peps/pep-0552/
287+
if sys.version_info >= (3, 7):
288+
flags = b"\x00\x00\x00\x00"
289+
fp.write(flags)
286290
# as of now, bytecode header expects 32-bit numbers for size and mtime (#4903)
287291
mtime = int(source_stat.st_mtime) & 0xFFFFFFFF
288292
size = source_stat.st_size & 0xFFFFFFFF
289-
# "<LL" stands for 2 unsigned longs, little-ending
293+
# "<LL" stands for 2 unsigned longs, little-endian.
290294
fp.write(struct.pack("<LL", mtime, size))
291295
fp.write(marshal.dumps(co))
292296

@@ -365,21 +369,33 @@ def _read_pyc(
365369
except OSError:
366370
return None
367371
with fp:
372+
# https://www.python.org/dev/peps/pep-0552/
373+
has_flags = sys.version_info >= (3, 7)
368374
try:
369375
stat_result = os.stat(os.fspath(source))
370376
mtime = int(stat_result.st_mtime)
371377
size = stat_result.st_size
372-
data = fp.read(12)
378+
data = fp.read(16 if has_flags else 12)
373379
except OSError as e:
374380
trace(f"_read_pyc({source}): OSError {e}")
375381
return None
376382
# Check for invalid or out of date pyc file.
377-
if (
378-
len(data) != 12
379-
or data[:4] != importlib.util.MAGIC_NUMBER
380-
or struct.unpack("<LL", data[4:]) != (mtime & 0xFFFFFFFF, size & 0xFFFFFFFF)
381-
):
382-
trace("_read_pyc(%s): invalid or out of date pyc" % source)
383+
if len(data) != (16 if has_flags else 12):
384+
trace("_read_pyc(%s): invalid pyc (too short)" % source)
385+
return None
386+
if data[:4] != importlib.util.MAGIC_NUMBER:
387+
trace("_read_pyc(%s): invalid pyc (bad magic number)" % source)
388+
return None
389+
if has_flags and data[4:8] != b"\x00\x00\x00\x00":
390+
trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source)
391+
return None
392+
mtime_data = data[8 if has_flags else 4 : 12 if has_flags else 8]
393+
if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF:
394+
trace("_read_pyc(%s): out of date" % source)
395+
return None
396+
size_data = data[12 if has_flags else 8 : 16 if has_flags else 12]
397+
if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF:
398+
trace("_read_pyc(%s): invalid pyc (incorrect size)" % source)
383399
return None
384400
try:
385401
co = marshal.load(fp)

testing/test_assertrewrite.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import errno
33
import glob
44
import importlib
5+
import marshal
56
import os
67
import py_compile
78
import stat
@@ -1063,12 +1064,60 @@ def test_read_pyc(self, tmp_path: Path) -> None:
10631064
py_compile.compile(str(source), str(pyc))
10641065

10651066
contents = pyc.read_bytes()
1066-
strip_bytes = 20 # header is around 8 bytes, strip a little more
1067+
strip_bytes = 20 # header is around 16 bytes, strip a little more
10671068
assert len(contents) > strip_bytes
10681069
pyc.write_bytes(contents[:strip_bytes])
10691070

10701071
assert _read_pyc(source, pyc) is None # no error
10711072

1073+
@pytest.mark.skipif(
1074+
sys.version_info < (3, 7), reason="Only the Python 3.7 format for simplicity"
1075+
)
1076+
def test_read_pyc_more_invalid(self, tmp_path: Path) -> None:
1077+
from _pytest.assertion.rewrite import _read_pyc
1078+
1079+
source = tmp_path / "source.py"
1080+
pyc = tmp_path / "source.pyc"
1081+
1082+
source_bytes = b"def test(): pass\n"
1083+
source.write_bytes(source_bytes)
1084+
1085+
magic = importlib.util.MAGIC_NUMBER
1086+
1087+
flags = b"\x00\x00\x00\x00"
1088+
1089+
mtime = b"\x58\x3c\xb0\x5f"
1090+
mtime_int = int.from_bytes(mtime, "little")
1091+
os.utime(source, (mtime_int, mtime_int))
1092+
1093+
size = len(source_bytes).to_bytes(4, "little")
1094+
1095+
code = marshal.dumps(compile(source_bytes, str(source), "exec"))
1096+
1097+
# Good header.
1098+
pyc.write_bytes(magic + flags + mtime + size + code)
1099+
assert _read_pyc(source, pyc, print) is not None
1100+
1101+
# Too short.
1102+
pyc.write_bytes(magic + flags + mtime)
1103+
assert _read_pyc(source, pyc, print) is None
1104+
1105+
# Bad magic.
1106+
pyc.write_bytes(b"\x12\x34\x56\x78" + flags + mtime + size + code)
1107+
assert _read_pyc(source, pyc, print) is None
1108+
1109+
# Unsupported flags.
1110+
pyc.write_bytes(magic + b"\x00\xff\x00\x00" + mtime + size + code)
1111+
assert _read_pyc(source, pyc, print) is None
1112+
1113+
# Bad mtime.
1114+
pyc.write_bytes(magic + flags + b"\x58\x3d\xb0\x5f" + size + code)
1115+
assert _read_pyc(source, pyc, print) is None
1116+
1117+
# Bad size.
1118+
pyc.write_bytes(magic + flags + mtime + b"\x99\x00\x00\x00" + code)
1119+
assert _read_pyc(source, pyc, print) is None
1120+
10721121
def test_reload_is_same_and_reloads(self, pytester: Pytester) -> None:
10731122
"""Reloading a (collected) module after change picks up the change."""
10741123
pytester.makeini(

0 commit comments

Comments
 (0)