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 io 30import os 31import sys 32import tokenize 33import shutil 34import contextlib 35import tempfile 36import warnings 37 38import setuptools 39import distutils 40from ._reqs import parse_strings 41from .extern.more_itertools import always_iterable 42 43 44__all__ = ['get_requires_for_build_sdist', 45 'get_requires_for_build_wheel', 46 'prepare_metadata_for_build_wheel', 47 'build_wheel', 48 'build_sdist', 49 '__legacy__', 50 'SetupRequirementsError'] 51 52 53class SetupRequirementsError(BaseException): 54 def __init__(self, specifiers): 55 self.specifiers = specifiers 56 57 58class Distribution(setuptools.dist.Distribution): 59 def fetch_build_eggs(self, specifiers): 60 specifier_list = list(parse_strings(specifiers)) 61 62 raise SetupRequirementsError(specifier_list) 63 64 @classmethod 65 @contextlib.contextmanager 66 def patch(cls): 67 """ 68 Replace 69 distutils.dist.Distribution with this class 70 for the duration of this context. 71 """ 72 orig = distutils.core.Distribution 73 distutils.core.Distribution = cls 74 try: 75 yield 76 finally: 77 distutils.core.Distribution = orig 78 79 80@contextlib.contextmanager 81def no_install_setup_requires(): 82 """Temporarily disable installing setup_requires 83 84 Under PEP 517, the backend reports build dependencies to the frontend, 85 and the frontend is responsible for ensuring they're installed. 86 So setuptools (acting as a backend) should not try to install them. 87 """ 88 orig = setuptools._install_setup_requires 89 setuptools._install_setup_requires = lambda attrs: None 90 try: 91 yield 92 finally: 93 setuptools._install_setup_requires = orig 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 _file_with_extension(directory, extension): 102 matching = ( 103 f for f in os.listdir(directory) 104 if f.endswith(extension) 105 ) 106 try: 107 file, = matching 108 except ValueError: 109 raise ValueError( 110 'No distribution was found. Ensure that `setup.py` ' 111 'is not empty and that it calls `setup()`.') 112 return file 113 114 115def _open_setup_script(setup_script): 116 if not os.path.exists(setup_script): 117 # Supply a default setup.py 118 return io.StringIO(u"from setuptools import setup; setup()") 119 120 return getattr(tokenize, 'open', open)(setup_script) 121 122 123@contextlib.contextmanager 124def suppress_known_deprecation(): 125 with warnings.catch_warnings(): 126 warnings.filterwarnings('ignore', 'setup.py install is deprecated') 127 yield 128 129 130class _BuildMetaBackend: 131 132 @staticmethod 133 def _fix_config(config_settings): 134 """ 135 Ensure config settings meet certain expectations. 136 137 >>> fc = _BuildMetaBackend._fix_config 138 >>> fc(None) 139 {'--global-option': []} 140 >>> fc({}) 141 {'--global-option': []} 142 >>> fc({'--global-option': 'foo'}) 143 {'--global-option': ['foo']} 144 >>> fc({'--global-option': ['foo']}) 145 {'--global-option': ['foo']} 146 """ 147 config_settings = config_settings or {} 148 config_settings['--global-option'] = list(always_iterable( 149 config_settings.get('--global-option'))) 150 return config_settings 151 152 def _get_build_requires(self, config_settings, requirements): 153 config_settings = self._fix_config(config_settings) 154 155 sys.argv = sys.argv[:1] + ['egg_info'] + \ 156 config_settings["--global-option"] 157 try: 158 with Distribution.patch(): 159 self.run_setup() 160 except SetupRequirementsError as e: 161 requirements += e.specifiers 162 163 return requirements 164 165 def run_setup(self, setup_script='setup.py'): 166 # Note that we can reuse our build directory between calls 167 # Correctness comes first, then optimization later 168 __file__ = setup_script 169 __name__ = '__main__' 170 171 with _open_setup_script(__file__) as f: 172 code = f.read().replace(r'\r\n', r'\n') 173 174 exec(compile(code, __file__, 'exec'), locals()) 175 176 def get_requires_for_build_wheel(self, config_settings=None): 177 return self._get_build_requires( 178 config_settings, requirements=['wheel']) 179 180 def get_requires_for_build_sdist(self, config_settings=None): 181 return self._get_build_requires(config_settings, requirements=[]) 182 183 def prepare_metadata_for_build_wheel(self, metadata_directory, 184 config_settings=None): 185 sys.argv = sys.argv[:1] + [ 186 'dist_info', '--egg-base', metadata_directory] 187 with no_install_setup_requires(): 188 self.run_setup() 189 190 dist_info_directory = metadata_directory 191 while True: 192 dist_infos = [f for f in os.listdir(dist_info_directory) 193 if f.endswith('.dist-info')] 194 195 if ( 196 len(dist_infos) == 0 and 197 len(_get_immediate_subdirectories(dist_info_directory)) == 1 198 ): 199 200 dist_info_directory = os.path.join( 201 dist_info_directory, os.listdir(dist_info_directory)[0]) 202 continue 203 204 assert len(dist_infos) == 1 205 break 206 207 # PEP 517 requires that the .dist-info directory be placed in the 208 # metadata_directory. To comply, we MUST copy the directory to the root 209 if dist_info_directory != metadata_directory: 210 shutil.move( 211 os.path.join(dist_info_directory, dist_infos[0]), 212 metadata_directory) 213 shutil.rmtree(dist_info_directory, ignore_errors=True) 214 215 return dist_infos[0] 216 217 def _build_with_temp_dir(self, setup_command, result_extension, 218 result_directory, config_settings): 219 config_settings = self._fix_config(config_settings) 220 result_directory = os.path.abspath(result_directory) 221 222 # Build in a temporary directory, then copy to the target. 223 os.makedirs(result_directory, exist_ok=True) 224 with tempfile.TemporaryDirectory(dir=result_directory) as tmp_dist_dir: 225 sys.argv = (sys.argv[:1] + setup_command + 226 ['--dist-dir', tmp_dist_dir] + 227 config_settings["--global-option"]) 228 with no_install_setup_requires(): 229 self.run_setup() 230 231 result_basename = _file_with_extension( 232 tmp_dist_dir, result_extension) 233 result_path = os.path.join(result_directory, result_basename) 234 if os.path.exists(result_path): 235 # os.rename will fail overwriting on non-Unix. 236 os.remove(result_path) 237 os.rename(os.path.join(tmp_dist_dir, result_basename), result_path) 238 239 return result_basename 240 241 def build_wheel(self, wheel_directory, config_settings=None, 242 metadata_directory=None): 243 with suppress_known_deprecation(): 244 return self._build_with_temp_dir(['bdist_wheel'], '.whl', 245 wheel_directory, config_settings) 246 247 def build_sdist(self, sdist_directory, config_settings=None): 248 return self._build_with_temp_dir(['sdist', '--formats', 'gztar'], 249 '.tar.gz', sdist_directory, 250 config_settings) 251 252 253class _BuildMetaLegacyBackend(_BuildMetaBackend): 254 """Compatibility backend for setuptools 255 256 This is a version of setuptools.build_meta that endeavors 257 to maintain backwards 258 compatibility with pre-PEP 517 modes of invocation. It 259 exists as a temporary 260 bridge between the old packaging mechanism and the new 261 packaging mechanism, 262 and will eventually be removed. 263 """ 264 def run_setup(self, setup_script='setup.py'): 265 # In order to maintain compatibility with scripts assuming that 266 # the setup.py script is in a directory on the PYTHONPATH, inject 267 # '' into sys.path. (pypa/setuptools#1642) 268 sys_path = list(sys.path) # Save the original path 269 270 script_dir = os.path.dirname(os.path.abspath(setup_script)) 271 if script_dir not in sys.path: 272 sys.path.insert(0, script_dir) 273 274 # Some setup.py scripts (e.g. in pygame and numpy) use sys.argv[0] to 275 # get the directory of the source code. They expect it to refer to the 276 # setup.py script. 277 sys_argv_0 = sys.argv[0] 278 sys.argv[0] = setup_script 279 280 try: 281 super(_BuildMetaLegacyBackend, 282 self).run_setup(setup_script=setup_script) 283 finally: 284 # While PEP 517 frontends should be calling each hook in a fresh 285 # subprocess according to the standard (and thus it should not be 286 # strictly necessary to restore the old sys.path), we'll restore 287 # the original path so that the path manipulation does not persist 288 # within the hook after run_setup is called. 289 sys.path[:] = sys_path 290 sys.argv[0] = sys_argv_0 291 292 293# The primary backend 294_BACKEND = _BuildMetaBackend() 295 296get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel 297get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist 298prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel 299build_wheel = _BACKEND.build_wheel 300build_sdist = _BACKEND.build_sdist 301 302 303# The legacy backend 304__legacy__ = _BuildMetaLegacyBackend() 305