Skip to content

Commit baee920

Browse files
authored
Merge pull request #51 from kurtmckee/py3.14
Support Python 3.14
2 parents 2c1d121 + 3b0dfe6 commit baee920

File tree

6 files changed

+72
-32
lines changed

6 files changed

+72
-32
lines changed

README.rst

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ There are three steps required to start using static compression:
2929
-----------------------------
3030

3131
At minimum, you'll need to install the pelican_precompress plugin.
32-
It will automatically generate gzip files because gzip is built into the
33-
Python standard library.
32+
It will automatically generate gzip files on all versions of Python,
33+
and zstandard files on Python 3.14 and higher,
34+
because those compression algorithms are built into the Python standard library.
3435

3536
However, if you want better compression you'll need to install additional packages.
3637
pelican_precompress exposes each compression algorithm by name as a package extra:
3738

3839
* ``brotli``
3940
* ``zopfli``
40-
* ``zstandard``
41+
* ``zstandard`` (for Python 3.13 and lower)
4142

4243
These can be selected as a comma-separated list during install:
4344

@@ -138,11 +139,12 @@ You set them in your Pelican configuration file.
138139
If you set this to ``True`` when the brotli module isn't installed
139140
then nothing will happen.
140141

141-
* ``PRECOMPRESS_ZSTANDARD`` (bool, default is True if pyzstd is installed)
142+
* ``PRECOMPRESS_ZSTANDARD`` (bool, default is True if zstandard is available)
142143

143-
If the pyzstd module is installed this will default to ``True``.
144+
When running on Python 3.14 or higher with zstandard support compiled in,
145+
or if the pyzstd module is installed, this will default to ``True``.
144146
You might set this to ``False`` during development.
145-
If you set this to ``True`` when the pyzstd module isn't installed
147+
If you set this to ``True`` when the zstandard compression isn't available
146148
then nothing will happen.
147149

148150
* ``PRECOMPRESS_ZOPFLI`` (bool, default is True if zopfli is installed)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Python support
2+
--------------
3+
4+
* Support Python 3.14.
5+
6+
Changed
7+
-------
8+
9+
* zstandard support now uses ``compression.zstd`` on Python 3.14 and higher.
10+
11+
This means that zstandard is now enabled by default on Python 3.14 and higher.
12+
``pyzstd`` is still needed for Python 3.13 and lower.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ dependencies = [
3030
[project.optional-dependencies]
3131
brotli = ["brotli"]
3232
zopfli = ["zopfli"]
33-
zstandard = ["pyzstd"]
33+
zstandard = ["pyzstd; python_version<'3.14'"]
3434

3535
[project.urls]
3636
Source = "https://github.com/kurtmckee/pelican-precompress"

src/pelican/plugins/precompress/__init__.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from __future__ import annotations
66

77
import functools
8-
import gzip
98
import logging
109
import multiprocessing
1110
import pathlib
@@ -15,6 +14,8 @@
1514
import blinker
1615
import pelican.plugins.granular_signals
1716

17+
from .compat import compression
18+
1819
log = logging.getLogger(__name__)
1920

2021
# brotli support is optional.
@@ -32,13 +33,6 @@
3233
log.debug("Note: pelican_precompress only targets zopfli, not zopflipy.")
3334
zopfli = None
3435

35-
# zstandard support is optional.
36-
try:
37-
import pyzstd
38-
except ModuleNotFoundError:
39-
log.debug("pyzstd is not installed.")
40-
pyzstd = None
41-
4236

4337
DEFAULT_TEXT_EXTENSIONS: set[str] = {
4438
".atom",
@@ -77,7 +71,7 @@ def get_settings(instance) -> dict[str, bool | pathlib.Path | set[str]]:
7771
"PRECOMPRESS_GZIP": instance.settings.get("PRECOMPRESS_GZIP", True),
7872
"PRECOMPRESS_ZOPFLI": instance.settings.get("PRECOMPRESS_ZOPFLI", bool(zopfli)),
7973
"PRECOMPRESS_ZSTANDARD": instance.settings.get(
80-
"PRECOMPRESS_ZSTANDARD", bool(pyzstd)
74+
"PRECOMPRESS_ZSTANDARD", bool(compression.zstd)
8175
),
8276
"PRECOMPRESS_OVERWRITE": instance.settings.get("PRECOMPRESS_OVERWRITE", False),
8377
"PRECOMPRESS_MIN_SIZE": instance.settings.get("PRECOMPRESS_MIN_SIZE", 20),
@@ -103,7 +97,7 @@ def get_settings(instance) -> dict[str, bool | pathlib.Path | set[str]]:
10397
settings["PRECOMPRESS_GZIP"] = False
10498

10599
# If zstandard is enabled, it must be installed.
106-
if settings["PRECOMPRESS_ZSTANDARD"] and not pyzstd:
100+
if settings["PRECOMPRESS_ZSTANDARD"] and not compression.zstd:
107101
log.error("Disabling zstandard pre-compression because it is not installed.")
108102
settings["PRECOMPRESS_ZSTANDARD"] = False
109103

@@ -248,7 +242,7 @@ def decompress_with_gzip(path: pathlib.Path) -> bytes | None:
248242
"""Decompress a file using gzip decompression."""
249243

250244
try:
251-
return gzip.decompress(path.read_bytes())
245+
return compression.gzip.decompress(path.read_bytes())
252246
except OSError:
253247
return None
254248

@@ -257,7 +251,7 @@ def decompress_with_gzip(path: pathlib.Path) -> bytes | None:
257251
def compress_with_gzip(data: bytes) -> bytes:
258252
"""Compress binary data using gzip compression."""
259253

260-
compressor = zlib.compressobj(level=9, wbits=16 + zlib.MAX_WBITS)
254+
compressor = compression.zlib.compressobj(level=9, wbits=16 + zlib.MAX_WBITS)
261255
return compressor.compress(data) + compressor.flush()
262256

263257

@@ -272,16 +266,16 @@ def decompress_with_zstandard(path: pathlib.Path) -> bytes | None:
272266
"""Decompress a file using zstandard decompression."""
273267

274268
try:
275-
return pyzstd.decompress(path.read_bytes())
276-
except pyzstd.ZstdError:
269+
return compression.zstd.decompress(path.read_bytes())
270+
except compression.zstd.ZstdError:
277271
return None
278272

279273

280274
@validate_file_sizes
281275
def compress_with_zstandard(data: bytes) -> bytes:
282276
"""Compress binary data using zstandard compression."""
283277

284-
return pyzstd.compress(data)
278+
return compression.zstd.compress(data)
285279

286280

287281
def register():
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# This file is part of the pelican-precompress plugin.
2+
# Copyright 2019-2025 Kurt McKee <[email protected]>
3+
# Released under the MIT license.
4+
5+
from __future__ import annotations
6+
7+
import logging
8+
import types
9+
10+
log = logging.getLogger(__name__)
11+
12+
compression: types.SimpleNamespace | types.ModuleType
13+
14+
try:
15+
import compression.gzip
16+
import compression.zlib
17+
import compression.zstd
18+
except ModuleNotFoundError:
19+
import gzip
20+
import zlib
21+
22+
# zstandard support is optional.
23+
try:
24+
import pyzstd
25+
except ModuleNotFoundError:
26+
log.debug("pyzstd is not installed.")
27+
pyzstd = None
28+
29+
compression = types.SimpleNamespace()
30+
compression.gzip = gzip
31+
compression.zlib = zlib
32+
compression.zstd = pyzstd

tests/test_pelican_precompress.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def apply_async_mock(fn, args, *extra_args, **kwargs):
5858
},
5959
),
6060
(
61-
{"pyzstd"},
61+
{"compression.zstd"},
6262
{
6363
"PRECOMPRESS_GZIP": True,
6464
"PRECOMPRESS_BROTLI": False,
@@ -67,7 +67,7 @@ def apply_async_mock(fn, args, *extra_args, **kwargs):
6767
},
6868
),
6969
(
70-
{"brotli", "zopfli", "pyzstd"},
70+
{"brotli", "zopfli", "compression.zstd"},
7171
{
7272
"PRECOMPRESS_GZIP": False,
7373
"PRECOMPRESS_BROTLI": True,
@@ -83,7 +83,7 @@ def test_get_settings_compression_support(installed_modules, expected_settings):
8383

8484
patches = [
8585
patch(f"pelican.plugins.precompress.{module}", module in installed_modules)
86-
for module in {"brotli", "zopfli", "pyzstd"}
86+
for module in {"brotli", "zopfli", "compression.zstd"}
8787
]
8888
[patch_.start() for patch_ in patches]
8989

@@ -111,7 +111,7 @@ def test_get_settings_compression_validation():
111111
patches = [
112112
patch("pelican.plugins.precompress.brotli", None),
113113
patch("pelican.plugins.precompress.zopfli", None),
114-
patch("pelican.plugins.precompress.pyzstd", None),
114+
patch("pelican.plugins.precompress.compression.zstd", None),
115115
patch("pelican.plugins.precompress.log", log),
116116
]
117117
[patch_.start() for patch_ in patches]
@@ -192,14 +192,14 @@ def test_compress_with_gzip_exception():
192192
pp.compress_with_gzip(b"")
193193

194194

195+
@pytest.mark.skipif(pp.compression.zstd is None, reason="zstandard is unavailable")
195196
def test_compress_with_zstandard():
196-
zstandard = pytest.importorskip("pyzstd")
197197
data = b"a" * 100
198-
assert zstandard.decompress(pp.compress_with_zstandard(data)) == data
198+
assert pp.compression.zstd.decompress(pp.compress_with_zstandard(data)) == data
199199

200200

201+
@pytest.mark.skipif(pp.compression.zstd is None, reason="zstandard is unavailable")
201202
def test_compress_with_zstandard_error():
202-
pytest.importorskip("pyzstd")
203203
with pytest.raises(pp.FileSizeIncrease):
204204
pp.compress_with_zstandard(b"")
205205

@@ -334,8 +334,8 @@ def test_compress_files_overwrite_gz(fs, multiprocessing):
334334
assert gzip.decompress(file.read()) == b"a" * 100
335335

336336

337+
@pytest.mark.skipif(pp.compression.zstd is None, reason="zstandard is unavailable")
337338
def test_compress_files_overwrite_zst(fs, multiprocessing):
338-
zstandard = pytest.importorskip("pyzstd")
339339
with open("/test.txt", "wb") as file:
340340
file.write(b"a" * 100)
341341
with open("/test.txt.zst", "wb") as file:
@@ -353,7 +353,7 @@ def test_compress_files_overwrite_zst(fs, multiprocessing):
353353
pp.compress_files(instance)
354354
log.warning.assert_called_once()
355355
with pathlib.Path("/test.txt.zst").open("rb") as file:
356-
assert zstandard.decompress(file.read()) == b"a" * 100
356+
assert pp.compression.zstd.decompress(file.read()) == b"a" * 100
357357

358358

359359
def test_compress_files_file_size_increase(fs, multiprocessing):
@@ -421,10 +421,10 @@ def test_compress_files_overwrite_erase_existing_file(fs, multiprocessing):
421421
assert not pathlib.Path("/test.txt.gz").exists()
422422

423423

424+
@pytest.mark.skipif(pp.compression.zstd is None, reason="zstandard is unavailable")
424425
def test_compress_files_success_all_algorithms(fs, multiprocessing):
425426
pytest.importorskip("brotli")
426427
pytest.importorskip("zopfli")
427-
pytest.importorskip("pyzstd")
428428
with open("/test.txt", "wb") as file:
429429
file.write(b"a" * 100)
430430
instance = Mock()

0 commit comments

Comments
 (0)