Coverage for documents/export/compression.py: 0%
46 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 09:07 +0000
1from __future__ import annotations
3import importlib
4import zipfile
6# ZIP_ZSTANDARD exists only on Python 3.14+ (PEP 784). None elsewhere.
7ZSTD: int | None = getattr(zipfile, "ZIP_ZSTANDARD", None)
9# CLI choices are fixed across runtimes so argparse never hides zstd; runtime
10# availability is enforced separately in compression_available().
11COMPRESSION_CHOICES: tuple[str, ...] = (
12 "stored",
13 "deflated",
14 "bzip2",
15 "lzma",
16 "zstd",
17)
19# Method name -> zipfile compression constant (zstd only when supported).
20COMPRESSION_METHODS: dict[str, int] = {
21 "stored": zipfile.ZIP_STORED,
22 "deflated": zipfile.ZIP_DEFLATED,
23 "bzip2": zipfile.ZIP_BZIP2,
24 "lzma": zipfile.ZIP_LZMA,
25}
26if ZSTD is not None:
27 COMPRESSION_METHODS["zstd"] = ZSTD
29# Inclusive (min, max) level bounds per method; None => level not applicable.
30# Verified on CPython 3.14.3.
31#
32# zstd's raw library bounds are (-131072, 22)
33# (compression.zstd.CompressionParameter.compression_level.bounds()) — the
34# minimum is an internal implementation constant (-ZSTD_TARGETLENGTH_MAX),
35# not a meaningful distinct "level"; deeper negative values than -22 buy
36# nothing over -22 in practice. We expose the conventional zstd CLI range
37# instead of the raw library bounds.
38LEVEL_BOUNDS: dict[str, tuple[int, int] | None] = {
39 "stored": None,
40 "deflated": (0, 9),
41 "bzip2": (1, 9),
42 "lzma": None,
43 "zstd": (-22, 22),
44}
46# zipfile compress_type id -> method name.
47_COMPRESS_TYPE_TO_METHOD: dict[int, str] = {
48 zipfile.ZIP_STORED: "stored",
49 zipfile.ZIP_DEFLATED: "deflated",
50 zipfile.ZIP_BZIP2: "bzip2",
51 zipfile.ZIP_LZMA: "lzma",
52 93: "zstd",
53}
56def compression_available(method: str) -> bool:
57 """Whether the running interpreter can actually use the given method."""
58 if method in ("stored", "deflated"):
59 # zlib is a hard CPython dependency; stored needs nothing.
60 return True
61 if method == "bzip2":
62 return _module_importable("bz2")
63 if method == "lzma":
64 return _module_importable("lzma")
65 if method == "zstd":
66 return ZSTD is not None and _module_importable("compression.zstd")
67 return False # pragma: no cover -- method is always one of COMPRESSION_CHOICES
70def _module_importable(name: str) -> bool:
71 try:
72 importlib.import_module(name)
73 except ImportError:
74 return False
75 return True
78def level_error(method: str, level: int | None) -> str | None:
79 """Return a human message if (method, level) is invalid, else None."""
80 if level is None:
81 return None
82 bounds = LEVEL_BOUNDS[method]
83 if bounds is None:
84 return f"--zip-compression-level has no effect for '{method}'"
85 low, high = bounds
86 if not (low <= level <= high):
87 return (
88 f"--zip-compression-level for '{method}' must be between {low} and {high}"
89 )
90 return None
93def compress_type_readable(compress_type: int) -> bool:
94 """Whether this interpreter can decompress an entry of the given type."""
95 method = _COMPRESS_TYPE_TO_METHOD.get(compress_type)
96 if method is None:
97 return False
98 return compression_available(method)
101def unreadable_method_names(compress_types: set[int]) -> set[str]:
102 """Map a set of compress_type ids to human method names for error messages."""
103 names: set[str] = set()
104 for ct in compress_types:
105 names.add(_COMPRESS_TYPE_TO_METHOD.get(ct, f"method {ct}"))
106 return names