Coverage for src/iso_freeze/get_requirements.py: 78%

45 statements  

« prev     ^ index     » next       coverage.py v6.4.2, created at 2022-08-01 13:01 +0200

1"""Getting requirements from pip install --report.""" 

2 

3import json 

4import sys 

5 

6from pathlib import Path 

7from typing import Any, Union, Optional 

8 

9if sys.version_info >= (3, 11, 0): 

10 import tomllib 

11else: 

12 import tomli as tomllib # type: ignore 

13 

14from iso_freeze.lib import PyPackage, run_pip 

15 

16 

17def get_pip_report_requirements( 

18 file: Path, 

19 python_exec: Path, 

20 pip_args: Optional[list[str]], 

21 optional_dependency: Optional[str], 

22) -> Optional[list[PyPackage]]: 

23 """Get dependencies to pass to pip install --report. 

24 

25 Arguments: 

26 file -- Input file to parse (Path) 

27 python_exec -- Path to Python executable (Path) 

28 pip_args -- Args to pass to pip install (Optional[list[str]]) 

29 optional_dependency -- Optional dependency to include (Optional[str]) 

30 

31 Returns: 

32 Pip packages from pip install --report output (Optional[list[PyPackage]]) 

33 """ 

34 if file.suffix == ".toml": 

35 # pip_report_input type is either just list[str] (for TOML dependencies) or 

36 # list[Union[str, Path]] (for requirements file). 

37 pip_report_input: Union[list[str], list[Union[str, Path]]] = read_toml( 

38 toml_dict=load_toml_file(file), optional_dependency=optional_dependency 

39 ) 

40 else: 

41 # Telling mypy to ignore type here because it thinks that ["-r", file] 

42 # is list[object] rather than list[Union[str, Path]] 

43 pip_report_input = ["-r", file] # type: ignore 

44 pip_report_command: list[Union[str, Path]] = build_pip_report_command( 

45 pip_report_input=pip_report_input, 

46 python_exec=python_exec, 

47 pip_args=pip_args, 

48 ) 

49 pip_report: dict[str, Any] = get_pip_report(pip_report_command=pip_report_command) 

50 return read_pip_report(pip_report) 

51 

52 

53def build_pip_report_command( 

54 pip_report_input: Union[list[str], list[Union[str, Path]]], 

55 python_exec: Path, 

56 pip_args: Optional[list[str]], 

57) -> list[Union[str, Path]]: 

58 """Build pip command to to generate report. 

59 

60 Arguments: 

61 pip_report_input -- Packages or file to pass to pip report 

62 (Union[list[str], list[Union[str, Path]]]) 

63 python_exec -- Path to Python interpreter to use (Path) 

64 pip_args -- Arguments to be passed to pip install (Optional[list[str]]) 

65 

66 Returns: 

67 Pip command to pass to run_pip_report (list[Union[str, Path]]) 

68 """ 

69 pip_report_command: list[Union[str, Path]] = [ 

70 "env", 

71 "PIP_REQUIRE_VIRTUALENV=false", 

72 python_exec, 

73 "-m", 

74 "pip", 

75 "install", 

76 ] 

77 # If pip_args have been provided, inject them after the 'install' keyword 

78 if pip_args: 

79 pip_report_command.extend(pip_args) 

80 # Add necessary flags for calling pip install report 

81 pip_report_command.extend( 

82 ["-q", "--dry-run", "--ignore-installed", "--report", "-", *pip_report_input] 

83 ) 

84 # # Finally, either append dependencies from TOML file or '-r requirements-file' 

85 # if toml_dependencies: 

86 # pip_report_command.extend([dependency for dependency in toml_dependencies]) 

87 # else: 

88 # pip_report_command.extend(["-r", file]) 

89 return pip_report_command 

90 

91 

92def load_toml_file(toml_file: Path) -> dict[str, Any]: 

93 """Load TOML file and return its contents 

94 

95 Arguments: 

96 toml_file -- Path to TOML file (Path) 

97 

98 Returns: 

99 Contents of TOML file (dict[str, str]) 

100 """ 

101 with open(toml_file, "rb") as f: 

102 return tomllib.load(f) 

103 

104 

105def read_toml( 

106 toml_dict: dict[str, Any], 

107 optional_dependency: Optional[str] = None, 

108) -> list[str]: 

109 """Read TOML dict and return listed dependencies. 

110 

111 Includes requirements for optional dependency if any has been specified. 

112 

113 Keyword Arguments: 

114 toml_file -- Path to pyproject.toml file 

115 optional_dependency -- Optional dependency to include (default: None) 

116 

117 Returns: 

118 List of dependency names (list[str]) 

119 """ 

120 if not toml_dict.get("project"): 

121 sys.exit("TOML file does not contain a 'project' section.") 

122 dependencies: list[str] = toml_dict["project"].get("dependencies") 

123 if optional_dependency: 

124 if not toml_dict["project"].get("optional-dependencies"): 

125 sys.exit("No optional dependencies defined in TOML file.") 

126 optional_dependency_reqs: Optional[list[str]] = ( 

127 toml_dict["project"].get("optional-dependencies").get(optional_dependency) 

128 ) 

129 if optional_dependency_reqs: 

130 dependencies.extend(optional_dependency_reqs) 

131 else: 

132 sys.exit( 

133 f"No optional dependency '{optional_dependency}' found in TOML file." 

134 ) 

135 return dependencies 

136 

137 

138def get_pip_report(pip_report_command: list[Union[Path, str]]) -> dict[str, Any]: 

139 """Capture pip install --report output. 

140 

141 Arguments: 

142 pip_report_command -- Command for subprocess (list[Union[Path, str]]) 

143 

144 Returns: 

145 Json pip report response (dict[str, Any]) 

146 """ 

147 return json.loads(run_pip(command=pip_report_command, check_output=True)) 

148 

149 

150def read_pip_report(pip_report: dict[str, Any]) -> Optional[list[PyPackage]]: 

151 """Extract package informations from pip report. 

152 

153 Arguments: 

154 pip_report -- Json pip report (dict[str, Any]) 

155 

156 Returns: 

157 List of PyPackage objects containing infos to pin requirements (list[PyPackage]) 

158 """ 

159 if pip_report.get("install"): 

160 dependencies: list[PyPackage] = [] 

161 for package in pip_report["install"]: 

162 dependencies.append( 

163 PyPackage( 

164 name=package["metadata"]["name"], 

165 version=package["metadata"]["version"], 

166 requested=package["requested"], 

167 # pip report provides hashes in the form 'sha256=<hash>', but pip 

168 # install requires 'sha256:<hash>', so we replace '=' with ':' 

169 hash=package["download_info"]["archive_info"]["hash"].replace( 

170 "=", ":" 

171 ), 

172 ) 

173 ) 

174 return dependencies 

175 else: 

176 return None