Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
- "**"
pull_request:

permissions: {}

env:
PY_COLORS: 1

Expand All @@ -26,9 +28,12 @@ jobs:
- "pypy-3.10"
- "pypy-3.11"
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: true
- name: Setup Python ${{ matrix.python }}
uses: actions/setup-python@v6
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python }}
- name: Install dependencies
Expand All @@ -46,12 +51,14 @@ jobs:
- lint
- type
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# build env: fetch all commits for version computation
fetch-depth: ${{ matrix.env == 'build' && '0' || '1' }}
persist-credentials: false
submodules: true
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
- name: Install dependencies
Expand All @@ -60,7 +67,7 @@ jobs:
run: tox run -e ${{ matrix.env }}
- if: ${{ matrix.env == 'build' }}
name: Save ${{ matrix.env }} artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ matrix.env }}
path: dist
Expand All @@ -76,12 +83,12 @@ jobs:
id-token: write # This permission is mandatory for trusted publishing
steps:
- name: Restore build artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build
path: dist
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
verbose: true
print-hash: true
22 changes: 22 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: check

on:
push:
branches:
- "ci-*"
pull_request:
workflow_dispatch:

permissions: {}

jobs:
zizmor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: zizmor
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
with:
advanced-security: false
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project
adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

[unreleased]: https://github.com/rogdham/python-xz/compare/v0.6.0...HEAD

### :house: Internal

- Update dev dependencies
- Necessary code changes following dev dependency update: mypy, ruff

## [0.6.0] - 2025-10-18

[0.6.0]: https://github.com/rogdham/python-xz/releases/tag/v0.6.0
Expand Down
10 changes: 5 additions & 5 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
-e .

# build
build==1.3.0
build==1.6.1

# lint
ruff==0.13.1
ruff==0.16.10

# tests
pytest==8.4.2
pytest-cov==7.0.0
pytest==9.1.1
pytest-cov==7.1.0

# type
mypy==1.18.2
mypy==2.4.0
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,15 @@ select = ["ALL"]
ignore = [
"C901",
"COM812",
"CPY001",
"D",
"E501",
"EM",
"ERA001",
"FA100",
"PLR0912",
"PLR0913",
"PLR0917",
"TRY003",
"TRY301",
]
Expand Down
6 changes: 3 additions & 3 deletions src/xz/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def _write(self, data: bytes) -> None:
self.fileobj.write(data)
self.pos += len(data)

def compress(self, data: bytes) -> None:
def compress(self, data: bytes | memoryview) -> None:
self._write(self.compressor.compress(data))

def finish(self) -> tuple[int, int]:
Expand Down Expand Up @@ -136,7 +136,7 @@ def __init__(
def uncompressed_size(self) -> int:
return self._length

def _read(self, size: int) -> bytes:
def _read(self, size: int) -> bytes | memoryview:
# enforce read mode
if not isinstance(self.operation, BlockRead):
self._write_end()
Expand Down Expand Up @@ -164,7 +164,7 @@ def _read(self, size: int) -> bytes:
def writable(self) -> bool:
return isinstance(self.operation, BlockWrite) or not self._length

def _write(self, data: bytes) -> int:
def _write(self, data: memoryview) -> int:
# enforce write mode
if not isinstance(self.operation, BlockWrite):
self.clear()
Expand Down
29 changes: 15 additions & 14 deletions src/xz/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ruff: noqa: PLR2004

from binascii import crc32 as crc32int
from collections.abc import Iterable
import lzma
from struct import pack, unpack
from typing import cast
Expand All @@ -13,7 +14,7 @@ class XZError(Exception):
pass


def encode_mbi(value: int) -> bytes:
def encode_mbi(value: int) -> bytearray:
data = bytearray()
while value >= 0x80:
data.append((value & 0x7F) | 0x80)
Expand All @@ -22,7 +23,7 @@ def encode_mbi(value: int) -> bytes:
return data


def decode_mbi(data: bytes) -> tuple[int, int]:
def decode_mbi(data: Iterable[int]) -> tuple[int, int]:
value = 0
for size, byte in enumerate(data):
value |= (byte & 0x7F) << (size * 7)
Expand Down Expand Up @@ -92,31 +93,31 @@ def parse_xz_header(header: bytes) -> int:
def parse_xz_index(index: bytes) -> list[tuple[int, int]]:
if len(index) < 8 or len(index) % 4:
raise XZError("index length")
index = memoryview(index)
if index[0]:
index_view = memoryview(index)
if index_view[0]:
raise XZError("index indicator")
if crc32(index[:-4]) != index[-4:]:
if crc32(index_view[:-4].tobytes()) != index_view[-4:]:
raise XZError("index crc32")
size, nb_records = decode_mbi(index[1:])
index = index[1 + size : -4]
size, nb_records = decode_mbi(index_view[1:])
index_view = index_view[1 + size : -4]
# records
records = []
for _ in range(nb_records):
if not index:
if not index_view:
raise XZError("index size")
size, unpadded_size = decode_mbi(index)
size, unpadded_size = decode_mbi(index_view)
if not unpadded_size:
raise XZError("index record unpadded size")
index = index[size:]
if not index:
index_view = index_view[size:]
if not index_view:
raise XZError("index size")
size, uncompressed_size = decode_mbi(index)
size, uncompressed_size = decode_mbi(index_view)
if not uncompressed_size:
raise XZError("index record uncompressed size")
index = index[size:]
index_view = index_view[size:]
records.append((unpadded_size, uncompressed_size))
# index padding
if any(index):
if any(index_view):
raise XZError("index padding")
return records

Expand Down
31 changes: 18 additions & 13 deletions src/xz/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,19 @@ def write(self, data: bytes) -> int:
if padding_size > 0:
null_bytes = memoryview(bytearray(DEFAULT_BUFFER_SIZE))
self._pos = self._length
data = memoryview(data)
while padding_size or data:
data_view = memoryview(data)
while padding_size or data_view:
self._write_start()
if padding_size > 0:
# pad with null bytes, not counted in written_bytes
padding = null_bytes[:padding_size]
written_len = self._write(padding) # do not stop if nothing was written
padding_size -= written_len
else:
written_len = self._write(data) # do not stop if nothing was written
data = data[written_len:]
written_len = self._write(
data_view
) # do not stop if nothing was written
data_view = data_view[written_len:]
self._pos += written_len
self._length = max(self._length, self._pos)
return written_bytes
Expand Down Expand Up @@ -186,7 +188,10 @@ def close(self) -> None:

# the methods below are expected to be implemented by subclasses

def _read(self, size: int) -> bytes: # pragma: no cover # noqa: ARG002
def _read(
self,
size: int, # noqa: ARG002
) -> bytes | memoryview: # pragma: no cover
"""Read and return up to size bytes, where size is an int.

The size will not exceed the number of bytes between self._pos and
Expand All @@ -203,7 +208,7 @@ def _write_before(self) -> None:
def _write_after(self) -> None:
"""This method is called after the last write operation (usually on file close)."""

def _write(self, data: bytes) -> int: # pragma: no cover # noqa: ARG002
def _write(self, data: memoryview) -> int: # pragma: no cover # noqa: ARG002
"""Writes as many bytes from data as possible, and return the number
of bytes written.

Expand All @@ -229,13 +234,13 @@ def _truncate(self, size: int) -> None: # pragma: no cover # noqa: ARG002

class IOStatic(IOAbstract):
def __init__(self, data: bytes) -> None:
self.data = bytearray(data)
self.data = memoryview(data)
super().__init__(len(self.data))

def writable(self) -> bool:
return False

def _read(self, size: int) -> bytes:
def _read(self, size: int) -> bytes | memoryview:
return self.data[self._pos : self._pos + size]


Expand All @@ -250,11 +255,11 @@ def __init__(
self.fileobj = fileobj
self.start = start

def _read(self, size: int) -> bytes:
def _read(self, size: int) -> bytes | memoryview:
self.fileobj.seek(self.start + self._pos, SEEK_SET)
return self.fileobj.read(size) # size already restricted by caller

def _write(self, data: bytes) -> int:
def _write(self, data: memoryview) -> int:
self.fileobj.seek(self.start + self._pos, SEEK_SET)
return self.fileobj.write(data)

Expand All @@ -277,7 +282,7 @@ def _get_fileobj(self) -> T:
fileobj.seek(self._pos - start, SEEK_SET)
return fileobj

def _read(self, size: int) -> bytes:
def _read(self, size: int) -> bytes | memoryview:
return self._get_fileobj().read(size)

def _write_after(self) -> None:
Expand All @@ -288,7 +293,7 @@ def _write_after(self) -> None:
else:
del self._fileobjs[self._fileobjs.last_key]

def _write(self, data: bytes) -> int:
def _write(self, data: memoryview) -> int:
if self._fileobjs:
fileobj: T | None = self._get_fileobj()
else:
Expand All @@ -300,7 +305,7 @@ def _write(self, data: bytes) -> int:

# newly created fileobj should be writable
# otherwise this will raise UnsupportedOperation
return fileobj.write(data)
return fileobj.write(data.tobytes())

def _truncate(self, size: int) -> None:
start, fileobj = self._fileobjs.get_with_index(size)
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_file_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test(
for block in stream["blocks"]:
xzfile.filters = block.get("filters")
xzfile.change_block()
xzfile.write(data[: block["length"]])
xzfile.write(data[: block["length"]].tobytes())
data = data[block["length"] :]

assert not data
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/test_ioabstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ class Impl(IOAbstract):
def __init__(self) -> None:
super().__init__(10)

def _read(self, size: int) -> bytes:
def _read(self, size: int) -> bytes | memoryview:
# for tests, does not rely on position
return b"xyz"[:size]

Expand Down Expand Up @@ -188,7 +188,7 @@ def __init__(self) -> None:
super().__init__(10)
self.empty_reads = 100

def _read(self, size: int) -> bytes: # noqa: ARG002
def _read(self, size: int) -> bytes | memoryview: # noqa: ARG002
self.empty_reads -= 1
if self.empty_reads > 0:
return b""
Expand Down Expand Up @@ -232,7 +232,7 @@ def _write_before(self) -> None:
def _write_after(self) -> None:
self.mock.write_finish()

def _write(self, data: bytes) -> int:
def _write(self, data: memoryview) -> int:
self.mock.write(bytes(data))
if write_partial:
return min(2, len(data))
Expand Down Expand Up @@ -350,7 +350,7 @@ def _write_before(self) -> None:
def _write_after(self) -> None:
self.mock.write_finish()

def _write(self, data: bytes) -> int: # noqa: ARG002
def _write(self, data: memoryview) -> int: # noqa: ARG002
raise RuntimeError("should not be called")

def _truncate(self, size: int) -> None:
Expand Down
Loading
Loading