Repository: pdm-project/pdm

Issue:
Maximum recursion depth exceeded
- [x] I have searched the issue tracker and believe that this is not a duplicate.

**Make sure you run commands with `-v` flag before pasting the output.**

## Steps to reproduce

<!--Describe the minimized example of how to reproduce the bug-->

1. Initialize a PDM project with `pdm init -n`
2. Run `pdm add -v "mlserver @ git+https://github.com/SeldonIO/MLServer.git@1.4.0"`

## Actual behavior

<!--A clear and concise description the result of the above steps-->

An error is being repeated endlessly for maximum recursion depth excession:

```
pdm.termui: Error parsing pyproject.toml, metadata may be incomplete.
group_dependencies: maximum recursion depth exceeded in comparison at line 111 col 0
pdm.termui: Error parsing pyproject.toml, metadata may be incomplete.
group_dependencies: maximum recursion depth exceeded while calling a Python object at line 107 col 0
```

Is this because MLServer is a poetry project?

## Expected behavior

<!--A clear and concise description of what you expected to happen.-->

A dependency is added into pyproject.toml for the SeldonIO/MLServer git repository with specific git tag revision.

## Environment Information

```bash
# Paste the output of `pdm info && pdm info --env` below:
pdm info --env
{
  "implementation_name": "cpython",
  "implementation_version": "3.11.9",
  "os_name": "posix",
  "platform_machine": "arm64",
  "platform_release": "23.5.0",
  "platform_system": "Darwin",
  "platform_version": "Darwin Kernel Version 23.5.0: Wed May  1 20:12:58 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6000",
  "python_full_version": "3.11.9",
  "platform_python_implementation": "CPython",
  "python_version": "3.11",
  "sys_platform": "darwin"
}
```

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pdm-project/pdm/pull/2905?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 84.78%. Comparing base [(`06d4592`)](https://app.codecov.io/gh/pdm-project/pdm/commit/06d459247d9e5109e9321c6165afa941dd20fdca?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) to head [(`4b6ddac`)](https://app.codecov.io/gh/pdm-project/pdm/commit/4b6ddac7a11e575a9cf296e32a830accf80d09a7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2905      +/-   ##
==========================================
- Coverage   85.32%   84.78%   -0.55%     
==========================================
  Files         106      106              
  Lines       10596    10605       +9     
  Branches     2307     2308       +1     
==========================================
- Hits         9041     8991      -50     
- Misses       1089     1137      +48     
- Partials      466      477      +11     
```

| [Flag](https://app.codecov.io/gh/pdm-project/pdm/pull/2905/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/pdm-project/pdm/pull/2905/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | `84.66% <100.00%> (-0.44%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pdm-project/pdm/pull/2905?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).


Patch:
diff --git a/news/2900.bugfix.md b/news/2900.bugfix.md
new file mode 100644
index 00000000..0fd7acac
--- /dev/null
+++ b/news/2900.bugfix.md
@@ -0,0 +1,1 @@
+Fix an issue of max recursion depth error when parsing a poetry project with circular dependencies on local packages.
diff --git a/src/pdm/formats/poetry.py b/src/pdm/formats/poetry.py
index 440bb9cb..0f7a9cf8 100644
--- a/src/pdm/formats/poetry.py
+++ b/src/pdm/formats/poetry.py
@@ -17,7 +17,7 @@ from pdm.formats.base import (
     make_inline_table,
 )
 from pdm.models.markers import Marker, get_marker
-from pdm.models.requirements import Requirement
+from pdm.models.requirements import FileRequirement, Requirement
 from pdm.models.specifiers import PySpecSet
 from pdm.utils import cd
 
@@ -67,11 +67,21 @@ def _convert_python(python: str) -> PySpecSet:
 
 
 def _convert_req(name: str, req_dict: RequirementDict | list[RequirementDict]) -> Iterable[str]:
+    from pdm.models.backends import DEFAULT_BACKEND
+
+    backend = DEFAULT_BACKEND(Path.cwd())
+
+    def fix_req_path(req: Requirement) -> Requirement:
+        if isinstance(req, FileRequirement):
+            req.relocate(backend)
+        return req
+
     if isinstance(req_dict, list):
         for req in req_dict:
             yield from _convert_req(name, req)
     elif isinstance(req_dict, str):
-        yield Requirement.from_req_dict(name, _convert_specifier(req_dict)).as_line()
+        pdm_req = fix_req_path(Requirement.from_req_dict(name, _convert_specifier(req_dict), check_installable=False))
+        yield pdm_req.as_line()
     else:
         assert isinstance(req_dict, dict)
         req_dict = dict(req_dict)
@@ -90,7 +100,8 @@ def _convert_req(name: str, req_dict: RequirementDict | list[RequirementDict]) -
                 "rev",
                 req_dict.pop("tag", req_dict.pop("branch", None)),  # type: ignore[arg-type]
             )
-        yield Requirement.from_req_dict(name, req_dict).as_line()
+        pdm_req = fix_req_path(Requirement.from_req_dict(name, req_dict, check_installable=False))
+        yield pdm_req.as_line()
 
 
 # See https://github.com/python-poetry/poetry-core/pull/521#issuecomment-1327689551
diff --git a/src/pdm/models/requirements.py b/src/pdm/models/requirements.py
index ec85fd72..37c644f8 100644
--- a/src/pdm/models/requirements.py
+++ b/src/pdm/models/requirements.py
@@ -169,7 +169,7 @@ class Requirement:
         return NamedRequirement.create(name=dist.metadata["Name"], version=f"=={dist.version}")
 
     @classmethod
-    def from_req_dict(cls, name: str, req_dict: RequirementDict) -> Requirement:
+    def from_req_dict(cls, name: str, req_dict: RequirementDict, check_installable: bool = True) -> Requirement:
         if isinstance(req_dict, str):  # Version specifier only.
             return NamedRequirement(name=name, specifier=get_specifier(req_dict))
         for vcs in VCS_SCHEMA:
@@ -178,7 +178,7 @@ class Requirement:
                 url = f"{vcs}+{repo}"
                 return VcsRequirement.create(name=name, vcs=vcs, url=url, **req_dict)
         if "path" in req_dict or "url" in req_dict:
-            return FileRequirement.create(name=name, **req_dict)
+            return FileRequirement.create(name=name, **req_dict, check_installable=check_installable)
         return NamedRequirement.create(name=name, **req_dict)
 
     @property
@@ -241,11 +241,12 @@ class FileRequirement(Requirement):
     url: str = ""
     path: Path | None = None
     subdirectory: str | None = None
+    check_installable: bool = True
 
     def __post_init__(self) -> None:
         super().__post_init__()
         self._parse_url()
-        if self.is_local_dir:
+        if self.is_local_dir and self.check_installable:
             self._check_installable()
 
     def _hash_key(self) -> tuple:


Test patch:
diff --git a/tests/fixtures/projects/poetry-with-circular-dep/packages/child/child/__init__.py b/tests/fixtures/projects/poetry-with-circular-dep/packages/child/child/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/fixtures/projects/poetry-with-circular-dep/packages/child/pyproject.toml b/tests/fixtures/projects/poetry-with-circular-dep/packages/child/pyproject.toml
new file mode 100644
index 00000000..a9946e76
--- /dev/null
+++ b/tests/fixtures/projects/poetry-with-circular-dep/packages/child/pyproject.toml
@@ -0,0 +1,17 @@
+[tool.poetry]
+name = "child"
+version = "0.1.0"
+authors = ["PDM <admin@pdm-project.org>"]
+description = "Child project"
+license = "Apache-2.0"
+packages = [{include = "child"}]
+
+[tool.poetry.dependencies]
+python = "^3.8.1"
+
+[tool.poetry.group.dev.dependencies]
+parent = {path = "../..", develop = true}
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/fixtures/projects/poetry-with-circular-dep/parent/__init__.py b/tests/fixtures/projects/poetry-with-circular-dep/parent/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/fixtures/projects/poetry-with-circular-dep/pyproject.toml b/tests/fixtures/projects/poetry-with-circular-dep/pyproject.toml
new file mode 100644
index 00000000..241ea498
--- /dev/null
+++ b/tests/fixtures/projects/poetry-with-circular-dep/pyproject.toml
@@ -0,0 +1,17 @@
+[tool.poetry]
+name = "parent"
+version = "0.1.0"
+authors = ["PDM <admin@pdm-project.org>"]
+description = "Parent project"
+license = "Apache-2.0"
+packages = [{include = "parent"}]
+
+[tool.poetry.dependencies]
+python = "^3.8.1"
+
+[tool.poetry.group.dev.dependencies]
+child = {path = "./packages/child", develop = true}
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/tests/test_formats.py b/tests/test_formats.py
index c72f4db0..52831118 100644
--- a/tests/test_formats.py
+++ b/tests/test_formats.py
@@ -234,3 +234,14 @@ def test_convert_setup_py_project(project):
         "scripts": {"mycli": "mymodule:main"},
     }
     assert settings == {"package-dir": "src"}
+
+
+def test_convert_poetry_project_with_circular_dependency(project):
+    parent_file = FIXTURES / "projects/poetry-with-circular-dep/pyproject.toml"
+    child_file = FIXTURES / "projects/poetry-with-circular-dep/packages/child/pyproject.toml"
+
+    _, settings = poetry.convert(project, parent_file, ns())
+    assert settings["dev-dependencies"]["dev"] == ["child @ file:///${PROJECT_ROOT}/packages/child"]
+
+    _, settings = poetry.convert(project, child_file, ns())
+    assert settings["dev-dependencies"]["dev"] == ["parent @ file:///${PROJECT_ROOT}/../.."]