Repository: python-poetry/poetry

Issue:
Infinite loop on add conflict
<!-- All the below information must be provided for others to understand and help with your issue. -->
- **Poetry version**: Poetry (version 1.3.2)
- **Python version**: 3.9.11
- **OS version and name**: macOS 13.0.1
- **pyproject.toml**: [Link](https://gist.github.com/JuliusDegesys/1973490db2e8cf8208461e81f9df3e26)

- [X] I am on the [latest](https://github.com/python-poetry/poetry/releases/latest) stable Poetry version, installed using a recommended method.
- [X] I have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repo and believe that this is not a duplicate.
- [X] I have consulted the [FAQ](https://python-poetry.org/docs/faq/) and [blog](https://python-poetry.org/blog/) for any relevant entries or release notes.
- [X] If an exception occurs when executing a command, I executed it again in debug mode (`-vvv` option) and have included the output below.



## Issue
When using a path-based dependency, trying to add a new package that conflicts with a sub-dependency leads to an infinite loop. 

```
$ tree .
.
├── my-package
│   ├── setup.py
│   └── src
└── pyproject.toml
```

cat pyproject.toml
```toml
[tool.poetry]
name = "debug-poetry"
version = "0.1.0"
description = ""
authors = []
readme = "README.md"
packages = [{include = "debug_poetry"}]

[tool.poetry.dependencies]
python = "^3.9"
foo = { path = "./my-package", develop = true }

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
```
my-package/setup.py:
```python
from distutils.core import setup

setup(
    name="foo",
    version="1",
    py_modules=["foo"],
    package_dir={"src": "src"},
    install_requires=["requests~=2.25.1"],
)
```

At first, I run `poetry install --sync --no-root`
```
$ poetry install --sync --no-root              
Creating virtualenv debug-poetry-_DvXptOW-py3.9 in /Users/julius/Library/Caches/pypoetry/virtualenvs
Installing dependencies from lock file

Package operations: 6 installs, 0 updates, 0 removals

  • Installing certifi (2022.12.7)
  • Installing chardet (4.0.0)
  • Installing idna (2.10)
  • Installing urllib3 (1.26.14)
  • Installing requests (2.25.1)
  • Installing foo (1 /Users/julius/src/debug_poetry/my-package)
```
Then, if I try to add `requests@latest`, poetry goes into an infinite loop:
```
$ poetry add -vvv requests@latest | less       
Using virtualenv: /Users/julius/Library/Caches/pypoetry/virtualenvs/debug-poetry-_DvXptOW-py3.9
[keyring.backend] Loading KWallet
[keyring.backend] Loading SecretService
[keyring.backend] Loading Windows
[keyring.backend] Loading chainer
[keyring.backend] Loading libsecret
[keyring.backend] Loading macOS
Creating new session for pypi.org
Source (PyPI): 145 packages found for requests *
Using version ^2.28.2 for requests

Updating dependencies
Resolving dependencies...
   1: fact: debug-poetry is 0.1.0
   1: derived: debug-poetry
   1: fact: debug-poetry depends on foo (*)
   1: fact: debug-poetry depends on requests (^2.28.2)
   1: selecting debug-poetry (0.1.0)
   1: derived: requests (>=2.28.2,<3.0.0)
   1: derived: foo (*) @ file:///Users/julius/src/debug_poetry/my-package
Source (PyPI): 1 packages found for requests >=2.28.2,<3.0.0
   1: fact: foo (1) depends on requests (>=2.25.1,<2.26.0)
   1: derived: not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: fact: foo (1) depends on requests (>=2.25.1,<2.26.0)
   1: conflict: foo (1) depends on requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
...
...
...
```
Infinite loop on add conflict
<!-- All the below information must be provided for others to understand and help with your issue. -->
- **Poetry version**: Poetry (version 1.3.2)
- **Python version**: 3.9.11
- **OS version and name**: macOS 13.0.1
- **pyproject.toml**: [Link](https://gist.github.com/JuliusDegesys/1973490db2e8cf8208461e81f9df3e26)

- [X] I am on the [latest](https://github.com/python-poetry/poetry/releases/latest) stable Poetry version, installed using a recommended method.
- [X] I have searched the [issues](https://github.com/python-poetry/poetry/issues) of this repo and believe that this is not a duplicate.
- [X] I have consulted the [FAQ](https://python-poetry.org/docs/faq/) and [blog](https://python-poetry.org/blog/) for any relevant entries or release notes.
- [X] If an exception occurs when executing a command, I executed it again in debug mode (`-vvv` option) and have included the output below.



## Issue
When using a path-based dependency, trying to add a new package that conflicts with a sub-dependency leads to an infinite loop. 

```
$ tree .
.
├── my-package
│   ├── setup.py
│   └── src
└── pyproject.toml
```

cat pyproject.toml
```toml
[tool.poetry]
name = "debug-poetry"
version = "0.1.0"
description = ""
authors = []
readme = "README.md"
packages = [{include = "debug_poetry"}]

[tool.poetry.dependencies]
python = "^3.9"
foo = { path = "./my-package", develop = true }

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
```
my-package/setup.py:
```python
from distutils.core import setup

setup(
    name="foo",
    version="1",
    py_modules=["foo"],
    package_dir={"src": "src"},
    install_requires=["requests~=2.25.1"],
)
```

At first, I run `poetry install --sync --no-root`
```
$ poetry install --sync --no-root              
Creating virtualenv debug-poetry-_DvXptOW-py3.9 in /Users/julius/Library/Caches/pypoetry/virtualenvs
Installing dependencies from lock file

Package operations: 6 installs, 0 updates, 0 removals

  • Installing certifi (2022.12.7)
  • Installing chardet (4.0.0)
  • Installing idna (2.10)
  • Installing urllib3 (1.26.14)
  • Installing requests (2.25.1)
  • Installing foo (1 /Users/julius/src/debug_poetry/my-package)
```
Then, if I try to add `requests@latest`, poetry goes into an infinite loop:
```
$ poetry add -vvv requests@latest | less       
Using virtualenv: /Users/julius/Library/Caches/pypoetry/virtualenvs/debug-poetry-_DvXptOW-py3.9
[keyring.backend] Loading KWallet
[keyring.backend] Loading SecretService
[keyring.backend] Loading Windows
[keyring.backend] Loading chainer
[keyring.backend] Loading libsecret
[keyring.backend] Loading macOS
Creating new session for pypi.org
Source (PyPI): 145 packages found for requests *
Using version ^2.28.2 for requests

Updating dependencies
Resolving dependencies...
   1: fact: debug-poetry is 0.1.0
   1: derived: debug-poetry
   1: fact: debug-poetry depends on foo (*)
   1: fact: debug-poetry depends on requests (^2.28.2)
   1: selecting debug-poetry (0.1.0)
   1: derived: requests (>=2.28.2,<3.0.0)
   1: derived: foo (*) @ file:///Users/julius/src/debug_poetry/my-package
Source (PyPI): 1 packages found for requests >=2.28.2,<3.0.0
   1: fact: foo (1) depends on requests (>=2.25.1,<2.26.0)
   1: derived: not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: fact: foo (1) depends on requests (>=2.25.1,<2.26.0)
   1: conflict: foo (1) depends on requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
   1: ! foo (1) @ file:///Users/julius/src/debug_poetry/my-package is partially satisfied by not foo (1) @ file:///Users/julius/src/debug_poetry/my-package
   1: ! which is caused by "foo (1) @ file:///Users/julius/src/debug_poetry/my-package depends on requests (>=2.25.1,<2.26.0)"
   1: ! thus: foo (1) @ file:///Users/julius/src/debug_poetry/my-package requires requests (>=2.25.1,<2.26.0)
...
...
...
```


Discussion:

Resolving patch:
diff --git a/src/poetry/mixology/version_solver.py b/src/poetry/mixology/version_solver.py
--- 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)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- 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
--- 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
--- /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
--- /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
--- /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
--- 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:
         ...