Skip to content
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

Add per-requirement --no-deps option support in requirements.txt #10837

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions news/9948.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add per-requirement ``--no-deps`` option support in requirements.txt.
1 change: 1 addition & 0 deletions src/pip/_internal/cli/req_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ def make_resolver(
"""
make_install_req = partial(
install_req_from_req_string,
ignore_dependencies=options.ignore_dependencies,
isolated=options.isolated_mode,
use_pep517=use_pep517,
)
Expand Down
6 changes: 5 additions & 1 deletion src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,11 @@ 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 any((i for i in to_install if i.ignore_dependencies))
)
and options.warn_about_conflicts
)
if should_warn_about_conflicts:
conflicts = self._determine_conflicts(to_install)
Expand Down
4 changes: 3 additions & 1 deletion src/pip/_internal/operations/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ def check_install_conflicts(to_install: List[InstallRequirement]) -> ConflictDet
# Start from the current state
package_set, _ = create_package_set_from_installed()
# Install packages
would_be_installed = _simulate_installation_of(to_install, package_set)
would_be_installed = _simulate_installation_of(
[x for x in to_install if not x.ignore_dependencies], package_set
)

# Only warn about directly-dependent packages; create a whitelist of them
whitelist = _create_whitelist(would_be_installed, package_set)
Expand Down
10 changes: 10 additions & 0 deletions src/pip/_internal/req/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ def install_req_from_editable(
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
ignore_dependencies=options.get("ignore_dependencies", False)
if options
else False,
extras=parts.extras,
)

Expand Down Expand Up @@ -399,6 +402,9 @@ def install_req_from_line(
install_options=options.get("install_options", []) if options else [],
global_options=options.get("global_options", []) if options else [],
hash_options=options.get("hashes", {}) if options else {},
ignore_dependencies=options.get("ignore_dependencies", False)
if options
else False,
constraint=constraint,
extras=parts.extras,
user_supplied=user_supplied,
Expand All @@ -409,6 +415,7 @@ def install_req_from_req_string(
req_string: str,
comes_from: Optional[InstallRequirement] = None,
isolated: bool = False,
ignore_dependencies: bool = False,
use_pep517: Optional[bool] = None,
user_supplied: bool = False,
) -> InstallRequirement:
Expand Down Expand Up @@ -438,6 +445,7 @@ def install_req_from_req_string(
req,
comes_from,
isolated=isolated,
ignore_dependencies=ignore_dependencies,
use_pep517=use_pep517,
user_supplied=user_supplied,
)
Expand All @@ -456,6 +464,7 @@ def install_req_from_parsed_requirement(
use_pep517=use_pep517,
constraint=parsed_req.constraint,
isolated=isolated,
options=parsed_req.options,
user_supplied=user_supplied,
)

Expand Down Expand Up @@ -486,5 +495,6 @@ def install_req_from_link_and_ireq(
isolated=ireq.isolated,
install_options=ireq.install_options,
global_options=ireq.global_options,
ignore_dependencies=ireq.ignore_dependencies,
hash_options=ireq.hash_options,
)
1 change: 1 addition & 0 deletions src/pip/_internal/req/req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
cmdoptions.install_options,
cmdoptions.global_options,
cmdoptions.hash,
cmdoptions.no_deps,
]

# the 'dest' string values
Expand Down
7 changes: 7 additions & 0 deletions src/pip/_internal/req/req_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def __init__(
install_options: Optional[List[str]] = None,
global_options: Optional[List[str]] = None,
hash_options: Optional[Dict[str, List[str]]] = None,
ignore_dependencies: bool = False,
constraint: bool = False,
extras: Collection[str] = (),
user_supplied: bool = False,
Expand Down Expand Up @@ -138,6 +139,12 @@ def __init__(
self.install_options = install_options if install_options else []
self.global_options = global_options if global_options else []
self.hash_options = hash_options if hash_options else {}
self.ignore_dependencies = ignore_dependencies
if (
isinstance(comes_from, InstallRequirement)
and comes_from.ignore_dependencies
):
self.ignore_dependencies = True
# Set to True after successful preparation of this requirement
self.prepared = False
# User supplied requirement are explicitly requested for installation
Expand Down
4 changes: 4 additions & 0 deletions src/pip/_internal/resolution/resolvelib/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ def is_editable(self) -> bool:
def source_link(self) -> Optional[Link]:
raise NotImplementedError("Override in subclass")

@property
def ignore_dependencies(self) -> bool:
raise NotImplementedError("Override in subclass")

def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
raise NotImplementedError("Override in subclass")

Expand Down
16 changes: 16 additions & 0 deletions src/pip/_internal/resolution/resolvelib/candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def make_install_req_from_link(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
ignore_dependencies=template.ignore_dependencies,
),
)
ireq.original_link = template.original_link
Expand All @@ -91,6 +92,7 @@ def make_install_req_from_editable(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
ignore_dependencies=template.ignore_dependencies,
),
)

Expand All @@ -115,6 +117,7 @@ def _make_install_req_from_dist(
install_options=template.install_options,
global_options=template.global_options,
hashes=template.hash_options,
ignore_dependencies=template.ignore_dependencies,
),
)
ireq.satisfied_by = dist
Expand Down Expand Up @@ -239,6 +242,10 @@ def _prepare(self) -> BaseDistribution:
self._check_metadata_consistency(dist)
return dist

@property
def ignore_dependencies(self) -> bool:
return self._ireq.ignore_dependencies

def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
requires = self.dist.iter_dependencies() if with_requires else ()
for r in requires:
Expand Down Expand Up @@ -377,6 +384,10 @@ def version(self) -> CandidateVersion:
def is_editable(self) -> bool:
return self.dist.editable

@property
def ignore_dependencies(self) -> bool:
return self._ireq.ignore_dependencies

def format_for_error(self) -> str:
return f"{self.name} {self.version} (Installed)"

Expand Down Expand Up @@ -472,6 +483,10 @@ def is_editable(self) -> bool:
def source_link(self) -> Optional[Link]:
return self.base.source_link

@property
def ignore_dependencies(self) -> bool:
return self.base.ignore_dependencies

def iter_dependencies(self, with_requires: bool) -> Iterable[Optional[Requirement]]:
factory = self.base._factory

Expand Down Expand Up @@ -510,6 +525,7 @@ def get_install_requirement(self) -> Optional[InstallRequirement]:
class RequiresPythonCandidate(Candidate):
is_installed = False
source_link = None
ignore_dependencies = False

def __init__(self, py_version_info: Optional[Tuple[int, ...]]) -> None:
if py_version_info is not None:
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/resolution/resolvelib/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ 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
with_requires = not (self._ignore_dependencies or candidate.ignore_dependencies)
return [r for r in candidate.iter_dependencies(with_requires) if r is not None]

@staticmethod
Expand Down
35 changes: 35 additions & 0 deletions tests/functional/test_install_reqs.py
Original file line number Diff line number Diff line change
Expand Up @@ -816,3 +816,38 @@ def test_location_related_install_option_fails(script: PipTestEnvironment) -> No
expect_error=True,
)
assert "['--home'] from simple" in result.stderr


def test_install_options_no_deps(script: PipTestEnvironment) -> None:
create_basic_wheel_for_package(
script, "A", "0.1.0", depends=["B==0.1.0"], extras={"C": ["C"]}
)
create_basic_wheel_for_package(script, "B", "0.1.0")
create_basic_wheel_for_package(script, "C", "0.1.0")

requirements_txt = script.scratch_path / "requirements.txt"
requirements_txt.write_text("A[C] --no-deps")
q0w marked this conversation as resolved.
Show resolved Hide resolved
q0w marked this conversation as resolved.
Show resolved Hide resolved

script.pip(
"install",
"--no-cache-dir",
"--find-links",
script.scratch_path,
"-r",
requirements_txt,
"--only-binary=:all:",
)
script.assert_installed(A="0.1.0")
script.assert_not_installed("B", "C")

# AlreadyInstalledCandidate should not install dependencies
script.pip(
"install",
"--no-cache-dir",
"--find-links",
script.scratch_path,
"-r",
requirements_txt,
"--only-binary=:all:",
)
script.assert_not_installed("B", "C")