Skip to content

Prefer native Zstandard compression workers - #185

Open
zhou-jianwen wants to merge 1 commit into
pycompression:mainfrom
zhou-jianwen:codex/native-zstd-threads
Open

zhou-jianwen wants to merge 1 commit into
pycompression:mainfrom
zhou-jianwen:codex/native-zstd-threads

Conversation

@zhou-jianwen

@zhou-jianwen zhou-jianwen commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Zstandard writes and appends currently prefer an external zstd process whenever threads is nonzero or omitted. Prefer compression.zstd / backports.zstd compression workers when the linked library supports them, keeping the existing automatic limit of four workers and honoring explicit counts.

Pass the compression level and worker count together through options, and enable the content checksum used by the external CLI. Check threading capability before opening output; unsupported builds retain the external-then-single-thread fallback. Native errors propagate without retrying another backend after output has been opened. Reading and threads=0 behavior are unchanged.

Fixes #180.

Validation on macOS arm64:

  • Python 3.13.2 (backports.zstd) and Python 3.14.10 (compression.zstd): full suites each passed 404 tests, with 4 skips; unmodified baseline passed 379 tests, with 4 skips.
  • The 25 new cases cover real 10 MiB roundtrips, append, file and memory streams, compression levels, worker counts, checksum corruption, fallback and error propagation. Against the original implementation, 19 fail and 6 compatibility controls pass. All 25 pass with the change on both Python versions.
  • Simulating a library without threading support produces 6 passes and 19 intentional native-only skips on both versions. This is capability simulation, not testing against a separately compiled thread-disabled library.
  • Pinned Black 26.10, Ruff 0.17 and git diff --check pass. Local mypy reports the same 9 errors on pristine main and this branch: missing optional isal/zlib_ng imports and associated redefinitions, plus macOS os.sched_getaffinity typing. All 13 hosted test/lint jobs now pass, including Linux mypy and the Linux, macOS and Windows test matrix.

README backend selection and the changelog are updated. No throughput benchmark is claimed.

AI assistance: OpenAI Codex was used for implementation, tests and automated review.

Coverage status: Codecov project is still red. Its comparison uses 89bfc5d (June 3) rather than the actual PR base ca60aa1, for which Codecov has no report. The comparison includes intervening upstream changes. All added executable production lines are covered; some new test skip/fail guards are not, and this project includes tests in coverage. This does not establish how much of the reported drop belongs to this PR. No coverage thresholds or exclusions were changed.

Related follow-up: #186 restores the previously unreachable native LZ4 CI step on an independent branch. Its hosted tests and Codecov pass; it has not been merged into this branch.

@marcelm

marcelm commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

@rhpvorderman Any opinion on entirely AI-generated PRs with overly verbose PR descriptions?

@rhpvorderman

rhpvorderman commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

The verbose PR descriptions are annoying, but reviewing fixes is always less time consuming than making them ourselves. Provided the fixes are of good quality. So I'd judge the merit of the PR based on that.

Currently python-isal is undergoing a veritable bombardment of high quality AI assisted improves by a user who likes the project. It is really improving the project, so I am not complaining.

Also, given that all the AIs are probably trained on our code without permission it is about time they give something back ;-).

@rhpvorderman rhpvorderman left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This AI generated PR is quite poor. If the native library does not accept threads, there is no reason why a fallback to the application should not be used. The change is not properly integrate in the logic flow. The tests are also testing all sorts of things about the code flow, but not if the returned object is the desired one. Which is what matters.

Comment thread src/xopen/__init__.py
assert compresslevel != 0
if compresslevel is None:
compresslevel = XOPEN_DEFAULT_ZST_COMPRESSION
if (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like how this extensive if clause is placed at the start of the logic. I feel that could be handled quite a bit better. There are already several tests for zstd and the number of threads. Adding extra makes the function harder to maintain. Please refactor the function so it flows more logically.

Comment thread src/xopen/__init__.py
(that is, no subprocess is spawned).

Set threads to 0 to force opening the file without using a subprocess.
Zstandard writing and appending prefer native compression threads when the

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true for multiple backends. No need to add a specific paragraph for zstandard here.

Comment thread tests/test_xopen.py
def test_zst_native_roundtrip_append(tmp_path, monkeypatch, threads, file_like):
if threads != 0 and zstd.CompressionParameter.nb_workers.bounds()[1] == 0:
pytest.skip("Native Zstandard multithreading not supported")
import xopen as xopen_module

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would the import be in the function? It is at the top of the test file.

Comment thread tests/test_xopen.py
def unexpected_process(*args, **kwargs):
pytest.fail("Native Zstandard writing must not spawn a process")

monkeypatch.setattr(xopen_module, "_PipedCompressionProgram", unexpected_process)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What a convoluted way to test this. Simply test if the return object is what you want rather than inserting all sorts of nonsense throughout the library.

Comment thread tests/test_xopen.py
"threads,cpus,workers", [(None, 2, 2), (None, 8, 4), (1, 8, 1), (7, 2, 7)]
)
@pytest.mark.parametrize("compresslevel", [None, 1, 9])
def test_zst_native_options(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole test seems nonsense to me. It inserts code to capture a dictionary, then tests that dictionary. But we are not really testing the result here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use compression.zstd’s multithreading

3 participants