import io
import os
import shlex
import sys
import tokenize
import shutil
import contextlib
import tempfile
import warnings
from pathlib import Path
from typing import Dict, Iterator, List, Optional, Union
import setuptools
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
__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):
self.specifiers = specifiers
class Distribution(setuptools.dist.Distribution):
def fetch_build_eggs(self, specifiers):
specifier_list = list(parse_strings(specifiers))
raise SetupRequirementsError(specifier_list)
@classmethod
@contextlib.contextmanager
def patch(cls):
orig = distutils.core.Distribution
distutils.core.Distribution = cls
try:
yield
finally:
distutils.core.Distribution = orig
@contextlib.contextmanager
def no_install_setup_requires():
orig = setuptools._install_setup_requires
setuptools._install_setup_requires = lambda attrs: None
try:
yield
finally:
setuptools._install_setup_requires = orig
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))]
def _file_with_extension(directory, extension):
matching = (
f for f in os.listdir(directory)
if f.endswith(extension)
)
try:
file, = matching
except ValueError:
raise ValueError(
'No distribution was found. Ensure that `setup.py` '
'is not empty and that it calls `setup()`.')
return file
def _open_setup_script(setup_script):
if not os.path.exists(setup_script):
return io.StringIO(u"from setuptools import setup; setup()")
return getattr(tokenize, 'open', open)(setup_script)
@contextlib.contextmanager
def suppress_known_deprecation():
with warnings.catch_warnings():
warnings.filterwarnings('ignore', 'setup.py install is deprecated')
yield
_ConfigSettings = Optional[Dict[str, Union[str, List[str], None]]]
class _ConfigSettingsTranslator:
def _get_config(self, key: str, config_settings: _ConfigSettings) -> List[str]:
cfg = config_settings or {}
opts = cfg.get(key) or []
return shlex.split(opts) if isinstance(opts, str) else opts
def _valid_global_options(self):
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]:
cfg = config_settings or {}
falsey = {"false", "no", "0", "off"}
if "verbose" in cfg or "--verbose" in cfg:
level = str(cfg.get("verbose") or cfg.get("--verbose") or "1")
yield ("-q" if level.lower() in falsey else "-v")
if "quiet" in cfg or "--quiet" in cfg:
level = str(cfg.get("quiet") or cfg.get("--quiet") or "1")
yield ("-v" if level.lower() in falsey else "-q")
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]:
cfg = config_settings or {}
if "tag-date" in cfg:
val = strtobool(str(cfg["tag-date"] or "false"))
yield ("--tag-date" if val else "--no-date")
if "tag-build" in cfg:
yield from ["--tag-build", str(cfg["tag-build"])]
def _editable_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
cfg = config_settings or {}
mode = cfg.get("editable-mode") or cfg.get("editable_mode")
if not mode:
return
yield from ["--mode", str(mode)]
def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]:
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, requirements):
sys.argv = [
*sys.argv[:1],
*self._global_args(config_settings),
"egg_info",
*self._arbitrary_args(config_settings),
]
try:
with Distribution.patch():
self.run_setup()
except SetupRequirementsError as e:
requirements += e.specifiers
return requirements
def run_setup(self, setup_script='setup.py'):
__file__ = setup_script
__name__ = '__main__'
with _open_setup_script(__file__) as f:
code = f.read().replace(r'\r\n', r'\n')
exec(code, locals())
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 _bubble_up_info_directory(self, metadata_directory: str, suffix: str) -> str:
info_dir = self._find_info_directory(metadata_directory, suffix)
if not same_path(info_dir.parent, metadata_directory):
shutil.move(str(info_dir), metadata_directory)
return info_dir.name
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)]
if len(candidates) != 0 or len(dirs) != 1:
assert len(candidates) == 1, f"Multiple {suffix} directories found"
return Path(parent, candidates[0])
msg = f"No {suffix} directory found in {metadata_directory}"
raise errors.InternalError(msg)
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", metadata_directory,
"--keep-egg-info",
]
with no_install_setup_requires():
self.run_setup()
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, result_extension,
result_directory, config_settings):
result_directory = os.path.abspath(result_directory)
os.makedirs(result_directory, exist_ok=True)
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,
*self._arbitrary_args(config_settings),
]
with no_install_setup_requires():
self.run_setup()
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.remove(result_path)
os.rename(os.path.join(tmp_dist_dir, result_basename), result_path)
return result_basename
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)
def build_sdist(self, sdist_directory, config_settings=None):
return self._build_with_temp_dir(['sdist', '--formats', 'gztar'],
'.tar.gz', sdist_directory,
config_settings)
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
if not LEGACY_EDITABLE:
def build_editable(
self, wheel_directory, config_settings=None, metadata_directory=None
):
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
)
class _BuildMetaLegacyBackend(_BuildMetaBackend):
def run_setup(self, setup_script='setup.py'):
sys_path = list(sys.path)
script_dir = os.path.dirname(os.path.abspath(setup_script))
if script_dir not in sys.path:
sys.path.insert(0, script_dir)
sys_argv_0 = sys.argv[0]
sys.argv[0] = setup_script
try:
super(_BuildMetaLegacyBackend,
self).run_setup(setup_script=setup_script)
finally:
sys.path[:] = sys_path
sys.argv[0] = sys_argv_0
_BACKEND = _BuildMetaBackend()
get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel
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
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
__legacy__ = _BuildMetaLegacyBackend()