From 3d01aeb305adc34cc7bf1c63d64ccc20f8a1b548 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 23:03:44 +0200 Subject: [PATCH 01/18] Add changelog entry for lz4 support --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 3690fd3..66eef81 100644 --- a/README.rst +++ b/README.rst @@ -187,6 +187,12 @@ the ``compression.zstd`` module in the standard library is used. Changelog --------- +development version +~~~~~~~~~~~~~~~~~~~ + +* #181/#168: Added lz4 as supported compression format. + Contributed by @Dev-iL and @gnzsnz. + v2.1.0 (2026-06-03) ~~~~~~~~~~~~~~~~~~~ From d12b1656f22a377c1648d9cc6ad7f599be270524 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 21:18:25 +0200 Subject: [PATCH 02/18] Bump black --- src/xopen/__init__.py | 6 ++---- tests/test_piped.py | 1 + tests/test_xopen.py | 1 + tox.toml | 3 ++- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index 7462a9c..eb091f8 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -791,8 +791,7 @@ def xopen( errors: Optional[str] = ..., newline: Optional[str] = ..., format: Optional[str] = ..., -) -> io.TextIOWrapper: - ... +) -> io.TextIOWrapper: ... @overload @@ -806,8 +805,7 @@ def xopen( errors: None = ..., newline: None = ..., format: Optional[str] = ..., -) -> BinaryIO: - ... +) -> BinaryIO: ... def xopen( # noqa: C901 diff --git a/tests/test_piped.py b/tests/test_piped.py index 554676b..c66f67f 100644 --- a/tests/test_piped.py +++ b/tests/test_piped.py @@ -1,6 +1,7 @@ """ Tests for the PipedCompression classes """ + import gzip import io import os diff --git a/tests/test_xopen.py b/tests/test_xopen.py index a9fbb7a..6876860 100644 --- a/tests/test_xopen.py +++ b/tests/test_xopen.py @@ -1,6 +1,7 @@ """ Tests for the xopen.xopen function """ + import bz2 import functools import gzip diff --git a/tox.toml b/tox.toml index 015ed52..2c9924d 100644 --- a/tox.toml +++ b/tox.toml @@ -32,7 +32,8 @@ commands = [ ] [env.black] -deps = ["black==22.3.0"] +base_python = "3.14" +deps = ["black==26.10"] skip_install = true commands = [["black", "--check", "src/", "tests/"]] From e91723bf1be5f80f10dca83c050e176bd28aa0e8 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 21:28:48 +0200 Subject: [PATCH 03/18] Configure ruff to ignore all existing issues --- pyproject.toml | 23 ++++++++++++++++++++++- tox.toml | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 71a6825..6618a7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,28 @@ zstd = [] # Leave this in here for backwards compatibility (Zstandard support u write_to = "src/xopen/_version.py" [tool.ruff.lint] -select = ["E4", "E7", "E9", "F"] +ignore = [ + "B008", # Do not perform function call `_ProgramSettings` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable + "C408", # Unnecessary `dict()` call (rewrite as a literal) + "F401", # `typing.Tuple` imported but unused + "FURB161", # Use of `bin(int(m.group(1).replace(",", ""), 16)).count('1')` + "PLW1510", # `subprocess.run` without explicit `check` argument + "SIM115", # Use a context manager for opening files + "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements + "UP007", # Use `X | Y` for type annotations + "UP035", # `typing.Tuple` is deprecated, use `tuple` instead + "YTT204", # `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple + # fixable with --fix: + "B010", # Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. + "I001", # Import block is un-sorted or un-formatted + "PIE808", # Unnecessary `start` argument in `range` + "RUF022", # `__all__` is not sorted + "RUF100", # Unused `noqa` directive (non-enabled: `C901`) + "SIM114", # Combine `if` branches using logical `or` operator + "UP006", # Use `dict` instead of `Dict` for type annotation + "UP032", # Use f-string instead of `format` call + "UP045", # Use `X | None` for type annotations +] [tool.pytest.ini_options] addopts = "--strict-markers" diff --git a/tox.toml b/tox.toml index 2c9924d..58b905d 100644 --- a/tox.toml +++ b/tox.toml @@ -38,7 +38,7 @@ skip_install = true commands = [["black", "--check", "src/", "tests/"]] [env.ruff] -deps = ["ruff"] +deps = ["ruff==0.17"] commands = [["ruff", "check", "src/", "tests/"]] skip_install = true From cf4f5c824986dfa0bd46d90c398baf8e6c8c9225 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 22:59:20 +0200 Subject: [PATCH 04/18] Run ruff check --fix --- pyproject.toml | 10 ---- src/xopen/__init__.py | 114 +++++++++++++++++++++--------------------- tests/conftest.py | 1 + tests/test_piped.py | 11 ++-- tests/test_xopen.py | 5 +- 5 files changed, 65 insertions(+), 76 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6618a7b..5225313 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,16 +46,6 @@ ignore = [ "UP007", # Use `X | Y` for type annotations "UP035", # `typing.Tuple` is deprecated, use `tuple` instead "YTT204", # `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple - # fixable with --fix: - "B010", # Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. - "I001", # Import block is un-sorted or un-formatted - "PIE808", # Unnecessary `start` argument in `range` - "RUF022", # `__all__` is not sorted - "RUF100", # Unused `noqa` directive (non-enabled: `C901`) - "SIM114", # Combine `if` branches using logical `or` operator - "UP006", # Use `dict` instead of `Dict` for type annotation - "UP032", # Use f-string instead of `format` call - "UP045", # Use `X | None` for type annotations ] [tool.pytest.ini_options] diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index eb091f8..358a9be 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -3,35 +3,35 @@ """ __all__ = [ - "xopen", "_PipedCompressionProgram", "__version__", + "xopen", ] +import bz2 import dataclasses import gzip -import stat -import sys import io -import os -import bz2 import lzma +import os import signal +import stat import subprocess +import sys import tempfile import threading import time +from types import ModuleType from typing import ( - Dict, - Optional, - Union, IO, - overload, BinaryIO, + Dict, Literal, + Optional, Tuple, + Union, + overload, ) -from types import ModuleType from ._version import version as __version__ @@ -43,12 +43,12 @@ XOPEN_DEFAULT_XZ_COMPRESSION = 6 XOPEN_DEFAULT_ZST_COMPRESSION = 3 -igzip: Optional[ModuleType] -isal_zlib: Optional[ModuleType] -igzip_threaded: Optional[ModuleType] -zlib_ng: Optional[ModuleType] -gzip_ng: Optional[ModuleType] -gzip_ng_threaded: Optional[ModuleType] +igzip: ModuleType | None +isal_zlib: ModuleType | None +igzip_threaded: ModuleType | None +zlib_ng: ModuleType | None +gzip_ng: ModuleType | None +gzip_ng_threaded: ModuleType | None try: from isal import igzip, igzip_threaded, isal_zlib @@ -99,17 +99,17 @@ @dataclasses.dataclass class _ProgramSettings: - program_args: Tuple[str, ...] - acceptable_compression_levels: Tuple[int, ...] = tuple(range(1, 10)) - threads_flag: Optional[str] = None + program_args: tuple[str, ...] + acceptable_compression_levels: tuple[int, ...] = tuple(range(1, 10)) + threads_flag: str | None = None # This exit code is not interpreted as an error when terminating the process - allowed_exit_code: Optional[int] = -signal.SIGTERM + allowed_exit_code: int | None = -signal.SIGTERM # If this message is printed on stderr on terminating the process, # it is not interpreted as an error - allowed_exit_message: Optional[bytes] = None + allowed_exit_message: bytes | None = None -_PROGRAM_SETTINGS: Dict[str, _ProgramSettings] = { +_PROGRAM_SETTINGS: dict[str, _ProgramSettings] = { "pbzip2": _ProgramSettings( ("pbzip2",), tuple(range(1, 10)), @@ -117,9 +117,9 @@ class _ProgramSettings: allowed_exit_code=None, allowed_exit_message=b"\n *Control-C or similar caught [sig=15], quitting...", ), - "xz": _ProgramSettings(("xz",), tuple(range(0, 10)), "-T"), + "xz": _ProgramSettings(("xz",), tuple(range(10)), "-T"), "zstd": _ProgramSettings(("zstd",), tuple(range(1, 20)), "-T"), - "pigz": _ProgramSettings(("pigz", "--no-name"), tuple(range(0, 10)) + (11,), "-p"), + "pigz": _ProgramSettings(("pigz", "--no-name"), tuple(range(10)) + (11,), "-p"), "gzip": _ProgramSettings(("gzip", "--no-name"), tuple(range(1, 10))), "lz4": _ProgramSettings(("lz4",), tuple(range(_LZ4_LEVEL_MIN, _LZ4_LEVEL_MAX + 1))), } @@ -159,8 +159,8 @@ def __init__( self, filename: FileOrPath, mode="rb", - compresslevel: Optional[int] = None, - threads: Optional[int] = None, + compresslevel: int | None = None, + threads: int | None = None, program_settings: _ProgramSettings = _ProgramSettings(("gzip", "--no-name")), ): """ @@ -195,7 +195,7 @@ def __init__( self.name: str = str(self._path) self._mode: str = mode self._stderr = tempfile.TemporaryFile("w+b") - self._threads_flag: Optional[str] = program_settings.threads_flag + self._threads_flag: str | None = program_settings.threads_flag if threads is None: if "r" in mode: @@ -305,7 +305,7 @@ def seekable(self) -> bool: def tell(self) -> int: return self._file.tell() - def peek(self, n: Optional[int] = None): + def peek(self, n: int | None = None): return self._file.peek(n) # type: ignore def seek(self, offset, whence=0) -> int: @@ -438,8 +438,8 @@ def _open_stdin_or_out(mode: str) -> BinaryIO: def _open_bz2( filename: FileOrPath, mode: str, - compresslevel: Optional[int], - threads: Optional[int], + compresslevel: int | None, + threads: int | None, ): assert mode in ("rb", "ab", "wb") if compresslevel is None: @@ -467,8 +467,8 @@ def _open_bz2( def _open_xz( filename: FileOrPath, mode: str, - compresslevel: Optional[int], - threads: Optional[int], + compresslevel: int | None, + threads: int | None, ): assert mode in ("rb", "ab", "wb") if compresslevel is None: @@ -496,8 +496,8 @@ def _open_xz( def _open_zst( filename: FileOrPath, mode: str, - compresslevel: Optional[int], - threads: Optional[int], + compresslevel: int | None, + threads: int | None, ): assert mode in ("rb", "ab", "wb") assert compresslevel != 0 @@ -510,7 +510,7 @@ def _open_zst( if threads != 0: try: # zstd can compress using multiple cores - program_args: Tuple[str, ...] = ("zstd",) + program_args: tuple[str, ...] = ("zstd",) if "r" in mode: # Only use --long=31 for decompression. Using it for # compression overrides level settings for window size and @@ -549,8 +549,8 @@ def _open_zst( def _open_lz4( filename: FileOrPath, mode: str, - compresslevel: Optional[int], - threads: Optional[int], + compresslevel: int | None, + threads: int | None, ): assert mode in ("rb", "ab", "wb") if compresslevel is None: @@ -586,8 +586,8 @@ def _open_lz4( def _open_gz( filename: FileOrPath, mode: str, - compresslevel: Optional[int], - threads: Optional[int], + compresslevel: int | None, + threads: int | None, ): """ Open a gzip file. The ISA-L library is preferred when applicable because @@ -685,7 +685,7 @@ def _open_reproducible_gzip(filename, mode: str, compresslevel: int): return gzip_file -def _detect_format_from_content(filename: FileOrPath) -> Optional[str]: +def _detect_format_from_content(filename: FileOrPath) -> str | None: """ Attempts to detect file format from the content by reading the first 6 bytes. Returns None if no format could be detected. @@ -725,7 +725,7 @@ def _detect_format_from_content(filename: FileOrPath) -> Optional[str]: fileobj.close() -def _detect_format_from_extension(filename: Union[str, bytes]) -> Optional[str]: +def _detect_format_from_extension(filename: Union[str, bytes]) -> str | None: """ Attempt to detect file format from the filename extension. Return None if no format could be detected. @@ -742,7 +742,7 @@ def _detect_format_from_extension(filename: Union[str, bytes]) -> Optional[str]: def _file_or_path_to_binary_stream( file_or_path: FileOrPath, binary_mode: str -) -> Tuple[BinaryIO, bool]: +) -> tuple[BinaryIO, bool]: assert binary_mode in ("rb", "wb", "ab") if isinstance(file_or_path, (str, bytes)) or hasattr(file_or_path, "__fspath__"): return open(os.fspath(file_or_path), binary_mode), True # type: ignore @@ -784,13 +784,13 @@ def _file_is_a_socket_or_pipe(filepath): def xopen( filename: FileOrPath, mode: Literal["r", "w", "a", "rt", "wt", "at"] = ..., - compresslevel: Optional[int] = ..., - threads: Optional[int] = ..., + compresslevel: int | None = ..., + threads: int | None = ..., *, encoding: str = ..., - errors: Optional[str] = ..., - newline: Optional[str] = ..., - format: Optional[str] = ..., + errors: str | None = ..., + newline: str | None = ..., + format: str | None = ..., ) -> io.TextIOWrapper: ... @@ -798,26 +798,26 @@ def xopen( def xopen( filename: FileOrPath, mode: Literal["rb", "wb", "ab"], - compresslevel: Optional[int] = ..., - threads: Optional[int] = ..., + compresslevel: int | None = ..., + threads: int | None = ..., *, encoding: str = ..., errors: None = ..., newline: None = ..., - format: Optional[str] = ..., + format: str | None = ..., ) -> BinaryIO: ... -def xopen( # noqa: C901 +def xopen( filename: FileOrPath, mode: Literal["r", "w", "a", "rt", "rb", "wt", "wb", "at", "ab"] = "r", - compresslevel: Optional[int] = None, - threads: Optional[int] = None, + compresslevel: int | None = None, + threads: int | None = None, *, encoding: str = "utf-8", - errors: Optional[str] = None, - newline: Optional[str] = None, - format: Optional[str] = None, + errors: str | None = None, + newline: str | None = None, + format: str | None = None, ) -> IO: """ A replacement for the "open" function that can also read and write @@ -868,7 +868,7 @@ def xopen( # noqa: C901 if mode in ("r", "w", "a"): mode += "t" # type: ignore if mode not in ("rt", "rb", "wt", "wb", "at", "ab"): - raise ValueError("Mode '{}' not supported".format(mode)) + raise ValueError(f"Mode '{mode}' not supported") binary_mode = mode[0] + "b" filepath = _filepath_from_path_or_filelike(filename) diff --git a/tests/conftest.py b/tests/conftest.py index 2a63905..c6a7b6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import os import random import string + import pytest from xopen import xopen diff --git a/tests/test_piped.py b/tests/test_piped.py index c66f67f..38e1728 100644 --- a/tests/test_piped.py +++ b/tests/test_piped.py @@ -7,16 +7,17 @@ import os import shutil import sys -import pytest -from pathlib import Path from itertools import cycle +from pathlib import Path + +import pytest from xopen import ( - xopen, - _PipedCompressionProgram, _MAX_PIPE_SIZE, _PROGRAM_SETTINGS, + _PipedCompressionProgram, _ProgramSettings, + xopen, ) extensions = ["", ".gz", ".bz2", ".xz", ".zst", ".lz4"] @@ -25,7 +26,7 @@ import fcntl if not hasattr(fcntl, "F_GETPIPE_SZ") and sys.platform == "linux": - setattr(fcntl, "F_GETPIPE_SZ", 1032) + fcntl.F_GETPIPE_SZ = 1032 except ImportError: fcntl = None diff --git a/tests/test_xopen.py b/tests/test_xopen.py index 6876860..3c8dfe7 100644 --- a/tests/test_xopen.py +++ b/tests/test_xopen.py @@ -15,7 +15,6 @@ from contextlib import contextmanager from pathlib import Path - import pytest from xopen import _detect_format_from_content, xopen @@ -620,9 +619,7 @@ def test_lz4_program_without_bindings(tmp_path, monkeypatch): @pytest.mark.parametrize("threads", (0, 1)) def test_xopen_zst_long_window_size(threads): - if threads == 0 and zstd is None: - return - elif threads == 1 and not shutil.which("zstd"): + if threads == 0 and zstd is None or threads == 1 and not shutil.which("zstd"): return # File created with: # cat /dev/zero | head -c 2147483648 > only_zeroes From 35f009ef64c4547e1c50d9907b6b11f8d9218cb6 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 23:07:01 +0200 Subject: [PATCH 05/18] Fix ruff FURB161 --- pyproject.toml | 1 - src/xopen/__init__.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5225313..16d81d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,6 @@ ignore = [ "B008", # Do not perform function call `_ProgramSettings` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable "C408", # Unnecessary `dict()` call (rewrite as a literal) "F401", # `typing.Tuple` imported but unused - "FURB161", # Use of `bin(int(m.group(1).replace(",", ""), 16)).count('1')` "PLW1510", # `subprocess.run` without explicit `check` argument "SIM115", # Use a context manager for opening files "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index 358a9be..7666f78 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -141,7 +141,7 @@ def _available_cpu_count() -> int: status = f.read() m = re.search(r"(?m)^Cpus_allowed:\s*(.*)$", status) if m: - res = bin(int(m.group(1).replace(",", ""), 16)).count("1") + res = int(m.group(1).replace(",", ""), 16).bit_count() if res > 0: return res except OSError: From 1f41714d11e62307dbb22962c184dfca6a400a18 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 23:10:25 +0200 Subject: [PATCH 06/18] Fix ruff UP007 --- pyproject.toml | 1 - src/xopen/__init__.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 16d81d4..d9c61e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ ignore = [ "PLW1510", # `subprocess.run` without explicit `check` argument "SIM115", # Use a context manager for opening files "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements - "UP007", # Use `X | Y` for type annotations "UP035", # `typing.Tuple` is deprecated, use `tuple` instead "YTT204", # `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple ] diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index 7666f78..7fe449e 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -93,8 +93,8 @@ _MAX_PIPE_SIZE = -1 -FilePath = Union[str, bytes, os.PathLike] -FileOrPath = Union[FilePath, IO] +FilePath = str | bytes | os.PathLike +FileOrPath = FilePath | IO @dataclasses.dataclass @@ -725,7 +725,7 @@ def _detect_format_from_content(filename: FileOrPath) -> str | None: fileobj.close() -def _detect_format_from_extension(filename: Union[str, bytes]) -> str | None: +def _detect_format_from_extension(filename: str | bytes) -> str | None: """ Attempt to detect file format from the filename extension. Return None if no format could be detected. From ff8a9eb1fcf025ca6379fa81818dd9a282606d63 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 23:11:46 +0200 Subject: [PATCH 07/18] Fix ruff UP035 --- pyproject.toml | 1 - src/xopen/__init__.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d9c61e1..6fecfdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ ignore = [ "PLW1510", # `subprocess.run` without explicit `check` argument "SIM115", # Use a context manager for opening files "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements - "UP035", # `typing.Tuple` is deprecated, use `tuple` instead "YTT204", # `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple ] diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index 7fe449e..78c8abe 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -25,10 +25,8 @@ from typing import ( IO, BinaryIO, - Dict, Literal, Optional, - Tuple, Union, overload, ) From 0fb9ae4ab429af85123b2042c372d5fac344afc1 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 23:12:36 +0200 Subject: [PATCH 08/18] Remove unused imports --- pyproject.toml | 1 - src/xopen/__init__.py | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6fecfdd..cb04379 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ write_to = "src/xopen/_version.py" ignore = [ "B008", # Do not perform function call `_ProgramSettings` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable "C408", # Unnecessary `dict()` call (rewrite as a literal) - "F401", # `typing.Tuple` imported but unused "PLW1510", # `subprocess.run` without explicit `check` argument "SIM115", # Use a context manager for opening files "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index 78c8abe..c61aa25 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -22,14 +22,7 @@ import threading import time from types import ModuleType -from typing import ( - IO, - BinaryIO, - Literal, - Optional, - Union, - overload, -) +from typing import IO, BinaryIO, Literal, overload from ._version import version as __version__ From 772e8abad5731f0bcedca39f27f5b0c094f6ccc2 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 23:15:08 +0200 Subject: [PATCH 09/18] Fix ruff YTT204 --- pyproject.toml | 1 - tests/test_xopen.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cb04379..d0e25bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,6 @@ ignore = [ "PLW1510", # `subprocess.run` without explicit `check` argument "SIM115", # Use a context manager for opening files "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements - "YTT204", # `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple ] [tool.pytest.ini_options] diff --git a/tests/test_xopen.py b/tests/test_xopen.py index 3c8dfe7..b56c7a6 100644 --- a/tests/test_xopen.py +++ b/tests/test_xopen.py @@ -426,7 +426,7 @@ def test_write_gzip_no_threads_no_isal(tmp_path, xopen_without_igzip): import gzip with xopen_without_igzip(tmp_path / "out.gz", "wb", threads=0) as f: - if sys.version_info.major == 3 and sys.version_info.minor >= 12: + if sys.version_info >= (3, 12): assert isinstance(f, gzip.GzipFile), f else: assert isinstance(f.raw, gzip.GzipFile) From 230341d514ffb7c1206e2a5770db4e3881bd4886 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 23:17:37 +0200 Subject: [PATCH 10/18] Fix ruff PLW1510 --- pyproject.toml | 1 - tests/test_xopen.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d0e25bc..04fd1e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ write_to = "src/xopen/_version.py" ignore = [ "B008", # Do not perform function call `_ProgramSettings` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable "C408", # Unnecessary `dict()` call (rewrite as a literal) - "PLW1510", # `subprocess.run` without explicit `check` argument "SIM115", # Use a context manager for opening files "SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements ] diff --git a/tests/test_xopen.py b/tests/test_xopen.py index b56c7a6..9f68c0f 100644 --- a/tests/test_xopen.py +++ b/tests/test_xopen.py @@ -742,5 +742,6 @@ def test_xopen_dev_stdin_read(threads, ext): shell=True, stdout=subprocess.PIPE, encoding="ascii", + check=True, ) assert result.stdout == CONTENT + "\n" From 48776f9708fd9e0b97deab81d986577adcb372ce Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Thu, 22 Oct 2026 21:08:09 +0200 Subject: [PATCH 11/18] Drop support for Python 3.10 as it is EOL --- .github/workflows/ci.yml | 16 ++++++++-------- README.rst | 1 + pyproject.toml | 2 +- tox.toml | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b5f329..2b45147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10"] + python-version: ["3.14"] toxenv: [black, ruff, mypy] steps: - uses: actions/checkout@v6 @@ -34,28 +34,28 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.14t"] optional-deps: [true] with-libs: [true] include: - os: macos-latest - python-version: "3.10" + python-version: "3.14" optional-deps: true - os: ubuntu-latest - python-version: "3.10" + python-version: "3.14" with-libs: false optional-deps: false - os: ubuntu-latest - python-version: "3.10" + python-version: "3.14" with-libs: false optional-deps: true - os: ubuntu-latest - python-version: "3.10" + python-version: "3.14" optional-deps: false with-libs: false with-zstandard: true - os: windows-latest - python-version: "3.10" + python-version: "3.14" steps: - name: Install optional tools macOS if: runner.os == 'macOS' && matrix.optional-deps @@ -102,7 +102,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: "3.10" + python-version: "3.14" - name: Make distributions run: | python -m pip install build diff --git a/README.rst b/README.rst index 66eef81..2ee8ae6 100644 --- a/README.rst +++ b/README.rst @@ -192,6 +192,7 @@ development version * #181/#168: Added lz4 as supported compression format. Contributed by @Dev-iL and @gnzsnz. +* Dropped support for Python 3.10 v2.1.0 (2026-06-03) ~~~~~~~~~~~~~~~~~~~ diff --git a/pyproject.toml b/pyproject.toml index 04fd1e3..0922244 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3" ] -requires-python = ">=3.10" +requires-python = ">=3.11" dynamic = ["version"] dependencies = [ 'isal>=1.6.1; platform.machine == "x86_64" or platform.machine == "AMD64" or platform.machine == "aarch64"', diff --git a/tox.toml b/tox.toml index 58b905d..7f7af1f 100644 --- a/tox.toml +++ b/tox.toml @@ -1,7 +1,7 @@ -env_list = ["black", "ruff", "mypy", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] +env_list = ["black", "ruff", "mypy", "3.11", "3.12", "3.13", "3.14", "3.14t"] [env_run_base] -default_base_python = "3.10" +default_base_python = "3.14" deps = [ "pytest", "pytest-timeout", From d4e790b5fa27709af0755c113ccd23f115460bb3 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Mon, 26 Oct 2026 08:49:06 +0100 Subject: [PATCH 12/18] Bump codecov-action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b45147..e2bd9cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10' && matrix.optional-deps && matrix.with-libs run: tox run -e lz4 - name: Upload coverage report - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v7 deploy: timeout-minutes: 10 From d9dfb60d2494686496ef7d636cc213944bc42ea9 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Mon, 26 Oct 2026 08:57:44 +0100 Subject: [PATCH 13/18] Enable dependabot for github-actions --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e5f73e9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + groups: + github-actions: + patterns: + - "*" + schedule: + interval: "weekly" From ddd41a6dc756df5dbdc5571a2736912d070e3bba Mon Sep 17 00:00:00 2001 From: Ruben Vorderman Date: Mon, 26 Oct 2026 11:20:17 +0100 Subject: [PATCH 14/18] Update black revision for pre-commit hook --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f902a9a..9046008 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,6 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 26.10.0 hooks: - id: black From 86ca21d39d0281661a6f960fa3481c3e499d304a Mon Sep 17 00:00:00 2001 From: Ruben Vorderman Date: Mon, 26 Oct 2026 11:20:33 +0100 Subject: [PATCH 15/18] Remove read statement on posssibly closed file --- src/xopen/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index c61aa25..816dc48 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -321,7 +321,6 @@ def close(self) -> None: check_allowed_code_and_message = True self.process.wait() self._feeding = False - self._file.read() if self.in_thread: self.in_thread.join() self._file.close() From 89298afe57955204acba5f948e97dea07c5abe9e Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Tue, 27 Oct 2026 11:18:14 +0100 Subject: [PATCH 16/18] Revert "Add test for seek() and tell()" This reverts commit 4551240fe829fc9580aea0482877d517b0cdbe71. --- tests/test_piped.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/test_piped.py b/tests/test_piped.py index 38e1728..3e76fb1 100644 --- a/tests/test_piped.py +++ b/tests/test_piped.py @@ -314,19 +314,6 @@ def test_piped_compression_reader_peek_binary(reader): assert read_h.peek(1).startswith(b"T") -@pytest.mark.skipif( - sys.platform != "win32", reason="seeking only works on Windows for now" -) -def test_piped_compression_reader_seek_and_tell(reader): - program_settings, extension = reader - filegz = TEST_DIR / f"file.txt{extension}" - with _PipedCompressionProgram(filegz, "rb", program_settings=program_settings) as f: - original_position = f.tell() - assert f.read(4) == b"Test" - f.seek(original_position) - assert f.read(8) == b"Testing," - - @pytest.mark.parametrize("mode", ["r", "rb"]) def test_piped_compression_reader_peek_text(reader, mode): program_settings, extension = reader From b9337b8f3c35e0000cf8b69fcc41ad0eb6ffd228 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Tue, 27 Oct 2026 11:18:14 +0100 Subject: [PATCH 17/18] Revert "Add seek() and tell() to PipedCompressionReader" This reverts commit d95ef08dd28b3ec3e785bf25724fbee9ad35b11a. --- src/xopen/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index 816dc48..e5a3151 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -293,15 +293,9 @@ def readline(self, *args) -> bytes: def seekable(self) -> bool: return self._file.seekable() - def tell(self) -> int: - return self._file.tell() - def peek(self, n: int | None = None): return self._file.peek(n) # type: ignore - def seek(self, offset, whence=0) -> int: - return self._file.seek(offset, whence) - def close(self) -> None: if self.closed: return From 46690a2f83d55d199f5e57de3e68cd41ef4728b8 Mon Sep 17 00:00:00 2001 From: Marcel Martin Date: Tue, 27 Oct 2026 11:29:03 +0100 Subject: [PATCH 18/18] Let seekable() return False --- README.rst | 2 ++ src/xopen/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2ee8ae6..b854367 100644 --- a/README.rst +++ b/README.rst @@ -192,6 +192,8 @@ development version * #181/#168: Added lz4 as supported compression format. Contributed by @Dev-iL and @gnzsnz. +* PipedCompressionReader no longer has seek/tell methods. This only worked + on Windows before (and only sometimes). * Dropped support for Python 3.10 v2.1.0 (2026-06-03) diff --git a/src/xopen/__init__.py b/src/xopen/__init__.py index e5a3151..356a31c 100644 --- a/src/xopen/__init__.py +++ b/src/xopen/__init__.py @@ -291,7 +291,7 @@ def readline(self, *args) -> bytes: return self._file.readline(*args) def seekable(self) -> bool: - return self._file.seekable() + return False def peek(self, n: int | None = None): return self._file.peek(n) # type: ignore