Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PEP 517 Build Backend #1039

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
from .py36compat import Distribution_parse_config_files


_skip_install_eggs = False

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So again I want to emphasize that this is an internal setuptools implementation detail that should not be used outside of setuptools.


class SetupRequirementsError(BaseException):
def __init__(self, specifiers):
self.specifiers = specifiers


def _get_unpatched(cls):
warnings.warn("Do not call this function", DeprecationWarning)
return get_unpatched(cls)
Expand Down Expand Up @@ -315,6 +323,8 @@ def __init__(self, attrs=None):
self.dependency_links = attrs.pop('dependency_links', [])
assert_string_list(self, 'dependency_links', self.dependency_links)
if attrs and 'setup_requires' in attrs:
if _skip_install_eggs:
raise SetupRequirementsError(attrs['setup_requires'])
self.fetch_build_eggs(attrs['setup_requires'])
for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'):
vars(self).setdefault(ep.name, None)
Expand Down
91 changes: 91 additions & 0 deletions setuptools/pep517.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import os
import sys
import subprocess
import tokenize
import shutil
import tempfile

from setuptools import dist
from setuptools.dist import SetupRequirementsError


SETUPTOOLS_IMPLEMENTATION_REVISION = 0.1

def _run_setup(setup_script='setup.py'): #
# Note that we can reuse our build directory between calls
# Correctness comes first, then optimization later
__file__=setup_script
f=getattr(tokenize, 'open', open)(__file__)
code=f.read().replace('\\r\\n', '\\n')
f.close()
exec(compile(code, __file__, 'exec'))


def fix_config(config_settings):
config_settings = config_settings or {}
config_settings.setdefault('--global-option', [])
return config_settings

def get_build_requires(config_settings):
config_settings = fix_config(config_settings)
requirements = ['setuptools', 'wheel']
dist._skip_install_eggs = True

sys.argv = sys.argv[:1] + ['egg_info'] + \
config_settings["--global-option"]
try:
_run_setup()
except SetupRequirementsError as e:
requirements += e.specifiers

dist._skip_install_eggs = False

return requirements


def get_requires_for_build_wheel(config_settings=None):
config_settings = fix_config(config_settings)
return get_build_requires(config_settings)


def get_requires_for_build_sdist(config_settings=None):
config_settings = fix_config(config_settings)
return get_build_requires(config_settings)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two get_requires functions are the public API. They will be used by pip to determine the setup requirements.


def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', metadata_directory]
_run_setup()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will start working once pypa/wheel#190 is merged but there is no need to wait for that.

def build_wheel(wheel_directory, config_settings=None,
metadata_directory=None):
config_settings = fix_config(config_settings)
wheel_directory = os.path.abspath(wheel_directory)
sys.argv = sys.argv[:1] + ['bdist_wheel'] + \
config_settings["--global-option"]
_run_setup()
if wheel_directory != 'dist':
shutil.rmtree(wheel_directory)
shutil.copytree('dist', wheel_directory)

wheels = [f for f in os.listdir(wheel_directory)
if f.endswith('.whl')]

assert len(wheels) == 1
return wheels[0]

def build_sdist(sdist_directory, config_settings=None):
config_settings = fix_config(config_settings)
sdist_directory = os.path.abspath(sdist_directory)
sys.argv = sys.argv[:1] + ['sdist'] + \
config_settings["--global-option"]
_run_setup()
if sdist_directory != 'dist':
shutil.rmtree(sdist_directory)
shutil.copytree('dist', sdist_directory)

sdists = [f for f in os.listdir(sdist_directory)
if f.endswith('.tar.gz')]

assert len(sdists) == 1
return sdists[0]
107 changes: 107 additions & 0 deletions setuptools/tests/test_pep517.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import pytest
import os

# Only test the backend on Python 3
# because we don't want to require
# a concurrent.futures backport for testing
pytest.importorskip('concurrent.futures')

from contextlib import contextmanager
from importlib import import_module
from tempfile import mkdtemp
from concurrent.futures import ProcessPoolExecutor
from .files import build_files
from .textwrap import DALS
from . import contexts


class BuildBackend(object):
"""PEP 517 Build Backend"""
def __init__(self, cwd=None, env={}, backend_name='setuptools.pep517'):
self.cwd = cwd
self.env = env
self.backend_name = backend_name
self.pool = ProcessPoolExecutor()

def __getattr__(self, name):
"""Handles aribrary function invokations on the build backend."""

def method(*args, **kw):
return self.pool.submit(
BuildBackendCaller(os.path.abspath(self.cwd), self.env,
self.backend_name),
(name, args, kw)).result()

return method


class BuildBackendCaller(object):
def __init__(self, cwd, env, backend_name):
self.cwd = cwd
self.env = env
self.backend_name = backend_name

def __call__(self, info):
"""Handles aribrary function invokations on the build backend."""
os.chdir(self.cwd)
os.environ.update(self.env)
name, args, kw = info
return getattr(import_module(self.backend_name), name)(*args, **kw)


@contextmanager
def enter_directory(dir, val=None):
original_dir = os.getcwd()
os.chdir(dir)
yield val
os.chdir(original_dir)


@pytest.fixture
def build_backend():
tmpdir = mkdtemp()
with enter_directory(tmpdir):
setup_script = DALS("""
from setuptools import setup

setup(
name='foo',
py_modules=['hello'],
setup_requires=['six'],
entry_points={'console_scripts': ['hi = hello.run']},
zip_safe=False,
)
""")

build_files({
'setup.py': setup_script,
'hello.py': DALS("""
def run():
print('hello')
""")
})

return enter_directory(tmpdir, BuildBackend(cwd='.'))


def test_get_requires_for_build_wheel(build_backend):
with build_backend as b:
assert list(sorted(b.get_requires_for_build_wheel())) == \
list(sorted(['six', 'setuptools', 'wheel']))

def test_build_wheel(build_backend):
with build_backend as b:
dist_dir = os.path.abspath('pip-wheel')
os.makedirs(dist_dir)
wheel_name = b.build_wheel(dist_dir)

assert os.path.isfile(os.path.join(dist_dir, wheel_name))


def test_build_sdist(build_backend):
with build_backend as b:
dist_dir = os.path.abspath('pip-sdist')
os.makedirs(dist_dir)
sdist_name = b.build_sdist(dist_dir)

assert os.path.isfile(os.path.join(dist_dir, sdist_name))