From dc0502e5dd01d5c1215fcadefdfe24ef994ff506 Mon Sep 17 00:00:00 2001 From: finswimmer Date: Thu, 9 Jun 2022 20:50:30 +0200 Subject: [PATCH 1/2] feat: fallback to gather metadata via pep517 if reading as Poetry project raises RuntimeError --- src/poetry/inspection/info.py | 4 +- src/poetry/installation/executor.py | 55 ++++++++++--------- src/poetry/installation/pip_installer.py | 67 +++++++++++++----------- 3 files changed, 69 insertions(+), 57 deletions(-) diff --git a/src/poetry/inspection/info.py b/src/poetry/inspection/info.py index 1495bc7c62c..0d3e760bb9e 100644 --- a/src/poetry/inspection/info.py +++ b/src/poetry/inspection/info.py @@ -463,7 +463,9 @@ def _get_poetry_package(path: Path) -> ProjectPackage | None: # Note: we ignore any setup.py file at this step # TODO: add support for handling non-poetry PEP-517 builds if PyProjectTOML(path.joinpath("pyproject.toml")).is_poetry_project(): - return Factory().create_poetry(path).package + with contextlib.suppress(RuntimeError): + return Factory().create_poetry(path).package + return None @classmethod diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index e3b56cf2a13..14f31c55292 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -553,32 +553,37 @@ def _install_directory(self, operation: Install | Update) -> int: self._env.pip_version < self._env.pip_version.__class__.from_parts(19, 0, 0) ) - package_poetry = Factory().create_poetry(pyproject.file.path.parent) - builder: Builder - if package.develop and not package_poetry.package.build_script: - from poetry.masonry.builders.editable import EditableBuilder - - # This is a Poetry package in editable mode - # we can use the EditableBuilder without going through pip - # to install it, unless it has a build script. - builder = EditableBuilder(package_poetry, self._env, NullIO()) - builder.build() - - return 0 - elif legacy_pip or package_poetry.package.build_script: - from poetry.core.masonry.builders.sdist import SdistBuilder - - # We need to rely on creating a temporary setup.py - # file since the version of pip does not support - # build-systems - # We also need it for non-PEP-517 packages - builder = SdistBuilder(package_poetry) - - with builder.setup_py(): - if package.develop: - return self.pip_install(req, upgrade=True, editable=True) - return self.pip_install(req, upgrade=True) + try: + package_poetry = Factory().create_poetry(pyproject.file.path.parent) + except RuntimeError: + package_poetry = None + + if package_poetry is not None: + builder: Builder + if package.develop and not package_poetry.package.build_script: + from poetry.masonry.builders.editable import EditableBuilder + + # This is a Poetry package in editable mode + # we can use the EditableBuilder without going through pip + # to install it, unless it has a build script. + builder = EditableBuilder(package_poetry, self._env, NullIO()) + builder.build() + + return 0 + elif legacy_pip or package_poetry.package.build_script: + from poetry.core.masonry.builders.sdist import SdistBuilder + + # We need to rely on creating a temporary setup.py + # file since the version of pip does not support + # build-systems + # We also need it for non-PEP-517 packages + builder = SdistBuilder(package_poetry) + + with builder.setup_py(): + if package.develop: + return self.pip_install(req, upgrade=True, editable=True) + return self.pip_install(req, upgrade=True) if package.develop: return self.pip_install(req, upgrade=True, editable=True) diff --git a/src/poetry/installation/pip_installer.py b/src/poetry/installation/pip_installer.py index 21c0e013771..a6ea6f46c50 100644 --- a/src/poetry/installation/pip_installer.py +++ b/src/poetry/installation/pip_installer.py @@ -231,39 +231,44 @@ def install_directory(self, package: Package) -> str | int: # so we need to check the version of pip to know # if we can rely on the build system legacy_pip = self._env.pip_version < Version.from_parts(19, 0, 0) - package_poetry = Factory().create_poetry(pyproject.file.path.parent) - - builder: Builder - if package.develop and not package_poetry.package.build_script: - from poetry.masonry.builders.editable import EditableBuilder - - # This is a Poetry package in editable mode - # we can use the EditableBuilder without going through pip - # to install it, unless it has a build script. - builder = EditableBuilder(package_poetry, self._env, NullIO()) - builder.build() - - return 0 - elif legacy_pip or package_poetry.package.build_script: - from poetry.core.masonry.builders.sdist import SdistBuilder - - # We need to rely on creating a temporary setup.py - # file since the version of pip does not support - # build-systems - # We also need it for non-PEP-517 packages - builder = SdistBuilder(package_poetry) - - with builder.setup_py(): - if package.develop: + + try: + package_poetry = Factory().create_poetry(pyproject.file.path.parent) + except RuntimeError: + package_poetry = None + + if package_poetry is not None: + builder: Builder + if package.develop and not package_poetry.package.build_script: + from poetry.masonry.builders.editable import EditableBuilder + + # This is a Poetry package in editable mode + # we can use the EditableBuilder without going through pip + # to install it, unless it has a build script. + builder = EditableBuilder(package_poetry, self._env, NullIO()) + builder.build() + + return 0 + elif legacy_pip or package_poetry.package.build_script: + from poetry.core.masonry.builders.sdist import SdistBuilder + + # We need to rely on creating a temporary setup.py + # file since the version of pip does not support + # build-systems + # We also need it for non-PEP-517 packages + builder = SdistBuilder(package_poetry) + + with builder.setup_py(): + if package.develop: + return pip_install( + path=req, + environment=self._env, + upgrade=True, + editable=True, + ) return pip_install( - path=req, - environment=self._env, - upgrade=True, - editable=True, + path=req, environment=self._env, deps=False, upgrade=True ) - return pip_install( - path=req, environment=self._env, deps=False, upgrade=True - ) if package.develop: return pip_install( From f348738112d338d72d8b75cbea04e230095559a0 Mon Sep 17 00:00:00 2001 From: finswimmer Date: Sat, 11 Jun 2022 07:22:42 +0200 Subject: [PATCH 2/2] test: added tests for fallback to gather metadata via pep517 --- .../demo_poetry_package/pyproject.toml | 15 +++++ tests/inspection/test_info.py | 18 ++++++ tests/installation/test_executor.py | 56 +++++++++++++++++++ tests/installation/test_pip_installer.py | 32 +++++++++++ 4 files changed, 121 insertions(+) create mode 100644 tests/fixtures/inspection/demo_poetry_package/pyproject.toml diff --git a/tests/fixtures/inspection/demo_poetry_package/pyproject.toml b/tests/fixtures/inspection/demo_poetry_package/pyproject.toml new file mode 100644 index 00000000000..011338ea91a --- /dev/null +++ b/tests/fixtures/inspection/demo_poetry_package/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "demo-poetry" +version = "0.1.0" +description = "" +authors = ["John Doe "] + +[tool.poetry.dependencies] +python = "^3.10" +pendulum = "*" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/inspection/test_info.py b/tests/inspection/test_info.py index c242fb90937..4c7b34bd4da 100644 --- a/tests/inspection/test_info.py +++ b/tests/inspection/test_info.py @@ -131,6 +131,24 @@ def test_info_from_poetry_directory(): demo_check_info(info) +def test_info_from_poetry_directory_fallback_on_poetry_create_error( + mocker: MockerFixture, +): + mock_create_poetry = mocker.patch( + "poetry.inspection.info.Factory.create_poetry", side_effect=RuntimeError + ) + mock_get_poetry_package = mocker.spy(PackageInfo, "_get_poetry_package") + mock_get_pep517_metadata = mocker.patch( + "poetry.inspection.info.get_pep517_metadata" + ) + + PackageInfo.from_directory(FIXTURE_DIR_INSPECTIONS / "demo_poetry_package") + + assert mock_create_poetry.call_count == 1 + assert mock_get_poetry_package.call_count == 1 + assert mock_get_pep517_metadata.call_count == 1 + + def test_info_from_requires_txt(): info = PackageInfo.from_metadata( FIXTURE_DIR_INSPECTIONS / "demo_only_requires_txt.egg-info" diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index edfb6c27f98..b2e06de6f99 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -704,3 +704,59 @@ def test_executor_should_be_initialized_with_correct_workers( executor = Executor(tmp_venv, pool, config, io) assert executor._max_workers == expected_workers + + +def test_executer_fallback_on_poetry_create_error( + mocker: MockerFixture, + config: Config, + pool: Pool, + io: BufferedIO, + tmp_dir: str, + mock_file_downloads: None, + env: MockEnv, +): + mock_pip_install = mocker.patch("poetry.installation.executor.pip_install") + mock_sdist_builder = mocker.patch("poetry.core.masonry.builders.sdist.SdistBuilder") + mock_editable_builder = mocker.patch( + "poetry.masonry.builders.editable.EditableBuilder" + ) + mock_create_poetry = mocker.patch( + "poetry.factory.Factory.create_poetry", side_effect=RuntimeError + ) + + config.merge({"cache-dir": tmp_dir}) + + executor = Executor(env, pool, config, io) + + directory_package = Package( + "simple-project", + "1.2.3", + source_type="directory", + source_url=Path(__file__) + .parent.parent.joinpath("fixtures/simple_project") + .resolve() + .as_posix(), + ) + + return_code = executor.execute( + [ + Install(directory_package), + ] + ) + + expected = f""" +Package operations: 1 install, 0 updates, 0 removals + + • Installing simple-project (1.2.3 {directory_package.source_url}) +""" + + expected = set(expected.splitlines()) + output = set(io.fetch_output().splitlines()) + assert output == expected + assert return_code == 0 + assert mock_create_poetry.call_count == 1 + assert mock_sdist_builder.call_count == 0 + assert mock_editable_builder.call_count == 0 + assert mock_pip_install.call_count == 1 + assert mock_pip_install.call_args[1].get("upgrade") is True + assert mock_pip_install.call_args[1].get("editable") is False diff --git a/tests/installation/test_pip_installer.py b/tests/installation/test_pip_installer.py index 4763d71bb42..62624b6845f 100644 --- a/tests/installation/test_pip_installer.py +++ b/tests/installation/test_pip_installer.py @@ -266,3 +266,35 @@ def test_install_with_trusted_host(config: Config): assert "--trusted-host" in cmd cert_index = cmd.index("--trusted-host") assert cmd[cert_index + 1] == "foo.bar" + + +def test_install_directory_fallback_on_poetry_create_error( + mocker: MockerFixture, tmp_venv: VirtualEnv, pool: Pool +): + mock_create_poetry = mocker.patch( + "poetry.factory.Factory.create_poetry", side_effect=RuntimeError + ) + mock_sdist_builder = mocker.patch("poetry.core.masonry.builders.sdist.SdistBuilder") + mock_editable_builder = mocker.patch( + "poetry.masonry.builders.editable.EditableBuilder" + ) + mock_pip_install = mocker.patch("poetry.installation.pip_installer.pip_install") + + package = Package( + "demo", + "1.0.0", + source_type="directory", + source_url=str( + Path(__file__).parent.parent / "fixtures/inspection/demo_poetry_package" + ), + ) + + installer = PipInstaller(tmp_venv, NullIO(), pool) + installer.install_directory(package) + + assert mock_create_poetry.call_count == 1 + assert mock_sdist_builder.call_count == 0 + assert mock_editable_builder.call_count == 0 + assert mock_pip_install.call_count == 1 + assert mock_pip_install.call_args[1].get("deps") is False + assert mock_pip_install.call_args[1].get("upgrade") is True