diff options
Diffstat (limited to 'setuptools/build_meta.py')
-rw-r--r-- | setuptools/build_meta.py | 302 |
1 files changed, 217 insertions, 85 deletions
diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index 609ea1e..5dc65e2 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -26,14 +26,28 @@ bug reports or API stability): Again, this is not a formal definition! Just a "taste" of the module. """ +import io import os import sys import tokenize import shutil import contextlib +import tempfile +import warnings import setuptools import distutils +from ._reqs import parse_strings +from .extern.more_itertools import always_iterable + + +__all__ = ['get_requires_for_build_sdist', + 'get_requires_for_build_wheel', + 'prepare_metadata_for_build_wheel', + 'build_wheel', + 'build_sdist', + '__legacy__', + 'SetupRequirementsError'] class SetupRequirementsError(BaseException): @@ -43,7 +57,9 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - raise SetupRequirementsError(specifiers) + specifier_list = list(parse_strings(specifiers)) + + raise SetupRequirementsError(specifier_list) @classmethod @contextlib.contextmanager @@ -61,36 +77,20 @@ class Distribution(setuptools.dist.Distribution): distutils.core.Distribution = orig -def _run_setup(setup_script='setup.py'): - # Note that we can reuse our build directory between calls - # Correctness comes first, then optimization later - __file__ = setup_script - __name__ = '__main__' - f = getattr(tokenize, 'open', open)(__file__) - code = f.read().replace('\\r\\n', '\\n') - f.close() - exec(compile(code, __file__, 'exec'), locals()) - - -def _fix_config(config_settings): - config_settings = config_settings or {} - config_settings.setdefault('--global-option', []) - return config_settings - - -def _get_build_requires(config_settings): - config_settings = _fix_config(config_settings) - requirements = ['setuptools', 'wheel'] +@contextlib.contextmanager +def no_install_setup_requires(): + """Temporarily disable installing setup_requires - sys.argv = sys.argv[:1] + ['egg_info'] + \ - config_settings["--global-option"] + 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: - with Distribution.patch(): - _run_setup() - except SetupRequirementsError as e: - requirements += e.specifiers - - return requirements + yield + finally: + setuptools._install_setup_requires = orig def _get_immediate_subdirectories(a_dir): @@ -98,75 +98,207 @@ def _get_immediate_subdirectories(a_dir): if os.path.isdir(os.path.join(a_dir, name))] -def get_requires_for_build_wheel(config_settings=None): - config_settings = _fix_config(config_settings) - return _get_build_requires(config_settings) +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 get_requires_for_build_sdist(config_settings=None): - config_settings = _fix_config(config_settings) - return _get_build_requires(config_settings) +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) -def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): - sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', metadata_directory] - _run_setup() - - dist_info_directory = metadata_directory - while True: - dist_infos = [f for f in os.listdir(dist_info_directory) - if f.endswith('.dist-info')] - if len(dist_infos) == 0 and \ - len(_get_immediate_subdirectories(dist_info_directory)) == 1: - dist_info_directory = os.path.join( - dist_info_directory, os.listdir(dist_info_directory)[0]) - continue +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'setup.py install is deprecated') + yield - assert len(dist_infos) == 1 - break - # PEP 517 requires that the .dist-info directory be placed in the - # metadata_directory. To comply, we MUST copy the directory to the root - if dist_info_directory != metadata_directory: - shutil.move( - os.path.join(dist_info_directory, dist_infos[0]), - metadata_directory) - shutil.rmtree(dist_info_directory, ignore_errors=True) +class _BuildMetaBackend: - return dist_infos[0] + @staticmethod + def _fix_config(config_settings): + """ + Ensure config settings meet certain expectations. + + >>> fc = _BuildMetaBackend._fix_config + >>> fc(None) + {'--global-option': []} + >>> fc({}) + {'--global-option': []} + >>> fc({'--global-option': 'foo'}) + {'--global-option': ['foo']} + >>> fc({'--global-option': ['foo']}) + {'--global-option': ['foo']} + """ + config_settings = config_settings or {} + config_settings['--global-option'] = list(always_iterable( + config_settings.get('--global-option'))) + return config_settings + def _get_build_requires(self, config_settings, requirements): + config_settings = self._fix_config(config_settings) -def build_wheel(wheel_directory, config_settings=None, - metadata_directory=None): - config_settings = _fix_config(config_settings) - wheel_directory = os.path.abspath(wheel_directory) - sys.argv = sys.argv[:1] + ['bdist_wheel'] + \ - config_settings["--global-option"] - _run_setup() - if wheel_directory != 'dist': - shutil.rmtree(wheel_directory) - shutil.copytree('dist', wheel_directory) + sys.argv = sys.argv[:1] + ['egg_info'] + \ + config_settings["--global-option"] + 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__ = setup_script + __name__ = '__main__' + + with _open_setup_script(__file__) as f: + code = f.read().replace(r'\r\n', r'\n') + + exec(compile(code, __file__, 'exec'), 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 prepare_metadata_for_build_wheel(self, metadata_directory, + config_settings=None): + sys.argv = sys.argv[:1] + [ + 'dist_info', '--egg-base', metadata_directory] + with no_install_setup_requires(): + self.run_setup() + + dist_info_directory = metadata_directory + while True: + dist_infos = [f for f in os.listdir(dist_info_directory) + if f.endswith('.dist-info')] + + if ( + len(dist_infos) == 0 and + len(_get_immediate_subdirectories(dist_info_directory)) == 1 + ): + + dist_info_directory = os.path.join( + dist_info_directory, os.listdir(dist_info_directory)[0]) + continue + + assert len(dist_infos) == 1 + break + + # PEP 517 requires that the .dist-info directory be placed in the + # metadata_directory. To comply, we MUST copy the directory to the root + if dist_info_directory != metadata_directory: + shutil.move( + os.path.join(dist_info_directory, dist_infos[0]), + metadata_directory) + shutil.rmtree(dist_info_directory, ignore_errors=True) + + return dist_infos[0] + + def _build_with_temp_dir(self, setup_command, result_extension, + result_directory, config_settings): + config_settings = self._fix_config(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(dir=result_directory) as tmp_dist_dir: + sys.argv = (sys.argv[:1] + setup_command + + ['--dist-dir', tmp_dist_dir] + + config_settings["--global-option"]) + 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) + + +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 - wheels = [f for f in os.listdir(wheel_directory) - if f.endswith('.whl')] + 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 - assert len(wheels) == 1 - return wheels[0] +# The primary backend +_BACKEND = _BuildMetaBackend() -def build_sdist(sdist_directory, config_settings=None): - config_settings = _fix_config(config_settings) - sdist_directory = os.path.abspath(sdist_directory) - sys.argv = sys.argv[:1] + ['sdist'] + \ - config_settings["--global-option"] - _run_setup() - if sdist_directory != 'dist': - shutil.rmtree(sdist_directory) - shutil.copytree('dist', sdist_directory) +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 - sdists = [f for f in os.listdir(sdist_directory) - if f.endswith('.tar.gz')] - assert len(sdists) == 1 - return sdists[0] +# The legacy backend +__legacy__ = _BuildMetaLegacyBackend() |