Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pip/_vendor/pyproject_hooks/_impl.py: 32%

113 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:48 +0000

1import json 

2import os 

3import sys 

4import tempfile 

5from contextlib import contextmanager 

6from os.path import abspath 

7from os.path import join as pjoin 

8from subprocess import STDOUT, check_call, check_output 

9 

10from ._in_process import _in_proc_script_path 

11 

12 

13def write_json(obj, path, **kwargs): 

14 with open(path, 'w', encoding='utf-8') as f: 

15 json.dump(obj, f, **kwargs) 

16 

17 

18def read_json(path): 

19 with open(path, encoding='utf-8') as f: 

20 return json.load(f) 

21 

22 

23class BackendUnavailable(Exception): 

24 """Will be raised if the backend cannot be imported in the hook process.""" 

25 def __init__(self, traceback): 

26 self.traceback = traceback 

27 

28 

29class BackendInvalid(Exception): 

30 """Will be raised if the backend is invalid.""" 

31 def __init__(self, backend_name, backend_path, message): 

32 super().__init__(message) 

33 self.backend_name = backend_name 

34 self.backend_path = backend_path 

35 

36 

37class HookMissing(Exception): 

38 """Will be raised on missing hooks (if a fallback can't be used).""" 

39 def __init__(self, hook_name): 

40 super().__init__(hook_name) 

41 self.hook_name = hook_name 

42 

43 

44class UnsupportedOperation(Exception): 

45 """May be raised by build_sdist if the backend indicates that it can't.""" 

46 def __init__(self, traceback): 

47 self.traceback = traceback 

48 

49 

50def default_subprocess_runner(cmd, cwd=None, extra_environ=None): 

51 """The default method of calling the wrapper subprocess. 

52 

53 This uses :func:`subprocess.check_call` under the hood. 

54 """ 

55 env = os.environ.copy() 

56 if extra_environ: 

57 env.update(extra_environ) 

58 

59 check_call(cmd, cwd=cwd, env=env) 

60 

61 

62def quiet_subprocess_runner(cmd, cwd=None, extra_environ=None): 

63 """Call the subprocess while suppressing output. 

64 

65 This uses :func:`subprocess.check_output` under the hood. 

66 """ 

67 env = os.environ.copy() 

68 if extra_environ: 

69 env.update(extra_environ) 

70 

71 check_output(cmd, cwd=cwd, env=env, stderr=STDOUT) 

72 

73 

74def norm_and_check(source_tree, requested): 

75 """Normalise and check a backend path. 

76 

77 Ensure that the requested backend path is specified as a relative path, 

78 and resolves to a location under the given source tree. 

79 

80 Return an absolute version of the requested path. 

81 """ 

82 if os.path.isabs(requested): 

83 raise ValueError("paths must be relative") 

84 

85 abs_source = os.path.abspath(source_tree) 

86 abs_requested = os.path.normpath(os.path.join(abs_source, requested)) 

87 # We have to use commonprefix for Python 2.7 compatibility. So we 

88 # normalise case to avoid problems because commonprefix is a character 

89 # based comparison :-( 

90 norm_source = os.path.normcase(abs_source) 

91 norm_requested = os.path.normcase(abs_requested) 

92 if os.path.commonprefix([norm_source, norm_requested]) != norm_source: 

93 raise ValueError("paths must be inside source tree") 

94 

95 return abs_requested 

96 

97 

98class BuildBackendHookCaller: 

99 """A wrapper to call the build backend hooks for a source directory. 

100 """ 

101 

102 def __init__( 

103 self, 

104 source_dir, 

105 build_backend, 

106 backend_path=None, 

107 runner=None, 

108 python_executable=None, 

109 ): 

110 """ 

111 :param source_dir: The source directory to invoke the build backend for 

112 :param build_backend: The build backend spec 

113 :param backend_path: Additional path entries for the build backend spec 

114 :param runner: The :ref:`subprocess runner <Subprocess Runners>` to use 

115 :param python_executable: 

116 The Python executable used to invoke the build backend 

117 """ 

118 if runner is None: 

119 runner = default_subprocess_runner 

120 

121 self.source_dir = abspath(source_dir) 

122 self.build_backend = build_backend 

123 if backend_path: 

124 backend_path = [ 

125 norm_and_check(self.source_dir, p) for p in backend_path 

126 ] 

127 self.backend_path = backend_path 

128 self._subprocess_runner = runner 

129 if not python_executable: 

130 python_executable = sys.executable 

131 self.python_executable = python_executable 

132 

133 @contextmanager 

134 def subprocess_runner(self, runner): 

135 """A context manager for temporarily overriding the default 

136 :ref:`subprocess runner <Subprocess Runners>`. 

137 

138 .. code-block:: python 

139 

140 hook_caller = BuildBackendHookCaller(...) 

141 with hook_caller.subprocess_runner(quiet_subprocess_runner): 

142 ... 

143 """ 

144 prev = self._subprocess_runner 

145 self._subprocess_runner = runner 

146 try: 

147 yield 

148 finally: 

149 self._subprocess_runner = prev 

150 

151 def _supported_features(self): 

152 """Return the list of optional features supported by the backend.""" 

153 return self._call_hook('_supported_features', {}) 

154 

155 def get_requires_for_build_wheel(self, config_settings=None): 

156 """Get additional dependencies required for building a wheel. 

157 

158 :returns: A list of :pep:`dependency specifiers <508>`. 

159 :rtype: list[str] 

160 

161 .. admonition:: Fallback 

162 

163 If the build backend does not defined a hook with this name, an 

164 empty list will be returned. 

165 """ 

166 return self._call_hook('get_requires_for_build_wheel', { 

167 'config_settings': config_settings 

168 }) 

169 

170 def prepare_metadata_for_build_wheel( 

171 self, metadata_directory, config_settings=None, 

172 _allow_fallback=True): 

173 """Prepare a ``*.dist-info`` folder with metadata for this project. 

174 

175 :returns: Name of the newly created subfolder within 

176 ``metadata_directory``, containing the metadata. 

177 :rtype: str 

178 

179 .. admonition:: Fallback 

180 

181 If the build backend does not define a hook with this name and 

182 ``_allow_fallback`` is truthy, the backend will be asked to build a 

183 wheel via the ``build_wheel`` hook and the dist-info extracted from 

184 that will be returned. 

185 """ 

186 return self._call_hook('prepare_metadata_for_build_wheel', { 

187 'metadata_directory': abspath(metadata_directory), 

188 'config_settings': config_settings, 

189 '_allow_fallback': _allow_fallback, 

190 }) 

191 

192 def build_wheel( 

193 self, wheel_directory, config_settings=None, 

194 metadata_directory=None): 

195 """Build a wheel from this project. 

196 

197 :returns: 

198 The name of the newly created wheel within ``wheel_directory``. 

199 

200 .. admonition:: Interaction with fallback 

201 

202 If the ``build_wheel`` hook was called in the fallback for 

203 :meth:`prepare_metadata_for_build_wheel`, the build backend would 

204 not be invoked. Instead, the previously built wheel will be copied 

205 to ``wheel_directory`` and the name of that file will be returned. 

206 """ 

207 if metadata_directory is not None: 

208 metadata_directory = abspath(metadata_directory) 

209 return self._call_hook('build_wheel', { 

210 'wheel_directory': abspath(wheel_directory), 

211 'config_settings': config_settings, 

212 'metadata_directory': metadata_directory, 

213 }) 

214 

215 def get_requires_for_build_editable(self, config_settings=None): 

216 """Get additional dependencies required for building an editable wheel. 

217 

218 :returns: A list of :pep:`dependency specifiers <508>`. 

219 :rtype: list[str] 

220 

221 .. admonition:: Fallback 

222 

223 If the build backend does not defined a hook with this name, an 

224 empty list will be returned. 

225 """ 

226 return self._call_hook('get_requires_for_build_editable', { 

227 'config_settings': config_settings 

228 }) 

229 

230 def prepare_metadata_for_build_editable( 

231 self, metadata_directory, config_settings=None, 

232 _allow_fallback=True): 

233 """Prepare a ``*.dist-info`` folder with metadata for this project. 

234 

235 :returns: Name of the newly created subfolder within 

236 ``metadata_directory``, containing the metadata. 

237 :rtype: str 

238 

239 .. admonition:: Fallback 

240 

241 If the build backend does not define a hook with this name and 

242 ``_allow_fallback`` is truthy, the backend will be asked to build a 

243 wheel via the ``build_editable`` hook and the dist-info 

244 extracted from that will be returned. 

245 """ 

246 return self._call_hook('prepare_metadata_for_build_editable', { 

247 'metadata_directory': abspath(metadata_directory), 

248 'config_settings': config_settings, 

249 '_allow_fallback': _allow_fallback, 

250 }) 

251 

252 def build_editable( 

253 self, wheel_directory, config_settings=None, 

254 metadata_directory=None): 

255 """Build an editable wheel from this project. 

256 

257 :returns: 

258 The name of the newly created wheel within ``wheel_directory``. 

259 

260 .. admonition:: Interaction with fallback 

261 

262 If the ``build_editable`` hook was called in the fallback for 

263 :meth:`prepare_metadata_for_build_editable`, the build backend 

264 would not be invoked. Instead, the previously built wheel will be 

265 copied to ``wheel_directory`` and the name of that file will be 

266 returned. 

267 """ 

268 if metadata_directory is not None: 

269 metadata_directory = abspath(metadata_directory) 

270 return self._call_hook('build_editable', { 

271 'wheel_directory': abspath(wheel_directory), 

272 'config_settings': config_settings, 

273 'metadata_directory': metadata_directory, 

274 }) 

275 

276 def get_requires_for_build_sdist(self, config_settings=None): 

277 """Get additional dependencies required for building an sdist. 

278 

279 :returns: A list of :pep:`dependency specifiers <508>`. 

280 :rtype: list[str] 

281 """ 

282 return self._call_hook('get_requires_for_build_sdist', { 

283 'config_settings': config_settings 

284 }) 

285 

286 def build_sdist(self, sdist_directory, config_settings=None): 

287 """Build an sdist from this project. 

288 

289 :returns: 

290 The name of the newly created sdist within ``wheel_directory``. 

291 """ 

292 return self._call_hook('build_sdist', { 

293 'sdist_directory': abspath(sdist_directory), 

294 'config_settings': config_settings, 

295 }) 

296 

297 def _call_hook(self, hook_name, kwargs): 

298 extra_environ = {'PEP517_BUILD_BACKEND': self.build_backend} 

299 

300 if self.backend_path: 

301 backend_path = os.pathsep.join(self.backend_path) 

302 extra_environ['PEP517_BACKEND_PATH'] = backend_path 

303 

304 with tempfile.TemporaryDirectory() as td: 

305 hook_input = {'kwargs': kwargs} 

306 write_json(hook_input, pjoin(td, 'input.json'), indent=2) 

307 

308 # Run the hook in a subprocess 

309 with _in_proc_script_path() as script: 

310 python = self.python_executable 

311 self._subprocess_runner( 

312 [python, abspath(str(script)), hook_name, td], 

313 cwd=self.source_dir, 

314 extra_environ=extra_environ 

315 ) 

316 

317 data = read_json(pjoin(td, 'output.json')) 

318 if data.get('unsupported'): 

319 raise UnsupportedOperation(data.get('traceback', '')) 

320 if data.get('no_backend'): 

321 raise BackendUnavailable(data.get('traceback', '')) 

322 if data.get('backend_invalid'): 

323 raise BackendInvalid( 

324 backend_name=self.build_backend, 

325 backend_path=self.backend_path, 

326 message=data.get('backend_error', '') 

327 ) 

328 if data.get('hook_missing'): 

329 raise HookMissing(data.get('missing_hook_name') or hook_name) 

330 return data['return_val']