Spaces:
Running
Running
"""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 sys | |
import tokenize | |
import shutil | |
import contextlib | |
import setuptools | |
import distutils | |
from setuptools.py31compat import TemporaryDirectory | |
from pkg_resources import parse_requirements | |
from pkg_resources.py31compat import makedirs | |
__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): | |
def __init__(self, specifiers): | |
self.specifiers = specifiers | |
class Distribution(setuptools.dist.Distribution): | |
def fetch_build_eggs(self, specifiers): | |
specifier_list = list(map(str, parse_requirements(specifiers))) | |
raise SetupRequirementsError(specifier_list) | |
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 | |
def _to_str(s): | |
""" | |
Convert a filename to a string (on Python 2, explicitly | |
a byte string, not Unicode) as distutils checks for the | |
exact type str. | |
""" | |
if sys.version_info[0] == 2 and not isinstance(s, str): | |
# Assume it's Unicode, as that's what the PEP says | |
# should be provided. | |
return s.encode(sys.getfilesystemencoding()) | |
return s | |
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) | |
) | |
file, = matching | |
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) | |
class _BuildMetaBackend(object): | |
def _fix_config(self, config_settings): | |
config_settings = config_settings or {} | |
config_settings.setdefault('--global-option', []) | |
return config_settings | |
def _get_build_requires(self, config_settings, requirements): | |
config_settings = self._fix_config(config_settings) | |
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): | |
config_settings = self._fix_config(config_settings) | |
return self._get_build_requires(config_settings, requirements=['wheel']) | |
def get_requires_for_build_sdist(self, config_settings=None): | |
config_settings = self._fix_config(config_settings) | |
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', | |
_to_str(metadata_directory)] | |
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. | |
makedirs(result_directory, exist_ok=True) | |
with TemporaryDirectory(dir=result_directory) as tmp_dist_dir: | |
sys.argv = (sys.argv[:1] + setup_command + | |
['--dist-dir', tmp_dist_dir] + | |
config_settings["--global-option"]) | |
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): | |
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 | |
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 | |
# The legacy backend | |
__legacy__ = _BuildMetaLegacyBackend() | |