diff --git a/poetry.lock b/poetry.lock index 3c0a674ebfa..a7c47a95490 100644 --- a/poetry.lock +++ b/poetry.lock @@ -390,15 +390,20 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "poetry-core" -version = "1.1.0a7" +version = "1.1.0-alpha.7" description = "Poetry PEP 517 Build Backend" category = "main" optional = false -python-versions = ">=3.7,<4.0" +python-versions = "^3.7" +develop = true [package.dependencies] importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} +[package.source] +type = "directory" +url = "../poetry-core" + [[package]] name = "pre-commit" version = "2.17.0" @@ -707,7 +712,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "e1979eb8cccef74af26d7fc01bb77d64515a2633fdb28bdbc2837da52fb24b98" +content-hash = "dc08abc6f6c31200df3f9c163e59ff7b1fa32abb5ec1823b829d7bb760ad0cfc" [metadata.files] atomicwrites = [ @@ -984,10 +989,7 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -poetry-core = [ - {file = "poetry-core-1.1.0a7.tar.gz", hash = "sha256:4622ae680842ac9b1b9c3b0e8dc467c2e291d1a5c434b6bd413907a2e5571d92"}, - {file = "poetry_core-1.1.0a7-py3-none-any.whl", hash = "sha256:724e8b5368f270461e622396305d0c2e760ec9d4c14d072e6b944da9384c67de"}, -] +poetry-core = [] pre-commit = [ {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, diff --git a/pyproject.toml b/pyproject.toml index ba13316c4f1..50ff7ab6b42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ generate-setup-file = false [tool.poetry.dependencies] python = "^3.7" -poetry-core = "^1.1.0a7" +poetry-core = {path = "../poetry-core", develop = true} cachecontrol = { version = "^0.12.9", extras = ["filecache"] } cachy = "^0.3.0" cleo = "^1.0.0a4" diff --git a/src/poetry/console/commands/build.py b/src/poetry/console/commands/build.py index 9d5545326ca..2ae31807df3 100644 --- a/src/poetry/console/commands/build.py +++ b/src/poetry/console/commands/build.py @@ -1,8 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from cleo.helpers import option from poetry.console.commands.env_command import EnvCommand +from poetry.utils.env import build_environment + + +if TYPE_CHECKING: + from poetry.utils.env import Env class BuildCommand(EnvCommand): @@ -23,11 +30,13 @@ class BuildCommand(EnvCommand): def handle(self) -> None: from poetry.core.masonry.builder import Builder - fmt = self.option("format") or "all" - package = self.poetry.package - self.line( - f"Building {package.pretty_name} ({package.version})" - ) + env: Env + with build_environment(poetry=self.poetry, env=self.env, io=self.io) as env: + fmt = self.option("format") or "all" + package = self.poetry.package + self.line( + f"Building {package.pretty_name} ({package.version})" + ) - builder = Builder(self.poetry) - builder.build(fmt, executable=self.env.python) + builder = Builder(self.poetry) + builder.build(fmt, executable=env.python) # type: ignore[attr-defined] diff --git a/src/poetry/masonry/builders/editable.py b/src/poetry/masonry/builders/editable.py index 188ece7c47e..fe26e7ec64e 100644 --- a/src/poetry/masonry/builders/editable.py +++ b/src/poetry/masonry/builders/editable.py @@ -15,14 +15,15 @@ from poetry.utils._compat import WINDOWS from poetry.utils._compat import decode +from poetry.utils.env import build_environment from poetry.utils.helpers import is_dir_writable from poetry.utils.pip import pip_install if TYPE_CHECKING: from cleo.io.io import IO - from poetry.core.poetry import Poetry + from poetry.poetry import Poetry from poetry.utils.env import Env SCRIPT_TEMPLATE = """\ @@ -75,8 +76,12 @@ def build(self) -> None: self._add_dist_info(added_files) def _run_build_script(self, build_script: Path) -> None: - self._debug(f" - Executing build script: {build_script}") - self._env.run("python", str(self._path.joinpath(build_script)), call=True) + env: Env + with build_environment(poetry=self._poetry, env=self._env, io=self._io) as env: + self._debug(f" - Executing build script: {build_script}") + env.run( # type: ignore[attr-defined] + "python", str(self._path.joinpath(build_script)), call=True + ) def _setup_build(self) -> None: builder = SdistBuilder(self._poetry) diff --git a/src/poetry/utils/env.py b/src/poetry/utils/env.py index 544e5fe78ae..35265cf573f 100644 --- a/src/poetry/utils/env.py +++ b/src/poetry/utils/env.py @@ -1852,6 +1852,46 @@ def ephemeral_environment( yield VirtualEnv(venv_dir, venv_dir) +@contextmanager +def build_environment( + poetry: Poetry, env: Env | None = None, io: IO | None = None +) -> ContextManager[Env]: + """ + If a build script is specified for the project, there could be additional build + time dependencies, eg: cython, setuptools etc. In these cases, we create an + ephemeral build environment with all requirements specified under + `build-system.requires` and return this. Otherwise, the given default project + environment is returned. + """ + if not env or poetry.package.build_script: + with ephemeral_environment(executable=env.python if env else None) as venv: + overwrite = io and io.output.is_decorated() and not io.is_debug() + if io: + requires = map( + lambda r: f"{r}", poetry.pyproject.build_system.requires + ) + if not overwrite: + io.write_line("") + + io.overwrite( + "Preparing build environment with build-system requirements" + f" {', '.join(requires)}" + ) + venv.run_pip( + "install", + "--disable-pip-version-check", + "--ignore-installed", + *poetry.pyproject.build_system.requires, + ) + + if overwrite: + io.write_line("") + + yield venv + else: + yield env + + class MockEnv(NullEnv): def __init__( self, diff --git a/tests/fixtures/extended_project_without_setup/pyproject.toml b/tests/fixtures/extended_project_without_setup/pyproject.toml index 5c9dc2774c7..b3f4818f29c 100644 --- a/tests/fixtures/extended_project_without_setup/pyproject.toml +++ b/tests/fixtures/extended_project_without_setup/pyproject.toml @@ -27,3 +27,7 @@ generate-setup-file = false # Requirements [tool.poetry.dependencies] python = "~2.7 || ^3.4" + +[build-system] +requires = ["poetry-core", "cython"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/masonry/builders/test_editable_builder.py b/tests/masonry/builders/test_editable_builder.py index cbd91f4dcdd..373d81d9f40 100644 --- a/tests/masonry/builders/test_editable_builder.py +++ b/tests/masonry/builders/test_editable_builder.py @@ -229,9 +229,13 @@ def test_builder_installs_proper_files_when_packages_configured( def test_builder_should_execute_build_scripts( - extended_without_setup_poetry: Poetry, tmp_dir: str + mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str ): env = MockEnv(path=Path(tmp_dir) / "foo") + mocker.patch( + "poetry.masonry.builders.editable.build_environment" + ).return_value.__enter__.return_value = env + builder = EditableBuilder(extended_without_setup_poetry, env, NullIO()) builder.build() diff --git a/tests/utils/test_env.py b/tests/utils/test_env.py index afc5aa9207a..c50822f1b63 100644 --- a/tests/utils/test_env.py +++ b/tests/utils/test_env.py @@ -25,9 +25,11 @@ from poetry.utils.env import EnvManager from poetry.utils.env import GenericEnv from poetry.utils.env import InvalidCurrentPythonVersionError +from poetry.utils.env import MockEnv from poetry.utils.env import NoCompatiblePythonVersionFound from poetry.utils.env import SystemEnv from poetry.utils.env import VirtualEnv +from poetry.utils.env import build_environment if TYPE_CHECKING: @@ -1270,3 +1272,51 @@ def test_generate_env_name_ignores_case_for_case_insensitive_fs(tmp_dir: str): assert venv_name1 == venv_name2 else: assert venv_name1 != venv_name2 + + +@pytest.fixture() +def extended_without_setup_poetry() -> Poetry: + poetry = Factory().create_poetry( + Path(__file__).parent.parent / "fixtures" / "extended_project_without_setup" + ) + + return poetry + + +def test_build_environment_called_build_script_specified( + mocker: MockerFixture, extended_without_setup_poetry: Poetry, tmp_dir: str +): + project_env = MockEnv(path=Path(tmp_dir) / "project") + ephemeral_env = MockEnv(path=Path(tmp_dir) / "ephemeral") + + mocker.patch( + "poetry.utils.env.ephemeral_environment" + ).return_value.__enter__.return_value = ephemeral_env + + with build_environment(extended_without_setup_poetry, project_env) as env: + assert env == ephemeral_env + assert env.executed == [ + [ + "python", + env.pip_embedded, + "install", + "--disable-pip-version-check", + "--ignore-installed", + *extended_without_setup_poetry.pyproject.build_system.requires, + ] + ] + + +def test_build_environment_not_called_without_build_script_specified( + mocker: MockerFixture, poetry: Poetry, tmp_dir: str +): + project_env = MockEnv(path=Path(tmp_dir) / "project") + ephemeral_env = MockEnv(path=Path(tmp_dir) / "ephemeral") + + mocker.patch( + "poetry.utils.env.ephemeral_environment" + ).return_value.__enter__.return_value = ephemeral_env + + with build_environment(poetry, project_env) as env: + assert env == project_env + assert not env.executed