Skip to content

Allow ignoring sub-dependencies #12790

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions news/9948.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Implement the ``--no-deps-for`` switch for the ``install`` command and the
requirements file. This switch will prevent the installation of dependencies of
specific packages, as opposed to the ``--no-deps`` switch which prevents the
installation dependencies of all requested packages.

Also implement recognition of the global ``--no-deps`` switch in the requirements
file.
24 changes: 24 additions & 0 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,30 @@ def only_binary() -> Option:
)


def _handle_no_deps_for(
option: object,
opt_str: str,
value: str,
parser: OptionParser,
) -> None:
# ignore_dependencies_for is a set of strings
values = value.split(",")
parser.values.ignore_dependencies_for.update(values)


def no_deps_for() -> PipOption:
return PipOption(
"--no-deps-for",
dest="ignore_dependencies_for",
action="callback",
callback=_handle_no_deps_for,
type="package_name",
default=set(),
help="Do not install sub dependencies of the named package or packages. "
"Accepts comma separated list of values. Can be supplied multiple times.",
)


platforms: Callable[..., Option] = partial(
Option,
"--platform",
Expand Down
9 changes: 8 additions & 1 deletion src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ class RequirementCommand(IndexGroupCommand):
def __init__(self, *args: Any, **kw: Any) -> None:
super().__init__(*args, **kw)

self.cmd_opts.add_option(cmdoptions.no_clean())
self.cmd_opts.add_options(
[
cmdoptions.no_clean(),
cmdoptions.no_deps_for(),
]
)

@staticmethod
def determine_resolver_variant(options: Values) -> str:
Expand Down Expand Up @@ -186,6 +191,7 @@ def make_resolver(
force_reinstall=force_reinstall,
upgrade_strategy=upgrade_strategy,
py_version_info=py_version_info,
ignore_dependencies_for=options.ignore_dependencies_for,
)
import pip._internal.resolution.legacy.resolver

Expand All @@ -201,6 +207,7 @@ def make_resolver(
force_reinstall=force_reinstall,
upgrade_strategy=upgrade_strategy,
py_version_info=py_version_info,
ignore_dependencies_for=options.ignore_dependencies_for,
)

def get_requirements(
Expand Down
3 changes: 2 additions & 1 deletion src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,8 @@ def run(self, options: Values, args: List[str]) -> int:
# Check for conflicts in the package set we're installing.
conflicts: Optional[ConflictDetails] = None
should_warn_about_conflicts = (
not options.ignore_dependencies and options.warn_about_conflicts
not (options.ignore_dependencies or options.ignore_dependencies_for)
and options.warn_about_conflicts
)
if should_warn_about_conflicts:
conflicts = self._determine_conflicts(to_install)
Expand Down
4 changes: 4 additions & 0 deletions src/pip/_internal/req/req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
cmdoptions.find_links,
cmdoptions.no_binary,
cmdoptions.only_binary,
cmdoptions.no_deps,
cmdoptions.no_deps_for,
cmdoptions.prefer_binary,
cmdoptions.require_hashes,
cmdoptions.pre,
Expand Down Expand Up @@ -225,6 +227,8 @@ def handle_option_line(
options.features_enabled.extend(
f for f in opts.features_enabled if f not in options.features_enabled
)
options.ignore_dependencies |= opts.ignore_dependencies
options.ignore_dependencies_for.update(opts.ignore_dependencies_for)

# set finder options
if finder:
Expand Down
10 changes: 8 additions & 2 deletions src/pip/_internal/resolution/legacy/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import sys
from collections import defaultdict
from itertools import chain
from typing import DefaultDict, Iterable, List, Optional, Set, Tuple
from typing import Container, DefaultDict, Iterable, List, Optional, Set, Tuple

from pip._vendor.packaging import specifiers
from pip._vendor.packaging.requirements import Requirement
Expand Down Expand Up @@ -126,6 +126,7 @@ def __init__(
force_reinstall: bool,
upgrade_strategy: str,
py_version_info: Optional[Tuple[int, ...]] = None,
ignore_dependencies_for: Container[str] = frozenset(),
) -> None:
super().__init__()
assert upgrade_strategy in self._allowed_strategies
Expand All @@ -136,6 +137,7 @@ def __init__(
py_version_info = normalize_version_info(py_version_info)

self._py_version_info = py_version_info
self.ignore_dependencies_for = ignore_dependencies_for

self.preparer = preparer
self.finder = finder
Expand Down Expand Up @@ -542,7 +544,11 @@ def add_req(subreq: Requirement, extras_requested: Iterable[str]) -> None:
requirement_set, req_to_install, parent_req_name=None
)

if not self.ignore_dependencies:
ignore_dependencies = (
self.ignore_dependencies
or dist.canonical_name in self.ignore_dependencies_for
)
if not ignore_dependencies:
if req_to_install.extras:
logger.debug(
"Installing extra requirements: %r",
Expand Down
8 changes: 7 additions & 1 deletion src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from functools import lru_cache
from typing import (
TYPE_CHECKING,
Container,
Dict,
Iterable,
Iterator,
Expand Down Expand Up @@ -94,10 +95,12 @@ def __init__(
ignore_dependencies: bool,
upgrade_strategy: str,
user_requested: Dict[str, int],
ignore_dependencies_for: Container[str] = frozenset(),
) -> None:
self._factory = factory
self._constraints = constraints
self._ignore_dependencies = ignore_dependencies
self._ignore_dependencies_for = ignore_dependencies_for
self._upgrade_strategy = upgrade_strategy
self._user_requested = user_requested
self._known_depths: Dict[str, float] = collections.defaultdict(lambda: math.inf)
Expand Down Expand Up @@ -243,7 +246,10 @@ def is_satisfied_by(self, requirement: Requirement, candidate: Candidate) -> boo
return requirement.is_satisfied_by(candidate)

def get_dependencies(self, candidate: Candidate) -> Sequence[Requirement]:
with_requires = not self._ignore_dependencies
ignore_dependencies = (
self._ignore_dependencies or candidate.name in self._ignore_dependencies_for
)
with_requires = not ignore_dependencies
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]

@staticmethod
Expand Down
5 changes: 4 additions & 1 deletion src/pip/_internal/resolution/resolvelib/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, Container, Dict, List, Optional, Set, Tuple, cast

from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
Expand Down Expand Up @@ -51,6 +51,7 @@ def __init__(
force_reinstall: bool,
upgrade_strategy: str,
py_version_info: Optional[Tuple[int, ...]] = None,
ignore_dependencies_for: Container[str] = frozenset(),
):
super().__init__()
assert upgrade_strategy in self._allowed_strategies
Expand All @@ -67,6 +68,7 @@ def __init__(
py_version_info=py_version_info,
)
self.ignore_dependencies = ignore_dependencies
self.ignore_dependencies_for = ignore_dependencies_for
self.upgrade_strategy = upgrade_strategy
self._result: Optional[Result] = None

Expand All @@ -80,6 +82,7 @@ def resolve(
ignore_dependencies=self.ignore_dependencies,
upgrade_strategy=self.upgrade_strategy,
user_requested=collected.user_requested,
ignore_dependencies_for=self.ignore_dependencies_for,
)
if "PIP_RESOLVER_DEBUG" in os.environ:
reporter: BaseReporter = PipDebuggingReporter()
Expand Down
75 changes: 75 additions & 0 deletions tests/functional/test_new_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,81 @@ def test_new_resolver_ignore_dependencies(script: PipTestEnvironment) -> None:
script.assert_not_installed("dep")


def test_new_resolver_ignore_dependencies_for(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script,
"base",
"0.1.0",
depends=["dep"],
)
create_basic_wheel_for_package(
script,
"dep",
"0.1.0",
)
create_basic_wheel_for_package(
script,
"base2",
"0.1.0",
depends=["dep2"],
)
create_basic_wheel_for_package(
script,
"dep2",
"0.1.0",
)
script.pip(
"install",
"--no-cache-dir",
"--no-index",
"--no-deps-for=base",
"--find-links",
script.scratch_path,
"base",
"base2",
allow_stderr_error=True,
)
script.assert_installed(base="0.1.0", base2="0.1.0", dep2="0.1.0")
script.assert_not_installed("dep")


def test_new_resolver_no_deps_in_requirements(
tmpdir: pathlib.Path,
script: PipTestEnvironment,
) -> None:
create_basic_wheel_for_package(
script,
"base",
"0.1.0",
depends=["dep"],
)
create_basic_wheel_for_package(
script,
"dep",
"0.1.0",
)

req_file = tmpdir / "requirements.txt"
req_file.write_text(
"""
base==0.1.0
--no-deps
"""
)

script.pip(
"install",
"--no-cache-dir",
"--no-index",
"--find-links",
script.scratch_path,
"-r",
req_file,
)
script.assert_installed(base="0.1.0")
script.assert_not_installed("dep")


@pytest.mark.parametrize(
"root_dep",
[
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/test_req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ def options(session: PipSession) -> mock.Mock:
index_url="default_url",
format_control=FormatControl(set(), set()),
features_enabled=[],
ignore_dependencies=False,
ignore_dependencies_for=set(),
)


Expand Down Expand Up @@ -763,6 +765,43 @@ def test_join_lines(self, tmpdir: Path, finder: PackageFinder) -> None:

assert finder.index_urls == ["url1", "url2"]

def test_ignore_dependencies(self, tmpdir: Path, options: mock.Mock) -> None:
req = tmpdir / "req1.txt"
req.write_text(
"""
--no-deps
"""
)

list(
parse_reqfile(
req,
session=PipSession(),
options=options,
)
)

assert options.ignore_dependencies

def test_ignore_dependencies_for(self, tmpdir: Path, options: mock.Mock) -> None:
req = tmpdir / "req1.txt"
req.write_text(
"""
--no-deps-for=foo,bar
--no-deps-for=spam,eggs
"""
)

list(
parse_reqfile(
req,
session=PipSession(),
options=options,
)
)

assert options.ignore_dependencies_for == {"foo", "bar", "spam", "eggs"}

def test_req_file_parse_no_only_binary(
self, data: TestData, finder: PackageFinder
) -> None:
Expand Down
Loading
Loading