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