diff --git a/.travis.yml b/.travis.yml index b91c18a..712a531 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,6 @@ jobs: cache: pip install: -- pip install -U virtualenv -- pip install tox +- pip install tox tox-venv script: tox diff --git a/README.rst b/README.rst index 22fd1dc..2900943 100644 --- a/README.rst +++ b/README.rst @@ -8,21 +8,38 @@ provides: the current process. - Fallbacks for the optional hooks, so that frontends can call the hooks without checking which are defined. -- Functions to load the build system table from ``pyproject.toml``, with - optional fallback to setuptools. +- Higher-level functions which install the build dependencies into a + temporary environment and build a wheel/sdist using them. Run the tests with ``pytest`` or `tox `_. -Usage: +High level usage, with build requirements handled: .. code-block:: python import os - from pep517 import Pep517HookCaller - from pep517.pyproject import load_system + from pep517.envbuild import build_wheel, build_sdist src = 'path/to/source' # Folder containing 'pyproject.toml' - build_sys = load_system(src) + destination = 'also/a/folder' + whl_filename = build_wheel(src, destination) + assert os.path.isfile(os.path.join(destination, whl_filename)) + + targz_filename = build_sdist(src, destination) + assert os.path.isfile(os.path.join(destination, targz_filename)) + +Lower level usage—you are responsible for ensuring build requirements are +available: + +.. code-block:: python + + import os + import toml + from pep517.wrappers import Pep517HookCaller + + src = 'path/to/source' # Folder containing 'pyproject.toml' + with open(os.path.join(src, 'pyproject.toml')) as f: + build_sys = toml.load(f)['build-system'] print(build_sys['requires']) # List of static requirements @@ -40,9 +57,18 @@ Usage: whl_filename = hooks.build_wheel(destination, config_options) assert os.path.isfile(os.path.join(destination, whl_filename)) -The caller is responsible for installing build dependencies. -The static requirements should be installed before trying to call any hooks. +To test the build backend for a project, run in a system shell: + +.. code-block:: shell + + python3 -m pep517.check path/to/source # source dir containing pyproject.toml + +To build a backend into source and/or binary distributions, run in a shell: + +.. code-block:: shell + + python -m pep517.build path/to/source # source dir containing pyproject.toml -The ``buildtool_demo`` package in this repository gives a more complete -example of how to use the hooks. This is an example, and doesn't get installed -with the ``pep517`` package. +This 'build' module should be considered experimental while the PyPA `decides +on the best place for this functionality +`_. diff --git a/examples/README.rst b/examples/README.rst deleted file mode 100644 index 3b24a48..0000000 --- a/examples/README.rst +++ /dev/null @@ -1,5 +0,0 @@ -Examples of using the ``pep517`` library. - -* ``buildtool`` is about the simplest possible PEP 517 frontend, which can use - PEP 517 backends to build packages. It installs build dependencies into a - temporary but non-isolated environment using pip. diff --git a/examples/buildtool/__init__.py b/examples/buildtool/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/buildtool/tests/__init__.py b/examples/buildtool/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/examples/buildtool/tests/samples/buildsys_pkgs/buildsys.py b/examples/buildtool/tests/samples/buildsys_pkgs/buildsys.py deleted file mode 100644 index e1750c1..0000000 --- a/examples/buildtool/tests/samples/buildsys_pkgs/buildsys.py +++ /dev/null @@ -1,62 +0,0 @@ -"""This is a very stupid backend for testing purposes. - -Don't use this for any real code. -""" - -from glob import glob -from os.path import join as pjoin -import shutil -import tarfile -from zipfile import ZipFile - - -def get_requires_for_build_wheel(config_settings): - return ['wheelwright'] - - -def prepare_metadata_for_build_wheel(metadata_directory, config_settings): - for distinfo in glob('*.dist-info'): - shutil.copytree(distinfo, pjoin(metadata_directory, distinfo)) - - -def prepare_build_wheel_files(build_directory, config_settings): - shutil.copy('pyproject.toml', build_directory) - for pyfile in glob('*.py'): - shutil.copy(pyfile, build_directory) - for distinfo in glob('*.dist-info'): - shutil.copytree(distinfo, pjoin(build_directory, distinfo)) - - -def build_wheel(wheel_directory, config_settings, metadata_directory=None): - whl_file = 'pkg1-0.5-py2.py3-none-any.whl' - with ZipFile(pjoin(wheel_directory, whl_file), 'w') as zf: - for pyfile in glob('*.py'): - zf.write(pyfile) - for metadata in glob('*.dist-info/*'): - zf.write(metadata) - return whl_file - - -def get_requires_for_build_sdist(config_settings): - return ['frog'] - - -class UnsupportedOperation(Exception): - pass - - -def build_sdist(sdist_directory, config_settings): - if config_settings.get('test_unsupported', False): - raise UnsupportedOperation - - target = 'pkg1-0.5.tar.gz' - with tarfile.open(pjoin(sdist_directory, target), 'w:gz', - format=tarfile.PAX_FORMAT) as tf: - def _add(relpath): - tf.add(relpath, arcname='pkg1-0.5/' + relpath) - - _add('pyproject.toml') - for pyfile in glob('*.py'): - _add(pyfile) - - return target diff --git a/examples/buildtool/tests/samples/buildsys_pkgs/buildsys_minimal.py b/examples/buildtool/tests/samples/buildsys_pkgs/buildsys_minimal.py deleted file mode 100644 index 6a286b1..0000000 --- a/examples/buildtool/tests/samples/buildsys_pkgs/buildsys_minimal.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Test backend defining only the mandatory hooks. - -Don't use this for any real code. -""" -from glob import glob -from os.path import join as pjoin -import tarfile -from zipfile import ZipFile - - -def build_wheel(wheel_directory, config_settings, metadata_directory=None): - whl_file = 'pkg2-0.5-py2.py3-none-any.whl' - with ZipFile(pjoin(wheel_directory, whl_file), 'w') as zf: - for pyfile in glob('*.py'): - zf.write(pyfile) - for metadata in glob('*.dist-info/*'): - zf.write(metadata) - return whl_file - - -def build_sdist(sdist_directory, config_settings): - target = 'pkg2-0.5.tar.gz' - with tarfile.open(pjoin(sdist_directory, target), 'w:gz', - format=tarfile.PAX_FORMAT) as tf: - def _add(relpath): - tf.add(relpath, arcname='pkg2-0.5/' + relpath) - - _add('pyproject.toml') - for pyfile in glob('*.py'): - _add(pyfile) - for distinfo in glob('*.dist-info'): - _add(distinfo) - - return target diff --git a/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/LICENSE b/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/LICENSE deleted file mode 100644 index b0ae9db..0000000 --- a/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 Thomas Kluyver - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/METADATA b/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/METADATA deleted file mode 100644 index 7bf2758..0000000 --- a/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/METADATA +++ /dev/null @@ -1,9 +0,0 @@ -Metadata-Version: 1.2 -Name: pkg1 -Version: 0.5 -Summary: Sample package for tests -Home-page: https://github.com/takluyver/pep517 -License: UNKNOWN -Author: Thomas Kluyver -Author-email: thomas@kluyver.me.uk -Classifier: License :: OSI Approved :: MIT License diff --git a/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/RECORD b/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/RECORD deleted file mode 100644 index 74e8cbf..0000000 --- a/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/RECORD +++ /dev/null @@ -1,5 +0,0 @@ -pkg1.py,sha256=ZawKBtrxtdGEheOCWvwzGZsO8Q1OSzEzecGNsRz-ekc,52 -pkg1-0.5.dist-info/LICENSE,sha256=GyKwSbUmfW38I6Z79KhNjsBLn9-xpR02DkK0NCyLQVQ,1081 -pkg1-0.5.dist-info/WHEEL,sha256=jxKvNaDKHDacpaLi69-vnLKkBSynwBzmMS82pipt1T0,100 -pkg1-0.5.dist-info/METADATA,sha256=GDliGDwDPM11hoO79KhjyJuFgcm-TOj30gewsPNjkHw,251 -pkg1-0.5.dist-info/RECORD,, diff --git a/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/WHEEL b/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/WHEEL deleted file mode 100644 index 1612133..0000000 --- a/examples/buildtool/tests/samples/pkg1/pkg1-0.5.dist-info/WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: buildsys 0.1 -Root-Is-Purelib: true -Tag: py2-none-any -Tag: py3-none-any diff --git a/examples/buildtool/tests/samples/pkg1/pkg1.py b/examples/buildtool/tests/samples/pkg1/pkg1.py deleted file mode 100644 index b76d0a9..0000000 --- a/examples/buildtool/tests/samples/pkg1/pkg1.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Sample package for tests""" - -__version__ = '0.5' diff --git a/examples/buildtool/tests/samples/pkg1/pyproject.toml b/examples/buildtool/tests/samples/pkg1/pyproject.toml deleted file mode 100644 index 95ff87f..0000000 --- a/examples/buildtool/tests/samples/pkg1/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build-system] -requires = ["eg_buildsys"] -build-backend = "buildsys" diff --git a/pep517/__init__.py b/pep517/__init__.py index 677afd6..7355b68 100644 --- a/pep517/__init__.py +++ b/pep517/__init__.py @@ -2,5 +2,3 @@ """ __version__ = '0.8.2' - -from .wrappers import * # noqa: F401,F403 diff --git a/examples/buildtool/build.py b/pep517/build.py similarity index 62% rename from examples/buildtool/build.py rename to pep517/build.py index f24a332..7618c78 100644 --- a/examples/buildtool/build.py +++ b/pep517/build.py @@ -3,16 +3,58 @@ import argparse import logging import os +import toml import shutil from .envbuild import BuildEnvironment -from pep517 import Pep517HookCaller -from pep517.pyproject import load_system, validate_system +from .wrappers import Pep517HookCaller from .dirtools import tempdir, mkdir_p +from .compat import FileNotFoundError log = logging.getLogger(__name__) +def validate_system(system): + """ + Ensure build system has the requisite fields. + """ + required = {'requires', 'build-backend'} + if not (required <= set(system)): + message = "Missing required fields: {missing}".format( + missing=required-set(system), + ) + raise ValueError(message) + + +def load_system(source_dir): + """ + Load the build system from a source dir (pyproject.toml). + """ + pyproject = os.path.join(source_dir, 'pyproject.toml') + with open(pyproject) as f: + pyproject_data = toml.load(f) + return pyproject_data['build-system'] + + +def compat_system(source_dir): + """ + Given a source dir, attempt to get a build system backend + and requirements from pyproject.toml. Fallback to + setuptools but only if the file was not found or a build + system was not indicated. + """ + try: + system = load_system(source_dir) + except (FileNotFoundError, KeyError): + system = {} + system.setdefault( + 'build-backend', + 'setuptools.build_meta:__legacy__', + ) + system.setdefault('requires', ['setuptools', 'wheel']) + return system + + def _do_build(hooks, env, dist, dest): get_requires_name = 'get_requires_for_build_{dist}'.format(**locals()) get_requires = getattr(hooks, get_requires_name) diff --git a/examples/buildtool/check.py b/pep517/check.py similarity index 99% rename from examples/buildtool/check.py rename to pep517/check.py index 14ff988..9e0c068 100644 --- a/examples/buildtool/check.py +++ b/pep517/check.py @@ -14,7 +14,7 @@ from .colorlog import enable_colourful_output from .envbuild import BuildEnvironment -from pep517 import Pep517HookCaller +from .wrappers import Pep517HookCaller log = logging.getLogger(__name__) diff --git a/examples/buildtool/colorlog.py b/pep517/colorlog.py similarity index 100% rename from examples/buildtool/colorlog.py rename to pep517/colorlog.py diff --git a/examples/buildtool/dirtools.py b/pep517/dirtools.py similarity index 100% rename from examples/buildtool/dirtools.py rename to pep517/dirtools.py diff --git a/examples/buildtool/envbuild.py b/pep517/envbuild.py similarity index 87% rename from examples/buildtool/envbuild.py rename to pep517/envbuild.py index 4dd1536..cacd2b1 100644 --- a/examples/buildtool/envbuild.py +++ b/pep517/envbuild.py @@ -9,9 +9,8 @@ import sys from sysconfig import get_paths from tempfile import mkdtemp -import threading -from pep517 import Pep517HookCaller +from .wrappers import Pep517HookCaller, LoggerWrapper log = logging.getLogger(__name__) @@ -27,40 +26,6 @@ def _load_pyproject(source_dir): ) -class LoggerWrapper(threading.Thread): - """ - Read messages from a pipe and redirect them - to a logger (see python's logging module). - """ - - def __init__(self, logger, level): - threading.Thread.__init__(self) - self.daemon = True - - self.logger = logger - self.level = level - - # create the pipe and reader - self.fd_read, self.fd_write = os.pipe() - self.reader = os.fdopen(self.fd_read) - - self.start() - - def fileno(self): - return self.fd_write - - @staticmethod - def remove_newline(msg): - return msg[:-1] if msg.endswith(os.linesep) else msg - - def run(self): - for line in self.reader: - self._write(self.remove_newline(line)) - - def _write(self, message): - self.logger.log(self.level, message) - - class BuildEnvironment(object): """Context manager to install build deps in a simple temporary environment diff --git a/examples/buildtool/meta.py b/pep517/meta.py similarity index 94% rename from examples/buildtool/meta.py rename to pep517/meta.py index 1704c42..d525de5 100644 --- a/examples/buildtool/meta.py +++ b/pep517/meta.py @@ -17,9 +17,9 @@ from zipp import Path from .envbuild import BuildEnvironment -from pep517 import Pep517HookCaller, quiet_subprocess_runner -from pep517.pyproject import validate_system, load_system, compat_system +from .wrappers import Pep517HookCaller, quiet_subprocess_runner from .dirtools import tempdir, mkdir_p, dir_to_zipfile +from .build import validate_system, load_system, compat_system log = logging.getLogger(__name__) diff --git a/pep517/pyproject.py b/pep517/pyproject.py deleted file mode 100644 index 9015a5e..0000000 --- a/pep517/pyproject.py +++ /dev/null @@ -1,52 +0,0 @@ -import os - -import toml - -from .compat import FileNotFoundError - -__all__ = [ - 'validate_system', - 'load_system', - 'compat_system', -] - - -def validate_system(system): - """ - Ensure build system has the requisite fields. - """ - required = {'requires', 'build-backend'} - if not (required <= set(system)): - message = "Missing required fields: {missing}".format( - missing=required-set(system), - ) - raise ValueError(message) - - -def load_system(source_dir): - """ - Load the build system from a source dir (pyproject.toml). - """ - pyproject = os.path.join(source_dir, 'pyproject.toml') - with open(pyproject) as f: - pyproject_data = toml.load(f) - return pyproject_data['build-system'] - - -def compat_system(source_dir): - """ - Given a source dir, attempt to get a build system backend - and requirements from pyproject.toml. Fallback to - setuptools but only if the file was not found or a build - system was not indicated. - """ - try: - system = load_system(source_dir) - except (FileNotFoundError, KeyError): - system = {} - system.setdefault( - 'build-backend', - 'setuptools.build_meta:__legacy__', - ) - system.setdefault('requires', ['setuptools', 'wheel']) - return system diff --git a/pep517/wrappers.py b/pep517/wrappers.py index 85f574b..00a3d1a 100644 --- a/pep517/wrappers.py +++ b/pep517/wrappers.py @@ -1,3 +1,4 @@ +import threading from contextlib import contextmanager import os from os.path import dirname, abspath, join as pjoin @@ -8,6 +9,7 @@ from . import compat + try: import importlib.resources as resources @@ -19,17 +21,6 @@ def _in_proc_script_path(): yield pjoin(dirname(abspath(__file__)), '_in_process.py') -__all__ = [ - 'Pep517HookCaller', - 'BackendUnavailable', - 'BackendInvalid', - 'HookMissing', - 'UnsupportedOperation', - 'default_subprocess_runner', - 'quiet_subprocess_runner', -] - - @contextmanager def tempdir(): td = mkdtemp() @@ -281,3 +272,37 @@ def _call_hook(self, hook_name, kwargs): if data.get('hook_missing'): raise HookMissing(hook_name) return data['return_val'] + + +class LoggerWrapper(threading.Thread): + """ + Read messages from a pipe and redirect them + to a logger (see python's logging module). + """ + + def __init__(self, logger, level): + threading.Thread.__init__(self) + self.daemon = True + + self.logger = logger + self.level = level + + # create the pipe and reader + self.fd_read, self.fd_write = os.pipe() + self.reader = os.fdopen(self.fd_read) + + self.start() + + def fileno(self): + return self.fd_write + + @staticmethod + def remove_newline(msg): + return msg[:-1] if msg.endswith(os.linesep) else msg + + def run(self): + for line in self.reader: + self._write(self.remove_newline(line)) + + def _write(self, message): + self.logger.log(self.level, message) diff --git a/pyproject.toml b/pyproject.toml index fe7f337..65d764c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["flit_core >=2,<4"] +requires = ["flit_core >=2,<3"] build-backend = "flit_core.buildapi" [tool.flit.metadata] @@ -10,6 +10,8 @@ home-page = "https://github.com/pypa/pep517" description-file = "README.rst" requires = [ "toml", + "importlib_metadata;python_version<'3.8'", + "zipp;python_version<'3.8'", ] classifiers = [ "License :: OSI Approved :: MIT License", diff --git a/tests/test_build.py b/tests/test_build.py new file mode 100644 index 0000000..2a7c2bc --- /dev/null +++ b/tests/test_build.py @@ -0,0 +1,27 @@ +import pytest + +from pep517 import build + + +def system(*args): + return dict.fromkeys(args) + + +class TestValidateSystem: + def test_missing(self): + with pytest.raises(ValueError): + build.validate_system(system()) + with pytest.raises(ValueError): + build.validate_system(system('requires')) + with pytest.raises(ValueError): + build.validate_system(system('build-backend')) + with pytest.raises(ValueError): + build.validate_system(system('other')) + + def test_missing_and_extra(self): + with pytest.raises(ValueError): + build.validate_system(system('build-backend', 'other')) + + def test_satisfied(self): + build.validate_system(system('build-backend', 'requires')) + build.validate_system(system('build-backend', 'requires', 'other')) diff --git a/examples/buildtool/tests/test_envbuild.py b/tests/test_envbuild.py similarity index 95% rename from examples/buildtool/tests/test_envbuild.py rename to tests/test_envbuild.py index 12fbb82..79c094b 100644 --- a/examples/buildtool/tests/test_envbuild.py +++ b/tests/test_envbuild.py @@ -8,7 +8,7 @@ from mock import patch, call # Python 2 fallback import zipfile -from ..envbuild import build_sdist, build_wheel, BuildEnvironment +from pep517.envbuild import build_sdist, build_wheel, BuildEnvironment SAMPLES_DIR = pjoin(dirname(abspath(__file__)), 'samples') BUILDSYS_PKGS = pjoin(SAMPLES_DIR, 'buildsys_pkgs') diff --git a/tests/test_hook_fallbacks.py b/tests/test_hook_fallbacks.py index 7410de6..c8d4f21 100644 --- a/tests/test_hook_fallbacks.py +++ b/tests/test_hook_fallbacks.py @@ -4,7 +4,7 @@ from testpath import modified_env, assert_isfile from testpath.tempdir import TemporaryDirectory -from pep517 import HookMissing, Pep517HookCaller +from pep517.wrappers import HookMissing, Pep517HookCaller SAMPLES_DIR = pjoin(dirname(abspath(__file__)), 'samples') BUILDSYS_PKGS = pjoin(SAMPLES_DIR, 'buildsys_pkgs') diff --git a/tests/test_inplace_hooks.py b/tests/test_inplace_hooks.py index 191711c..2e6e886 100644 --- a/tests/test_inplace_hooks.py +++ b/tests/test_inplace_hooks.py @@ -3,7 +3,7 @@ from testpath import modified_env import pytest -from pep517 import Pep517HookCaller, BackendInvalid +from pep517.wrappers import Pep517HookCaller, BackendInvalid SAMPLES_DIR = pjoin(dirname(abspath(__file__)), 'samples') BUILDSYS_PKGS = pjoin(SAMPLES_DIR, 'buildsys_pkgs') diff --git a/examples/buildtool/tests/test_meta.py b/tests/test_meta.py similarity index 97% rename from examples/buildtool/tests/test_meta.py rename to tests/test_meta.py index 2b024e7..bd6a2e7 100644 --- a/examples/buildtool/tests/test_meta.py +++ b/tests/test_meta.py @@ -4,7 +4,7 @@ import pytest -from .. import meta +from pep517 import meta pep517_needs_python_3 = pytest.mark.xfail( diff --git a/tests/test_pyproject.py b/tests/test_pyproject.py deleted file mode 100644 index aaca65d..0000000 --- a/tests/test_pyproject.py +++ /dev/null @@ -1,27 +0,0 @@ -import pytest - -from pep517.pyproject import validate_system - - -def system(*args): - return dict.fromkeys(args) - - -class TestValidateSystem: - def test_missing(self): - with pytest.raises(ValueError): - validate_system(system()) - with pytest.raises(ValueError): - validate_system(system('requires')) - with pytest.raises(ValueError): - validate_system(system('build-backend')) - with pytest.raises(ValueError): - validate_system(system('other')) - - def test_missing_and_extra(self): - with pytest.raises(ValueError): - validate_system(system('build-backend', 'other')) - - def test_satisfied(self): - validate_system(system('build-backend', 'requires')) - validate_system(system('build-backend', 'requires', 'other')) diff --git a/tox.ini b/tox.ini index 4ffcb74..2502fcf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = py27, py34, py35, py36, py37, pypy, pypy3 -isolated_build = True +skipsdist = true [testenv] deps = -rdev-requirements.txt