1"""A PEP 517 interface to setuptools 2 3Previously, when a user or a command line tool (let's call it a "frontend") 4needed to make a request of setuptools to take a certain action, for 5example, generating a list of installation requirements, the frontend would 6would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line. 7 8PEP 517 defines a different method of interfacing with setuptools. Rather 9than calling "setup.py" directly, the frontend should: 10 11 1. Set the current directory to the directory with a setup.py file 12 2. Import this module into a safe python interpreter (one in which 13 setuptools can potentially set global variables or crash hard). 14 3. Call one of the functions defined in PEP 517. 15 16What each function does is defined in PEP 517. However, here is a "casual" 17definition of the functions (this definition should not be relied on for 18bug reports or API stability): 19 20 - `build_wheel`: build a wheel in the folder and return the basename 21 - `get_requires_for_build_wheel`: get the `setup_requires` to build 22 - `prepare_metadata_for_build_wheel`: get the `install_requires` 23 - `build_sdist`: build an sdist in the folder and return the basename 24 - `get_requires_for_build_sdist`: get the `setup_requires` to build 25 26Again, this is not a formal definition! Just a "taste" of the module. 27""" 28 29import os 30import sys 31import tokenize 32import shutil 33import contextlib 34 35import setuptools 36import distutils 37 38 39class SetupRequirementsError(BaseException): 40 def __init__(self, specifiers): 41 self.specifiers = specifiers 42 43 44class Distribution(setuptools.dist.Distribution): 45 def fetch_build_eggs(self, specifiers): 46 raise SetupRequirementsError(specifiers) 47 48 @classmethod 49 @contextlib.contextmanager 50 def patch(cls): 51 """ 52 Replace 53 distutils.dist.Distribution with this class 54 for the duration of this context. 55 """ 56 orig = distutils.core.Distribution 57 distutils.core.Distribution = cls 58 try: 59 yield 60 finally: 61 distutils.core.Distribution = orig 62 63 64def _run_setup(setup_script='setup.py'): 65 # Note that we can reuse our build directory between calls 66 # Correctness comes first, then optimization later 67 __file__ = setup_script 68 __name__ = '__main__' 69 f = getattr(tokenize, 'open', open)(__file__) 70 code = f.read().replace('\\r\\n', '\\n') 71 f.close() 72 exec(compile(code, __file__, 'exec'), locals()) 73 74 75def _fix_config(config_settings): 76 config_settings = config_settings or {} 77 config_settings.setdefault('--global-option', []) 78 return config_settings 79 80 81def _get_build_requires(config_settings): 82 config_settings = _fix_config(config_settings) 83 requirements = ['setuptools', 'wheel'] 84 85 sys.argv = sys.argv[:1] + ['egg_info'] + \ 86 config_settings["--global-option"] 87 try: 88 with Distribution.patch(): 89 _run_setup() 90 except SetupRequirementsError as e: 91 requirements += e.specifiers 92 93 return requirements 94 95 96def _get_immediate_subdirectories(a_dir): 97 return [name for name in os.listdir(a_dir) 98 if os.path.isdir(os.path.join(a_dir, name))] 99 100 101def get_requires_for_build_wheel(config_settings=None): 102 config_settings = _fix_config(config_settings) 103 return _get_build_requires(config_settings) 104 105 106def get_requires_for_build_sdist(config_settings=None): 107 config_settings = _fix_config(config_settings) 108 return _get_build_requires(config_settings) 109 110 111def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): 112 sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', metadata_directory] 113 _run_setup() 114 115 dist_info_directory = metadata_directory 116 while True: 117 dist_infos = [f for f in os.listdir(dist_info_directory) 118 if f.endswith('.dist-info')] 119 120 if len(dist_infos) == 0 and \ 121 len(_get_immediate_subdirectories(dist_info_directory)) == 1: 122 dist_info_directory = os.path.join( 123 dist_info_directory, os.listdir(dist_info_directory)[0]) 124 continue 125 126 assert len(dist_infos) == 1 127 break 128 129 # PEP 517 requires that the .dist-info directory be placed in the 130 # metadata_directory. To comply, we MUST copy the directory to the root 131 if dist_info_directory != metadata_directory: 132 shutil.move( 133 os.path.join(dist_info_directory, dist_infos[0]), 134 metadata_directory) 135 shutil.rmtree(dist_info_directory, ignore_errors=True) 136 137 return dist_infos[0] 138 139 140def build_wheel(wheel_directory, config_settings=None, 141 metadata_directory=None): 142 config_settings = _fix_config(config_settings) 143 wheel_directory = os.path.abspath(wheel_directory) 144 sys.argv = sys.argv[:1] + ['bdist_wheel'] + \ 145 config_settings["--global-option"] 146 _run_setup() 147 if wheel_directory != 'dist': 148 shutil.rmtree(wheel_directory) 149 shutil.copytree('dist', wheel_directory) 150 151 wheels = [f for f in os.listdir(wheel_directory) 152 if f.endswith('.whl')] 153 154 assert len(wheels) == 1 155 return wheels[0] 156 157 158def build_sdist(sdist_directory, config_settings=None): 159 config_settings = _fix_config(config_settings) 160 sdist_directory = os.path.abspath(sdist_directory) 161 sys.argv = sys.argv[:1] + ['sdist'] + \ 162 config_settings["--global-option"] 163 _run_setup() 164 if sdist_directory != 'dist': 165 shutil.rmtree(sdist_directory) 166 shutil.copytree('dist', sdist_directory) 167 168 sdists = [f for f in os.listdir(sdist_directory) 169 if f.endswith('.tar.gz')] 170 171 assert len(sdists) == 1 172 return sdists[0] 173