Repository: pdm-project/pdm

Issue:
PDM 1.15.4+ unable to update lockfile when package with extras is pinned but extras are not pinned
## Summary

When using 1.15.4 we have seen some projects with large lists of dependencies unable to lock, and PDM eventually quits when it hits the max number of rounds. I think it's due to PDM's algorithm for handling extra dependencies. See the following comment for details.

#### Background
~PDM is unable to find dependency resolution and update the lockfile when run from a docker container in CI, eventually the job times out after 1 hour. When run on a workstation the problem doesn't occur, and the lockfile is able to be successfully updated.~
 
_edit: I thought the issue was the CI environment but it was because our developers were using v1.15.3 on their workstations._

A) One project A, I was unable to figure out why the lock was unsuccessful but when I updated to PDM v2.0.0a it was able to successfully lock both locally and in CI.... 🤔 

B) On project B, a google-cloud-functions project I was able to investigate more, but I'm still confused and think the behavior is a bug.

report about project B continued in below

Comments:

Patch:
diff --git a/news/1173.bugfix.md b/news/1173.bugfix.md
new file mode 100644
index 00000000..d6271486
--- /dev/null
+++ b/news/1173.bugfix.md
@@ -0,0 +1,1 @@
+Fix a bug that requirement extras and underlying are resolved to the different version
diff --git a/pdm/models/repositories.py b/pdm/models/repositories.py
index 1565d40c..76cf7a45 100644
--- a/pdm/models/repositories.py
+++ b/pdm/models/repositories.py
@@ -78,7 +78,11 @@ class BaseRepository:
             # XXX: If the requirement has extras, add the original candidate
             # (without extras) as its dependency. This ensures the same package with
             # different extras resolve to the same version.
-            self_req = dataclasses.replace(candidate.req, extras=None, marker=None)
+            self_req = dataclasses.replace(
+                candidate.req.as_pinned_version(candidate.version),
+                extras=None,
+                marker=None,
+            )
             reqs.append(self_req)
         # Store the metadata on the candidate for caching
         candidate.requires_python = requires_python


Test patch:
diff --git a/tests/resolver/test_resolve.py b/tests/resolver/test_resolve.py
index 26106963..ac2b3acb 100644
--- a/tests/resolver/test_resolve.py
+++ b/tests/resolver/test_resolve.py
@@ -311,3 +311,14 @@ def test_resolve_extra_requirements_no_break_constraints(resolve, repository):
     result = resolve(["foo[chardet]<0.2.0"])
     assert "chardet" in result
     assert result["foo"].version == "0.1.0"
+
+
+def test_resolve_extra_and_underlying_to_the_same_version(resolve, repository):
+    repository.add_candidate("foo", "0.1.0")
+    repository.add_dependencies("foo", "0.1.0", ["chardet; extra=='enc'"])
+    repository.add_candidate("foo", "0.2.0")
+    repository.add_dependencies("foo", "0.2.0", ["chardet; extra=='enc'"])
+    repository.add_candidate("bar", "0.1.0")
+    repository.add_dependencies("bar", "0.1.0", ["foo[enc]>=0.1.0"])
+    result = resolve(["foo==0.1.0", "bar"])
+    assert result["foo"].version == result["foo[enc]"].version == "0.1.0"