From 2591618ded180bde5e2b7922a2922722cdf7b82a Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Mon, 10 Aug 2020 16:15:06 +0200 Subject: [PATCH] Support build dependencies Resolves: #2789 --- poetry/console/commands/build.py | 13 ++- poetry/installation/base_installer.py | 4 + poetry/installation/installer.py | 37 +++++++++ poetry/puzzle/solver.py | 2 + tests/console/commands/test_build.py | 83 +++++++++++++++++++ .../README.rst | 2 + .../build.py | 0 .../project/__init__.py | 0 .../pyproject.toml | 19 +++++ 9 files changed, 157 insertions(+), 3 deletions(-) create mode 100644 tests/console/commands/test_build.py create mode 100644 tests/fixtures/project_with_build_system_requires/README.rst create mode 100644 tests/fixtures/project_with_build_system_requires/build.py create mode 100644 tests/fixtures/project_with_build_system_requires/project/__init__.py create mode 100644 tests/fixtures/project_with_build_system_requires/pyproject.toml diff --git a/poetry/console/commands/build.py b/poetry/console/commands/build.py index 118fb210c53..073044698a9 100644 --- a/poetry/console/commands/build.py +++ b/poetry/console/commands/build.py @@ -1,9 +1,9 @@ from cleo import option -from .env_command import EnvCommand +from poetry.console.commands.installer_command import InstallerCommand -class BuildCommand(EnvCommand): +class BuildCommand(InstallerCommand): name = "build" description = "Builds a package, as a tarball and a wheel by default." @@ -33,4 +33,11 @@ def handle(self): ) builder = Builder(self.poetry) - builder.build(fmt) + + executable = None + if self.poetry.package.build_requires: + # ensure build requirements are available if specified + self.installer.categories({"build"}).run() + executable = self.installer.env.python + + builder.build(fmt, executable=executable) diff --git a/poetry/installation/base_installer.py b/poetry/installation/base_installer.py index 1e068d076cd..de8b0086d9a 100644 --- a/poetry/installation/base_installer.py +++ b/poetry/installation/base_installer.py @@ -1,4 +1,8 @@ class BaseInstaller: + @property + def env(self): + return getattr(self, "_env", None) + def install(self, package): raise NotImplementedError diff --git a/poetry/installation/installer.py b/poetry/installation/installer.py index f0c9a62d65d..36878da225a 100644 --- a/poetry/installation/installer.py +++ b/poetry/installation/installer.py @@ -1,3 +1,4 @@ +from typing import Iterable from typing import List from typing import Optional from typing import Union @@ -11,6 +12,7 @@ from poetry.repositories import Pool from poetry.repositories import Repository from poetry.repositories.installed_repository import InstalledRepository +from poetry.utils.env import Env from poetry.utils.extras import get_extra_package_names from poetry.utils.helpers import canonicalize_name @@ -47,10 +49,12 @@ def __init__( self._verbose = False self._write_lock = True self._dev_mode = True + self._build_mode = True self._execute_operations = True self._lock = False self._whitelist = [] + self._categories = set() self._extras = [] @@ -66,6 +70,10 @@ def __init__( self._installed_repository = installed + @property + def env(self): # type: () -> Env + return self._env + @property def executor(self): return self._executor @@ -132,6 +140,14 @@ def dev_mode(self, dev_mode=True): # type: (bool) -> Installer def is_dev_mode(self): # type: () -> bool return self._dev_mode + def build_mode(self, build_mode=True): # type: (bool) -> Installer + self._build_mode = build_mode + + return self + + def is_build_mode(self): # type: () -> bool + return self._build_mode or self._dev_mode + def update(self, update=True): # type: (bool) -> Installer self._update = update @@ -163,6 +179,13 @@ def whitelist(self, packages): # type: (dict) -> Installer return self + def categories( + self, categories=None + ): # type: (Optional[Iterable[str]]) -> Installer + self._categories = {category for category in categories or []} + + return self + def extras(self, extras): # type: (list) -> Installer self._extras = extras @@ -241,6 +264,10 @@ def _do_install(self, local_repo): root = root.clone() del root.dev_requires[:] + if not self.is_build_mode(): + root = root.clone() + del root.build_requires[:] + if self._io.is_verbose(): self._io.write_line("") self._io.write_line( @@ -455,6 +482,8 @@ def _get_operations_from_lock( is_installed = True if locked.category == "dev" and not self.is_dev_mode(): ops.append(Uninstall(locked)) + elif locked.category == "build" and not self.is_build_mode(): + ops.append(Uninstall(locked)) elif locked.optional and locked.name not in extra_packages: # Installed but optional and not requested in extras ops.append(Uninstall(locked)) @@ -506,11 +535,19 @@ def _filter_operations( if package.name not in extra_packages: op.skip("Not required") + if self._categories and package.category not in self._categories: + op.skip("Category ({}) not enabled".format(package.category)) + # If the package is a dev package and dev packages # are not requested, we skip it if package.category == "dev" and not self.is_dev_mode(): op.skip("Dev dependencies not requested") + # If the package is a build package and build packages + # are not requested, we skip it + if package.category == "build" and not self.is_build_mode(): + op.skip("Build dependencies not requested") + def _get_extra_packages(self, repo): # type: (Repository) -> List[str] """ Returns all package names required by extras. diff --git a/poetry/puzzle/solver.py b/poetry/puzzle/solver.py index 386d8f95a7f..5a82b47ab62 100644 --- a/poetry/puzzle/solver.py +++ b/poetry/puzzle/solver.py @@ -338,6 +338,8 @@ def _get_tags_for_package(self, package, graph, depth=0): if "main" in categories: category = "main" + elif "build" in categories: + category = "build" else: category = "dev" diff --git a/tests/console/commands/test_build.py b/tests/console/commands/test_build.py new file mode 100644 index 00000000000..f4d9781f2f6 --- /dev/null +++ b/tests/console/commands/test_build.py @@ -0,0 +1,83 @@ +import re + +import pytest + +from tests.helpers import get_package + + +@pytest.fixture +def command_tester_with_build_requires( + repo, make_poetry, make_installer_command_tester +): + repo.add_package(get_package("cython", "0.29.6")) + poetry = make_poetry("project_with_build_system_requires") + return make_installer_command_tester(poetry, "build") + + +@pytest.fixture +def command_tester(project_directory, make_poetry, make_installer_command_tester): + return make_installer_command_tester(make_poetry(project_directory), "build") + + +def test_build_project_complete(command_tester): + tester = command_tester + command_tester.execute() + + assert tester._command.installer.executor.installations_count == 0 + + output = tester.io.fetch_output() + + assert "Writing lock file" not in output + assert "Building sdist" in output + assert "Built simple-project-1.2.3.tar.gz" in output + assert "Building wheel" in output + assert re.search(r"Built simple_project-1\.2\.3-.*\.whl", output) is not None + + +def test_build_project_sdist(command_tester): + tester = command_tester + command_tester.execute("-f sdist") + + assert tester._command.installer.executor.installations_count == 0 + + output = tester.io.fetch_output() + + assert "Writing lock file" not in output + assert "Building sdist" in output + assert "Built simple-project-1.2.3.tar.gz" in output + assert "Building wheel" not in output + assert re.search(r"Built simple_project-1\.2\.3-.*\.whl", output) is None + + +def test_build_project_wheel(command_tester): + tester = command_tester + command_tester.execute("-f wheel") + + assert tester._command.installer.executor.installations_count == 0 + + output = tester.io.fetch_output() + + assert "Writing lock file" not in output + assert "Building sdist" not in output + assert "Built simple-project-1.2.3.tar.gz" not in output + assert "Building wheel" in output + assert re.search(r"Built simple_project-1\.2\.3-.*\.whl", output) is not None + + +def test_build_project_with_build_requires(command_tester_with_build_requires): + tester = command_tester_with_build_requires + tester.execute() + + assert tester._command.installer.executor.installations_count == 1 + + package = tester._command.installer.executor._installs[0] + assert package.name == "cython" + assert package.version.text == "0.29.6" + + output = tester.io.fetch_output() + + assert "Writing lock file" in output + assert "Building sdist" in output + assert "Built project-1.2.3.tar.gz" in output + assert "Building wheel" in output + assert re.search(r"Built project-1\.2\.3-.*\.whl", output) is not None diff --git a/tests/fixtures/project_with_build_system_requires/README.rst b/tests/fixtures/project_with_build_system_requires/README.rst new file mode 100644 index 00000000000..f7fe15470f9 --- /dev/null +++ b/tests/fixtures/project_with_build_system_requires/README.rst @@ -0,0 +1,2 @@ +My Package +========== diff --git a/tests/fixtures/project_with_build_system_requires/build.py b/tests/fixtures/project_with_build_system_requires/build.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/project_with_build_system_requires/project/__init__.py b/tests/fixtures/project_with_build_system_requires/project/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/fixtures/project_with_build_system_requires/pyproject.toml b/tests/fixtures/project_with_build_system_requires/pyproject.toml new file mode 100644 index 00000000000..65f884a70ba --- /dev/null +++ b/tests/fixtures/project_with_build_system_requires/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "project" +version = "1.2.3" +description = "Some description." +authors = [] +license = "MIT" + +[tool.poetry.build] +script = "build.py" +generate-setup-file = false + +[tool.poetry.dependencies] +python = "^3.7" + +[build-system] +requires = [ + "poetry-core", + "Cython>=0.29.6", +]