]> crepu.dev Git - config.git/blame_incremental - djavu-asus/emacs/elpy/rpc-venv/lib/python3.11/site-packages/setuptools/build_meta.py
Reorganización de directorios
[config.git] / djavu-asus / emacs / elpy / rpc-venv / lib / python3.11 / site-packages / setuptools / build_meta.py
... / ...
CommitLineData
1"""A PEP 517 interface to setuptools
2
3Previously, when a user or a command line tool (let's call it a "frontend")
4needed to make a request of setuptools to take a certain action, for
5example, generating a list of installation requirements, the frontend would
6would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line.
7
8PEP 517 defines a different method of interfacing with setuptools. Rather
9than calling "setup.py" directly, the frontend should:
10
11 1. Set the current directory to the directory with a setup.py file
12 2. Import this module into a safe python interpreter (one in which
13 setuptools can potentially set global variables or crash hard).
14 3. Call one of the functions defined in PEP 517.
15
16What each function does is defined in PEP 517. However, here is a "casual"
17definition of the functions (this definition should not be relied on for
18bug reports or API stability):
19
20 - `build_wheel`: build a wheel in the folder and return the basename
21 - `get_requires_for_build_wheel`: get the `setup_requires` to build
22 - `prepare_metadata_for_build_wheel`: get the `install_requires`
23 - `build_sdist`: build an sdist in the folder and return the basename
24 - `get_requires_for_build_sdist`: get the `setup_requires` to build
25
26Again, this is not a formal definition! Just a "taste" of the module.
27"""
28
29import io
30import os
31import shlex
32import sys
33import tokenize
34import shutil
35import contextlib
36import tempfile
37import warnings
38from pathlib import Path
39from typing import Dict, Iterator, List, Optional, Union
40
41import setuptools
42import distutils
43from . import errors
44from ._path import same_path
45from ._reqs import parse_strings
46from ._deprecation_warning import SetuptoolsDeprecationWarning
47from distutils.util import strtobool
48
49
50__all__ = ['get_requires_for_build_sdist',
51 'get_requires_for_build_wheel',
52 'prepare_metadata_for_build_wheel',
53 'build_wheel',
54 'build_sdist',
55 'get_requires_for_build_editable',
56 'prepare_metadata_for_build_editable',
57 'build_editable',
58 '__legacy__',
59 'SetupRequirementsError']
60
61SETUPTOOLS_ENABLE_FEATURES = os.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower()
62LEGACY_EDITABLE = "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES.replace("_", "-")
63
64
65class SetupRequirementsError(BaseException):
66 def __init__(self, specifiers):
67 self.specifiers = specifiers
68
69
70class Distribution(setuptools.dist.Distribution):
71 def fetch_build_eggs(self, specifiers):
72 specifier_list = list(parse_strings(specifiers))
73
74 raise SetupRequirementsError(specifier_list)
75
76 @classmethod
77 @contextlib.contextmanager
78 def patch(cls):
79 """
80 Replace
81 distutils.dist.Distribution with this class
82 for the duration of this context.
83 """
84 orig = distutils.core.Distribution
85 distutils.core.Distribution = cls
86 try:
87 yield
88 finally:
89 distutils.core.Distribution = orig
90
91
92@contextlib.contextmanager
93def no_install_setup_requires():
94 """Temporarily disable installing setup_requires
95
96 Under PEP 517, the backend reports build dependencies to the frontend,
97 and the frontend is responsible for ensuring they're installed.
98 So setuptools (acting as a backend) should not try to install them.
99 """
100 orig = setuptools._install_setup_requires
101 setuptools._install_setup_requires = lambda attrs: None
102 try:
103 yield
104 finally:
105 setuptools._install_setup_requires = orig
106
107
108def _get_immediate_subdirectories(a_dir):
109 return [name for name in os.listdir(a_dir)
110 if os.path.isdir(os.path.join(a_dir, name))]
111
112
113def _file_with_extension(directory, extension):
114 matching = (
115 f for f in os.listdir(directory)
116 if f.endswith(extension)
117 )
118 try:
119 file, = matching
120 except ValueError:
121 raise ValueError(
122 'No distribution was found. Ensure that `setup.py` '
123 'is not empty and that it calls `setup()`.')
124 return file
125
126
127def _open_setup_script(setup_script):
128 if not os.path.exists(setup_script):
129 # Supply a default setup.py
130 return io.StringIO(u"from setuptools import setup; setup()")
131
132 return getattr(tokenize, 'open', open)(setup_script)
133
134
135@contextlib.contextmanager
136def suppress_known_deprecation():
137 with warnings.catch_warnings():
138 warnings.filterwarnings('ignore', 'setup.py install is deprecated')
139 yield
140
141
142_ConfigSettings = Optional[Dict[str, Union[str, List[str], None]]]
143"""
144Currently the user can run::
145
146 pip install -e . --config-settings key=value
147 python -m build -C--key=value -C key=value
148
149- pip will pass both key and value as strings and overwriting repeated keys
150 (pypa/pip#11059).
151- build will accumulate values associated with repeated keys in a list.
152 It will also accept keys with no associated value.
153 This means that an option passed by build can be ``str | list[str] | None``.
154- PEP 517 specifies that ``config_settings`` is an optional dict.
155"""
156
157
158class _ConfigSettingsTranslator:
159 """Translate ``config_settings`` into distutils-style command arguments.
160 Only a limited number of options is currently supported.
161 """
162 # See pypa/setuptools#1928 pypa/setuptools#2491
163
164 def _get_config(self, key: str, config_settings: _ConfigSettings) -> List[str]:
165 """
166 Get the value of a specific key in ``config_settings`` as a list of strings.
167
168 >>> fn = _ConfigSettingsTranslator()._get_config
169 >>> fn("--global-option", None)
170 []
171 >>> fn("--global-option", {})
172 []
173 >>> fn("--global-option", {'--global-option': 'foo'})
174 ['foo']
175 >>> fn("--global-option", {'--global-option': ['foo']})
176 ['foo']
177 >>> fn("--global-option", {'--global-option': 'foo'})
178 ['foo']
179 >>> fn("--global-option", {'--global-option': 'foo bar'})
180 ['foo', 'bar']
181 """
182 cfg = config_settings or {}
183 opts = cfg.get(key) or []
184 return shlex.split(opts) if isinstance(opts, str) else opts
185
186 def _valid_global_options(self):
187 """Global options accepted by setuptools (e.g. quiet or verbose)."""
188 options = (opt[:2] for opt in setuptools.dist.Distribution.global_options)
189 return {flag for long_and_short in options for flag in long_and_short if flag}
190
191 def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
192 """
193 Let the user specify ``verbose`` or ``quiet`` + escape hatch via
194 ``--global-option``.
195 Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools,
196 so we just have to cover the basic scenario ``-v``.
197
198 >>> fn = _ConfigSettingsTranslator()._global_args
199 >>> list(fn(None))
200 []
201 >>> list(fn({"verbose": "False"}))
202 ['-q']
203 >>> list(fn({"verbose": "1"}))
204 ['-v']
205 >>> list(fn({"--verbose": None}))
206 ['-v']
207 >>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"}))
208 ['-v', '-q', '--no-user-cfg']
209 >>> list(fn({"--quiet": None}))
210 ['-q']
211 """
212 cfg = config_settings or {}
213 falsey = {"false", "no", "0", "off"}
214 if "verbose" in cfg or "--verbose" in cfg:
215 level = str(cfg.get("verbose") or cfg.get("--verbose") or "1")
216 yield ("-q" if level.lower() in falsey else "-v")
217 if "quiet" in cfg or "--quiet" in cfg:
218 level = str(cfg.get("quiet") or cfg.get("--quiet") or "1")
219 yield ("-v" if level.lower() in falsey else "-q")
220
221 valid = self._valid_global_options()
222 args = self._get_config("--global-option", config_settings)
223 yield from (arg for arg in args if arg.strip("-") in valid)
224
225 def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
226 """
227 The ``dist_info`` command accepts ``tag-date`` and ``tag-build``.
228
229 .. warning::
230 We cannot use this yet as it requires the ``sdist`` and ``bdist_wheel``
231 commands run in ``build_sdist`` and ``build_wheel`` to re-use the egg-info
232 directory created in ``prepare_metadata_for_build_wheel``.
233
234 >>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args
235 >>> list(fn(None))
236 []
237 >>> list(fn({"tag-date": "False"}))
238 ['--no-date']
239 >>> list(fn({"tag-date": None}))
240 ['--no-date']
241 >>> list(fn({"tag-date": "true", "tag-build": ".a"}))
242 ['--tag-date', '--tag-build', '.a']
243 """
244 cfg = config_settings or {}
245 if "tag-date" in cfg:
246 val = strtobool(str(cfg["tag-date"] or "false"))
247 yield ("--tag-date" if val else "--no-date")
248 if "tag-build" in cfg:
249 yield from ["--tag-build", str(cfg["tag-build"])]
250
251 def _editable_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
252 """
253 The ``editable_wheel`` command accepts ``editable-mode=strict``.
254
255 >>> fn = _ConfigSettingsTranslator()._editable_args
256 >>> list(fn(None))
257 []
258 >>> list(fn({"editable-mode": "strict"}))
259 ['--mode', 'strict']
260 """
261 cfg = config_settings or {}
262 mode = cfg.get("editable-mode") or cfg.get("editable_mode")
263 if not mode:
264 return
265 yield from ["--mode", str(mode)]
266
267 def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
268 """
269 Users may expect to pass arbitrary lists of arguments to a command
270 via "--global-option" (example provided in PEP 517 of a "escape hatch").
271
272 >>> fn = _ConfigSettingsTranslator()._arbitrary_args
273 >>> list(fn(None))
274 []
275 >>> list(fn({}))
276 []
277 >>> list(fn({'--build-option': 'foo'}))
278 ['foo']
279 >>> list(fn({'--build-option': ['foo']}))
280 ['foo']
281 >>> list(fn({'--build-option': 'foo'}))
282 ['foo']
283 >>> list(fn({'--build-option': 'foo bar'}))
284 ['foo', 'bar']
285 >>> warnings.simplefilter('error', SetuptoolsDeprecationWarning)
286 >>> list(fn({'--global-option': 'foo'})) # doctest: +IGNORE_EXCEPTION_DETAIL
287 Traceback (most recent call last):
288 SetuptoolsDeprecationWarning: ...arguments given via `--global-option`...
289 """
290 args = self._get_config("--global-option", config_settings)
291 global_opts = self._valid_global_options()
292 bad_args = []
293
294 for arg in args:
295 if arg.strip("-") not in global_opts:
296 bad_args.append(arg)
297 yield arg
298
299 yield from self._get_config("--build-option", config_settings)
300
301 if bad_args:
302 msg = f"""
303 The arguments {bad_args!r} were given via `--global-option`.
304 Please use `--build-option` instead,
305 `--global-option` is reserved to flags like `--verbose` or `--quiet`.
306 """
307 warnings.warn(msg, SetuptoolsDeprecationWarning)
308
309
310class _BuildMetaBackend(_ConfigSettingsTranslator):
311 def _get_build_requires(self, config_settings, requirements):
312 sys.argv = [
313 *sys.argv[:1],
314 *self._global_args(config_settings),
315 "egg_info",
316 *self._arbitrary_args(config_settings),
317 ]
318 try:
319 with Distribution.patch():
320 self.run_setup()
321 except SetupRequirementsError as e:
322 requirements += e.specifiers
323
324 return requirements
325
326 def run_setup(self, setup_script='setup.py'):
327 # Note that we can reuse our build directory between calls
328 # Correctness comes first, then optimization later
329 __file__ = setup_script
330 __name__ = '__main__'
331
332 with _open_setup_script(__file__) as f:
333 code = f.read().replace(r'\r\n', r'\n')
334
335 exec(code, locals())
336
337 def get_requires_for_build_wheel(self, config_settings=None):
338 return self._get_build_requires(config_settings, requirements=['wheel'])
339
340 def get_requires_for_build_sdist(self, config_settings=None):
341 return self._get_build_requires(config_settings, requirements=[])
342
343 def _bubble_up_info_directory(self, metadata_directory: str, suffix: str) -> str:
344 """
345 PEP 517 requires that the .dist-info directory be placed in the
346 metadata_directory. To comply, we MUST copy the directory to the root.
347
348 Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`.
349 """
350 info_dir = self._find_info_directory(metadata_directory, suffix)
351 if not same_path(info_dir.parent, metadata_directory):
352 shutil.move(str(info_dir), metadata_directory)
353 # PEP 517 allow other files and dirs to exist in metadata_directory
354 return info_dir.name
355
356 def _find_info_directory(self, metadata_directory: str, suffix: str) -> Path:
357 for parent, dirs, _ in os.walk(metadata_directory):
358 candidates = [f for f in dirs if f.endswith(suffix)]
359
360 if len(candidates) != 0 or len(dirs) != 1:
361 assert len(candidates) == 1, f"Multiple {suffix} directories found"
362 return Path(parent, candidates[0])
363
364 msg = f"No {suffix} directory found in {metadata_directory}"
365 raise errors.InternalError(msg)
366
367 def prepare_metadata_for_build_wheel(self, metadata_directory,
368 config_settings=None):
369 sys.argv = [
370 *sys.argv[:1],
371 *self._global_args(config_settings),
372 "dist_info",
373 "--output-dir", metadata_directory,
374 "--keep-egg-info",
375 ]
376 with no_install_setup_requires():
377 self.run_setup()
378
379 self._bubble_up_info_directory(metadata_directory, ".egg-info")
380 return self._bubble_up_info_directory(metadata_directory, ".dist-info")
381
382 def _build_with_temp_dir(self, setup_command, result_extension,
383 result_directory, config_settings):
384 result_directory = os.path.abspath(result_directory)
385
386 # Build in a temporary directory, then copy to the target.
387 os.makedirs(result_directory, exist_ok=True)
388 temp_opts = {"prefix": ".tmp-", "dir": result_directory}
389 with tempfile.TemporaryDirectory(**temp_opts) as tmp_dist_dir:
390 sys.argv = [
391 *sys.argv[:1],
392 *self._global_args(config_settings),
393 *setup_command,
394 "--dist-dir", tmp_dist_dir,
395 *self._arbitrary_args(config_settings),
396 ]
397 with no_install_setup_requires():
398 self.run_setup()
399
400 result_basename = _file_with_extension(
401 tmp_dist_dir, result_extension)
402 result_path = os.path.join(result_directory, result_basename)
403 if os.path.exists(result_path):
404 # os.rename will fail overwriting on non-Unix.
405 os.remove(result_path)
406 os.rename(os.path.join(tmp_dist_dir, result_basename), result_path)
407
408 return result_basename
409
410 def build_wheel(self, wheel_directory, config_settings=None,
411 metadata_directory=None):
412 with suppress_known_deprecation():
413 return self._build_with_temp_dir(['bdist_wheel'], '.whl',
414 wheel_directory, config_settings)
415
416 def build_sdist(self, sdist_directory, config_settings=None):
417 return self._build_with_temp_dir(['sdist', '--formats', 'gztar'],
418 '.tar.gz', sdist_directory,
419 config_settings)
420
421 def _get_dist_info_dir(self, metadata_directory: Optional[str]) -> Optional[str]:
422 if not metadata_directory:
423 return None
424 dist_info_candidates = list(Path(metadata_directory).glob("*.dist-info"))
425 assert len(dist_info_candidates) <= 1
426 return str(dist_info_candidates[0]) if dist_info_candidates else None
427
428 if not LEGACY_EDITABLE:
429
430 # PEP660 hooks:
431 # build_editable
432 # get_requires_for_build_editable
433 # prepare_metadata_for_build_editable
434 def build_editable(
435 self, wheel_directory, config_settings=None, metadata_directory=None
436 ):
437 # XXX can or should we hide our editable_wheel command normally?
438 info_dir = self._get_dist_info_dir(metadata_directory)
439 opts = ["--dist-info-dir", info_dir] if info_dir else []
440 cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)]
441 with suppress_known_deprecation():
442 return self._build_with_temp_dir(
443 cmd, ".whl", wheel_directory, config_settings
444 )
445
446 def get_requires_for_build_editable(self, config_settings=None):
447 return self.get_requires_for_build_wheel(config_settings)
448
449 def prepare_metadata_for_build_editable(self, metadata_directory,
450 config_settings=None):
451 return self.prepare_metadata_for_build_wheel(
452 metadata_directory, config_settings
453 )
454
455
456class _BuildMetaLegacyBackend(_BuildMetaBackend):
457 """Compatibility backend for setuptools
458
459 This is a version of setuptools.build_meta that endeavors
460 to maintain backwards
461 compatibility with pre-PEP 517 modes of invocation. It
462 exists as a temporary
463 bridge between the old packaging mechanism and the new
464 packaging mechanism,
465 and will eventually be removed.
466 """
467 def run_setup(self, setup_script='setup.py'):
468 # In order to maintain compatibility with scripts assuming that
469 # the setup.py script is in a directory on the PYTHONPATH, inject
470 # '' into sys.path. (pypa/setuptools#1642)
471 sys_path = list(sys.path) # Save the original path
472
473 script_dir = os.path.dirname(os.path.abspath(setup_script))
474 if script_dir not in sys.path:
475 sys.path.insert(0, script_dir)
476
477 # Some setup.py scripts (e.g. in pygame and numpy) use sys.argv[0] to
478 # get the directory of the source code. They expect it to refer to the
479 # setup.py script.
480 sys_argv_0 = sys.argv[0]
481 sys.argv[0] = setup_script
482
483 try:
484 super(_BuildMetaLegacyBackend,
485 self).run_setup(setup_script=setup_script)
486 finally:
487 # While PEP 517 frontends should be calling each hook in a fresh
488 # subprocess according to the standard (and thus it should not be
489 # strictly necessary to restore the old sys.path), we'll restore
490 # the original path so that the path manipulation does not persist
491 # within the hook after run_setup is called.
492 sys.path[:] = sys_path
493 sys.argv[0] = sys_argv_0
494
495
496# The primary backend
497_BACKEND = _BuildMetaBackend()
498
499get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel
500get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist
501prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel
502build_wheel = _BACKEND.build_wheel
503build_sdist = _BACKEND.build_sdist
504
505if not LEGACY_EDITABLE:
506 get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable
507 prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable
508 build_editable = _BACKEND.build_editable
509
510
511# The legacy backend
512__legacy__ = _BuildMetaLegacyBackend()