Repository: pdm-project/pdm
Pull request #3326 (2024-12-02 12:09:45)

feat: Add resolution.overrides support for use_uv
## Pull Request Checklist

Hello this is only a POC just to be sure what I'm doing is not wrong. It seems to work fine on my environment, but I'm not sure about it. @frostming if you think it works, I will be more than happy to add fragment in `news` and also `tests`, if not, don't hesitate to tell me so :)

- [x] A news fragment is added in `news/` describing what is new.
- [x] Test cases added for changed code.

## Describe what you have changed in this PR.


frostming: LGTM. Can you add a news entry?

Patch:
diff --git a/docs/usage/uv.md b/docs/usage/uv.md
index 64bd8992..072c4a9f 100644
--- a/docs/usage/uv.md
+++ b/docs/usage/uv.md
@@ -27,4 +27,4 @@ Despite the significant performance improvements brought by uv, it is important
 - `inherit_metadata` lock strategy is not supported by uv. This will be ignored when writing to the lock file.
 - Update strategies other than `all` and `reuse` are not supported.
 - Editable requirement must be a local path. Requirements like `-e git+<git_url>` are not supported.
-- `overrides` and `excludes` settings under `[tool.pdm.resolution]` are not supported.
+- `excludes` settings under `[tool.pdm.resolution]` are not supported.
diff --git a/news/3330.feature.md b/news/3330.feature.md
new file mode 100644
index 00000000..ff2d450d
--- /dev/null
+++ b/news/3330.feature.md
@@ -0,0 +1,1 @@
+Support `overrides` settings under `[tool.pdm.resolution]` with use_uv
\ No newline at end of file
diff --git a/src/pdm/formats/uv.py b/src/pdm/formats/uv.py
index f311119d..43fca308 100644
--- a/src/pdm/formats/uv.py
+++ b/src/pdm/formats/uv.py
@@ -15,7 +15,7 @@ from pdm.models.markers import Marker, get_marker
 from pdm.models.repositories import LockedRepository, Package
 from pdm.models.requirements import FileRequirement, Requirement, VcsRequirement, parse_requirement, strip_extras
 from pdm.project.core import Project
-from pdm.utils import normalize_name
+from pdm.utils import get_requirement_from_override, normalize_name
 
 
 @dataclass
@@ -35,6 +35,18 @@ class _UvFileBuilder:
 
     def build_pyproject_toml(self) -> Path:
         data = self.project.pyproject._data.unwrap()
+
+        uv_overrides = []
+        for override_key, override_value in (
+            data.get("tool", {}).get("pdm", {}).get("resolution", {}).get("overrides", {}).items()
+        ):
+            uv_overrides.append(f"{get_requirement_from_override(override_key, override_value)}")
+
+        if uv_overrides:
+            data.setdefault("tool", {}).setdefault("uv", {}).setdefault("override-dependencies", []).extend(
+                uv_overrides
+            )
+
         data.setdefault("project", {})["requires-python"] = self.requires_python
         data.setdefault("project", {})["dependencies"] = []
         data.pop("dependency-groups", None)
diff --git a/src/pdm/resolver/providers.py b/src/pdm/resolver/providers.py
index 9fcbf504..17e6cfd5 100644
--- a/src/pdm/resolver/providers.py
+++ b/src/pdm/resolver/providers.py
@@ -4,7 +4,7 @@ import os
 from functools import cached_property
 from typing import TYPE_CHECKING, Callable
 
-from packaging.specifiers import InvalidSpecifier, SpecifierSet
+from packaging.specifiers import InvalidSpecifier
 from packaging.version import InvalidVersion
 from resolvelib import AbstractProvider, RequirementsConflicted
 from resolvelib.resolvers import Criterion
@@ -16,7 +16,13 @@ from pdm.models.requirements import FileRequirement, Requirement, parse_requirem
 from pdm.models.specifiers import PySpecSet
 from pdm.resolver.python import PythonCandidate, PythonRequirement, find_python_matches, is_python_satisfied_by
 from pdm.termui import logger
-from pdm.utils import deprecation_warning, is_url, normalize_name, parse_version, url_without_fragments
+from pdm.utils import (
+    deprecation_warning,
+    get_requirement_from_override,
+    normalize_name,
+    parse_version,
+    url_without_fragments,
+)
 
 if TYPE_CHECKING:
     from typing import Any, Iterable, Iterator, Mapping, Sequence, TypeVar
@@ -136,15 +142,7 @@ class BaseProvider(AbstractProvider[Requirement, Candidate, str]):
         }
         requirements: dict[str, Requirement] = {}
         for name, value in project_overrides.items():
-            if is_url(value):
-                req = f"{name} @ {value}"
-            else:
-                try:
-                    SpecifierSet(value)
-                except InvalidSpecifier:
-                    req = f"{name}=={value}"
-                else:
-                    req = f"{name}{value}"
+            req = get_requirement_from_override(name, value)
             r = parse_requirement(req)
             requirements[r.identify()] = r
 
diff --git a/src/pdm/resolver/uv.py b/src/pdm/resolver/uv.py
index 7d001a5a..b5dd743c 100644
--- a/src/pdm/resolver/uv.py
+++ b/src/pdm/resolver/uv.py
@@ -92,9 +92,6 @@ class UvResolver(Resolver):
         if dt := self.project.core.state.exclude_newer:
             cmd.extend(["--exclude-newer", dt.isoformat()])
 
-        if self.project.core.state.overrides or self.project.pyproject.resolution.get("overrides", {}):
-            self.project.core.ui.warn("PDM overrides are not supported by uv resolver, they will be ignored")
-
         return cmd
 
     def _parse_uv_lock(self, path: Path) -> Resolution:
diff --git a/src/pdm/utils.py b/src/pdm/utils.py
index 262547a4..72e47d01 100644
--- a/src/pdm/utils.py
+++ b/src/pdm/utils.py
@@ -23,6 +23,7 @@ from os import name as os_name
 from pathlib import Path
 from typing import TYPE_CHECKING, Mapping
 
+from packaging.specifiers import InvalidSpecifier, SpecifierSet
 from packaging.version import Version, _cmpkey
 from pbs_installer import PythonVersion
 
@@ -568,3 +569,16 @@ def get_class_init_params(klass: type) -> set[str]:
         if not any(p.kind in (p.VAR_POSITIONAL, p.VAR_KEYWORD) for p in params.values()):
             break
     return arguments
+
+
+def get_requirement_from_override(name: str, value: str) -> str:
+    if is_url(value):
+        req = f"{name} @ {value}"
+    else:
+        try:
+            SpecifierSet(value)
+        except InvalidSpecifier:
+            req = f"{name}=={value}"
+        else:
+            req = f"{name}{value}"
+    return req


Test patch:
diff --git a/tests/resolver/test_resolve.py b/tests/resolver/test_resolve.py
index c7a62ef9..497d4d16 100644
--- a/tests/resolver/test_resolve.py
+++ b/tests/resolver/test_resolve.py
@@ -8,6 +8,7 @@ from pdm.models.requirements import parse_requirement
 from pdm.models.specifiers import PySpecSet
 from pdm.project.lockfile import FLAG_DIRECT_MINIMAL_VERSIONS, FLAG_INHERIT_METADATA
 from pdm.resolver.reporters import LockReporter
+from pdm.utils import get_requirement_from_override
 from tests import FIXTURES
 
 
@@ -426,3 +427,16 @@ def test_resolve_record_markers(resolve, repository, project):
         str(result["b"].req.marker) == 'os_name == "posix" or (os_name == "posix" or os_name == "nt") and '
         'platform_machine == "x86_64" and python_version < "3.8"'
     )
+
+
+@pytest.mark.parametrize(
+    "name,value,output",
+    [
+        ("foo", "1.0", "foo==1.0"),
+        ("foo", "==1.0", "foo==1.0"),
+        ("foo", ">=1.0", "foo>=1.0"),
+        ("foo", "http://foobar.com", "foo @ http://foobar.com"),
+    ],
+)
+def test_requirement_from_override(name, value, output):
+    assert get_requirement_from_override(name, value) == output
diff --git a/tests/resolver/test_uv_resolver.py b/tests/resolver/test_uv_resolver.py
index 54ce3e5f..ee50a8f9 100644
--- a/tests/resolver/test_uv_resolver.py
+++ b/tests/resolver/test_uv_resolver.py
@@ -68,3 +68,15 @@ def test_resolve_dependencies_with_nested_extras(project):
     assert resolution.collected_groups == {"default", "extra1", "extra2", "all"}
     mapping = {p.candidate.identify(): p.candidate for p in resolution.packages}
     assert set(mapping) == {"urllib3", "idna", "chardet"}
+
+
+@pytest.mark.parametrize("overrides", ("2.31.0", "==2.31.0"))
+def test_resolve_dependencies_with_overrides(project, overrides):
+    requirements = ["requests==2.32.0"]
+
+    project.pyproject.settings["resolution"] = {"overrides": {"requests": overrides}}
+
+    resolution = resolve(project.environment, requirements)
+
+    mapping = {p.candidate.identify(): p.candidate for p in resolution.packages}
+    assert mapping["requests"].version == "2.31.0"