From dd93dc9ee905aa337183370ee7892d003ea82220 Mon Sep 17 00:00:00 2001 From: finswimmer Date: Mon, 22 Aug 2022 08:20:49 +0200 Subject: [PATCH] feat: fallback to gather metadata via pep517 if reading as Poetry project raises RuntimeError --- poetry/inspection/info.py | 7 ++- poetry/installation/executor.py | 47 +++++++++-------- poetry/installation/pip_installer.py | 47 +++++++++-------- .../demo_poetry_package/pyproject.toml | 15 ++++++ tests/inspection/test_info.py | 16 ++++++ tests/installation/test_executor.py | 50 +++++++++++++++++++ tests/installation/test_pip_installer.py | 32 ++++++++++++ 7 files changed, 171 insertions(+), 43 deletions(-) create mode 100644 tests/fixtures/inspection/demo_poetry_package/pyproject.toml diff --git a/poetry/inspection/info.py b/poetry/inspection/info.py index dcd30a1e55d..c8a77af1f46 100644 --- a/poetry/inspection/info.py +++ b/poetry/inspection/info.py @@ -437,7 +437,12 @@ def _get_poetry_package(path): # type: (Path) -> Optional[ProjectPackage] # 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 + try: + return Factory().create_poetry(path).package + except RuntimeError: + return None + + return None @classmethod def _pep517_metadata(cls, path): # type (Path) -> PackageInfo diff --git a/poetry/installation/executor.py b/poetry/installation/executor.py index d90895db551..cf4856ec32d 100644 --- a/poetry/installation/executor.py +++ b/poetry/installation/executor.py @@ -513,34 +513,39 @@ def _install_directory(self, operation): legacy_pip = self._env.pip_version < self._env.pip_version.__class__( 19, 0, 0 ) - package_poetry = Factory().create_poetry(pyproject.file.path.parent) - if package.develop and not package_poetry.package.build_script: - from poetry.masonry.builders.editable import EditableBuilder + try: + package_poetry = Factory().create_poetry(pyproject.file.path.parent) + except RuntimeError: + package_poetry = None - # 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() + if package_poetry is not None: + if package.develop and not package_poetry.package.build_script: + from poetry.masonry.builders.editable import EditableBuilder - return 0 - elif legacy_pip or package_poetry.package.build_script: - from poetry.core.masonry.builders.sdist import SdistBuilder + # 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) + # 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: - args.append("-e") + with builder.setup_py(): + if package.develop: + args.append("-e") - args.append(req) + args.append(req) - return self.run_pip(*args) + return self.run_pip(*args) if package.develop: args.append("-e") diff --git a/poetry/installation/pip_installer.py b/poetry/installation/pip_installer.py index 652f9f4536d..7f4f6793d41 100644 --- a/poetry/installation/pip_installer.py +++ b/poetry/installation/pip_installer.py @@ -202,34 +202,39 @@ def install_directory(self, package): legacy_pip = self._env.pip_version < self._env.pip_version.__class__( 19, 0, 0 ) - package_poetry = Factory().create_poetry(pyproject.file.path.parent) - if package.develop and not package_poetry.package.build_script: - from poetry.masonry.builders.editable import EditableBuilder + try: + package_poetry = Factory().create_poetry(pyproject.file.path.parent) + except RuntimeError: + package_poetry = None + + if package_poetry is not None: + 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() + # 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 + 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) + # 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: - args.append("-e") + with builder.setup_py(): + if package.develop: + args.append("-e") - args.append(req) + args.append(req) - return self.run(*args) + return self.run(*args) if package.develop: args.append("-e") 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 43bd15383b2..0cdf6e79cc7 100644 --- a/tests/inspection/test_info.py +++ b/tests/inspection/test_info.py @@ -127,6 +127,22 @@ def test_info_from_poetry_directory(): demo_check_info(info) +def test_info_from_poetry_directory_fallback_on_poetry_create_error(mocker): + 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.PackageInfo._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 fb504dbcb8b..21404f7252c 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -358,3 +358,53 @@ def test_executor_should_use_cached_link_and_hash( ) assert archive == link_cached + + +def test_executer_fallback_on_poetry_create_error( + mocker, config, pool, io, tmp_dir, mock_file_downloads, +): + import poetry.installation.executor + + mock_pip_install = mocker.patch.object( + poetry.installation.executor.Executor, "run_pip" + ) + 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}) + + env = MockEnv(path=Path(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 = """ +Package operations: 1 install, 0 updates, 0 removals + • Installing simple-project (1.2.3 {source_url}) +""".format( + source_url=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 diff --git a/tests/installation/test_pip_installer.py b/tests/installation/test_pip_installer.py index d0e2e5a4dcd..4cb00c49d32 100644 --- a/tests/installation/test_pip_installer.py +++ b/tests/installation/test_pip_installer.py @@ -216,3 +216,35 @@ def copy_only(source, dest): # any command in the virtual environment should trigger the error message output = tmp_venv.run("python", "-m", "site") assert "Error processing line 1 of {}".format(pth_file_candidate) not in output + + +def test_install_directory_fallback_on_poetry_create_error(mocker, tmp_venv, pool): + import poetry.installation.pip_installer + + 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.object( + poetry.installation.pip_installer.PipInstaller, "run" + ) + + 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