diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 4b1f3365c..3ce4f1eec 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -34,6 +34,7 @@ from pip._internal.req.req_file import parse_requirements from pip._internal.req.req_install import InstallRequirement from pip._internal.resolution.base import BaseResolver from pip._internal.self_outdated_check import pip_self_version_check +from pip._internal.utils.misc import merge_config_settings from pip._internal.utils.temp_dir import ( TempDirectory, TempDirectoryTypeRegistry, @@ -434,14 +435,21 @@ class RequirementCommand(IndexGroupCommand): for parsed_req in parse_requirements( filename, finder=finder, options=options, session=session ): + config_settings = ( + parsed_req.options.get("config_settings") + if parsed_req.options + else None + ) + if config_settings and options.config_settings: + config_settings = merge_config_settings( + config_settings, options.config_settings + ) req_to_add = install_req_from_parsed_requirement( parsed_req, isolated=options.isolated_mode, use_pep517=options.use_pep517, user_supplied=True, - config_settings=parsed_req.options.get("config_settings") - if parsed_req.options - else None, + config_settings=config_settings, ) requirements.append(req_to_add) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index baa1ba7ea..63a8c3e1a 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -14,6 +14,8 @@ import stat import sys import sysconfig import urllib.parse +from collections import defaultdict +from collections.abc import Iterable from io import StringIO from itertools import filterfalse, tee, zip_longest from types import TracebackType @@ -24,7 +26,6 @@ from typing import ( ContextManager, Dict, Generator, - Iterable, Iterator, List, Optional, @@ -32,6 +33,7 @@ from typing import ( Tuple, Type, TypeVar, + Union, cast, ) @@ -60,6 +62,7 @@ __all__ = [ "remove_auth_from_url", "check_externally_managed", "ConfiguredBuildBackendHookCaller", + "merge_config_settings", ] logger = logging.getLogger(__name__) @@ -737,3 +740,17 @@ class ConfiguredBuildBackendHookCaller(BuildBackendHookCaller): config_settings=cs, _allow_fallback=_allow_fallback, ) + + +def merge_config_settings( + reqs_settings: Dict[str, Union[str, List[str]]], + cli_settings: Dict[str, Union[str, List[str]]], +) -> Dict[str, Union[str, List[str]]]: + dd = defaultdict(list) + for d in (reqs_settings, cli_settings): + for k, v in d.items(): + if isinstance(v, list): + dd[k].extend(v) + else: + dd[k].append(v) + return dict(dd) diff --git a/tests/functional/test_config_settings.py b/tests/functional/test_config_settings.py index c6a842213..0142fcb22 100644 --- a/tests/functional/test_config_settings.py +++ b/tests/functional/test_config_settings.py @@ -181,3 +181,30 @@ def test_install_config_reqs(script: PipTestEnvironment) -> None: config = script.site_packages_path / "config.json" with open(config, "rb") as f: assert json.load(f) == {"--build-option": ["--cffi", "--avx2"]} + + +def test_merge_cli_reqs_config_settings(script: PipTestEnvironment) -> None: + _, _, project_dir = make_project(script.scratch_path) + a_sdist = create_basic_sdist_for_package( + script, + "foo", + "1.0", + {"pyproject.toml": PYPROJECT_TOML, "backend/dummy_backend.py": BACKEND_SRC}, + ) + script.scratch_path.joinpath("reqs.txt").write_text( + 'foo --config-settings "FOO=HELLO" --config-settings "FOO=BAR"' + ) + script.pip( + "install", + "--no-index", + "-f", + str(a_sdist.parent), + "-r", + "reqs.txt", + "--config-settings", + "FOO=FOOBAR", + ) + script.assert_installed(foo="1.0") + config = script.site_packages_path / "config.json" + with open(config, "rb") as f: + assert json.load(f) == {"FOO": ["HELLO", "BAR", "FOOBAR"]}