comparison lib/python3.8/site-packages/pip/_vendor/pep517/wrappers.py @ 0:9e54283cc701 draft

"planemo upload commit d12c32a45bcd441307e632fca6d9af7d60289d44"
author guerler
date Mon, 27 Jul 2020 03:47:31 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:9e54283cc701
1 import threading
2 from contextlib import contextmanager
3 import os
4 from os.path import dirname, abspath, join as pjoin
5 import shutil
6 from subprocess import check_call, check_output, STDOUT
7 import sys
8 from tempfile import mkdtemp
9
10 from . import compat
11
12 _in_proc_script = pjoin(dirname(abspath(__file__)), '_in_process.py')
13
14
15 @contextmanager
16 def tempdir():
17 td = mkdtemp()
18 try:
19 yield td
20 finally:
21 shutil.rmtree(td)
22
23
24 class BackendUnavailable(Exception):
25 """Will be raised if the backend cannot be imported in the hook process."""
26 def __init__(self, traceback):
27 self.traceback = traceback
28
29
30 class BackendInvalid(Exception):
31 """Will be raised if the backend is invalid."""
32 def __init__(self, backend_name, backend_path, message):
33 self.backend_name = backend_name
34 self.backend_path = backend_path
35 self.message = message
36
37
38 class HookMissing(Exception):
39 """Will be raised on missing hooks."""
40 def __init__(self, hook_name):
41 super(HookMissing, self).__init__(hook_name)
42 self.hook_name = hook_name
43
44
45 class UnsupportedOperation(Exception):
46 """May be raised by build_sdist if the backend indicates that it can't."""
47 def __init__(self, traceback):
48 self.traceback = traceback
49
50
51 def default_subprocess_runner(cmd, cwd=None, extra_environ=None):
52 """The default method of calling the wrapper subprocess."""
53 env = os.environ.copy()
54 if extra_environ:
55 env.update(extra_environ)
56
57 check_call(cmd, cwd=cwd, env=env)
58
59
60 def quiet_subprocess_runner(cmd, cwd=None, extra_environ=None):
61 """A method of calling the wrapper subprocess while suppressing output."""
62 env = os.environ.copy()
63 if extra_environ:
64 env.update(extra_environ)
65
66 check_output(cmd, cwd=cwd, env=env, stderr=STDOUT)
67
68
69 def norm_and_check(source_tree, requested):
70 """Normalise and check a backend path.
71
72 Ensure that the requested backend path is specified as a relative path,
73 and resolves to a location under the given source tree.
74
75 Return an absolute version of the requested path.
76 """
77 if os.path.isabs(requested):
78 raise ValueError("paths must be relative")
79
80 abs_source = os.path.abspath(source_tree)
81 abs_requested = os.path.normpath(os.path.join(abs_source, requested))
82 # We have to use commonprefix for Python 2.7 compatibility. So we
83 # normalise case to avoid problems because commonprefix is a character
84 # based comparison :-(
85 norm_source = os.path.normcase(abs_source)
86 norm_requested = os.path.normcase(abs_requested)
87 if os.path.commonprefix([norm_source, norm_requested]) != norm_source:
88 raise ValueError("paths must be inside source tree")
89
90 return abs_requested
91
92
93 class Pep517HookCaller(object):
94 """A wrapper around a source directory to be built with a PEP 517 backend.
95
96 source_dir : The path to the source directory, containing pyproject.toml.
97 build_backend : The build backend spec, as per PEP 517, from
98 pyproject.toml.
99 backend_path : The backend path, as per PEP 517, from pyproject.toml.
100 runner : A callable that invokes the wrapper subprocess.
101
102 The 'runner', if provided, must expect the following:
103 cmd : a list of strings representing the command and arguments to
104 execute, as would be passed to e.g. 'subprocess.check_call'.
105 cwd : a string representing the working directory that must be
106 used for the subprocess. Corresponds to the provided source_dir.
107 extra_environ : a dict mapping environment variable names to values
108 which must be set for the subprocess execution.
109 """
110 def __init__(
111 self,
112 source_dir,
113 build_backend,
114 backend_path=None,
115 runner=None,
116 ):
117 if runner is None:
118 runner = default_subprocess_runner
119
120 self.source_dir = abspath(source_dir)
121 self.build_backend = build_backend
122 if backend_path:
123 backend_path = [
124 norm_and_check(self.source_dir, p) for p in backend_path
125 ]
126 self.backend_path = backend_path
127 self._subprocess_runner = runner
128
129 # TODO: Is this over-engineered? Maybe frontends only need to
130 # set this when creating the wrapper, not on every call.
131 @contextmanager
132 def subprocess_runner(self, runner):
133 """A context manager for temporarily overriding the default subprocess
134 runner.
135 """
136 prev = self._subprocess_runner
137 self._subprocess_runner = runner
138 yield
139 self._subprocess_runner = prev
140
141 def get_requires_for_build_wheel(self, config_settings=None):
142 """Identify packages required for building a wheel
143
144 Returns a list of dependency specifications, e.g.:
145 ["wheel >= 0.25", "setuptools"]
146
147 This does not include requirements specified in pyproject.toml.
148 It returns the result of calling the equivalently named hook in a
149 subprocess.
150 """
151 return self._call_hook('get_requires_for_build_wheel', {
152 'config_settings': config_settings
153 })
154
155 def prepare_metadata_for_build_wheel(
156 self, metadata_directory, config_settings=None,
157 _allow_fallback=True):
158 """Prepare a *.dist-info folder with metadata for this project.
159
160 Returns the name of the newly created folder.
161
162 If the build backend defines a hook with this name, it will be called
163 in a subprocess. If not, the backend will be asked to build a wheel,
164 and the dist-info extracted from that (unless _allow_fallback is
165 False).
166 """
167 return self._call_hook('prepare_metadata_for_build_wheel', {
168 'metadata_directory': abspath(metadata_directory),
169 'config_settings': config_settings,
170 '_allow_fallback': _allow_fallback,
171 })
172
173 def build_wheel(
174 self, wheel_directory, config_settings=None,
175 metadata_directory=None):
176 """Build a wheel from this project.
177
178 Returns the name of the newly created file.
179
180 In general, this will call the 'build_wheel' hook in the backend.
181 However, if that was previously called by
182 'prepare_metadata_for_build_wheel', and the same metadata_directory is
183 used, the previously built wheel will be copied to wheel_directory.
184 """
185 if metadata_directory is not None:
186 metadata_directory = abspath(metadata_directory)
187 return self._call_hook('build_wheel', {
188 'wheel_directory': abspath(wheel_directory),
189 'config_settings': config_settings,
190 'metadata_directory': metadata_directory,
191 })
192
193 def get_requires_for_build_sdist(self, config_settings=None):
194 """Identify packages required for building a wheel
195
196 Returns a list of dependency specifications, e.g.:
197 ["setuptools >= 26"]
198
199 This does not include requirements specified in pyproject.toml.
200 It returns the result of calling the equivalently named hook in a
201 subprocess.
202 """
203 return self._call_hook('get_requires_for_build_sdist', {
204 'config_settings': config_settings
205 })
206
207 def build_sdist(self, sdist_directory, config_settings=None):
208 """Build an sdist from this project.
209
210 Returns the name of the newly created file.
211
212 This calls the 'build_sdist' backend hook in a subprocess.
213 """
214 return self._call_hook('build_sdist', {
215 'sdist_directory': abspath(sdist_directory),
216 'config_settings': config_settings,
217 })
218
219 def _call_hook(self, hook_name, kwargs):
220 # On Python 2, pytoml returns Unicode values (which is correct) but the
221 # environment passed to check_call needs to contain string values. We
222 # convert here by encoding using ASCII (the backend can only contain
223 # letters, digits and _, . and : characters, and will be used as a
224 # Python identifier, so non-ASCII content is wrong on Python 2 in
225 # any case).
226 # For backend_path, we use sys.getfilesystemencoding.
227 if sys.version_info[0] == 2:
228 build_backend = self.build_backend.encode('ASCII')
229 else:
230 build_backend = self.build_backend
231 extra_environ = {'PEP517_BUILD_BACKEND': build_backend}
232
233 if self.backend_path:
234 backend_path = os.pathsep.join(self.backend_path)
235 if sys.version_info[0] == 2:
236 backend_path = backend_path.encode(sys.getfilesystemencoding())
237 extra_environ['PEP517_BACKEND_PATH'] = backend_path
238
239 with tempdir() as td:
240 hook_input = {'kwargs': kwargs}
241 compat.write_json(hook_input, pjoin(td, 'input.json'),
242 indent=2)
243
244 # Run the hook in a subprocess
245 self._subprocess_runner(
246 [sys.executable, _in_proc_script, hook_name, td],
247 cwd=self.source_dir,
248 extra_environ=extra_environ
249 )
250
251 data = compat.read_json(pjoin(td, 'output.json'))
252 if data.get('unsupported'):
253 raise UnsupportedOperation(data.get('traceback', ''))
254 if data.get('no_backend'):
255 raise BackendUnavailable(data.get('traceback', ''))
256 if data.get('backend_invalid'):
257 raise BackendInvalid(
258 backend_name=self.build_backend,
259 backend_path=self.backend_path,
260 message=data.get('backend_error', '')
261 )
262 if data.get('hook_missing'):
263 raise HookMissing(hook_name)
264 return data['return_val']
265
266
267 class LoggerWrapper(threading.Thread):
268 """
269 Read messages from a pipe and redirect them
270 to a logger (see python's logging module).
271 """
272
273 def __init__(self, logger, level):
274 threading.Thread.__init__(self)
275 self.daemon = True
276
277 self.logger = logger
278 self.level = level
279
280 # create the pipe and reader
281 self.fd_read, self.fd_write = os.pipe()
282 self.reader = os.fdopen(self.fd_read)
283
284 self.start()
285
286 def fileno(self):
287 return self.fd_write
288
289 @staticmethod
290 def remove_newline(msg):
291 return msg[:-1] if msg.endswith(os.linesep) else msg
292
293 def run(self):
294 for line in self.reader:
295 self._write(self.remove_newline(line))
296
297 def _write(self, message):
298 self.logger.log(self.level, message)