From daac87135bc0f558c239970401a50c8dc76c727f Mon Sep 17 00:00:00 2001 From: Christoph Reiter Date: Sat, 8 Nov 2025 17:33:50 +0100 Subject: [PATCH] Skip pyzstd with Python 3.14+ 3.14 has builtin zstd support, so use the builtin one there. --- msys2_devtools/db.py | 35 +------------------------- msys2_devtools/exttarfile.py | 48 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- tests/test_db.py | 2 +- uv.lock | 4 +-- 5 files changed, 53 insertions(+), 38 deletions(-) create mode 100644 msys2_devtools/exttarfile.py diff --git a/msys2_devtools/db.py b/msys2_devtools/db.py index d7fedd8..dbdf4ce 100644 --- a/msys2_devtools/db.py +++ b/msys2_devtools/db.py @@ -1,39 +1,6 @@ import io -from pyzstd import ZstdFile, ZstdError -import tarfile - - -class ExtTarFile(tarfile.TarFile): - """Extends TarFile to support zstandard""" - - @classmethod - def zstdopen(cls, name, mode="r", fileobj=None, **kwargs): # type: ignore - """Open zstd compressed tar archive""" - - if mode not in ("r", "w", "x", "a"): - raise ValueError("mode must be 'r', 'w' or 'x' or 'a'") - - zstfileobj = None - try: - zstfileobj = ZstdFile(fileobj or name, mode) - if "r" in mode: - zstfileobj.peek(1) # raises ZstdError if not a zstd file - except (ZstdError, EOFError) as e: - if zstfileobj is not None: - zstfileobj.close() - raise tarfile.ReadError("not a zstd file") from e - - try: - t = cls.taropen(name, mode, zstfileobj, **kwargs) - except Exception: - zstfileobj.close() - raise - - t._extfileobj = False - return t - - OPEN_METH = {"zstd": "zstdopen", **tarfile.TarFile.OPEN_METH} +from .exttarfile import ExtTarFile def parse_desc(t: str) -> dict[str, list[str]]: diff --git a/msys2_devtools/exttarfile.py b/msys2_devtools/exttarfile.py new file mode 100644 index 0000000..ca36d7f --- /dev/null +++ b/msys2_devtools/exttarfile.py @@ -0,0 +1,48 @@ +import tarfile + +HAS_ZSTD = True +try: + from compression import zstd +except ImportError: + HAS_ZSTD = False + +ExtTarFile: type[tarfile.TarFile] + +if not HAS_ZSTD: + from pyzstd import ZstdFile, ZstdError + + class ZstTarFile(tarfile.TarFile): + """Extends TarFile to support zstandard""" + + @classmethod + def zstdopen(cls, name, mode="r", fileobj=None, **kwargs): # type: ignore + """Open zstd compressed tar archive""" + + if mode not in ("r", "w", "x", "a"): + raise ValueError("mode must be 'r', 'w' or 'x' or 'a'") + + zstfileobj = None + try: + zstfileobj = ZstdFile(fileobj or name, mode) + if "r" in mode: + zstfileobj.peek(1) # raises ZstdError if not a zstd file + except (ZstdError, EOFError) as e: + if zstfileobj is not None: + zstfileobj.close() + raise tarfile.ReadError("not a zstd file") from e + + try: + t = cls.taropen(name, mode, zstfileobj, **kwargs) + except Exception: + zstfileobj.close() + raise + + t._extfileobj = False + return t + + OPEN_METH = {"zst": "zstdopen", **tarfile.TarFile.OPEN_METH} + + ExtTarFile = ZstTarFile +else: + assert zstd + ExtTarFile = tarfile.TarFile diff --git a/pyproject.toml b/pyproject.toml index d371fbb..ec707ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "tabulate>=0.9.0,<0.10", "requests>=2.28.2,<3", "pydantic>=2.0,<3", - "pyzstd>=0.18.0,<0.19", + "pyzstd>=0.18.0,<0.19; python_version < '3.14'", ] [project.optional-dependencies] diff --git a/tests/test_db.py b/tests/test_db.py index 281e6f7..78a5b1d 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -34,7 +34,7 @@ def test_parse_desc(): def test_zstd(): fileobj = io.BytesIO() - with ExtTarFile.open(fileobj=fileobj, mode='w:zstd') as tar: + with ExtTarFile.open(fileobj=fileobj, mode='w:zst') as tar: data = "Hello world!".encode('utf-8') info = tarfile.TarInfo("test.txt") info.size = len(data) diff --git a/uv.lock b/uv.lock index 08653d2..0794a0b 100644 --- a/uv.lock +++ b/uv.lock @@ -226,7 +226,7 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "pydantic" }, - { name = "pyzstd" }, + { name = "pyzstd", marker = "python_full_version < '3.14'" }, { name = "requests" }, { name = "tabulate" }, ] @@ -276,7 +276,7 @@ requires-dist = [ { name = "pgpdump", marker = "extra == 'all'", specifier = ">=1.5,<2" }, { name = "pgpdump", marker = "extra == 'sigstats'", specifier = ">=1.5,<2" }, { name = "pydantic", specifier = ">=2.0,<3" }, - { name = "pyzstd", specifier = ">=0.18.0,<0.19" }, + { name = "pyzstd", marker = "python_full_version < '3.14'", specifier = ">=0.18.0,<0.19" }, { name = "requests", specifier = ">=2.28.2,<3" }, { name = "requests-cache", marker = "extra == 'all'", specifier = ">=1.2.1,<2" }, { name = "requests-cache", marker = "extra == 'logstats'", specifier = ">=1.2.1,<2" },