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
« 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
10from ._in_process import _in_proc_script_path
13def write_json(obj, path, **kwargs):
14 with open(path, 'w', encoding='utf-8') as f:
15 json.dump(obj, f, **kwargs)
18def read_json(path):
19 with open(path, encoding='utf-8') as f:
20 return json.load(f)
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
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
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
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
50def default_subprocess_runner(cmd, cwd=None, extra_environ=None):
51 """The default method of calling the wrapper subprocess.
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)
59 check_call(cmd, cwd=cwd, env=env)
62def quiet_subprocess_runner(cmd, cwd=None, extra_environ=None):
63 """Call the subprocess while suppressing output.
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)
71 check_output(cmd, cwd=cwd, env=env, stderr=STDOUT)
74def norm_and_check(source_tree, requested):
75 """Normalise and check a backend path.
77 Ensure that the requested backend path is specified as a relative path,
78 and resolves to a location under the given source tree.
80 Return an absolute version of the requested path.
81 """
82 if os.path.isabs(requested):
83 raise ValueError("paths must be relative")
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")
95 return abs_requested
98class BuildBackendHookCaller:
99 """A wrapper to call the build backend hooks for a source directory.
100 """
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
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
133 @contextmanager
134 def subprocess_runner(self, runner):
135 """A context manager for temporarily overriding the default
136 :ref:`subprocess runner <Subprocess Runners>`.
138 .. code-block:: python
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
151 def _supported_features(self):
152 """Return the list of optional features supported by the backend."""
153 return self._call_hook('_supported_features', {})
155 def get_requires_for_build_wheel(self, config_settings=None):
156 """Get additional dependencies required for building a wheel.
158 :returns: A list of :pep:`dependency specifiers <508>`.
159 :rtype: list[str]
161 .. admonition:: Fallback
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 })
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.
175 :returns: Name of the newly created subfolder within
176 ``metadata_directory``, containing the metadata.
177 :rtype: str
179 .. admonition:: Fallback
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 })
192 def build_wheel(
193 self, wheel_directory, config_settings=None,
194 metadata_directory=None):
195 """Build a wheel from this project.
197 :returns:
198 The name of the newly created wheel within ``wheel_directory``.
200 .. admonition:: Interaction with fallback
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 })
215 def get_requires_for_build_editable(self, config_settings=None):
216 """Get additional dependencies required for building an editable wheel.
218 :returns: A list of :pep:`dependency specifiers <508>`.
219 :rtype: list[str]
221 .. admonition:: Fallback
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 })
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.
235 :returns: Name of the newly created subfolder within
236 ``metadata_directory``, containing the metadata.
237 :rtype: str
239 .. admonition:: Fallback
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 })
252 def build_editable(
253 self, wheel_directory, config_settings=None,
254 metadata_directory=None):
255 """Build an editable wheel from this project.
257 :returns:
258 The name of the newly created wheel within ``wheel_directory``.
260 .. admonition:: Interaction with fallback
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 })
276 def get_requires_for_build_sdist(self, config_settings=None):
277 """Get additional dependencies required for building an sdist.
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 })
286 def build_sdist(self, sdist_directory, config_settings=None):
287 """Build an sdist from this project.
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 })
297 def _call_hook(self, hook_name, kwargs):
298 extra_environ = {'PEP517_BUILD_BACKEND': self.build_backend}
300 if self.backend_path:
301 backend_path = os.pathsep.join(self.backend_path)
302 extra_environ['PEP517_BACKEND_PATH'] = backend_path
304 with tempfile.TemporaryDirectory() as td:
305 hook_input = {'kwargs': kwargs}
306 write_json(hook_input, pjoin(td, 'input.json'), indent=2)
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 )
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']