Repository: pdm-project/pdm (Python)

Issue:
PDM crashes in case of invalid package metadata
- [x] I have searched the issue tracker and believe that this is not a duplicate.


## Steps to reproduce

Not simple but the error is pretty telling

## Actual behavior

PDM (probably actually unearth) crashes if a package has invalid metadata

```
pdm.termui:   Adding requirement python>=3.11
pdm.termui: ======== Starting round 0 ========
unearth.preparer: Using cached <Link https://files.pythonhosted.org/packages/f4/72/464966f2c60696bae0eae55270f29883e03581d12aeecddf87ef5ffb6752/sentry_sdk-1.9.1-py2.py3-none-any.whl (from https://pypi.org/simple/sentry-sdk/)>
pdm.termui: Error occurs
Traceback (most recent call last):
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/termui.py", line 239, in logging
    yield logger
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/cli/actions.py", line 96, in do_lock
    mapping, dependencies = resolve(
                            ^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/resolver/core.py", line 36, in resolve
    result = resolver.resolve(requirements, max_rounds)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/resolvelib/resolvers.py", line 546, in resolve
    state = resolution.resolve(requirements, max_rounds=max_rounds)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/resolvelib/resolvers.py", line 426, in resolve
    name = min(unsatisfied_names, key=self._get_preference)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/resolvelib/resolvers.py", line 203, in _get_preference
    return self._p.get_preference(
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/resolver/providers.py", line 86, in get_preference
    is_backtrack_cause = any(dep.identify() in backtrack_identifiers for dep in self.get_dependencies(candidate))
                                                                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/resolver/providers.py", line 186, in get_dependencies
    deps, requires_python, _ = self.repository.get_dependencies(candidate)
                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/models/repositories.py", line 85, in get_dependencies
    requirements, requires_python, summary = getter(candidate)
                                             ^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/models/repositories.py", line 44, in wrapper
    result = func(self, candidate)
             ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/models/repositories.py", line 219, in _get_dependencies_from_metadata
    deps = prepared.get_dependencies_from_metadata()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/models/candidates.py", line 584, in get_dependencies_from_metadata
    return filter_requirements_with_extras(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/models/requirements.py", line 451, in filter_requirements_with_extras
    _r = parse_requirement(req)
         ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/mathieu/.local/pipx/venvs/pdm/lib/python3.11/site-packages/pdm/models/requirements.py", line 503, in parse_requirement
    raise RequirementError(str(e)) from None
pdm.exceptions.RequirementError: Expected matching RIGHT_PARENTHESIS for LEFT_PARENTHESIS, after version specifier
    urllib3 (>=1.26.11") ; python_version >="3.6"
            ~~~~~~~~~~^
```
## Expected behavior

Not crash and just discard the invalid package

## Environment Information

```
PDM version:
  2.9.2
Python Interpreter:
  /Users/mathieu/Library/Application Support/pdm/venvs/myproject-tUhlkEEl-venv3.11/bin/python (3.11)
Project Root:
  /Users/mathieu/dev/myproject
Local Packages:

{
  "implementation_name": "cpython",
  "implementation_version": "3.11.5",
  "os_name": "posix",
  "platform_machine": "arm64",
  "platform_release": "22.6.0",
  "platform_system": "Darwin",
  "platform_version": "Darwin Kernel Version 22.6.0: Wed Jul  5 22:17:35 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T8112",
  "python_full_version": "3.11.5",
  "platform_python_implementation": "CPython",
  "python_version": "3.11",
  "sys_platform": "darwin"
}


```



Discussion: Thank you so much 🙏 



Fix (patch):
diff --git a/news/2261.bugfix.md b/news/2261.bugfix.md
new file mode 100644
index 0000000000..53a05c30a4
--- /dev/null
+++ b/news/2261.bugfix.md
@@ -0,0 +1,1 @@
+Reject the candidate if it contains invalid metadata, to avoid a crash in the process of resolution.
diff --git a/src/pdm/resolver/providers.py b/src/pdm/resolver/providers.py
index 0de102f27a..d43b46d6de 100644
--- a/src/pdm/resolver/providers.py
+++ b/src/pdm/resolver/providers.py
@@ -5,8 +5,10 @@
 from typing import TYPE_CHECKING, Callable, cast
 
 from packaging.specifiers import InvalidSpecifier, SpecifierSet
-from resolvelib import AbstractProvider
+from resolvelib import AbstractProvider, RequirementsConflicted
+from resolvelib.resolvers import Criterion
 
+from pdm.exceptions import InvalidPyVersion, RequirementError
 from pdm.models.candidates import Candidate, make_candidate
 from pdm.models.repositories import LockedRepository
 from pdm.models.requirements import FileRequirement, parse_requirement, strip_extras
@@ -16,6 +18,7 @@
     find_python_matches,
     is_python_satisfied_by,
 )
+from pdm.termui import logger
 from pdm.utils import is_url, url_without_fragments
 
 if TYPE_CHECKING:
@@ -81,9 +84,15 @@ def get_preference(
             )
             dep_depth = min(parent_depths, default=0) + 1
         # Use the REAL identifier as it may be updated after candidate preparation.
-        candidate = next(candidates[identifier])
+        deps: list[Requirement] = []
+        for candidate in candidates[identifier]:
+            try:
+                deps = self.get_dependencies(candidate)
+            except RequirementsConflicted:
+                continue
+            break
         self._known_depth[self.identify(candidate)] = dep_depth
-        is_backtrack_cause = any(dep.identify() in backtrack_identifiers for dep in self.get_dependencies(candidate))
+        is_backtrack_cause = any(dep.identify() in backtrack_identifiers for dep in deps)
         is_file_or_url = any(not requirement.is_named for requirement, _ in information[identifier])
         operators = [
             spec.operator for req, _ in information[identifier] if req.specifier is not None for spec in req.specifier
@@ -183,7 +192,13 @@ def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> boo
     def get_dependencies(self, candidate: Candidate) -> list[Requirement]:
         if isinstance(candidate, PythonCandidate):
             return []
-        deps, requires_python, _ = self.repository.get_dependencies(candidate)
+        try:
+            deps, requires_python, _ = self.repository.get_dependencies(candidate)
+        except (RequirementError, InvalidPyVersion, InvalidSpecifier) as e:
+            # When the metadata is invalid, skip this candidate by marking it as conflicting.
+            # Here we pass an empty criterion so it doesn't provide any info to the resolution.
+            logger.error("Invalid metadata in %s: %s", candidate, e)
+            raise RequirementsConflicted(Criterion([], [], [])) from None
 
         # Filter out incompatible dependencies(e.g. functools32) early so that
         # we don't get errors when building wheels.
diff --git a/src/pdm/resolver/reporters.py b/src/pdm/resolver/reporters.py
index 54c5d91a31..a696bbfbd6 100644
--- a/src/pdm/resolver/reporters.py
+++ b/src/pdm/resolver/reporters.py
@@ -72,6 +72,9 @@ def adding_requirement(self, requirement: Requirement, parent: Candidate) -> Non
         logger.info("  Adding requirement %s%s", requirement.as_line(), parent_line)
 
     def rejecting_candidate(self, criterion: Criterion, candidate: Candidate) -> None:
+        if not criterion.information:
+            logger.info("Candidate rejected because it contains invalid metadata: %s", candidate)
+            return
         *others, last = criterion.information
         logger.info(
             "Candidate rejected: %s because it introduces a new requirement %s"