1 """A PEP 517 interface to setuptools
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.
8 PEP 517 defines a different method of interfacing with setuptools. Rather
9 than calling "setup.py" directly, the frontend should:
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.
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):
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
26 Again, this is not a formal definition! Just a "taste" of the module.
38 from pathlib
import Path
39 from typing
import Dict
, Iterator
, List
, Optional
, Union
44 from ._path
import same_path
45 from ._reqs
import parse_strings
46 from .warnings
import SetuptoolsDeprecationWarning
47 from distutils
.util
import strtobool
51 'get_requires_for_build_sdist',
52 'get_requires_for_build_wheel',
53 'prepare_metadata_for_build_wheel',
56 'get_requires_for_build_editable',
57 'prepare_metadata_for_build_editable',
60 'SetupRequirementsError',
63 SETUPTOOLS_ENABLE_FEATURES
= os
.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower()
64 LEGACY_EDITABLE
= "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES
.replace("_", "-")
67 class SetupRequirementsError(BaseException
):
68 def __init__(self
, specifiers
):
69 self
.specifiers
= specifiers
72 class Distribution(setuptools
.dist
.Distribution
):
73 def fetch_build_eggs(self
, specifiers
):
74 specifier_list
= list(parse_strings(specifiers
))
76 raise SetupRequirementsError(specifier_list
)
79 @contextlib.contextmanager
83 distutils.dist.Distribution with this class
84 for the duration of this context.
86 orig
= distutils
.core
.Distribution
87 distutils
.core
.Distribution
= cls
91 distutils
.core
.Distribution
= orig
94 @contextlib.contextmanager
95 def no_install_setup_requires():
96 """Temporarily disable installing setup_requires
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.
102 orig
= setuptools
._install
_setup
_requires
103 setuptools
._install
_setup
_requires
= lambda attrs
: None
107 setuptools
._install
_setup
_requires
= orig
110 def _get_immediate_subdirectories(a_dir
):
112 name
for name
in os
.listdir(a_dir
) if os
.path
.isdir(os
.path
.join(a_dir
, name
))
116 def _file_with_extension(directory
, extension
):
117 matching
= (f
for f
in os
.listdir(directory
) if f
.endswith(extension
))
122 'No distribution was found. Ensure that `setup.py` '
123 'is not empty and that it calls `setup()`.'
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()")
133 return getattr(tokenize
, 'open', open)(setup_script
)
136 @contextlib.contextmanager
137 def suppress_known_deprecation():
138 with warnings
.catch_warnings():
139 warnings
.filterwarnings('ignore', 'setup.py install is deprecated')
143 _ConfigSettings
= Optional
[Dict
[str, Union
[str, List
[str], None]]]
145 Currently the user can run::
147 pip install -e . --config-settings key=value
148 python -m build -C--key=value -C key=value
150 - pip will pass both key and value as strings and overwriting repeated keys
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.
159 class _ConfigSettingsTranslator
:
160 """Translate ``config_settings`` into distutils-style command arguments.
161 Only a limited number of options is currently supported.
164 # See pypa/setuptools#1928 pypa/setuptools#2491
166 def _get_config(self
, key
: str, config_settings
: _ConfigSettings
) -> List
[str]:
168 Get the value of a specific key in ``config_settings`` as a list of strings.
170 >>> fn = _ConfigSettingsTranslator()._get_config
171 >>> fn("--global-option", None)
173 >>> fn("--global-option", {})
175 >>> fn("--global-option", {'--global-option': 'foo'})
177 >>> fn("--global-option", {'--global-option': ['foo']})
179 >>> fn("--global-option", {'--global-option': 'foo'})
181 >>> fn("--global-option", {'--global-option': 'foo bar'})
184 cfg
= config_settings
or {}
185 opts
= cfg
.get(key
) or []
186 return shlex
.split(opts
) if isinstance(opts
, str) else opts
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}
193 def _global_args(self
, config_settings
: _ConfigSettings
) -> Iterator
[str]:
195 Let the user specify ``verbose`` or ``quiet`` + escape hatch via
197 Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools,
198 so we just have to cover the basic scenario ``-v``.
200 >>> fn = _ConfigSettingsTranslator()._global_args
203 >>> list(fn({"verbose": "False"}))
205 >>> list(fn({"verbose": "1"}))
207 >>> list(fn({"--verbose": None}))
209 >>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"}))
210 ['-v', '-q', '--no-user-cfg']
211 >>> list(fn({"--quiet": None}))
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")
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
)
227 def __dist_info_args(self
, config_settings
: _ConfigSettings
) -> Iterator
[str]:
229 The ``dist_info`` command accepts ``tag-date`` and ``tag-build``.
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``.
236 >>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args
239 >>> list(fn({"tag-date": "False"}))
241 >>> list(fn({"tag-date": None}))
243 >>> list(fn({"tag-date": "true", "tag-build": ".a"}))
244 ['--tag-date', '--tag-build', '.a']
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"])]
253 def _editable_args(self
, config_settings
: _ConfigSettings
) -> Iterator
[str]:
255 The ``editable_wheel`` command accepts ``editable-mode=strict``.
257 >>> fn = _ConfigSettingsTranslator()._editable_args
260 >>> list(fn({"editable-mode": "strict"}))
263 cfg
= config_settings
or {}
264 mode
= cfg
.get("editable-mode") or cfg
.get("editable_mode")
267 yield from ["--mode", str(mode
)]
269 def _arbitrary_args(self
, config_settings
: _ConfigSettings
) -> Iterator
[str]:
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").
274 >>> fn = _ConfigSettingsTranslator()._arbitrary_args
279 >>> list(fn({'--build-option': 'foo'}))
281 >>> list(fn({'--build-option': ['foo']}))
283 >>> list(fn({'--build-option': 'foo'}))
285 >>> list(fn({'--build-option': '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`...
292 args
= self
._get
_config
("--global-option", config_settings
)
293 global_opts
= self
._valid
_global
_options
()
297 if arg
.strip("-") not in global_opts
:
301 yield from self
._get
_config
("--build-option", config_settings
)
304 SetuptoolsDeprecationWarning
.emit(
305 "Incompatible `config_settings` passed to build backend.",
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`.
311 due_date
=(2023, 9, 26), # Warning introduced in v64.0.1, 11/Aug/2022.
315 class _BuildMetaBackend(_ConfigSettingsTranslator
):
316 def _get_build_requires(self
, config_settings
, requirements
):
319 *self
._global
_args
(config_settings
),
321 *self
._arbitrary
_args
(config_settings
),
324 with Distribution
.patch():
326 except SetupRequirementsError
as e
:
327 requirements
+= e
.specifiers
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__'
337 with _open_setup_script(__file__
) as f
:
338 code
= f
.read().replace(r
'\r\n', r
'\n')
342 except SystemExit as e
:
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",
354 def get_requires_for_build_wheel(self
, config_settings
=None):
355 return self
._get
_build
_requires
(config_settings
, requirements
=['wheel'])
357 def get_requires_for_build_sdist(self
, config_settings
=None):
358 return self
._get
_build
_requires
(config_settings
, requirements
=[])
360 def _bubble_up_info_directory(self
, metadata_directory
: str, suffix
: str) -> str:
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.
365 Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`.
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
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
)]
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])
381 msg
= f
"No {suffix} directory found in {metadata_directory}"
382 raise errors
.InternalError(msg
)
384 def prepare_metadata_for_build_wheel(
385 self
, metadata_directory
, config_settings
=None
389 *self
._global
_args
(config_settings
),
395 with no_install_setup_requires():
398 self
._bubble
_up
_info
_directory
(metadata_directory
, ".egg-info")
399 return self
._bubble
_up
_info
_directory
(metadata_directory
, ".dist-info")
401 def _build_with_temp_dir(
402 self
, setup_command
, result_extension
, result_directory
, config_settings
404 result_directory
= os
.path
.abspath(result_directory
)
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
:
412 *self
._global
_args
(config_settings
),
416 *self
._arbitrary
_args
(config_settings
),
418 with no_install_setup_requires():
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
)
428 return result_basename
431 self
, wheel_directory
, config_settings
=None, metadata_directory
=None
433 with suppress_known_deprecation():
434 return self
._build
_with
_temp
_dir
(
435 ['bdist_wheel'], '.whl', wheel_directory
, config_settings
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
443 def _get_dist_info_dir(self
, metadata_directory
: Optional
[str]) -> Optional
[str]:
444 if not metadata_directory
:
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
450 if not LEGACY_EDITABLE
:
453 # get_requires_for_build_editable
454 # prepare_metadata_for_build_editable
456 self
, wheel_directory
, config_settings
=None, metadata_directory
=None
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
467 def get_requires_for_build_editable(self
, config_settings
=None):
468 return self
.get_requires_for_build_wheel(config_settings
)
470 def prepare_metadata_for_build_editable(
471 self
, metadata_directory
, config_settings
=None
473 return self
.prepare_metadata_for_build_wheel(
474 metadata_directory
, config_settings
478 class _BuildMetaLegacyBackend(_BuildMetaBackend
):
479 """Compatibility backend for setuptools
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
487 and will eventually be removed.
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
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
)
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
503 sys_argv_0
= sys
.argv
[0]
504 sys
.argv
[0] = setup_script
507 super(_BuildMetaLegacyBackend
, self
).run_setup(setup_script
=setup_script
)
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
518 # The primary backend
519 _BACKEND
= _BuildMetaBackend()
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
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
534 __legacy__
= _BuildMetaLegacyBackend()