summaryrefslogtreecommitdiffstats
path: root/third_party/python/setuptools/setuptools/build_meta.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/python/setuptools/setuptools/build_meta.py')
-rw-r--r--third_party/python/setuptools/setuptools/build_meta.py515
1 files changed, 515 insertions, 0 deletions
diff --git a/third_party/python/setuptools/setuptools/build_meta.py b/third_party/python/setuptools/setuptools/build_meta.py
new file mode 100644
index 0000000000..ee8ef13faa
--- /dev/null
+++ b/third_party/python/setuptools/setuptools/build_meta.py
@@ -0,0 +1,515 @@
+"""A PEP 517 interface to setuptools
+
+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 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
+than calling "setup.py" directly, the frontend should:
+
+ 1. Set the current directory to the directory with a setup.py file
+ 2. Import this module into a safe python interpreter (one in which
+ setuptools can potentially set global variables or crash hard).
+ 3. Call one of the functions defined in PEP 517.
+
+What each function does is defined in PEP 517. However, here is a "casual"
+definition of the functions (this definition should not be relied on for
+bug reports or API stability):
+
+ - `build_wheel`: build a wheel in the folder and return the basename
+ - `get_requires_for_build_wheel`: get the `setup_requires` to build
+ - `prepare_metadata_for_build_wheel`: get the `install_requires`
+ - `build_sdist`: build an sdist in the folder and return the basename
+ - `get_requires_for_build_sdist`: get the `setup_requires` to build
+
+Again, this is not a formal definition! Just a "taste" of the module.
+"""
+
+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 .warnings 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):
+ """
+ Replace
+ distutils.dist.Distribution with this class
+ for the duration of this context.
+ """
+ orig = distutils.core.Distribution
+ distutils.core.Distribution = cls
+ try:
+ yield
+ finally:
+ distutils.core.Distribution = orig
+
+
+@contextlib.contextmanager
+def no_install_setup_requires():
+ """Temporarily disable installing setup_requires
+
+ Under PEP 517, the backend reports build dependencies to the frontend,
+ and the frontend is responsible for ensuring they're installed.
+ So setuptools (acting as a backend) should not try to install them.
+ """
+ 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):
+ # Supply a default setup.py
+ 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]]]
+"""
+Currently the user can run::
+
+ pip install -e . --config-settings key=value
+ python -m build -C--key=value -C key=value
+
+- pip will pass both key and value as strings and overwriting repeated keys
+ (pypa/pip#11059).
+- build will accumulate values associated with repeated keys in a list.
+ It will also accept keys with no associated value.
+ This means that an option passed by build can be ``str | list[str] | None``.
+- PEP 517 specifies that ``config_settings`` is an optional dict.
+"""
+
+
+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]:
+ """
+ Get the value of a specific key in ``config_settings`` as a list of strings.
+
+ >>> fn = _ConfigSettingsTranslator()._get_config
+ >>> fn("--global-option", None)
+ []
+ >>> fn("--global-option", {})
+ []
+ >>> fn("--global-option", {'--global-option': 'foo'})
+ ['foo']
+ >>> fn("--global-option", {'--global-option': ['foo']})
+ ['foo']
+ >>> fn("--global-option", {'--global-option': 'foo'})
+ ['foo']
+ >>> fn("--global-option", {'--global-option': 'foo bar'})
+ ['foo', 'bar']
+ """
+ cfg = config_settings or {}
+ 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
+ ``--global-option``.
+ Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools,
+ so we just have to cover the basic scenario ``-v``.
+
+ >>> fn = _ConfigSettingsTranslator()._global_args
+ >>> list(fn(None))
+ []
+ >>> list(fn({"verbose": "False"}))
+ ['-q']
+ >>> list(fn({"verbose": "1"}))
+ ['-v']
+ >>> list(fn({"--verbose": None}))
+ ['-v']
+ >>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"}))
+ ['-v', '-q', '--no-user-cfg']
+ >>> list(fn({"--quiet": None}))
+ ['-q']
+ """
+ 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]:
+ """
+ The ``dist_info`` command accepts ``tag-date`` and ``tag-build``.
+
+ .. warning::
+ We cannot use this yet as it requires the ``sdist`` and ``bdist_wheel``
+ 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
+ >>> list(fn(None))
+ []
+ >>> list(fn({"tag-date": "False"}))
+ ['--no-date']
+ >>> list(fn({"tag-date": None}))
+ ['--no-date']
+ >>> list(fn({"tag-date": "true", "tag-build": ".a"}))
+ ['--tag-date', '--tag-build', '.a']
+ """
+ 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]:
+ """
+ The ``editable_wheel`` command accepts ``editable-mode=strict``.
+
+ >>> fn = _ConfigSettingsTranslator()._editable_args
+ >>> list(fn(None))
+ []
+ >>> list(fn({"editable-mode": "strict"}))
+ ['--mode', 'strict']
+ """
+ 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]:
+ """
+ Users may expect to pass arbitrary lists of arguments to a command
+ via "--global-option" (example provided in PEP 517 of a "escape hatch").
+
+ >>> fn = _ConfigSettingsTranslator()._arbitrary_args
+ >>> list(fn(None))
+ []
+ >>> list(fn({}))
+ []
+ >>> list(fn({'--build-option': 'foo'}))
+ ['foo']
+ >>> list(fn({'--build-option': ['foo']}))
+ ['foo']
+ >>> list(fn({'--build-option': 'foo'}))
+ ['foo']
+ >>> list(fn({'--build-option': 'foo bar'}))
+ ['foo', 'bar']
+ >>> 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:
+ SetuptoolsDeprecationWarning.emit(
+ "Incompatible `config_settings` passed to build backend.",
+ f"""
+ The arguments {bad_args!r} were given via `--global-option`.
+ Please use `--build-option` instead,
+ `--global-option` is reserved for flags like `--verbose` or `--quiet`.
+ """,
+ due_date=(2023, 9, 26), # Warning introduced in v64.0.1, 11/Aug/2022.
+ )
+
+
+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'):
+ # Note that we can reuse our build directory between calls
+ # Correctness comes first, then optimization later
+ __file__ = os.path.abspath(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:
+ """
+ PEP 517 requires that the .dist-info directory be placed in the
+ metadata_directory. To comply, we MUST copy the directory to the root.
+
+ Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`.
+ """
+ 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)
+ # PEP 517 allow other files and dirs to exist in 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)
+
+ # Build in a temporary directory, then copy to the target.
+ os.makedirs(result_directory, exist_ok=True)
+ temp_opts = {"prefix": ".tmp-", "dir": result_directory}
+ with tempfile.TemporaryDirectory(**temp_opts) 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.rename will fail overwriting on non-Unix.
+ 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:
+
+ # 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
+ )
+
+
+class _BuildMetaLegacyBackend(_BuildMetaBackend):
+ """Compatibility backend for setuptools
+
+ This is a version of setuptools.build_meta that endeavors
+ to maintain backwards
+ compatibility with pre-PEP 517 modes of invocation. It
+ exists as a temporary
+ bridge between the old packaging mechanism and the new
+ packaging mechanism,
+ and will eventually be removed.
+ """
+ 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
+
+ script_dir = os.path.dirname(os.path.abspath(setup_script))
+ if script_dir not in sys.path:
+ sys.path.insert(0, script_dir)
+
+ # Some setup.py scripts (e.g. in pygame and numpy) use sys.argv[0] to
+ # get the directory of the source code. They expect it to refer to the
+ # setup.py script.
+ sys_argv_0 = sys.argv[0]
+ sys.argv[0] = setup_script
+
+ try:
+ 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
+ # strictly necessary to restore the old sys.path), we'll restore
+ # the original path so that the path manipulation does not persist
+ # within the hook after run_setup is called.
+ sys.path[:] = sys_path
+ sys.argv[0] = sys_argv_0
+
+
+# The primary backend
+_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
+
+
+# The legacy backend
+__legacy__ = _BuildMetaLegacyBackend()