diff --git a/news/9948.feature.rst b/news/9948.feature.rst new file mode 100644 index 00000000000..643181f7a75 --- /dev/null +++ b/news/9948.feature.rst @@ -0,0 +1 @@ +Add per-requirement ``--no-deps`` option support in requirements.txt. diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index c2f4e38bed8..95db23cd58d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -342,6 +342,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, ) @@ -441,6 +442,11 @@ def get_requirements( config_settings=parsed_req.options.get("config_settings") if parsed_req.options else None, + ignore_dependencies=parsed_req.options.get( + "ignore_dependencies", False + ) + if parsed_req.options + else False, ) requirements.append(req_to_add) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index f6a300804f4..5896c4f188d 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -438,7 +438,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) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 2610459228f..ad6e9356c19 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -105,7 +105,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) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index c5ca2d85d51..301d335defa 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -210,6 +210,7 @@ def install_req_from_editable( user_supplied: bool = False, permit_editable_wheels: bool = False, config_settings: Optional[Dict[str, Union[str, List[str]]]] = None, + ignore_dependencies: bool = False, ) -> InstallRequirement: parts = parse_req_from_editable(editable_req) @@ -226,6 +227,7 @@ def install_req_from_editable( global_options=global_options, hash_options=hash_options, config_settings=config_settings, + ignore_dependencies=ignore_dependencies, extras=parts.extras, ) @@ -385,6 +387,7 @@ def install_req_from_line( line_source: Optional[str] = None, user_supplied: bool = False, config_settings: Optional[Dict[str, Union[str, List[str]]]] = None, + ignore_dependencies: bool = False, ) -> InstallRequirement: """Creates an InstallRequirement from a name, which might be a requirement, directory containing 'setup.py', filename, or URL. @@ -404,6 +407,7 @@ def install_req_from_line( global_options=global_options, hash_options=hash_options, config_settings=config_settings, + ignore_dependencies=ignore_dependencies, constraint=constraint, extras=parts.extras, user_supplied=user_supplied, @@ -414,6 +418,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: @@ -443,6 +448,7 @@ def install_req_from_req_string( req, comes_from, isolated=isolated, + ignore_dependencies=ignore_dependencies, use_pep517=use_pep517, user_supplied=user_supplied, ) @@ -454,6 +460,7 @@ def install_req_from_parsed_requirement( use_pep517: Optional[bool] = None, user_supplied: bool = False, config_settings: Optional[Dict[str, Union[str, List[str]]]] = None, + ignore_dependencies: bool = False, ) -> InstallRequirement: if parsed_req.is_editable: req = install_req_from_editable( @@ -464,6 +471,7 @@ def install_req_from_parsed_requirement( isolated=isolated, user_supplied=user_supplied, config_settings=config_settings, + ignore_dependencies=ignore_dependencies, ) else: @@ -484,6 +492,7 @@ def install_req_from_parsed_requirement( line_source=parsed_req.line_source, user_supplied=user_supplied, config_settings=config_settings, + ignore_dependencies=ignore_dependencies, ) return req @@ -500,6 +509,7 @@ def install_req_from_link_and_ireq( use_pep517=ireq.use_pep517, isolated=ireq.isolated, global_options=ireq.global_options, + ignore_dependencies=ireq.ignore_dependencies, hash_options=ireq.hash_options, config_settings=ireq.config_settings, user_supplied=ireq.user_supplied, diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index f717c1ccc79..705f04a4e85 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -73,6 +73,7 @@ cmdoptions.global_options, cmdoptions.hash, cmdoptions.config_settings, + cmdoptions.no_deps, ] # the 'dest' string values @@ -192,6 +193,14 @@ def handle_requirement_line( req_options = {} for dest in SUPPORTED_OPTIONS_REQ_DEST: if dest in line.opts.__dict__ and line.opts.__dict__[dest]: + if ( + dest == "ignore_dependencies" + and options + and "legacy-resolver" in options.deprecated_features_enabled + ): + raise RequirementsFileParseError( + "Cannot ignore dependencies with legacy resolver" + ) req_options[dest] = line.opts.__dict__[dest] line_source = f"line {line.lineno} of {line.filename}" diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 1f479713a94..db80c45c81a 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -81,6 +81,7 @@ def __init__( global_options: Optional[List[str]] = None, hash_options: Optional[Dict[str, List[str]]] = None, config_settings: Optional[Dict[str, Union[str, List[str]]]] = None, + ignore_dependencies: bool = False, constraint: bool = False, extras: Collection[str] = (), user_supplied: bool = False, @@ -148,6 +149,12 @@ def __init__( self.global_options = global_options if global_options else [] self.hash_options = hash_options if hash_options else {} self.config_settings = config_settings + 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 diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index b206692a0a9..17938f475dd 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -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") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index de04e1d73f2..3c776046297 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -68,6 +68,7 @@ def make_install_req_from_link( global_options=template.global_options, hash_options=template.hash_options, config_settings=template.config_settings, + ignore_dependencies=template.ignore_dependencies, ) ireq.original_link = template.original_link ireq.link = link @@ -90,6 +91,7 @@ def make_install_req_from_editable( global_options=template.global_options, hash_options=template.hash_options, config_settings=template.config_settings, + ignore_dependencies=template.ignore_dependencies, ) ireq.extras = template.extras return ireq @@ -114,6 +116,7 @@ def _make_install_req_from_dist( global_options=template.global_options, hash_options=template.hash_options, config_settings=template.config_settings, + ignore_dependencies=template.ignore_dependencies, ) ireq.satisfied_by = dist return ireq @@ -237,6 +240,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: @@ -385,6 +392,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)" @@ -480,6 +491,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 @@ -518,6 +533,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: diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 315fb9c8902..80ee3b7035b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -240,7 +240,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 diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 96cff0dc5da..474da1864a3 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -817,3 +817,53 @@ def test_config_settings_local_to_package( assert "--verbose" not in simple3_args simple2_args = simple2_sdist.args() assert "--verbose" not in simple2_args + + +def test_install_options_no_deps( + script: PipTestEnvironment, resolver_variant: ResolverVariant +) -> 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") + create_basic_wheel_for_package(script, "D", "0.1.0", depends=["E==0.1.0"]) + create_basic_wheel_for_package(script, "E", "0.1.0") + + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text("A[C] --no-deps\nD") + + result = script.pip( + "install", + "--no-cache-dir", + "--find-links", + script.scratch_path, + "-r", + requirements_txt, + "--only-binary=:all:", + expect_error=(resolver_variant == "legacy"), + allow_stderr_warning=True, + ) + if resolver_variant == "legacy": + assert "Cannot ignore dependencies with legacy resolver" in result.stderr + else: + script.assert_installed(A="0.1.0", D="0.1.0", E="0.1.0") + script.assert_not_installed("B", "C") + + # AlreadyInstalledCandidate should not install dependencies + result = script.pip( + "install", + "--no-cache-dir", + "--find-links", + script.scratch_path, + "-r", + requirements_txt, + "--only-binary=:all:", + expect_error=(resolver_variant == "legacy"), + allow_stderr_warning=True, + ) + if resolver_variant == "legacy": + assert "Cannot ignore dependencies with legacy resolver" in result.stderr + else: + script.assert_installed(A="0.1.0", D="0.1.0", E="0.1.0") + script.assert_not_installed("B", "C")