From 846295249768a8aa1b32b4b2bec0b6387343a00d Mon Sep 17 00:00:00 2001 From: donBarbos Date: Tue, 14 Oct 2025 01:19:36 +0400 Subject: [PATCH 1/5] [compression] Add common _Decompressor Protocol --- stdlib/@tests/test_cases/check_compression.py | 61 +++++++++++++++++++ stdlib/_compression.pyi | 15 ++++- stdlib/compression/_common/_streams.pyi | 15 ++++- 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 stdlib/@tests/test_cases/check_compression.py diff --git a/stdlib/@tests/test_cases/check_compression.py b/stdlib/@tests/test_cases/check_compression.py new file mode 100644 index 000000000000..d5cc3b7e581e --- /dev/null +++ b/stdlib/@tests/test_cases/check_compression.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import io +from _compression import DecompressReader as DecompressReader_other, _Reader as _Reader_other +from _typeshed import ReadableBuffer +from bz2 import BZ2Decompressor +from compression._common._streams import DecompressReader, _Decompressor, _Reader +from compression.zstd import ZstdDecompressor +from lzma import LZMADecompressor +from typing import cast +from typing_extensions import assert_type +from zlib import decompressobj + +### +# Tests for DecompressReader/_Decompressor +### + + +class CustomDecompressor: + def decompress(self, data: ReadableBuffer, max_length: int = -1) -> bytes: + return b"" + + @property + def unused_data(self) -> bytes: + return b"" + + @property + def eof(self) -> bool: + return False + + @property + def needs_input(self) -> bool: + return False + + +def accept_decompressor(d: _Decompressor) -> None: + d.decompress(b"random bytes", 0) + assert_type(d.eof, bool) + assert_type(d.unused_data, bytes) + + +# Test objects from compression._common._streams +fp = cast(_Reader, io.BytesIO(b"hello world")) # type: ignore +DecompressReader(fp, decompressobj) +DecompressReader(fp, BZ2Decompressor) +DecompressReader(fp, LZMADecompressor) +DecompressReader(fp, ZstdDecompressor) +DecompressReader(fp, CustomDecompressor) +accept_decompressor(decompressobj()) +accept_decompressor(BZ2Decompressor()) +accept_decompressor(LZMADecompressor()) +accept_decompressor(ZstdDecompressor()) +accept_decompressor(CustomDecompressor()) + +# Test objects from _compression +fp = cast(_Reader_other, io.BytesIO(b"hello world")) # type: ignore +DecompressReader_other(fp, decompressobj) +DecompressReader_other(fp, BZ2Decompressor) +DecompressReader_other(fp, LZMADecompressor) +DecompressReader_other(fp, ZstdDecompressor) +DecompressReader_other(fp, CustomDecompressor) diff --git a/stdlib/_compression.pyi b/stdlib/_compression.pyi index aa67df2ab478..8e54cfa78b00 100644 --- a/stdlib/_compression.pyi +++ b/stdlib/_compression.pyi @@ -1,6 +1,6 @@ # _compression is replaced by compression._common._streams on Python 3.14+ (PEP-784) -from _typeshed import Incomplete, WriteableBuffer +from _typeshed import ReadableBuffer, WriteableBuffer from collections.abc import Callable from io import DEFAULT_BUFFER_SIZE, BufferedIOBase, RawIOBase from typing import Any, Protocol, type_check_only @@ -13,13 +13,24 @@ class _Reader(Protocol): def seekable(self) -> bool: ... def seek(self, n: int, /) -> Any: ... +@type_check_only +class _Decompressor(Protocol): + def decompress(self, data: ReadableBuffer, /, max_length: int = ...) -> bytes: ... + @property + def unused_data(self) -> bytes: ... + @property + def eof(self) -> bool: ... + # `zlib._Decompress` does not have next property, unlike other Decompressors: + # @property + # def needs_input(self) -> bool: ... + class BaseStream(BufferedIOBase): ... class DecompressReader(RawIOBase): def __init__( self, fp: _Reader, - decomp_factory: Callable[..., Incomplete], + decomp_factory: Callable[..., _Decompressor], trailing_error: type[Exception] | tuple[type[Exception], ...] = (), **decomp_args: Any, # These are passed to decomp_factory. ) -> None: ... diff --git a/stdlib/compression/_common/_streams.pyi b/stdlib/compression/_common/_streams.pyi index b8463973ec67..bcd012c26773 100644 --- a/stdlib/compression/_common/_streams.pyi +++ b/stdlib/compression/_common/_streams.pyi @@ -1,4 +1,4 @@ -from _typeshed import Incomplete, WriteableBuffer +from _typeshed import ReadableBuffer, WriteableBuffer from collections.abc import Callable from io import DEFAULT_BUFFER_SIZE, BufferedIOBase, RawIOBase from typing import Any, Protocol, type_check_only @@ -11,13 +11,24 @@ class _Reader(Protocol): def seekable(self) -> bool: ... def seek(self, n: int, /) -> Any: ... +@type_check_only +class _Decompressor(Protocol): + def decompress(self, data: ReadableBuffer, /, max_length: int = ...) -> bytes: ... + @property + def unused_data(self) -> bytes: ... + @property + def eof(self) -> bool: ... + # `zlib._Decompress` does not have next property, unlike other Decompressors: + # @property + # def needs_input(self) -> bool: ... + class BaseStream(BufferedIOBase): ... class DecompressReader(RawIOBase): def __init__( self, fp: _Reader, - decomp_factory: Callable[..., Incomplete], # Consider backporting changes to _compression + decomp_factory: Callable[..., _Decompressor], # Consider backporting changes to _compression trailing_error: type[Exception] | tuple[type[Exception], ...] = (), **decomp_args: Any, # These are passed to decomp_factory. ) -> None: ... From 60ae32103ae5c2b11a5aa542db2cf05d3600ca0c Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 15 Oct 2025 21:37:44 +0400 Subject: [PATCH 2/5] Update comments --- stdlib/@tests/test_cases/check_compression.py | 4 ++-- stdlib/_compression.pyi | 2 +- stdlib/compression/_common/_streams.pyi | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stdlib/@tests/test_cases/check_compression.py b/stdlib/@tests/test_cases/check_compression.py index d5cc3b7e581e..cf5a447ee209 100644 --- a/stdlib/@tests/test_cases/check_compression.py +++ b/stdlib/@tests/test_cases/check_compression.py @@ -40,7 +40,7 @@ def accept_decompressor(d: _Decompressor) -> None: # Test objects from compression._common._streams -fp = cast(_Reader, io.BytesIO(b"hello world")) # type: ignore +fp = cast(_Reader, io.BytesIO(b"hello world")) DecompressReader(fp, decompressobj) DecompressReader(fp, BZ2Decompressor) DecompressReader(fp, LZMADecompressor) @@ -53,7 +53,7 @@ def accept_decompressor(d: _Decompressor) -> None: accept_decompressor(CustomDecompressor()) # Test objects from _compression -fp = cast(_Reader_other, io.BytesIO(b"hello world")) # type: ignore +fp = cast(_Reader_other, io.BytesIO(b"hello world")) DecompressReader_other(fp, decompressobj) DecompressReader_other(fp, BZ2Decompressor) DecompressReader_other(fp, LZMADecompressor) diff --git a/stdlib/_compression.pyi b/stdlib/_compression.pyi index 8e54cfa78b00..6015bcb13f1c 100644 --- a/stdlib/_compression.pyi +++ b/stdlib/_compression.pyi @@ -20,7 +20,7 @@ class _Decompressor(Protocol): def unused_data(self) -> bytes: ... @property def eof(self) -> bool: ... - # `zlib._Decompress` does not have next property, unlike other Decompressors: + # `zlib._Decompress` does not have next property, but `DecompressReader` calls it: # @property # def needs_input(self) -> bool: ... diff --git a/stdlib/compression/_common/_streams.pyi b/stdlib/compression/_common/_streams.pyi index bcd012c26773..96aec24d1c2d 100644 --- a/stdlib/compression/_common/_streams.pyi +++ b/stdlib/compression/_common/_streams.pyi @@ -18,7 +18,7 @@ class _Decompressor(Protocol): def unused_data(self) -> bytes: ... @property def eof(self) -> bool: ... - # `zlib._Decompress` does not have next property, unlike other Decompressors: + # `zlib._Decompress` does not have next property, but `DecompressReader` calls it: # @property # def needs_input(self) -> bool: ... From e60ff44ea5070c80f37b9ad0b841ebbc1662177b Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 15 Oct 2025 21:43:09 +0400 Subject: [PATCH 3/5] Add version check to tests --- stdlib/@tests/test_cases/check_compression.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/stdlib/@tests/test_cases/check_compression.py b/stdlib/@tests/test_cases/check_compression.py index cf5a447ee209..1cdc8500c13c 100644 --- a/stdlib/@tests/test_cases/check_compression.py +++ b/stdlib/@tests/test_cases/check_compression.py @@ -1,16 +1,20 @@ from __future__ import annotations import io -from _compression import DecompressReader as DecompressReader_other, _Reader as _Reader_other +import sys from _typeshed import ReadableBuffer from bz2 import BZ2Decompressor -from compression._common._streams import DecompressReader, _Decompressor, _Reader from compression.zstd import ZstdDecompressor from lzma import LZMADecompressor from typing import cast from typing_extensions import assert_type from zlib import decompressobj +if sys.version_info >= (3, 14): + from compression._common._streams import DecompressReader, _Decompressor, _Reader +else: + from _compression import DecompressReader, _Reader + ### # Tests for DecompressReader/_Decompressor ### @@ -39,7 +43,6 @@ def accept_decompressor(d: _Decompressor) -> None: assert_type(d.unused_data, bytes) -# Test objects from compression._common._streams fp = cast(_Reader, io.BytesIO(b"hello world")) DecompressReader(fp, decompressobj) DecompressReader(fp, BZ2Decompressor) @@ -51,11 +54,3 @@ def accept_decompressor(d: _Decompressor) -> None: accept_decompressor(LZMADecompressor()) accept_decompressor(ZstdDecompressor()) accept_decompressor(CustomDecompressor()) - -# Test objects from _compression -fp = cast(_Reader_other, io.BytesIO(b"hello world")) -DecompressReader_other(fp, decompressobj) -DecompressReader_other(fp, BZ2Decompressor) -DecompressReader_other(fp, LZMADecompressor) -DecompressReader_other(fp, ZstdDecompressor) -DecompressReader_other(fp, CustomDecompressor) From bcb041b906fe9a8af54fa109b90a21be3abaef50 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 15 Oct 2025 21:48:02 +0400 Subject: [PATCH 4/5] add missing import --- stdlib/@tests/test_cases/check_compression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/@tests/test_cases/check_compression.py b/stdlib/@tests/test_cases/check_compression.py index 1cdc8500c13c..52d17a0a5264 100644 --- a/stdlib/@tests/test_cases/check_compression.py +++ b/stdlib/@tests/test_cases/check_compression.py @@ -13,7 +13,7 @@ if sys.version_info >= (3, 14): from compression._common._streams import DecompressReader, _Decompressor, _Reader else: - from _compression import DecompressReader, _Reader + from _compression import DecompressReader, _Decompressor, _Reader ### # Tests for DecompressReader/_Decompressor From 9937317e461a73f562126feab9fa910719f88090 Mon Sep 17 00:00:00 2001 From: donBarbos Date: Wed, 15 Oct 2025 22:48:07 +0400 Subject: [PATCH 5/5] Add version check for zstd --- stdlib/@tests/test_cases/check_compression.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/stdlib/@tests/test_cases/check_compression.py b/stdlib/@tests/test_cases/check_compression.py index 52d17a0a5264..7fc106f125c7 100644 --- a/stdlib/@tests/test_cases/check_compression.py +++ b/stdlib/@tests/test_cases/check_compression.py @@ -4,7 +4,6 @@ import sys from _typeshed import ReadableBuffer from bz2 import BZ2Decompressor -from compression.zstd import ZstdDecompressor from lzma import LZMADecompressor from typing import cast from typing_extensions import assert_type @@ -12,6 +11,7 @@ if sys.version_info >= (3, 14): from compression._common._streams import DecompressReader, _Decompressor, _Reader + from compression.zstd import ZstdDecompressor else: from _compression import DecompressReader, _Decompressor, _Reader @@ -47,10 +47,12 @@ def accept_decompressor(d: _Decompressor) -> None: DecompressReader(fp, decompressobj) DecompressReader(fp, BZ2Decompressor) DecompressReader(fp, LZMADecompressor) -DecompressReader(fp, ZstdDecompressor) DecompressReader(fp, CustomDecompressor) accept_decompressor(decompressobj()) accept_decompressor(BZ2Decompressor()) accept_decompressor(LZMADecompressor()) -accept_decompressor(ZstdDecompressor()) accept_decompressor(CustomDecompressor()) + +if sys.version_info >= (3, 14): + DecompressReader(fp, ZstdDecompressor) + accept_decompressor(ZstdDecompressor())