diff --git a/changelog/8014.trivial.rst b/changelog/8014.trivial.rst new file mode 100644 index 000000000..3b9fb7bc2 --- /dev/null +++ b/changelog/8014.trivial.rst @@ -0,0 +1,2 @@ +`.pyc` files created by pytest's assertion rewriting now conform to the newer PEP-552 format on Python>=3.7. +(These files are internal and only interpreted by pytest itself.) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 649726727..805d4c8b3 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -281,12 +281,16 @@ def _write_pyc_fp( ) -> None: # Technically, we don't have to have the same pyc format as # (C)Python, since these "pycs" should never be seen by builtin - # import. However, there's little reason deviate. + # import. However, there's little reason to deviate. fp.write(importlib.util.MAGIC_NUMBER) + # https://www.python.org/dev/peps/pep-0552/ + if sys.version_info >= (3, 7): + flags = b"\x00\x00\x00\x00" + fp.write(flags) # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) mtime = int(source_stat.st_mtime) & 0xFFFFFFFF size = source_stat.st_size & 0xFFFFFFFF - # "= (3, 7) try: stat_result = os.stat(os.fspath(source)) mtime = int(stat_result.st_mtime) size = stat_result.st_size - data = fp.read(12) + data = fp.read(16 if has_flags else 12) except OSError as e: trace(f"_read_pyc({source}): OSError {e}") return None # Check for invalid or out of date pyc file. - if ( - len(data) != 12 - or data[:4] != importlib.util.MAGIC_NUMBER - or struct.unpack(" strip_bytes pyc.write_bytes(contents[:strip_bytes]) assert _read_pyc(source, pyc) is None # no error + @pytest.mark.skipif( + sys.version_info < (3, 7), reason="Only the Python 3.7 format for simplicity" + ) + def test_read_pyc_more_invalid(self, tmp_path: Path) -> None: + from _pytest.assertion.rewrite import _read_pyc + + source = tmp_path / "source.py" + pyc = tmp_path / "source.pyc" + + source_bytes = b"def test(): pass\n" + source.write_bytes(source_bytes) + + magic = importlib.util.MAGIC_NUMBER + + flags = b"\x00\x00\x00\x00" + + mtime = b"\x58\x3c\xb0\x5f" + mtime_int = int.from_bytes(mtime, "little") + os.utime(source, (mtime_int, mtime_int)) + + size = len(source_bytes).to_bytes(4, "little") + + code = marshal.dumps(compile(source_bytes, str(source), "exec")) + + # Good header. + pyc.write_bytes(magic + flags + mtime + size + code) + assert _read_pyc(source, pyc, print) is not None + + # Too short. + pyc.write_bytes(magic + flags + mtime) + assert _read_pyc(source, pyc, print) is None + + # Bad magic. + pyc.write_bytes(b"\x12\x34\x56\x78" + flags + mtime + size + code) + assert _read_pyc(source, pyc, print) is None + + # Unsupported flags. + pyc.write_bytes(magic + b"\x00\xff\x00\x00" + mtime + size + code) + assert _read_pyc(source, pyc, print) is None + + # Bad mtime. + pyc.write_bytes(magic + flags + b"\x58\x3d\xb0\x5f" + size + code) + assert _read_pyc(source, pyc, print) is None + + # Bad size. + pyc.write_bytes(magic + flags + mtime + b"\x99\x00\x00\x00" + code) + assert _read_pyc(source, pyc, print) is None + def test_reload_is_same_and_reloads(self, pytester: Pytester) -> None: """Reloading a (collected) module after change picks up the change.""" pytester.makeini(