Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_internal/utils/wheel.py: 26%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

58 statements  

1"""Support functions for working with wheel files.""" 

2 

3import logging 

4from email.message import Message 

5from email.parser import Parser 

6from zipfile import BadZipFile, ZipFile 

7 

8from pip._vendor.packaging.utils import canonicalize_name 

9 

10from pip._internal.exceptions import UnsupportedWheel 

11 

12VERSION_COMPATIBLE = (1, 0) 

13 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18def parse_wheel(wheel_zip: ZipFile, name: str) -> tuple[str, Message]: 

19 """Extract information from the provided wheel, ensuring it meets basic 

20 standards. 

21 

22 Returns the name of the .dist-info directory and the parsed WHEEL metadata. 

23 """ 

24 try: 

25 info_dir = wheel_dist_info_dir(wheel_zip, name) 

26 metadata = wheel_metadata(wheel_zip, info_dir) 

27 version = wheel_version(metadata) 

28 except UnsupportedWheel as e: 

29 raise UnsupportedWheel(f"{name} has an invalid wheel, {e}") 

30 

31 check_compatibility(version, name) 

32 

33 return info_dir, metadata 

34 

35 

36def wheel_dist_info_dir(source: ZipFile, name: str) -> str: 

37 """Returns the name of the contained .dist-info directory. 

38 

39 Raises AssertionError or UnsupportedWheel if not found, >1 found, or 

40 it doesn't match the provided name. 

41 """ 

42 # Zip file path separators must be / 

43 subdirs = {p.split("/", 1)[0] for p in source.namelist()} 

44 

45 info_dirs = [s for s in subdirs if s.endswith(".dist-info")] 

46 

47 if not info_dirs: 

48 raise UnsupportedWheel(".dist-info directory not found") 

49 

50 if len(info_dirs) > 1: 

51 raise UnsupportedWheel( 

52 "multiple .dist-info directories found: {}".format(", ".join(info_dirs)) 

53 ) 

54 

55 info_dir = info_dirs[0] 

56 

57 info_dir_name = canonicalize_name(info_dir) 

58 canonical_name = canonicalize_name(name) 

59 if not info_dir_name.startswith(canonical_name): 

60 raise UnsupportedWheel( 

61 f".dist-info directory {info_dir!r} does not start with {canonical_name!r}" 

62 ) 

63 

64 return info_dir 

65 

66 

67def read_wheel_metadata_file(source: ZipFile, path: str) -> bytes: 

68 try: 

69 return source.read(path) 

70 # BadZipFile for general corruption, KeyError for missing entry, 

71 # and RuntimeError for password-protected files 

72 except (BadZipFile, KeyError, RuntimeError) as e: 

73 raise UnsupportedWheel(f"could not read {path!r} file: {e!r}") 

74 

75 

76def wheel_metadata(source: ZipFile, dist_info_dir: str) -> Message: 

77 """Return the WHEEL metadata of an extracted wheel, if possible. 

78 Otherwise, raise UnsupportedWheel. 

79 """ 

80 path = f"{dist_info_dir}/WHEEL" 

81 # Zip file path separators must be / 

82 wheel_contents = read_wheel_metadata_file(source, path) 

83 

84 try: 

85 wheel_text = wheel_contents.decode() 

86 except UnicodeDecodeError as e: 

87 raise UnsupportedWheel(f"error decoding {path!r}: {e!r}") 

88 

89 # FeedParser (used by Parser) does not raise any exceptions. The returned 

90 # message may have .defects populated, but for backwards-compatibility we 

91 # currently ignore them. 

92 return Parser().parsestr(wheel_text) 

93 

94 

95def wheel_version(wheel_data: Message) -> tuple[int, ...]: 

96 """Given WHEEL metadata, return the parsed Wheel-Version. 

97 Otherwise, raise UnsupportedWheel. 

98 """ 

99 version_text = wheel_data["Wheel-Version"] 

100 if version_text is None: 

101 raise UnsupportedWheel("WHEEL is missing Wheel-Version") 

102 

103 version = version_text.strip() 

104 

105 try: 

106 return tuple(map(int, version.split("."))) 

107 except ValueError: 

108 raise UnsupportedWheel(f"invalid Wheel-Version: {version!r}") 

109 

110 

111def check_compatibility(version: tuple[int, ...], name: str) -> None: 

112 """Raises errors or warns if called with an incompatible Wheel-Version. 

113 

114 pip should refuse to install a Wheel-Version that's a major series 

115 ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when 

116 installing a version only minor version ahead (e.g 1.2 > 1.1). 

117 

118 version: a 2-tuple representing a Wheel-Version (Major, Minor) 

119 name: name of wheel or package to raise exception about 

120 

121 :raises UnsupportedWheel: when an incompatible Wheel-Version is given 

122 """ 

123 if version[0] > VERSION_COMPATIBLE[0]: 

124 raise UnsupportedWheel( 

125 "{}'s Wheel-Version ({}) is not compatible with this version " 

126 "of pip".format(name, ".".join(map(str, version))) 

127 ) 

128 elif version > VERSION_COMPATIBLE: 

129 logger.warning( 

130 "Installing from a newer Wheel-Version (%s)", 

131 ".".join(map(str, version)), 

132 )