diff --git a/news/7839.feature.rst b/news/7839.feature.rst new file mode 100644 index 00000000000..dcf923559e5 --- /dev/null +++ b/news/7839.feature.rst @@ -0,0 +1,3 @@ +Add the ``--ignore-constraint`` option to ``pip install``, ``pip download`` +and ``pip wheel`` commands to ignore an individual constraint from a +constraints file. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index d05e502f908..6066441555d 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -415,6 +415,19 @@ def constraints() -> Option: ) +def ignored_constraints() -> Option: + return Option( + "--ignore-constraint", + dest="ignored_constraints", + action="append", + default=[], + metavar="package", + help="Ignore constraints for given package. This is commonly used " + "during development of a package when using a common constraints " + "file. This option be used multiple times.", + ) + + def requirements() -> Option: return Option( "-r", diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 6f2f79c6b3f..e6413806849 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -363,10 +363,13 @@ def make_resolver( ignore_requires_python=ignore_requires_python, force_reinstall=force_reinstall, upgrade_strategy=upgrade_strategy, + ignored_constraints=options.ignored_constraints, py_version_info=py_version_info, ) import pip._internal.resolution.legacy.resolver + # we intentionally don't pass ignored_constraints to this since the + # resolver is deprecated return pip._internal.resolution.legacy.resolver.Resolver( preparer=preparer, finder=finder, diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 54247a78a65..3f8610afe8f 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -37,6 +37,7 @@ class DownloadCommand(RequirementCommand): def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.constraints()) + self.cmd_opts.add_option(cmdoptions.ignored_constraints()) self.cmd_opts.add_option(cmdoptions.requirements()) self.cmd_opts.add_option(cmdoptions.no_deps()) self.cmd_opts.add_option(cmdoptions.global_options()) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index e944bb95a50..927f8e45185 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -72,6 +72,7 @@ class InstallCommand(RequirementCommand): def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.requirements()) self.cmd_opts.add_option(cmdoptions.constraints()) + self.cmd_opts.add_option(cmdoptions.ignored_constraints()) self.cmd_opts.add_option(cmdoptions.no_deps()) self.cmd_opts.add_option(cmdoptions.pre()) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index ed578aa2500..ed3b9de9b39 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -61,6 +61,7 @@ def add_options(self) -> None: self.cmd_opts.add_option(cmdoptions.no_use_pep517()) self.cmd_opts.add_option(cmdoptions.check_build_deps()) self.cmd_opts.add_option(cmdoptions.constraints()) + self.cmd_opts.add_option(cmdoptions.ignored_constraints()) self.cmd_opts.add_option(cmdoptions.editable()) self.cmd_opts.add_option(cmdoptions.requirements()) self.cmd_opts.add_option(cmdoptions.src()) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 315fb9c8902..203950dc44a 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -84,6 +84,8 @@ class PipProvider(_ProviderBase): :params upgrade_strategy: The user-specified upgrade strategy. :params user_requested: A set of canonicalized package names that the user supplied for pip to install/upgrade. + :params ignored_constraints: A list of canonicalized package names that the + user has asked us to ignore constraints for. """ def __init__( @@ -93,12 +95,14 @@ def __init__( ignore_dependencies: bool, upgrade_strategy: str, user_requested: Dict[str, int], + ignored_constraints: Sequence[str], ) -> None: self._factory = factory self._constraints = constraints self._ignore_dependencies = ignore_dependencies self._upgrade_strategy = upgrade_strategy self._user_requested = user_requested + self._ignored_constraints = ignored_constraints self._known_depths: Dict[str, float] = collections.defaultdict(lambda: math.inf) def identify(self, requirement_or_candidate: Union[Requirement, Candidate]) -> str: @@ -223,11 +227,15 @@ def _eligible_for_upgrade(identifier: str) -> bool: return user_order is not None return False - constraint = _get_with_identifier( - self._constraints, - identifier, - default=Constraint.empty(), - ) + if identifier not in self._ignored_constraints: + constraint = _get_with_identifier( + self._constraints, + identifier, + default=Constraint.empty(), + ) + else: + constraint = Constraint.empty() + return self._factory.find_candidates( identifier=identifier, requirements=requirements, diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index c12beef0b2a..5c2d927b49b 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -2,7 +2,7 @@ import functools import logging import os -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast +from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Set, Tuple, cast from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible @@ -50,6 +50,7 @@ def __init__( ignore_requires_python: bool, force_reinstall: bool, upgrade_strategy: str, + ignored_constraints: Sequence[str], py_version_info: Optional[Tuple[int, ...]] = None, ): super().__init__() @@ -68,6 +69,7 @@ def __init__( ) self.ignore_dependencies = ignore_dependencies self.upgrade_strategy = upgrade_strategy + self.ignored_constraints = ignored_constraints self._result: Optional[Result] = None def resolve( @@ -80,6 +82,7 @@ def resolve( ignore_dependencies=self.ignore_dependencies, upgrade_strategy=self.upgrade_strategy, user_requested=collected.user_requested, + ignored_constraints=self.ignored_constraints, ) if "PIP_RESOLVER_DEBUG" in os.environ: reporter: BaseReporter = PipDebuggingReporter() diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 62e56adb2b5..abb2ea1f172 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -681,6 +681,26 @@ def test_new_resolver_constraints( script.assert_not_installed("constraint_only") +def test_new_resolver_constraint_ignored(script: PipTestEnvironment) -> None: + "We can ignore constraints. Useful when hacking on a constrained package." + create_basic_wheel_for_package(script, "pkg", "1.1.dev1") + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("pkg==1.0") + script.pip( + "install", + "--no-cache-dir", + "--no-index", + "--find-links", + script.scratch_path, + "-c", + constraints_file, + "--ignore-constraint", + "pkg", + "pkg", + ) + script.assert_installed(pkg="1.1.dev1") + + def test_new_resolver_constraint_no_specifier(script: PipTestEnvironment) -> None: "It's allowed (but useless...) for a constraint to have no specifier" create_basic_wheel_for_package(script, "pkg", "1.0") diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index a4ee32444e2..4f137f132b9 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -75,4 +75,5 @@ def provider(factory: Factory) -> Iterator[PipProvider]: ignore_dependencies=False, upgrade_strategy="to-satisfy-only", user_requested={}, + ignored_constraints=[], ) diff --git a/tests/unit/resolution_resolvelib/test_provider.py b/tests/unit/resolution_resolvelib/test_provider.py index ab1dc74caa3..44426d0b4de 100644 --- a/tests/unit/resolution_resolvelib/test_provider.py +++ b/tests/unit/resolution_resolvelib/test_provider.py @@ -36,6 +36,7 @@ def test_provider_known_depths(factory: Factory) -> None: ignore_dependencies=False, upgrade_strategy="to-satisfy-only", user_requested={root_requirement_name: 0}, + ignored_constraints=[], ) root_requirement_information = build_requirement_information( diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index 87c2b5f3533..c308239b2a3 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -29,6 +29,7 @@ def resolver(preparer: RequirementPreparer, finder: PackageFinder) -> Resolver: ignore_requires_python=False, force_reinstall=False, upgrade_strategy="to-satisfy-only", + ignored_constraints=[], ) return resolver