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

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

115 statements  

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 

9from typing import TYPE_CHECKING, Any, Iterator, Mapping, Optional, Sequence 

10 

11from ._in_process import _in_proc_script_path 

12 

13if TYPE_CHECKING: 

14 from typing import Protocol 

15 

16 class SubprocessRunner(Protocol): 

17 """A protocol for the subprocess runner.""" 

18 

19 def __call__( 

20 self, 

21 cmd: Sequence[str], 

22 cwd: Optional[str] = None, 

23 extra_environ: Optional[Mapping[str, str]] = None, 

24 ) -> None: 

25 ... 

26 

27 

28def write_json(obj: Mapping[str, Any], path: str, **kwargs) -> None: 

29 with open(path, "w", encoding="utf-8") as f: 

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

31 

32 

33def read_json(path: str) -> Mapping[str, Any]: 

34 with open(path, encoding="utf-8") as f: 

35 return json.load(f) 

36 

37 

38class BackendUnavailable(Exception): 

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

40 

41 def __init__( 

42 self, 

43 traceback: str, 

44 message: Optional[str] = None, 

45 backend_name: Optional[str] = None, 

46 backend_path: Optional[Sequence[str]] = None, 

47 ) -> None: 

48 # Preserving arg order for the sake of API backward compatibility. 

49 self.backend_name = backend_name 

50 self.backend_path = backend_path 

51 self.traceback = traceback 

52 super().__init__(message or "Error while importing backend") 

53 

54 

55class HookMissing(Exception): 

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

57 

58 def __init__(self, hook_name: str) -> None: 

59 super().__init__(hook_name) 

60 self.hook_name = hook_name 

61 

62 

63class UnsupportedOperation(Exception): 

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

65 

66 def __init__(self, traceback: str) -> None: 

67 self.traceback = traceback 

68 

69 

70def default_subprocess_runner( 

71 cmd: Sequence[str], 

72 cwd: Optional[str] = None, 

73 extra_environ: Optional[Mapping[str, str]] = None, 

74) -> None: 

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

76 

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

78 """ 

79 env = os.environ.copy() 

80 if extra_environ: 

81 env.update(extra_environ) 

82 

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

84 

85 

86def quiet_subprocess_runner( 

87 cmd: Sequence[str], 

88 cwd: Optional[str] = None, 

89 extra_environ: Optional[Mapping[str, str]] = None, 

90) -> None: 

91 """Call the subprocess while suppressing output. 

92 

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

94 """ 

95 env = os.environ.copy() 

96 if extra_environ: 

97 env.update(extra_environ) 

98 

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

100 

101 

102def norm_and_check(source_tree: str, requested: str) -> str: 

103 """Normalise and check a backend path. 

104 

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

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

107 

108 Return an absolute version of the requested path. 

109 """ 

110 if os.path.isabs(requested): 

111 raise ValueError("paths must be relative") 

112 

113 abs_source = os.path.abspath(source_tree) 

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

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

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

117 # based comparison :-( 

118 norm_source = os.path.normcase(abs_source) 

119 norm_requested = os.path.normcase(abs_requested) 

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

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

122 

123 return abs_requested 

124 

125 

126class BuildBackendHookCaller: 

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

128 

129 def __init__( 

130 self, 

131 source_dir: str, 

132 build_backend: str, 

133 backend_path: Optional[Sequence[str]] = None, 

134 runner: Optional["SubprocessRunner"] = None, 

135 python_executable: Optional[str] = None, 

136 ) -> None: 

137 """ 

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

139 :param build_backend: The build backend spec 

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

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

142 :param python_executable: 

143 The Python executable used to invoke the build backend 

144 """ 

145 if runner is None: 

146 runner = default_subprocess_runner 

147 

148 self.source_dir = abspath(source_dir) 

149 self.build_backend = build_backend 

150 if backend_path: 

151 backend_path = [norm_and_check(self.source_dir, p) for p in backend_path] 

152 self.backend_path = backend_path 

153 self._subprocess_runner = runner 

154 if not python_executable: 

155 python_executable = sys.executable 

156 self.python_executable = python_executable 

157 

158 @contextmanager 

159 def subprocess_runner(self, runner: "SubprocessRunner") -> Iterator[None]: 

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

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

162 

163 :param runner: The new subprocess runner to use within the context. 

164 

165 .. code-block:: python 

166 

167 hook_caller = BuildBackendHookCaller(...) 

168 with hook_caller.subprocess_runner(quiet_subprocess_runner): 

169 ... 

170 """ 

171 prev = self._subprocess_runner 

172 self._subprocess_runner = runner 

173 try: 

174 yield 

175 finally: 

176 self._subprocess_runner = prev 

177 

178 def _supported_features(self) -> Sequence[str]: 

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

180 return self._call_hook("_supported_features", {}) 

181 

182 def get_requires_for_build_wheel( 

183 self, 

184 config_settings: Optional[Mapping[str, Any]] = None, 

185 ) -> Sequence[str]: 

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

187 

188 :param config_settings: The configuration settings for the build backend 

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

190 

191 .. admonition:: Fallback 

192 

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

194 empty list will be returned. 

195 """ 

196 return self._call_hook( 

197 "get_requires_for_build_wheel", {"config_settings": config_settings} 

198 ) 

199 

200 def prepare_metadata_for_build_wheel( 

201 self, 

202 metadata_directory: str, 

203 config_settings: Optional[Mapping[str, Any]] = None, 

204 _allow_fallback: bool = True, 

205 ) -> str: 

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

207 

208 :param metadata_directory: The directory to write the metadata to 

209 :param config_settings: The configuration settings for the build backend 

210 :param _allow_fallback: 

211 Whether to allow the fallback to building a wheel and extracting 

212 the metadata from it. Should be passed as a keyword argument only. 

213 

214 :returns: Name of the newly created subfolder within 

215 ``metadata_directory``, containing the metadata. 

216 

217 .. admonition:: Fallback 

218 

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

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

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

222 that will be returned. 

223 """ 

224 return self._call_hook( 

225 "prepare_metadata_for_build_wheel", 

226 { 

227 "metadata_directory": abspath(metadata_directory), 

228 "config_settings": config_settings, 

229 "_allow_fallback": _allow_fallback, 

230 }, 

231 ) 

232 

233 def build_wheel( 

234 self, 

235 wheel_directory: str, 

236 config_settings: Optional[Mapping[str, Any]] = None, 

237 metadata_directory: Optional[str] = None, 

238 ) -> str: 

239 """Build a wheel from this project. 

240 

241 :param wheel_directory: The directory to write the wheel to 

242 :param config_settings: The configuration settings for the build backend 

243 :param metadata_directory: The directory to reuse existing metadata from 

244 :returns: 

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

246 

247 .. admonition:: Interaction with fallback 

248 

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

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

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

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

253 """ 

254 if metadata_directory is not None: 

255 metadata_directory = abspath(metadata_directory) 

256 return self._call_hook( 

257 "build_wheel", 

258 { 

259 "wheel_directory": abspath(wheel_directory), 

260 "config_settings": config_settings, 

261 "metadata_directory": metadata_directory, 

262 }, 

263 ) 

264 

265 def get_requires_for_build_editable( 

266 self, 

267 config_settings: Optional[Mapping[str, Any]] = None, 

268 ) -> Sequence[str]: 

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

270 

271 :param config_settings: The configuration settings for the build backend 

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

273 

274 .. admonition:: Fallback 

275 

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

277 empty list will be returned. 

278 """ 

279 return self._call_hook( 

280 "get_requires_for_build_editable", {"config_settings": config_settings} 

281 ) 

282 

283 def prepare_metadata_for_build_editable( 

284 self, 

285 metadata_directory: str, 

286 config_settings: Optional[Mapping[str, Any]] = None, 

287 _allow_fallback: bool = True, 

288 ) -> Optional[str]: 

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

290 

291 :param metadata_directory: The directory to write the metadata to 

292 :param config_settings: The configuration settings for the build backend 

293 :param _allow_fallback: 

294 Whether to allow the fallback to building a wheel and extracting 

295 the metadata from it. Should be passed as a keyword argument only. 

296 :returns: Name of the newly created subfolder within 

297 ``metadata_directory``, containing the metadata. 

298 

299 .. admonition:: Fallback 

300 

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

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

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

304 extracted from that will be returned. 

305 """ 

306 return self._call_hook( 

307 "prepare_metadata_for_build_editable", 

308 { 

309 "metadata_directory": abspath(metadata_directory), 

310 "config_settings": config_settings, 

311 "_allow_fallback": _allow_fallback, 

312 }, 

313 ) 

314 

315 def build_editable( 

316 self, 

317 wheel_directory: str, 

318 config_settings: Optional[Mapping[str, Any]] = None, 

319 metadata_directory: Optional[str] = None, 

320 ) -> str: 

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

322 

323 :param wheel_directory: The directory to write the wheel to 

324 :param config_settings: The configuration settings for the build backend 

325 :param metadata_directory: The directory to reuse existing metadata from 

326 :returns: 

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

328 

329 .. admonition:: Interaction with fallback 

330 

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

332 :meth:`prepare_metadata_for_build_editable`, the build backend 

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

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

335 returned. 

336 """ 

337 if metadata_directory is not None: 

338 metadata_directory = abspath(metadata_directory) 

339 return self._call_hook( 

340 "build_editable", 

341 { 

342 "wheel_directory": abspath(wheel_directory), 

343 "config_settings": config_settings, 

344 "metadata_directory": metadata_directory, 

345 }, 

346 ) 

347 

348 def get_requires_for_build_sdist( 

349 self, 

350 config_settings: Optional[Mapping[str, Any]] = None, 

351 ) -> Sequence[str]: 

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

353 

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

355 """ 

356 return self._call_hook( 

357 "get_requires_for_build_sdist", {"config_settings": config_settings} 

358 ) 

359 

360 def build_sdist( 

361 self, 

362 sdist_directory: str, 

363 config_settings: Optional[Mapping[str, Any]] = None, 

364 ) -> str: 

365 """Build an sdist from this project. 

366 

367 :returns: 

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

369 """ 

370 return self._call_hook( 

371 "build_sdist", 

372 { 

373 "sdist_directory": abspath(sdist_directory), 

374 "config_settings": config_settings, 

375 }, 

376 ) 

377 

378 def _call_hook(self, hook_name: str, kwargs: Mapping[str, Any]) -> Any: 

379 extra_environ = {"_PYPROJECT_HOOKS_BUILD_BACKEND": self.build_backend} 

380 

381 if self.backend_path: 

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

383 extra_environ["_PYPROJECT_HOOKS_BACKEND_PATH"] = backend_path 

384 

385 with tempfile.TemporaryDirectory() as td: 

386 hook_input = {"kwargs": kwargs} 

387 write_json(hook_input, pjoin(td, "input.json"), indent=2) 

388 

389 # Run the hook in a subprocess 

390 with _in_proc_script_path() as script: 

391 python = self.python_executable 

392 self._subprocess_runner( 

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

394 cwd=self.source_dir, 

395 extra_environ=extra_environ, 

396 ) 

397 

398 data = read_json(pjoin(td, "output.json")) 

399 if data.get("unsupported"): 

400 raise UnsupportedOperation(data.get("traceback", "")) 

401 if data.get("no_backend"): 

402 raise BackendUnavailable( 

403 data.get("traceback", ""), 

404 message=data.get("backend_error", ""), 

405 backend_name=self.build_backend, 

406 backend_path=self.backend_path, 

407 ) 

408 if data.get("hook_missing"): 

409 raise HookMissing(data.get("missing_hook_name") or hook_name) 

410 return data["return_val"]