From d2e6ad6e456c6688c1c4be691aaa5e230644ccd6 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Mon, 10 Apr 2023 13:06:25 +0100 Subject: [PATCH] avoid infinite loop when adding a dependency (#7405) --- src/poetry/mixology/version_solver.py | 4 +- tests/conftest.py | 13 ++- tests/console/commands/test_add.py | 33 +++++++ .../with_path_dependency/bazz/setup.py | 12 +++ .../fixtures/with_path_dependency/poetry.lock | 98 +++++++++++++++++++ .../with_path_dependency/pyproject.toml | 15 +++ tests/types.py | 1 + 7 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/with_path_dependency/bazz/setup.py create mode 100644 tests/fixtures/with_path_dependency/poetry.lock create mode 100644 tests/fixtures/with_path_dependency/pyproject.toml diff --git a/src/poetry/mixology/version_solver.py b/src/poetry/mixology/version_solver.py index 6c66dc4c5e9..04ba762d963 100644 --- a/src/poetry/mixology/version_solver.py +++ b/src/poetry/mixology/version_solver.py @@ -331,7 +331,9 @@ def _resolve_conflict(self, incompatibility: Incompatibility) -> Incompatibility # .. _algorithm documentation: # https://github.com/dart-lang/pub/tree/master/doc/solver.md#conflict-resolution # noqa: E501 if difference is not None: - new_terms.append(difference.inverse) + inverse = difference.inverse + if inverse.dependency != most_recent_satisfier.dependency: + new_terms.append(inverse) incompatibility = Incompatibility( new_terms, ConflictCause(incompatibility, most_recent_satisfier.cause) diff --git a/tests/conftest.py b/tests/conftest.py index 47c11049479..5094c97b667 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -378,6 +378,7 @@ def _factory( install_deps: bool = True, source: Path | None = None, locker_config: dict[str, Any] | None = None, + use_test_locker: bool = True, ) -> Poetry: project_dir = workspace / f"poetry-fixture-{name}" dependencies = dependencies or {} @@ -412,12 +413,14 @@ def _factory( poetry = Factory().create_poetry(project_dir) - locker = TestLocker( - poetry.locker.lock, locker_config or poetry.locker._local_config - ) - locker.write() + if use_test_locker: + locker = TestLocker( + poetry.locker.lock, locker_config or poetry.locker._local_config + ) + locker.write() + + poetry.set_locker(locker) - poetry.set_locker(locker) poetry.set_config(config) pool = RepositoryPool() diff --git a/tests/console/commands/test_add.py b/tests/console/commands/test_add.py index 516934c4b32..706aa637edc 100644 --- a/tests/console/commands/test_add.py +++ b/tests/console/commands/test_add.py @@ -10,6 +10,7 @@ from poetry.core.constraints.version import Version from poetry.core.packages.package import Package +from poetry.puzzle.exceptions import SolverProblemError from poetry.repositories.legacy_repository import LegacyRepository from tests.helpers import get_dependency from tests.helpers import get_package @@ -45,6 +46,20 @@ def poetry_with_up_to_date_lockfile( ) +@pytest.fixture +def poetry_with_path_dependency( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + source = fixture_dir("with_path_dependency") + + poetry = project_factory( + name="foobar", + source=source, + use_test_locker=False, + ) + return poetry + + @pytest.fixture() def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("add") @@ -2238,3 +2253,21 @@ def error(_: Any) -> int: assert poetry_with_up_to_date_lockfile.file.read() == original_pyproject_content assert poetry_with_up_to_date_lockfile.locker.lock_data == original_lockfile_content assert tester.io.fetch_output() == expected + + +def test_add_with_path_dependency_no_loopiness( + poetry_with_path_dependency: Poetry, + repo: TestRepository, + command_tester_factory: CommandTesterFactory, +) -> None: + """https://github.com/python-poetry/poetry/issues/7398""" + tester = command_tester_factory("add", poetry=poetry_with_path_dependency) + + requests_old = get_package("requests", "2.25.1") + requests_new = get_package("requests", "2.28.2") + + repo.add_package(requests_old) + repo.add_package(requests_new) + + with pytest.raises(SolverProblemError): + tester.execute("requests") diff --git a/tests/fixtures/with_path_dependency/bazz/setup.py b/tests/fixtures/with_path_dependency/bazz/setup.py new file mode 100644 index 00000000000..97223196678 --- /dev/null +++ b/tests/fixtures/with_path_dependency/bazz/setup.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from distutils.core import setup + + +setup( + name="bazz", + version="1", + py_modules=["demo"], + package_dir={"src": "src"}, + install_requires=["requests~=2.25.1"], +) diff --git a/tests/fixtures/with_path_dependency/poetry.lock b/tests/fixtures/with_path_dependency/poetry.lock new file mode 100644 index 00000000000..3c2e1b9b7c6 --- /dev/null +++ b/tests/fixtures/with_path_dependency/poetry.lock @@ -0,0 +1,98 @@ +# This file is automatically @generated by Poetry 1.4.0.dev0 and should not be changed by hand. + +[[package]] +name = "bazz" +version = "1" +description = "" +category = "main" +optional = false +python-versions = "*" +files = [] +develop = true + +[package.dependencies] +requests = ">=2.25.1,<2.26.0" + +[package.source] +type = "directory" +url = "bazz" + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "urllib3" +version = "1.26.14" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "81853eb3be9c7faf1950f23273d0fb3dae75833f6e4aa21b6c3038da25ba26f6" diff --git a/tests/fixtures/with_path_dependency/pyproject.toml b/tests/fixtures/with_path_dependency/pyproject.toml new file mode 100644 index 00000000000..705ba4fe229 --- /dev/null +++ b/tests/fixtures/with_path_dependency/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "foobar" +version = "0.1.0" +description = "" +authors = [] +readme = "README.md" +packages = [{include = "foobar"}] + +[tool.poetry.dependencies] +python = "^3.9" +bazz = { path = "./bazz", develop = true } + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/types.py b/tests/types.py index 95ce4cbc940..c990414f52e 100644 --- a/tests/types.py +++ b/tests/types.py @@ -46,6 +46,7 @@ def __call__( poetry_lock_content: str | None = None, install_deps: bool = True, source: Path | None = None, + use_test_locker: bool = True, ) -> Poetry: ...