Skip to content

Commit 825b81b

Browse files
authored
Merge pull request #8014 from bluetech/pyc-pep552
assertion/rewrite: write pyc's according to PEP-552 on Python>=3.7
2 parents 767cbeb + 1d532da commit 825b81b

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)