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 ._deprecation
_warning
import SetuptoolsDeprecationWarning
47 from distutils
.util
import strtobool
50 __all__
= ['get_requires_for_build_sdist',
51 'get_requires_for_build_wheel',
52 'prepare_metadata_for_build_wheel',
55 'get_requires_for_build_editable',
56 'prepare_metadata_for_build_editable',
59 'SetupRequirementsError']
61 SETUPTOOLS_ENABLE_FEATURES
= os
.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower()
62 LEGACY_EDITABLE
= "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES
.replace("_", "-")
65 class SetupRequirementsError(BaseException
):
66 def __init__(self
, specifiers
):
67 self
.specifiers
= specifiers
70 class Distribution(setuptools
.dist
.Distribution
):
71 def fetch_build_eggs(self
, specifiers
):
72 specifier_list
= list(parse_strings(specifiers
))
74 raise SetupRequirementsError(specifier_list
)
77 @contextlib.contextmanager
81 distutils.dist.Distribution with this class
82 for the duration of this context.
84 orig
= distutils
.core
.Distribution
85 distutils
.core
.Distribution
= cls
89 distutils
.core
.Distribution
= orig
92 @contextlib.contextmanager
93 def no_install_setup_requires():
94 """Temporarily disable installing setup_requires
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.
100 orig
= setuptools
._install
_setup
_requires
101 setuptools
._install
_setup
_requires
= lambda attrs
: None
105 setuptools
._install
_setup
_requires
= orig
108 def _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
))]
113 def _file_with_extension(directory
, extension
):
115 f
for f
in os
.listdir(directory
)
116 if f
.endswith(extension
)
122 'No distribution was found. Ensure that `setup.py` '
123 'is not empty and that it calls `setup()`.')
127 def _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()")
132 return getattr(tokenize
, 'open', open)(setup_script
)
135 @contextlib.contextmanager
136 def suppress_known_deprecation():
137 with warnings
.catch_warnings():
138 warnings
.filterwarnings('ignore', 'setup.py install is deprecated')
142 _ConfigSettings
= Optional
[Dict
[str, Union
[str, List
[str], None]]]
144 Currently the user can run::
146 pip install -e . --config-settings key=value
147 python -m build -C--key=value -C key=value
149 - pip will pass both key and value as strings and overwriting repeated keys
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.
158 class _ConfigSettingsTranslator
:
159 """Translate ``config_settings`` into distutils-style command arguments.
160 Only a limited number of options is currently supported.
162 # See pypa/setuptools#1928 pypa/setuptools#2491
164 def _get_config(self
, key
: str, config_settings
: _ConfigSettings
) -> List
[str]:
166 Get the value of a specific key in ``config_settings`` as a list of strings.
168 >>> fn = _ConfigSettingsTranslator()._get_config
169 >>> fn("--global-option", None)
171 >>> fn("--global-option", {})
173 >>> fn("--global-option", {'--global-option': 'foo'})
175 >>> fn("--global-option", {'--global-option': ['foo']})
177 >>> fn("--global-option", {'--global-option': 'foo'})
179 >>> fn("--global-option", {'--global-option': 'foo bar'})
182 cfg
= config_settings
or {}
183 opts
= cfg
.get(key
) or []
184 return shlex
.split(opts
) if isinstance(opts
, str) else opts
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
}
191 def _global_args(self
, config_settings
: _ConfigSettings
) -> Iterator
[str]:
193 Let the user specify ``verbose`` or ``quiet`` + escape hatch via
195 Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools,
196 so we just have to cover the basic scenario ``-v``.
198 >>> fn = _ConfigSettingsTranslator()._global_args
201 >>> list(fn({"verbose": "False"}))
203 >>> list(fn({"verbose": "1"}))
205 >>> list(fn({"--verbose": None}))
207 >>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"}))
208 ['-v', '-q', '--no-user-cfg']
209 >>> list(fn({"--quiet": None}))
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")
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
)
225 def __dist_info_args(self
, config_settings
: _ConfigSettings
) -> Iterator
[str]:
227 The ``dist_info`` command accepts ``tag-date`` and ``tag-build``.
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``.
234 >>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args
237 >>> list(fn({"tag-date": "False"}))
239 >>> list(fn({"tag-date": None}))
241 >>> list(fn({"tag-date": "true", "tag-build": ".a"}))
242 ['--tag-date', '--tag-build', '.a']
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"])]
251 def _editable_args(self
, config_settings
: _ConfigSettings
) -> Iterator
[str]:
253 The ``editable_wheel`` command accepts ``editable-mode=strict``.
255 >>> fn = _ConfigSettingsTranslator()._editable_args
258 >>> list(fn({"editable-mode": "strict"}))
261 cfg
= config_settings
or {}
262 mode
= cfg
.get("editable-mode") or cfg
.get("editable_mode")
265 yield from ["--mode", str(mode
)]
267 def _arbitrary_args(self
, config_settings
: _ConfigSettings
) -> Iterator
[str]:
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").
272 >>> fn = _ConfigSettingsTranslator()._arbitrary_args
277 >>> list(fn({'--build-option': 'foo'}))
279 >>> list(fn({'--build-option': ['foo']}))
281 >>> list(fn({'--build-option': 'foo'}))
283 >>> list(fn({'--build-option': '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`...
290 args
= self
._get
_config
("--global-option", config_settings
)
291 global_opts
= self
._valid
_global
_options
()
295 if arg
.strip("-") not in global_opts
:
299 yield from self
._get
_config
("--build-option", config_settings
)
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`.
307 warnings
.warn(msg
, SetuptoolsDeprecationWarning
)
310 class _BuildMetaBackend(_ConfigSettingsTranslator
):
311 def _get_build_requires(self
, config_settings
, requirements
):
314 *self
._global
_args
(config_settings
),
316 *self
._arbitrary
_args
(config_settings
),
319 with Distribution
.patch():
321 except SetupRequirementsError
as e
:
322 requirements
+= e
.specifiers
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__'
332 with
_open_setup_script(__file__
) as f
:
333 code
= f
.read().replace(r
'\r\n', r
'\n')
337 def get_requires_for_build_wheel(self
, config_settings
=None):
338 return self
._get
_build
_requires
(config_settings
, requirements
=['wheel'])
340 def get_requires_for_build_sdist(self
, config_settings
=None):
341 return self
._get
_build
_requires
(config_settings
, requirements
=[])
343 def _bubble_up_info_directory(self
, metadata_directory
: str, suffix
: str) -> str:
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.
348 Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`.
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
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
)]
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])
364 msg
= f
"No {suffix} directory found in {metadata_directory}"
365 raise errors
.InternalError(msg
)
367 def prepare_metadata_for_build_wheel(self
, metadata_directory
,
368 config_settings
=None):
371 *self
._global
_args
(config_settings
),
373 "--output-dir", metadata_directory
,
376 with
no_install_setup_requires():
379 self
._bubble
_up
_info
_directory
(metadata_directory
, ".egg-info")
380 return self
._bubble
_up
_info
_directory
(metadata_directory
, ".dist-info")
382 def _build_with_temp_dir(self
, setup_command
, result_extension
,
383 result_directory
, config_settings
):
384 result_directory
= os
.path
.abspath(result_directory
)
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
:
392 *self
._global
_args
(config_settings
),
394 "--dist-dir", tmp_dist_dir
,
395 *self
._arbitrary
_args
(config_settings
),
397 with
no_install_setup_requires():
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
)
408 return result_basename
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
)
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
,
421 def _get_dist_info_dir(self
, metadata_directory
: Optional
[str]) -> Optional
[str]:
422 if not metadata_directory
:
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
428 if not LEGACY_EDITABLE
:
432 # get_requires_for_build_editable
433 # prepare_metadata_for_build_editable
435 self
, wheel_directory
, config_settings
=None, metadata_directory
=None
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
446 def get_requires_for_build_editable(self
, config_settings
=None):
447 return self
.get_requires_for_build_wheel(config_settings
)
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
456 class _BuildMetaLegacyBackend(_BuildMetaBackend
):
457 """Compatibility backend for setuptools
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
465 and will eventually be removed.
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
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
)
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
480 sys_argv_0
= sys
.argv
[0]
481 sys
.argv
[0] = setup_script
484 super(_BuildMetaLegacyBackend
,
485 self
).run_setup(setup_script
=setup_script
)
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
496 # The primary backend
497 _BACKEND
= _BuildMetaBackend()
499 get_requires_for_build_wheel
= _BACKEND
.get_requires_for_build_wheel
500 get_requires_for_build_sdist
= _BACKEND
.get_requires_for_build_sdist
501 prepare_metadata_for_build_wheel
= _BACKEND
.prepare_metadata_for_build_wheel
502 build_wheel
= _BACKEND
.build_wheel
503 build_sdist
= _BACKEND
.build_sdist
505 if 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
512 __legacy__
= _BuildMetaLegacyBackend()