diff --git a/news/7122.feature.rst b/news/7122.feature.rst new file mode 100644 index 00000000000..1faf15c21f8 --- /dev/null +++ b/news/7122.feature.rst @@ -0,0 +1 @@ +Print an error message (and exit with a non-zero exit code) when an invalid extra is requested diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 8ceb818a35d..559e2586333 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -13,7 +13,16 @@ import re import sys from itertools import chain, groupby, repeat -from typing import TYPE_CHECKING, Dict, Iterator, List, Literal, Optional, Union +from typing import ( + TYPE_CHECKING, + Dict, + Iterable, + Iterator, + List, + Literal, + Optional, + Union, +) from pip._vendor.rich.console import Console, ConsoleOptions, RenderResult from pip._vendor.rich.markup import escape @@ -22,6 +31,8 @@ if TYPE_CHECKING: from hashlib import _Hash + from pip._vendor.packaging.utils import NormalizedName + from pip._vendor.packaging.version import Version from pip._vendor.requests.models import Request, Response from pip._internal.metadata import BaseDistribution @@ -775,3 +786,26 @@ def __init__(self, *, distribution: "BaseDistribution") -> None: ), hint_stmt=None, ) + + +class UnavailableExtra(InstallationError): + """A requested extra is not available.""" + + def __init__( + self, + base: str, + version: "Version", + extra: str, + available_extras: Iterable["NormalizedName"], + ): + self.base = base + self.version = version + self.extra = extra + self.available_extras = available_extras + + def __str__(self) -> str: + nice_available = " ".join(f'"{e}", ' for e in self.available_extras)[:-2] + return ( + f"{self.base} {self.version} does not provide the extra '{self.extra}'" + f"\n{self.base} provides extras: {nice_available}" + ) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index d30d477be68..142290cd651 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -11,6 +11,7 @@ InstallationSubprocessError, MetadataInconsistent, MetadataInvalid, + UnavailableExtra, ) from pip._internal.metadata import BaseDistribution from pip._internal.models.link import Link, links_equivalent @@ -508,11 +509,11 @@ def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requiremen valid_extras = self.extras.intersection(self.base.dist.iter_provided_extras()) invalid_extras = self.extras.difference(self.base.dist.iter_provided_extras()) for extra in sorted(invalid_extras): - logger.warning( - "%s %s does not provide the extra '%s'", - self.base.name, - self.version, - extra, + raise UnavailableExtra( + base=self.base.name, + version=self.version, + extra=extra, + available_extras=self.base.dist.iter_provided_extras(), ) for r in self.base.dist.iter_dependencies(valid_extras):