diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96e0cb1..3a4908c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,6 @@ jobs: strategy: matrix: python: - - "3.9" - "3.10" - "3.11" - "3.12" diff --git a/CHANGELOG.md b/CHANGELOG.md index e03d0c9..18fb4aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ adheres to [Semantic Versioning](https://semver.org/). ### :boom: Breaking changes -- End of Python 3.7 and 3.8 support +- End of Python 3.7, 3.8, and 3.9 support ### :house: Internal diff --git a/pyproject.toml b/pyproject.toml index 943c83c..f56c53c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -23,7 +22,7 @@ classifiers = [ "Topic :: System :: Archiving", "Topic :: System :: Archiving :: Compression", ] -requires-python = ">=3.9" +requires-python = ">=3.10" [project.urls] Homepage = "https://github.com/rogdham/python-xz" @@ -139,7 +138,7 @@ testpaths = ["tests"] [tool.ruff] src = ["src"] -target-version = "py39" +target-version = "py310" [tool.ruff.lint] select = ["ALL"] diff --git a/src/xz/block.py b/src/xz/block.py index 6b4a00a..9f31a8b 100644 --- a/src/xz/block.py +++ b/src/xz/block.py @@ -1,6 +1,5 @@ from io import DEFAULT_BUFFER_SIZE, SEEK_SET from lzma import FORMAT_XZ, LZMACompressor, LZMADecompressor, LZMAError -from typing import Optional, Union from xz.common import ( XZError, @@ -122,7 +121,7 @@ def __init__( uncompressed_size: int, preset: _LZMAPresetType = None, filters: _LZMAFiltersType = None, - block_read_strategy: Optional[_BlockReadStrategyType] = None, + block_read_strategy: _BlockReadStrategyType | None = None, ) -> None: super().__init__(uncompressed_size) self.fileobj = fileobj @@ -131,7 +130,7 @@ def __init__( self.filters = filters self.block_read_strategy = block_read_strategy or KeepBlockReadStrategy() self.unpadded_size = unpadded_size - self.operation: Union[BlockRead, BlockWrite, None] = None + self.operation: BlockRead | BlockWrite | None = None @property def uncompressed_size(self) -> int: diff --git a/src/xz/file.py b/src/xz/file.py index e81ccb3..7d96993 100644 --- a/src/xz/file.py +++ b/src/xz/file.py @@ -1,7 +1,6 @@ from io import SEEK_CUR, SEEK_END import os -import sys -from typing import BinaryIO, Optional, cast +from typing import BinaryIO, cast import warnings from xz.common import DEFAULT_CHECK, XZError @@ -36,7 +35,7 @@ def __init__( check: int = -1, preset: _LZMAPresetType = None, filters: _LZMAFiltersType = None, - block_read_strategy: Optional[_BlockReadStrategyType] = None, + block_read_strategy: _BlockReadStrategyType | None = None, ) -> None: """Open an XZ file in binary mode. @@ -113,7 +112,7 @@ def __init__( self._close_check_empty = self._mode[0] != "r" @property - def _last_stream(self) -> Optional[XZStream]: + def _last_stream(self) -> XZStream | None: try: return self._fileobjs.last_item except KeyError: @@ -145,10 +144,9 @@ def close(self) -> None: finally: if self._close_fileobj: self.fileobj.close() # self.fileobj exists at this point - if sys.version_info < (3, 10): # pragma: no cover - # fix coverage issue on some Python versions - # see https://github.com/nedbat/coveragepy/issues/1480 - pass + + # fix coverage issue on some Python versions + pass # noqa: PIE790 @property def stream_boundaries(self) -> list[int]: diff --git a/src/xz/io.py b/src/xz/io.py index 8dcd1fc..b03074f 100644 --- a/src/xz/io.py +++ b/src/xz/io.py @@ -6,7 +6,7 @@ IOBase, UnsupportedOperation, ) -from typing import BinaryIO, Generic, Optional, TypeVar, Union, cast +from typing import BinaryIO, Generic, TypeVar, cast from xz.utils import FloorDict @@ -150,7 +150,7 @@ def write(self, data: bytes) -> int: self._length = max(self._length, self._pos) return written_bytes - def truncate(self, size: Optional[int] = None) -> int: + def truncate(self, size: int | None = None) -> int: """Truncate file to size bytes. Size defaults to the current IO position as reported by tell(). @@ -242,7 +242,7 @@ def _read(self, size: int) -> bytes: class IOProxy(IOAbstract): def __init__( self, - fileobj: Union[BinaryIO, IOBase], # see typing note on top of this file + fileobj: BinaryIO | IOBase, # see typing note on top of this file start: int, end: int, ) -> None: @@ -290,7 +290,7 @@ def _write_after(self) -> None: def _write(self, data: bytes) -> int: if self._fileobjs: - fileobj: Optional[T] = self._get_fileobj() + fileobj: T | None = self._get_fileobj() else: fileobj = None diff --git a/src/xz/open.py b/src/xz/open.py index 1608a14..b16b0f8 100644 --- a/src/xz/open.py +++ b/src/xz/open.py @@ -1,5 +1,5 @@ from io import TextIOWrapper -from typing import BinaryIO, Optional, Union, cast, overload +from typing import BinaryIO, cast, overload from xz.file import XZFile from xz.typing import ( @@ -22,10 +22,10 @@ def __init__( check: int = -1, preset: _LZMAPresetType = None, filters: _LZMAFiltersType = None, - block_read_strategy: Optional[_BlockReadStrategyType] = None, - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, + block_read_strategy: _BlockReadStrategyType | None = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, ) -> None: self.xz_file = XZFile( filename, @@ -75,11 +75,11 @@ def xz_open( check: int = -1, preset: _LZMAPresetType = None, filters: _LZMAFiltersType = None, - block_read_strategy: Optional[_BlockReadStrategyType] = None, + block_read_strategy: _BlockReadStrategyType | None = None, # text-mode kwargs - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, ) -> XZFile: ... @@ -92,11 +92,11 @@ def xz_open( check: int = -1, preset: _LZMAPresetType = None, filters: _LZMAFiltersType = None, - block_read_strategy: Optional[_BlockReadStrategyType] = None, + block_read_strategy: _BlockReadStrategyType | None = None, # text-mode kwargs - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, ) -> _XZFileText: ... @@ -109,12 +109,12 @@ def xz_open( check: int = -1, preset: _LZMAPresetType = None, filters: _LZMAFiltersType = None, - block_read_strategy: Optional[_BlockReadStrategyType] = None, + block_read_strategy: _BlockReadStrategyType | None = None, # text-mode kwargs - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, -) -> Union[XZFile, _XZFileText]: ... + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, +) -> XZFile | _XZFileText: ... def xz_open( @@ -125,12 +125,12 @@ def xz_open( check: int = -1, preset: _LZMAPresetType = None, filters: _LZMAFiltersType = None, - block_read_strategy: Optional[_BlockReadStrategyType] = None, + block_read_strategy: _BlockReadStrategyType | None = None, # text-mode kwargs - encoding: Optional[str] = None, - errors: Optional[str] = None, - newline: Optional[str] = None, -) -> Union[XZFile, _XZFileText]: + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, +) -> XZFile | _XZFileText: """Open an XZ file in binary or text mode. filename can be either an actual file name (given as a str, bytes, diff --git a/src/xz/stream.py b/src/xz/stream.py index 49992b8..10f2797 100644 --- a/src/xz/stream.py +++ b/src/xz/stream.py @@ -1,5 +1,5 @@ from io import SEEK_CUR -from typing import BinaryIO, Optional +from typing import BinaryIO from xz.block import XZBlock from xz.common import ( @@ -22,7 +22,7 @@ def __init__( check: int, preset: _LZMAPresetType = None, filters: _LZMAFiltersType = None, - block_read_strategy: Optional[_BlockReadStrategyType] = None, + block_read_strategy: _BlockReadStrategyType | None = None, ) -> None: super().__init__() self.fileobj = fileobj @@ -49,7 +49,7 @@ def _fileobj_blocks_end_pos(self) -> int: def parse( cls, fileobj: BinaryIO, - block_read_strategy: Optional[_BlockReadStrategyType] = None, + block_read_strategy: _BlockReadStrategyType | None = None, ) -> "XZStream": """Parse one XZ stream from a fileobj. diff --git a/src/xz/typing.py b/src/xz/typing.py index 5d1c722..a327255 100644 --- a/src/xz/typing.py +++ b/src/xz/typing.py @@ -1,8 +1,8 @@ from collections.abc import Mapping, Sequence from os import PathLike -from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Optional, Protocol, Union +from typing import TYPE_CHECKING, Any, BinaryIO, Literal, Protocol -_LZMAFilenameType = Union[str, bytes, PathLike[str], PathLike[bytes], BinaryIO] +_LZMAFilenameType = str | bytes | PathLike[str] | PathLike[bytes] | BinaryIO if TYPE_CHECKING: @@ -10,8 +10,8 @@ from xz.block import XZBlock -_LZMAPresetType = Optional[int] -_LZMAFiltersType = Optional[Sequence[Mapping[str, Any]]] +_LZMAPresetType = int | None +_LZMAFiltersType = Sequence[Mapping[str, Any]] | None # all valid modes if we don't consider changing order nor repetitions diff --git a/tests/integration/test_file_read.py b/tests/integration/test_file_read.py index 302a4f1..27c150d 100644 --- a/tests/integration/test_file_read.py +++ b/tests/integration/test_file_read.py @@ -14,7 +14,9 @@ def test_read_all(integration_case: _IntegrationCase, data_pattern: bytes) -> No pos = 0 stream_boundaries = [] block_boundaries = [] - for stream_item, metadata_stream in zip(streams_items, metadata["streams"]): + for stream_item, metadata_stream in zip( + streams_items, metadata["streams"], strict=True + ): stream_boundaries.append(pos) stream_pos, stream = stream_item assert stream_pos == pos @@ -22,7 +24,7 @@ def test_read_all(integration_case: _IntegrationCase, data_pattern: bytes) -> No block_items = list(stream._fileobjs.items()) assert len(block_items) == len(metadata_stream["blocks"]) for block_item, metadata_block in zip( - block_items, metadata_stream["blocks"] + block_items, metadata_stream["blocks"], strict=True ): block_boundaries.append(pos) block_pos, block = block_item diff --git a/tests/integration/test_ram_usage.py b/tests/integration/test_ram_usage.py index f6e99a0..39e0dc4 100644 --- a/tests/integration/test_ram_usage.py +++ b/tests/integration/test_ram_usage.py @@ -3,7 +3,7 @@ from lzma import compress from pathlib import Path from random import randbytes, seed -from typing import BinaryIO, Optional, cast +from typing import BinaryIO, cast import pytest @@ -70,7 +70,7 @@ def test_read_linear(fileobj: BinaryIO, ram_usage: Callable[[], int]) -> None: def test_partial_read_each_block( fileobj: BinaryIO, ram_usage: Callable[[], int] ) -> None: - one_block_memory: Optional[int] = None + one_block_memory: int | None = None with XZFile(fileobj) as xz_file: for pos in xz_file.block_boundaries[1:]: @@ -93,7 +93,7 @@ def test_write(tmp_path: Path, ram_usage: Callable[[], int]) -> None: seed(0) - one_block_memory: Optional[int] = None + one_block_memory: int | None = None with XZFile(tmp_path / "archive.xz", "w") as xz_file: for i in range(nb_blocks): diff --git a/tests/integration/test_readme.py b/tests/integration/test_readme.py index 095f37b..7df5ad3 100644 --- a/tests/integration/test_readme.py +++ b/tests/integration/test_readme.py @@ -3,7 +3,6 @@ import os from pathlib import Path import shutil -from typing import Optional import pytest @@ -22,7 +21,7 @@ def change_dir(tmp_path: Path) -> Iterator[None]: def _parse_readme() -> list[tuple[int, str]]: code_blocks = [] current_code_block = "" - current_code_block_line: Optional[int] = None + current_code_block_line: int | None = None with (Path(__file__).parent.parent.parent / "README.md").open() as fin: for line_no, line in enumerate(fin): if line.startswith("```"): diff --git a/tests/unit/test_attr_proxy.py b/tests/unit/test_attr_proxy.py index 46508c6..4cefd05 100644 --- a/tests/unit/test_attr_proxy.py +++ b/tests/unit/test_attr_proxy.py @@ -1,5 +1,3 @@ -from typing import Optional - import pytest from xz.utils import AttrProxy @@ -10,7 +8,7 @@ class Dest: class Src: - proxy: Optional[Dest] = None + proxy: Dest | None = None abc = AttrProxy[str]("proxy") diff --git a/tests/unit/test_file.py b/tests/unit/test_file.py index 9c7641e..09379b8 100644 --- a/tests/unit/test_file.py +++ b/tests/unit/test_file.py @@ -2,7 +2,7 @@ from io import SEEK_END, SEEK_SET, BytesIO, UnsupportedOperation import os from pathlib import Path -from typing import Optional, Union, cast +from typing import cast from unittest.mock import Mock, call import pytest @@ -115,7 +115,7 @@ def test_read( tmp_path: Path, data_pattern_locate: Callable[[bytes], tuple[int, int]], ) -> None: - filename: Union[Path, BytesIO, str] + filename: Path | BytesIO | str if filetype == "fileobj": filename = BytesIO(FILE_BYTES) @@ -186,7 +186,7 @@ def test_read_with_mode( tmp_path: Path, data_pattern_locate: Callable[[bytes], tuple[int, int]], ) -> None: - filename: Union[Path, BytesIO] + filename: Path | BytesIO if from_file: filename = tmp_path / "archive.xz" @@ -268,7 +268,7 @@ def test_read_strategy_calls() -> None: @pytest.mark.parametrize("max_block_read_nb", [None, 1, 2, 7, 100]) -def test_read_default_strategy(max_block_read_nb: Optional[int]) -> None: +def test_read_default_strategy(max_block_read_nb: int | None) -> None: fileobj = Mock(wraps=BytesIO(FILE_BYTES_MANY_SMALL_BLOCKS)) max_block_read_nb_ = 8 if max_block_read_nb is None else max_block_read_nb @@ -432,7 +432,7 @@ def test_write_with_mode( "1fb6f37d010000000004595a" # footer ) - filename: Union[Path, BytesIO] + filename: Path | BytesIO if from_file: filename = tmp_path / "archive.xz" diff --git a/tests/unit/test_open.py b/tests/unit/test_open.py index 4a377cd..0feb7ed 100644 --- a/tests/unit/test_open.py +++ b/tests/unit/test_open.py @@ -1,7 +1,6 @@ from io import BytesIO import lzma from pathlib import Path -from typing import Optional from unittest.mock import Mock import pytest @@ -99,9 +98,7 @@ def test_mode_rt_encoding(encoding: str, expected: str) -> None: ), ], ) -def test_mode_rt_encoding_errors( - errors: Optional[str], expected: Optional[str] -) -> None: +def test_mode_rt_encoding_errors(errors: str | None, expected: str | None) -> None: fileobj = BytesIO( bytes.fromhex( "fd377a585a000000ff12d9410200210116000000742fe5a301000a656e99636f" @@ -127,7 +124,7 @@ def test_mode_rt_encoding_errors( pytest.param("\r\n", ["a\nb\rc\r\n", "d"], id="'\r\n'"), ], ) -def test_mode_rt_newline(newline: Optional[str], expected: list[str]) -> None: +def test_mode_rt_newline(newline: str | None, expected: list[str]) -> None: fileobj = BytesIO( bytes.fromhex( "fd377a585a000000ff12d9410200210116000000742fe5a3010007610a620d63" @@ -443,7 +440,7 @@ def test_mode_wt_encoding(encoding: str, data: str) -> None: ), ], ) -def test_mode_wt_encoding_errors(errors: Optional[str], data: Optional[bytes]) -> None: +def test_mode_wt_encoding_errors(errors: str | None, data: bytes | None) -> None: fileobj = BytesIO() with xz_open(fileobj, "wt", errors=errors) as xzfile: @@ -468,7 +465,7 @@ def test_mode_wt_encoding_errors(errors: Optional[str], data: Optional[bytes]) - pytest.param("\r\n", b"a\r\nb\r\n", id="'\r\n'"), ], ) -def test_mode_wt_newline(newline: Optional[str], data: bytes) -> None: +def test_mode_wt_newline(newline: str | None, data: bytes) -> None: fileobj = BytesIO() with xz_open(fileobj, "wt", newline=newline) as xzfile: diff --git a/tox.ini b/tox.ini index 35b16fd..80ff81f 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ [tox] envlist = - py, py39, py310, py311, py312, py313, py314, pypy3 + py, py310, py311, py312, py313, py314, pypy3 build, generate-integration-files, lint, type [testenv]