• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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