up
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Previously, when a user or a command line tool (let's call it a "frontend")
|
||||
needed to make a request of setuptools to take a certain action, for
|
||||
example, generating a list of installation requirements, the frontend
|
||||
example, generating a list of installation requirements, the frontend would
|
||||
would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line.
|
||||
|
||||
PEP 517 defines a different method of interfacing with setuptools. Rather
|
||||
@@ -26,50 +26,44 @@ bug reports or API stability):
|
||||
Again, this is not a formal definition! Just a "taste" of the module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import tokenize
|
||||
import shutil
|
||||
import contextlib
|
||||
import tempfile
|
||||
import warnings
|
||||
from collections.abc import Iterable, Iterator, Mapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import Dict, Iterator, List, Optional, Union
|
||||
|
||||
import setuptools
|
||||
|
||||
from . import errors
|
||||
from ._path import StrPath, same_path
|
||||
from ._reqs import parse_strings
|
||||
from .warnings import SetuptoolsDeprecationWarning
|
||||
|
||||
import distutils
|
||||
from . import errors
|
||||
from ._path import same_path
|
||||
from ._reqs import parse_strings
|
||||
from ._deprecation_warning import SetuptoolsDeprecationWarning
|
||||
from distutils.util import strtobool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
__all__ = [
|
||||
'get_requires_for_build_sdist',
|
||||
'get_requires_for_build_wheel',
|
||||
'prepare_metadata_for_build_wheel',
|
||||
'build_wheel',
|
||||
'build_sdist',
|
||||
'get_requires_for_build_editable',
|
||||
'prepare_metadata_for_build_editable',
|
||||
'build_editable',
|
||||
'__legacy__',
|
||||
'SetupRequirementsError',
|
||||
]
|
||||
__all__ = ['get_requires_for_build_sdist',
|
||||
'get_requires_for_build_wheel',
|
||||
'prepare_metadata_for_build_wheel',
|
||||
'build_wheel',
|
||||
'build_sdist',
|
||||
'get_requires_for_build_editable',
|
||||
'prepare_metadata_for_build_editable',
|
||||
'build_editable',
|
||||
'__legacy__',
|
||||
'SetupRequirementsError']
|
||||
|
||||
SETUPTOOLS_ENABLE_FEATURES = os.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower()
|
||||
LEGACY_EDITABLE = "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES.replace("_", "-")
|
||||
|
||||
|
||||
class SetupRequirementsError(BaseException):
|
||||
def __init__(self, specifiers) -> None:
|
||||
def __init__(self, specifiers):
|
||||
self.specifiers = specifiers
|
||||
|
||||
|
||||
@@ -88,11 +82,11 @@ class Distribution(setuptools.dist.Distribution):
|
||||
for the duration of this context.
|
||||
"""
|
||||
orig = distutils.core.Distribution
|
||||
distutils.core.Distribution = cls # type: ignore[misc] # monkeypatching
|
||||
distutils.core.Distribution = cls
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
distutils.core.Distribution = orig # type: ignore[misc] # monkeypatching
|
||||
distutils.core.Distribution = orig
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -112,29 +106,30 @@ def no_install_setup_requires():
|
||||
|
||||
|
||||
def _get_immediate_subdirectories(a_dir):
|
||||
return [
|
||||
name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name))
|
||||
]
|
||||
return [name for name in os.listdir(a_dir)
|
||||
if os.path.isdir(os.path.join(a_dir, name))]
|
||||
|
||||
|
||||
def _file_with_extension(directory: StrPath, extension: str | tuple[str, ...]):
|
||||
matching = (f for f in os.listdir(directory) if f.endswith(extension))
|
||||
def _file_with_extension(directory, extension):
|
||||
matching = (
|
||||
f for f in os.listdir(directory)
|
||||
if f.endswith(extension)
|
||||
)
|
||||
try:
|
||||
(file,) = matching
|
||||
file, = matching
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'No distribution was found. Ensure that `setup.py` '
|
||||
'is not empty and that it calls `setup()`.'
|
||||
) from None
|
||||
'is not empty and that it calls `setup()`.')
|
||||
return file
|
||||
|
||||
|
||||
def _open_setup_script(setup_script):
|
||||
if not os.path.exists(setup_script):
|
||||
# Supply a default setup.py
|
||||
return io.StringIO("from setuptools import setup; setup()")
|
||||
return io.StringIO(u"from setuptools import setup; setup()")
|
||||
|
||||
return tokenize.open(setup_script)
|
||||
return getattr(tokenize, 'open', open)(setup_script)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
@@ -144,7 +139,7 @@ def suppress_known_deprecation():
|
||||
yield
|
||||
|
||||
|
||||
_ConfigSettings: TypeAlias = Union[Mapping[str, Union[str, list[str], None]], None]
|
||||
_ConfigSettings = Optional[Dict[str, Union[str, List[str], None]]]
|
||||
"""
|
||||
Currently the user can run::
|
||||
|
||||
@@ -164,10 +159,9 @@ class _ConfigSettingsTranslator:
|
||||
"""Translate ``config_settings`` into distutils-style command arguments.
|
||||
Only a limited number of options is currently supported.
|
||||
"""
|
||||
|
||||
# See pypa/setuptools#1928 pypa/setuptools#2491
|
||||
|
||||
def _get_config(self, key: str, config_settings: _ConfigSettings) -> list[str]:
|
||||
def _get_config(self, key: str, config_settings: _ConfigSettings) -> List[str]:
|
||||
"""
|
||||
Get the value of a specific key in ``config_settings`` as a list of strings.
|
||||
|
||||
@@ -189,6 +183,11 @@ class _ConfigSettingsTranslator:
|
||||
opts = cfg.get(key) or []
|
||||
return shlex.split(opts) if isinstance(opts, str) else opts
|
||||
|
||||
def _valid_global_options(self):
|
||||
"""Global options accepted by setuptools (e.g. quiet or verbose)."""
|
||||
options = (opt[:2] for opt in setuptools.dist.Distribution.global_options)
|
||||
return {flag for long_and_short in options for flag in long_and_short if flag}
|
||||
|
||||
def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
|
||||
"""
|
||||
Let the user specify ``verbose`` or ``quiet`` + escape hatch via
|
||||
@@ -219,7 +218,9 @@ class _ConfigSettingsTranslator:
|
||||
level = str(cfg.get("quiet") or cfg.get("--quiet") or "1")
|
||||
yield ("-v" if level.lower() in falsey else "-q")
|
||||
|
||||
yield from self._get_config("--global-option", config_settings)
|
||||
valid = self._valid_global_options()
|
||||
args = self._get_config("--global-option", config_settings)
|
||||
yield from (arg for arg in args if arg.strip("-") in valid)
|
||||
|
||||
def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
|
||||
"""
|
||||
@@ -227,7 +228,7 @@ class _ConfigSettingsTranslator:
|
||||
|
||||
.. warning::
|
||||
We cannot use this yet as it requires the ``sdist`` and ``bdist_wheel``
|
||||
commands run in ``build_sdist`` and ``build_wheel`` to reuse the egg-info
|
||||
commands run in ``build_sdist`` and ``build_wheel`` to re-use the egg-info
|
||||
directory created in ``prepare_metadata_for_build_wheel``.
|
||||
|
||||
>>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args
|
||||
@@ -281,20 +282,38 @@ class _ConfigSettingsTranslator:
|
||||
['foo']
|
||||
>>> list(fn({'--build-option': 'foo bar'}))
|
||||
['foo', 'bar']
|
||||
>>> list(fn({'--global-option': 'foo'}))
|
||||
[]
|
||||
>>> warnings.simplefilter('error', SetuptoolsDeprecationWarning)
|
||||
>>> list(fn({'--global-option': 'foo'})) # doctest: +IGNORE_EXCEPTION_DETAIL
|
||||
Traceback (most recent call last):
|
||||
SetuptoolsDeprecationWarning: ...arguments given via `--global-option`...
|
||||
"""
|
||||
args = self._get_config("--global-option", config_settings)
|
||||
global_opts = self._valid_global_options()
|
||||
bad_args = []
|
||||
|
||||
for arg in args:
|
||||
if arg.strip("-") not in global_opts:
|
||||
bad_args.append(arg)
|
||||
yield arg
|
||||
|
||||
yield from self._get_config("--build-option", config_settings)
|
||||
|
||||
if bad_args:
|
||||
msg = f"""
|
||||
The arguments {bad_args!r} were given via `--global-option`.
|
||||
Please use `--build-option` instead,
|
||||
`--global-option` is reserved to flags like `--verbose` or `--quiet`.
|
||||
"""
|
||||
warnings.warn(msg, SetuptoolsDeprecationWarning)
|
||||
|
||||
|
||||
class _BuildMetaBackend(_ConfigSettingsTranslator):
|
||||
def _get_build_requires(
|
||||
self, config_settings: _ConfigSettings, requirements: list[str]
|
||||
):
|
||||
def _get_build_requires(self, config_settings, requirements):
|
||||
sys.argv = [
|
||||
*sys.argv[:1],
|
||||
*self._global_args(config_settings),
|
||||
"egg_info",
|
||||
*self._arbitrary_args(config_settings),
|
||||
]
|
||||
try:
|
||||
with Distribution.patch():
|
||||
@@ -304,38 +323,24 @@ class _BuildMetaBackend(_ConfigSettingsTranslator):
|
||||
|
||||
return requirements
|
||||
|
||||
def run_setup(self, setup_script: str = 'setup.py'):
|
||||
def run_setup(self, setup_script='setup.py'):
|
||||
# Note that we can reuse our build directory between calls
|
||||
# Correctness comes first, then optimization later
|
||||
__file__ = os.path.abspath(setup_script)
|
||||
__file__ = setup_script
|
||||
__name__ = '__main__'
|
||||
|
||||
with _open_setup_script(__file__) as f:
|
||||
code = f.read().replace(r'\r\n', r'\n')
|
||||
|
||||
try:
|
||||
exec(code, locals())
|
||||
except SystemExit as e:
|
||||
if e.code:
|
||||
raise
|
||||
# We ignore exit code indicating success
|
||||
SetuptoolsDeprecationWarning.emit(
|
||||
"Running `setup.py` directly as CLI tool is deprecated.",
|
||||
"Please avoid using `sys.exit(0)` or similar statements "
|
||||
"that don't fit in the paradigm of a configuration file.",
|
||||
see_url="https://blog.ganssle.io/articles/2021/10/"
|
||||
"setup-py-deprecated.html",
|
||||
)
|
||||
exec(code, locals())
|
||||
|
||||
def get_requires_for_build_wheel(self, config_settings: _ConfigSettings = None):
|
||||
def get_requires_for_build_wheel(self, config_settings=None):
|
||||
return self._get_build_requires(config_settings, requirements=['wheel'])
|
||||
|
||||
def get_requires_for_build_sdist(self, config_settings=None):
|
||||
return self._get_build_requires(config_settings, requirements=[])
|
||||
|
||||
def get_requires_for_build_sdist(self, config_settings: _ConfigSettings = None):
|
||||
return self._get_build_requires(config_settings, requirements=[])
|
||||
|
||||
def _bubble_up_info_directory(
|
||||
self, metadata_directory: StrPath, suffix: str
|
||||
) -> str:
|
||||
def _bubble_up_info_directory(self, metadata_directory: str, suffix: str) -> str:
|
||||
"""
|
||||
PEP 517 requires that the .dist-info directory be placed in the
|
||||
metadata_directory. To comply, we MUST copy the directory to the root.
|
||||
@@ -348,7 +353,7 @@ class _BuildMetaBackend(_ConfigSettingsTranslator):
|
||||
# PEP 517 allow other files and dirs to exist in metadata_directory
|
||||
return info_dir.name
|
||||
|
||||
def _find_info_directory(self, metadata_directory: StrPath, suffix: str) -> Path:
|
||||
def _find_info_directory(self, metadata_directory: str, suffix: str) -> Path:
|
||||
for parent, dirs, _ in os.walk(metadata_directory):
|
||||
candidates = [f for f in dirs if f.endswith(suffix)]
|
||||
|
||||
@@ -359,15 +364,13 @@ class _BuildMetaBackend(_ConfigSettingsTranslator):
|
||||
msg = f"No {suffix} directory found in {metadata_directory}"
|
||||
raise errors.InternalError(msg)
|
||||
|
||||
def prepare_metadata_for_build_wheel(
|
||||
self, metadata_directory: StrPath, config_settings: _ConfigSettings = None
|
||||
):
|
||||
def prepare_metadata_for_build_wheel(self, metadata_directory,
|
||||
config_settings=None):
|
||||
sys.argv = [
|
||||
*sys.argv[:1],
|
||||
*self._global_args(config_settings),
|
||||
"dist_info",
|
||||
"--output-dir",
|
||||
str(metadata_directory),
|
||||
"--output-dir", metadata_directory,
|
||||
"--keep-egg-info",
|
||||
]
|
||||
with no_install_setup_requires():
|
||||
@@ -376,34 +379,25 @@ class _BuildMetaBackend(_ConfigSettingsTranslator):
|
||||
self._bubble_up_info_directory(metadata_directory, ".egg-info")
|
||||
return self._bubble_up_info_directory(metadata_directory, ".dist-info")
|
||||
|
||||
def _build_with_temp_dir(
|
||||
self,
|
||||
setup_command: Iterable[str],
|
||||
result_extension: str | tuple[str, ...],
|
||||
result_directory: StrPath,
|
||||
config_settings: _ConfigSettings,
|
||||
arbitrary_args: Iterable[str] = (),
|
||||
):
|
||||
def _build_with_temp_dir(self, setup_command, result_extension,
|
||||
result_directory, config_settings):
|
||||
result_directory = os.path.abspath(result_directory)
|
||||
|
||||
# Build in a temporary directory, then copy to the target.
|
||||
os.makedirs(result_directory, exist_ok=True)
|
||||
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix=".tmp-", dir=result_directory
|
||||
) as tmp_dist_dir:
|
||||
with tempfile.TemporaryDirectory(dir=result_directory) as tmp_dist_dir:
|
||||
sys.argv = [
|
||||
*sys.argv[:1],
|
||||
*self._global_args(config_settings),
|
||||
*setup_command,
|
||||
"--dist-dir",
|
||||
tmp_dist_dir,
|
||||
*arbitrary_args,
|
||||
"--dist-dir", tmp_dist_dir,
|
||||
*self._arbitrary_args(config_settings),
|
||||
]
|
||||
with no_install_setup_requires():
|
||||
self.run_setup()
|
||||
|
||||
result_basename = _file_with_extension(tmp_dist_dir, result_extension)
|
||||
result_basename = _file_with_extension(
|
||||
tmp_dist_dir, result_extension)
|
||||
result_path = os.path.join(result_directory, result_basename)
|
||||
if os.path.exists(result_path):
|
||||
# os.rename will fail overwriting on non-Unix.
|
||||
@@ -412,73 +406,51 @@ class _BuildMetaBackend(_ConfigSettingsTranslator):
|
||||
|
||||
return result_basename
|
||||
|
||||
def build_wheel(
|
||||
self,
|
||||
wheel_directory: StrPath,
|
||||
config_settings: _ConfigSettings = None,
|
||||
metadata_directory: StrPath | None = None,
|
||||
):
|
||||
def _build(cmd: list[str]):
|
||||
with suppress_known_deprecation():
|
||||
return self._build_with_temp_dir(
|
||||
cmd,
|
||||
'.whl',
|
||||
wheel_directory,
|
||||
config_settings,
|
||||
self._arbitrary_args(config_settings),
|
||||
)
|
||||
def build_wheel(self, wheel_directory, config_settings=None,
|
||||
metadata_directory=None):
|
||||
with suppress_known_deprecation():
|
||||
return self._build_with_temp_dir(['bdist_wheel'], '.whl',
|
||||
wheel_directory, config_settings)
|
||||
|
||||
if metadata_directory is None:
|
||||
return _build(['bdist_wheel'])
|
||||
def build_sdist(self, sdist_directory, config_settings=None):
|
||||
return self._build_with_temp_dir(['sdist', '--formats', 'gztar'],
|
||||
'.tar.gz', sdist_directory,
|
||||
config_settings)
|
||||
|
||||
try:
|
||||
return _build(['bdist_wheel', '--dist-info-dir', str(metadata_directory)])
|
||||
except SystemExit as ex: # pragma: nocover
|
||||
# pypa/setuptools#4683
|
||||
if "--dist-info-dir not recognized" not in str(ex):
|
||||
raise
|
||||
_IncompatibleBdistWheel.emit()
|
||||
return _build(['bdist_wheel'])
|
||||
|
||||
def build_sdist(
|
||||
self, sdist_directory: StrPath, config_settings: _ConfigSettings = None
|
||||
):
|
||||
return self._build_with_temp_dir(
|
||||
['sdist', '--formats', 'gztar'], '.tar.gz', sdist_directory, config_settings
|
||||
)
|
||||
|
||||
def _get_dist_info_dir(self, metadata_directory: StrPath | None) -> str | None:
|
||||
def _get_dist_info_dir(self, metadata_directory: Optional[str]) -> Optional[str]:
|
||||
if not metadata_directory:
|
||||
return None
|
||||
dist_info_candidates = list(Path(metadata_directory).glob("*.dist-info"))
|
||||
assert len(dist_info_candidates) <= 1
|
||||
return str(dist_info_candidates[0]) if dist_info_candidates else None
|
||||
|
||||
def build_editable(
|
||||
self,
|
||||
wheel_directory: StrPath,
|
||||
config_settings: _ConfigSettings = None,
|
||||
metadata_directory: StrPath | None = None,
|
||||
):
|
||||
# XXX can or should we hide our editable_wheel command normally?
|
||||
info_dir = self._get_dist_info_dir(metadata_directory)
|
||||
opts = ["--dist-info-dir", info_dir] if info_dir else []
|
||||
cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)]
|
||||
with suppress_known_deprecation():
|
||||
return self._build_with_temp_dir(
|
||||
cmd, ".whl", wheel_directory, config_settings
|
||||
if not LEGACY_EDITABLE:
|
||||
|
||||
# PEP660 hooks:
|
||||
# build_editable
|
||||
# get_requires_for_build_editable
|
||||
# prepare_metadata_for_build_editable
|
||||
def build_editable(
|
||||
self, wheel_directory, config_settings=None, metadata_directory=None
|
||||
):
|
||||
# XXX can or should we hide our editable_wheel command normally?
|
||||
info_dir = self._get_dist_info_dir(metadata_directory)
|
||||
opts = ["--dist-info-dir", info_dir] if info_dir else []
|
||||
cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)]
|
||||
with suppress_known_deprecation():
|
||||
return self._build_with_temp_dir(
|
||||
cmd, ".whl", wheel_directory, config_settings
|
||||
)
|
||||
|
||||
def get_requires_for_build_editable(self, config_settings=None):
|
||||
return self.get_requires_for_build_wheel(config_settings)
|
||||
|
||||
def prepare_metadata_for_build_editable(self, metadata_directory,
|
||||
config_settings=None):
|
||||
return self.prepare_metadata_for_build_wheel(
|
||||
metadata_directory, config_settings
|
||||
)
|
||||
|
||||
def get_requires_for_build_editable(self, config_settings: _ConfigSettings = None):
|
||||
return self.get_requires_for_build_wheel(config_settings)
|
||||
|
||||
def prepare_metadata_for_build_editable(
|
||||
self, metadata_directory: StrPath, config_settings: _ConfigSettings = None
|
||||
):
|
||||
return self.prepare_metadata_for_build_wheel(
|
||||
metadata_directory, config_settings
|
||||
)
|
||||
|
||||
|
||||
class _BuildMetaLegacyBackend(_BuildMetaBackend):
|
||||
"""Compatibility backend for setuptools
|
||||
@@ -491,12 +463,11 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend):
|
||||
packaging mechanism,
|
||||
and will eventually be removed.
|
||||
"""
|
||||
|
||||
def run_setup(self, setup_script: str = 'setup.py'):
|
||||
def run_setup(self, setup_script='setup.py'):
|
||||
# In order to maintain compatibility with scripts assuming that
|
||||
# the setup.py script is in a directory on the PYTHONPATH, inject
|
||||
# '' into sys.path. (pypa/setuptools#1642)
|
||||
sys_path = list(sys.path) # Save the original path
|
||||
sys_path = list(sys.path) # Save the original path
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(setup_script))
|
||||
if script_dir not in sys.path:
|
||||
@@ -509,7 +480,8 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend):
|
||||
sys.argv[0] = setup_script
|
||||
|
||||
try:
|
||||
super().run_setup(setup_script=setup_script)
|
||||
super(_BuildMetaLegacyBackend,
|
||||
self).run_setup(setup_script=setup_script)
|
||||
finally:
|
||||
# While PEP 517 frontends should be calling each hook in a fresh
|
||||
# subprocess according to the standard (and thus it should not be
|
||||
@@ -520,17 +492,6 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend):
|
||||
sys.argv[0] = sys_argv_0
|
||||
|
||||
|
||||
class _IncompatibleBdistWheel(SetuptoolsDeprecationWarning):
|
||||
_SUMMARY = "wheel.bdist_wheel is deprecated, please import it from setuptools"
|
||||
_DETAILS = """
|
||||
Ensure that any custom bdist_wheel implementation is a subclass of
|
||||
setuptools.command.bdist_wheel.bdist_wheel.
|
||||
"""
|
||||
_DUE_DATE = (2025, 10, 15)
|
||||
# Initially introduced in 2024/10/15, but maybe too disruptive to be enforced?
|
||||
_SEE_URL = "https://github.com/pypa/wheel/pull/631"
|
||||
|
||||
|
||||
# The primary backend
|
||||
_BACKEND = _BuildMetaBackend()
|
||||
|
||||
@@ -539,9 +500,11 @@ get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist
|
||||
prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel
|
||||
build_wheel = _BACKEND.build_wheel
|
||||
build_sdist = _BACKEND.build_sdist
|
||||
get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable
|
||||
prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable
|
||||
build_editable = _BACKEND.build_editable
|
||||
|
||||
if not LEGACY_EDITABLE:
|
||||
get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable
|
||||
prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable
|
||||
build_editable = _BACKEND.build_editable
|
||||
|
||||
|
||||
# The legacy backend
|
||||
|
||||
Reference in New Issue
Block a user