Skip to content

Commit 749c3fe

Browse files
committed
Merge pull request #1143 from xoviat pep517.
2 parents ba46991 + 8c385a1 commit 749c3fe

File tree

5 files changed

+285
-1
lines changed

5 files changed

+285
-1
lines changed

CHANGES.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
v36.6.0
2+
-------
3+
4+
* #1143: Added ``setuptools.build_meta`` module, an implementation
5+
of PEP-517 for Setuptools-defined packages.
6+
17
v36.5.0
28
-------
39

setuptools/build_meta.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"""A PEP 517 interface to setuptools
2+
3+
Previously, when a user or a command line tool (let's call it a "frontend")
4+
needed to make a request of setuptools to take a certain action, for
5+
example, generating a list of installation requirements, the frontend would
6+
would call "setup.py egg_info" or "setup.py bdist_wheel" on the command line.
7+
8+
PEP 517 defines a different method of interfacing with setuptools. Rather
9+
than 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+
16+
What each function does is defined in PEP 517. However, here is a "casual"
17+
definition of the functions (this definition should not be relied on for
18+
bug 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+
26+
Again, this is not a formal definition! Just a "taste" of the module.
27+
"""
28+
29+
import os
30+
import sys
31+
import tokenize
32+
import shutil
33+
import contextlib
34+
35+
import setuptools
36+
import distutils
37+
38+
39+
class SetupRequirementsError(BaseException):
40+
def __init__(self, specifiers):
41+
self.specifiers = specifiers
42+
43+
44+
class 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+
64+
def _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+
f = getattr(tokenize, 'open', open)(__file__)
69+
code = f.read().replace('\\r\\n', '\\n')
70+
f.close()
71+
exec(compile(code, __file__, 'exec'))
72+
73+
74+
def _fix_config(config_settings):
75+
config_settings = config_settings or {}
76+
config_settings.setdefault('--global-option', [])
77+
return config_settings
78+
79+
80+
def _get_build_requires(config_settings):
81+
config_settings = _fix_config(config_settings)
82+
requirements = ['setuptools', 'wheel']
83+
84+
sys.argv = sys.argv[:1] + ['egg_info'] + \
85+
config_settings["--global-option"]
86+
try:
87+
with Distribution.patch():
88+
_run_setup()
89+
except SetupRequirementsError as e:
90+
requirements += e.specifiers
91+
92+
return requirements
93+
94+
95+
def get_requires_for_build_wheel(config_settings=None):
96+
config_settings = _fix_config(config_settings)
97+
return _get_build_requires(config_settings)
98+
99+
100+
def get_requires_for_build_sdist(config_settings=None):
101+
config_settings = _fix_config(config_settings)
102+
return _get_build_requires(config_settings)
103+
104+
105+
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
106+
sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', metadata_directory]
107+
_run_setup()
108+
109+
dist_infos = [f for f in os.listdir(metadata_directory)
110+
if f.endswith('.dist-info')]
111+
112+
assert len(dist_infos) == 1
113+
return dist_infos[0]
114+
115+
116+
def build_wheel(wheel_directory, config_settings=None,
117+
metadata_directory=None):
118+
config_settings = _fix_config(config_settings)
119+
wheel_directory = os.path.abspath(wheel_directory)
120+
sys.argv = sys.argv[:1] + ['bdist_wheel'] + \
121+
config_settings["--global-option"]
122+
_run_setup()
123+
if wheel_directory != 'dist':
124+
shutil.rmtree(wheel_directory)
125+
shutil.copytree('dist', wheel_directory)
126+
127+
wheels = [f for f in os.listdir(wheel_directory)
128+
if f.endswith('.whl')]
129+
130+
assert len(wheels) == 1
131+
return wheels[0]
132+
133+
134+
def build_sdist(sdist_directory, config_settings=None):
135+
config_settings = _fix_config(config_settings)
136+
sdist_directory = os.path.abspath(sdist_directory)
137+
sys.argv = sys.argv[:1] + ['sdist'] + \
138+
config_settings["--global-option"]
139+
_run_setup()
140+
if sdist_directory != 'dist':
141+
shutil.rmtree(sdist_directory)
142+
shutil.copytree('dist', sdist_directory)
143+
144+
sdists = [f for f in os.listdir(sdist_directory)
145+
if f.endswith('.tar.gz')]
146+
147+
assert len(sdists) == 1
148+
return sdists[0]

setuptools/command/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop',
33
'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts',
44
'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts',
5-
'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib',
5+
'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib', 'dist_info',
66
]
77

88
from distutils.command.bdist import bdist

setuptools/command/dist_info.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Create a dist_info directory
3+
As defined in the wheel specification
4+
"""
5+
6+
import os
7+
import shutil
8+
9+
from distutils.core import Command
10+
11+
12+
class dist_info(Command):
13+
14+
description = 'create a .dist-info directory'
15+
16+
user_options = [
17+
('egg-base=', 'e', "directory containing .egg-info directories"
18+
" (default: top of the source tree)"),
19+
]
20+
21+
def initialize_options(self):
22+
self.egg_base = None
23+
24+
def finalize_options(self):
25+
pass
26+
27+
def run(self):
28+
egg_info = self.get_finalized_command('egg_info')
29+
egg_info.run()
30+
dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info'
31+
32+
bdist_wheel = self.get_finalized_command('bdist_wheel')
33+
bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir)
34+
35+
if self.egg_base:
36+
shutil.move(dist_info_dir, os.path.join(
37+
self.egg_base, dist_info_dir))
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import os
2+
3+
import pytest
4+
5+
from .files import build_files
6+
from .textwrap import DALS
7+
8+
9+
futures = pytest.importorskip('concurrent.futures')
10+
importlib = pytest.importorskip('importlib')
11+
12+
13+
class BuildBackendBase(object):
14+
def __init__(self, cwd=None, env={}, backend_name='setuptools.build_meta'):
15+
self.cwd = cwd
16+
self.env = env
17+
self.backend_name = backend_name
18+
19+
20+
class BuildBackend(BuildBackendBase):
21+
"""PEP 517 Build Backend"""
22+
def __init__(self, *args, **kwargs):
23+
super(BuildBackend, self).__init__(*args, **kwargs)
24+
self.pool = futures.ProcessPoolExecutor()
25+
26+
def __getattr__(self, name):
27+
"""Handles aribrary function invocations on the build backend."""
28+
def method(*args, **kw):
29+
root = os.path.abspath(self.cwd)
30+
caller = BuildBackendCaller(root, self.env, self.backend_name)
31+
return self.pool.submit(caller, name, *args, **kw).result()
32+
33+
return method
34+
35+
36+
class BuildBackendCaller(BuildBackendBase):
37+
def __call__(self, name, *args, **kw):
38+
"""Handles aribrary function invocations on the build backend."""
39+
os.chdir(self.cwd)
40+
os.environ.update(self.env)
41+
mod = importlib.import_module(self.backend_name)
42+
return getattr(mod, name)(*args, **kw)
43+
44+
45+
@pytest.fixture
46+
def build_backend(tmpdir):
47+
defn = {
48+
'setup.py': DALS("""
49+
__import__('setuptools').setup(
50+
name='foo',
51+
py_modules=['hello'],
52+
setup_requires=['six'],
53+
)
54+
"""),
55+
'hello.py': DALS("""
56+
def run():
57+
print('hello')
58+
"""),
59+
}
60+
build_files(defn, prefix=str(tmpdir))
61+
with tmpdir.as_cwd():
62+
yield BuildBackend(cwd='.')
63+
64+
65+
def test_get_requires_for_build_wheel(build_backend):
66+
actual = build_backend.get_requires_for_build_wheel()
67+
expected = ['six', 'setuptools', 'wheel']
68+
assert sorted(actual) == sorted(expected)
69+
70+
71+
def test_build_wheel(build_backend):
72+
dist_dir = os.path.abspath('pip-wheel')
73+
os.makedirs(dist_dir)
74+
wheel_name = build_backend.build_wheel(dist_dir)
75+
76+
assert os.path.isfile(os.path.join(dist_dir, wheel_name))
77+
78+
79+
def test_build_sdist(build_backend):
80+
dist_dir = os.path.abspath('pip-sdist')
81+
os.makedirs(dist_dir)
82+
sdist_name = build_backend.build_sdist(dist_dir)
83+
84+
assert os.path.isfile(os.path.join(dist_dir, sdist_name))
85+
86+
87+
def test_prepare_metadata_for_build_wheel(build_backend):
88+
dist_dir = os.path.abspath('pip-dist-info')
89+
os.makedirs(dist_dir)
90+
91+
dist_info = build_backend.prepare_metadata_for_build_wheel(dist_dir)
92+
93+
assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA'))

0 commit comments

Comments
 (0)