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

1from __future__ import annotations 

2 

3import importlib 

4import zipfile 

5 

6# ZIP_ZSTANDARD exists only on Python 3.14+ (PEP 784). None elsewhere. 

7ZSTD: int | None = getattr(zipfile, "ZIP_ZSTANDARD", None) 

8 

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) 

18 

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 

28 

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} 

45 

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} 

54 

55 

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 

68 

69 

70def _module_importable(name: str) -> bool: 

71 try: 

72 importlib.import_module(name) 

73 except ImportError: 

74 return False 

75 return True 

76 

77 

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 

91 

92 

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) 

99 

100 

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