Repository: pdm-project/pdm

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"
}


```


Comments:

Patch:
diff --git a/news/2071.doc.md b/news/2071.doc.md
new file mode 100644
index 00000000..20f852c2
--- /dev/null
+++ b/news/2071.doc.md
@@ -0,0 +1,1 @@
+Clarify what `--no-isolated` does.
\ No newline at end of file
diff --git a/news/2261.bugfix.md b/news/2261.bugfix.md
new file mode 100644
index 00000000..53a05c30
--- /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/cli/options.py b/src/pdm/cli/options.py
index ec5f5f4c..1f431c08 100644
--- a/src/pdm/cli/options.py
+++ b/src/pdm/cli/options.py
@@ -172,7 +172,13 @@ install_group.add_argument(
 install_group.add_argument("--fail-fast", "-x", action="store_true", help="Abort on first installation error")
 
 
-@Option("--no-isolation", dest="build_isolation", nargs=0, help="Do not isolate the build in a clean environment")
+@Option(
+    "--no-isolation",
+    dest="build_isolation",
+    nargs=0,
+    help="Disable isolation when building a source distribution that follows PEP 517, "
+    "as in: build dependencies specified by PEP 518 must be already installed if this option is used.",
+)
 def no_isolation_option(project: Project, namespace: argparse.Namespace, values: str | Sequence[Any] | None) -> None:
     os.environ["PDM_BUILD_ISOLATION"] = "no"
 
diff --git a/src/pdm/resolver/providers.py b/src/pdm/resolver/providers.py
index 0de102f2..d43b46d6 100644
--- a/src/pdm/resolver/providers.py
+++ b/src/pdm/resolver/providers.py
@@ -5,8 +5,10 @@ import os
 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 @@ from pdm.resolver.python import (
     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 @@ class BaseProvider(AbstractProvider):
             )
             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 @@ class BaseProvider(AbstractProvider):
     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 54c5d91a..a696bbfb 100644
--- a/src/pdm/resolver/reporters.py
+++ b/src/pdm/resolver/reporters.py
@@ -72,6 +72,9 @@ class SpinnerReporter(BaseReporter):
         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"


Test patch:
diff --git a/tests/resolver/test_resolve.py b/tests/resolver/test_resolve.py
index 0b338c7a..1ac8e8f5 100644
--- a/tests/resolver/test_resolve.py
+++ b/tests/resolver/test_resolve.py
@@ -320,3 +320,10 @@ def test_resolve_extra_and_underlying_to_the_same_version(resolve, repository):
     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"
+
+
+def test_resolve_skip_candidate_with_invalid_metadata(resolve, repository):
+    repository.add_candidate("sqlparse", "0.4.0")
+    repository.add_dependencies("sqlparse", "0.4.0", ["django>=1.11'"])
+    result = resolve(["sqlparse"], ">=3.6")
+    assert result["sqlparse"].version == "0.3.0"