Skip to content

Commit 3ac7e10

Browse files
authored
Merge pull request #11634 from q0w/per-req-config-settings
2 parents 7cb863e + 110a26f commit 3ac7e10

File tree

7 files changed

+166
-13
lines changed

7 files changed

+166
-13
lines changed

news/11325.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support a per-requirement ``--config-settings`` option in requirements files.

src/pip/_internal/cli/req_command.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,9 @@ def get_requirements(
438438
isolated=options.isolated_mode,
439439
use_pep517=options.use_pep517,
440440
user_supplied=True,
441+
config_settings=parsed_req.options.get("config_settings")
442+
if parsed_req.options
443+
else None,
441444
)
442445
requirements.append(req_to_add)
443446

src/pip/_internal/req/constructors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ def install_req_from_parsed_requirement(
453453
isolated: bool = False,
454454
use_pep517: Optional[bool] = None,
455455
user_supplied: bool = False,
456+
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
456457
) -> InstallRequirement:
457458
if parsed_req.is_editable:
458459
req = install_req_from_editable(
@@ -462,6 +463,7 @@ def install_req_from_parsed_requirement(
462463
constraint=parsed_req.constraint,
463464
isolated=isolated,
464465
user_supplied=user_supplied,
466+
config_settings=config_settings,
465467
)
466468

467469
else:
@@ -481,6 +483,7 @@ def install_req_from_parsed_requirement(
481483
constraint=parsed_req.constraint,
482484
line_source=parsed_req.line_source,
483485
user_supplied=user_supplied,
486+
config_settings=config_settings,
484487
)
485488
return req
486489

src/pip/_internal/req/req_file.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
7272
cmdoptions.global_options,
7373
cmdoptions.hash,
74+
cmdoptions.config_settings,
7475
]
7576

7677
# the 'dest' string values

tests/functional/test_config_settings.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from zipfile import ZipFile
66

77
from pip._internal.utils.urls import path_to_url
8-
from tests.lib import PipTestEnvironment
8+
from tests.lib import PipTestEnvironment, create_basic_sdist_for_package
99

1010
PYPROJECT_TOML = """\
1111
[build-system]
@@ -123,6 +123,20 @@ def test_backend_sees_config(script: PipTestEnvironment) -> None:
123123
assert json.loads(output) == {"FOO": "Hello"}
124124

125125

126+
def test_backend_sees_config_reqs(script: PipTestEnvironment) -> None:
127+
name, version, project_dir = make_project(script.scratch_path)
128+
script.scratch_path.joinpath("reqs.txt").write_text(
129+
f"{project_dir} --config-settings FOO=Hello"
130+
)
131+
script.pip("wheel", "-r", "reqs.txt")
132+
wheel_file_name = f"{name}-{version}-py3-none-any.whl"
133+
wheel_file_path = script.cwd / wheel_file_name
134+
with open(wheel_file_path, "rb") as f:
135+
with ZipFile(f) as z:
136+
output = z.read(f"{name}-config.json")
137+
assert json.loads(output) == {"FOO": "Hello"}
138+
139+
126140
def test_backend_sees_config_via_constraint(script: PipTestEnvironment) -> None:
127141
name, version, project_dir = make_project(script.scratch_path)
128142
constraints_file = script.scratch_path / "constraints.txt"
@@ -244,6 +258,17 @@ def test_install_sees_config(script: PipTestEnvironment) -> None:
244258
assert json.load(f) == {"FOO": "Hello"}
245259

246260

261+
def test_install_sees_config_reqs(script: PipTestEnvironment) -> None:
262+
name, _, project_dir = make_project(script.scratch_path)
263+
script.scratch_path.joinpath("reqs.txt").write_text(
264+
f"{project_dir} --config-settings FOO=Hello"
265+
)
266+
script.pip("install", "-r", "reqs.txt")
267+
config = script.site_packages_path / f"{name}-config.json"
268+
with open(config, "rb") as f:
269+
assert json.load(f) == {"FOO": "Hello"}
270+
271+
247272
def test_install_editable_sees_config(script: PipTestEnvironment) -> None:
248273
name, _, project_dir = make_project(script.scratch_path)
249274
script.pip(
@@ -256,3 +281,23 @@ def test_install_editable_sees_config(script: PipTestEnvironment) -> None:
256281
config = script.site_packages_path / f"{name}-config.json"
257282
with open(config, "rb") as f:
258283
assert json.load(f) == {"FOO": "Hello"}
284+
285+
286+
def test_install_config_reqs(script: PipTestEnvironment) -> None:
287+
name, _, project_dir = make_project(script.scratch_path)
288+
a_sdist = create_basic_sdist_for_package(
289+
script,
290+
"foo",
291+
"1.0",
292+
{"pyproject.toml": PYPROJECT_TOML, "backend/dummy_backend.py": BACKEND_SRC},
293+
)
294+
script.scratch_path.joinpath("reqs.txt").write_text(
295+
f'{project_dir} --config-settings "--build-option=--cffi" '
296+
'--config-settings "--build-option=--avx2" '
297+
"--config-settings FOO=BAR"
298+
)
299+
script.pip("install", "--no-index", "-f", str(a_sdist.parent), "-r", "reqs.txt")
300+
script.assert_installed(foo="1.0")
301+
config = script.site_packages_path / f"{name}-config.json"
302+
with open(config, "rb") as f:
303+
assert json.load(f) == {"--build-option": ["--cffi", "--avx2"], "FOO": "BAR"}

tests/functional/test_install_reqs.py

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import textwrap
44
from pathlib import Path
5-
from typing import Any, Callable
5+
from typing import TYPE_CHECKING, Any
66

77
import pytest
88

@@ -18,6 +18,11 @@
1818
)
1919
from tests.lib.local_repos import local_checkout
2020

21+
if TYPE_CHECKING:
22+
from typing import Protocol
23+
else:
24+
Protocol = object
25+
2126

2227
class ArgRecordingSdist:
2328
def __init__(self, sdist_path: Path, args_path: Path) -> None:
@@ -28,33 +33,42 @@ def args(self) -> Any:
2833
return json.loads(self._args_path.read_text())
2934

3035

36+
class ArgRecordingSdistMaker(Protocol):
37+
def __call__(self, name: str, **kwargs: Any) -> ArgRecordingSdist:
38+
...
39+
40+
3141
@pytest.fixture()
3242
def arg_recording_sdist_maker(
3343
script: PipTestEnvironment,
34-
) -> Callable[[str], ArgRecordingSdist]:
35-
arg_writing_setup_py = textwrap.dedent(
44+
) -> ArgRecordingSdistMaker:
45+
arg_writing_setup_py_prelude = textwrap.dedent(
3646
"""
3747
import io
3848
import json
3949
import os
4050
import sys
4151
42-
from setuptools import setup
43-
4452
args_path = os.path.join(os.environ["OUTPUT_DIR"], "{name}.json")
4553
with open(args_path, 'w') as f:
4654
json.dump(sys.argv, f)
47-
48-
setup(name={name!r}, version="0.1.0")
4955
"""
5056
)
5157
output_dir = script.scratch_path.joinpath("args_recording_sdist_maker_output")
5258
output_dir.mkdir(parents=True)
5359
script.environ["OUTPUT_DIR"] = str(output_dir)
5460

55-
def _arg_recording_sdist_maker(name: str) -> ArgRecordingSdist:
56-
extra_files = {"setup.py": arg_writing_setup_py.format(name=name)}
57-
sdist_path = create_basic_sdist_for_package(script, name, "0.1.0", extra_files)
61+
def _arg_recording_sdist_maker(
62+
name: str,
63+
**kwargs: Any,
64+
) -> ArgRecordingSdist:
65+
sdist_path = create_basic_sdist_for_package(
66+
script,
67+
name,
68+
"0.1.0",
69+
setup_py_prelude=arg_writing_setup_py_prelude.format(name=name),
70+
**kwargs,
71+
)
5872
args_path = output_dir / f"{name}.json"
5973
return ArgRecordingSdist(sdist_path, args_path)
6074

@@ -727,3 +741,79 @@ def test_install_unsupported_wheel_file(
727741
in result.stderr
728742
)
729743
assert len(result.files_created) == 0
744+
745+
746+
def test_config_settings_local_to_package(
747+
script: PipTestEnvironment,
748+
common_wheels: Path,
749+
arg_recording_sdist_maker: ArgRecordingSdistMaker,
750+
) -> None:
751+
pyproject_toml = textwrap.dedent(
752+
"""
753+
[build-system]
754+
requires = ["setuptools"]
755+
build-backend = "setuptools.build_meta"
756+
"""
757+
)
758+
simple0_sdist = arg_recording_sdist_maker(
759+
"simple0",
760+
extra_files={"pyproject.toml": pyproject_toml},
761+
depends=["foo"],
762+
)
763+
foo_sdist = arg_recording_sdist_maker(
764+
"foo",
765+
extra_files={"pyproject.toml": pyproject_toml},
766+
)
767+
simple1_sdist = arg_recording_sdist_maker(
768+
"simple1",
769+
extra_files={"pyproject.toml": pyproject_toml},
770+
depends=["bar"],
771+
)
772+
bar_sdist = arg_recording_sdist_maker(
773+
"bar",
774+
extra_files={"pyproject.toml": pyproject_toml},
775+
depends=["simple3"],
776+
)
777+
simple3_sdist = arg_recording_sdist_maker(
778+
"simple3", extra_files={"pyproject.toml": pyproject_toml}
779+
)
780+
simple2_sdist = arg_recording_sdist_maker(
781+
"simple2",
782+
extra_files={"pyproject.toml": pyproject_toml},
783+
)
784+
785+
reqs_file = script.scratch_path.joinpath("reqs.txt")
786+
reqs_file.write_text(
787+
textwrap.dedent(
788+
"""
789+
simple0 --config-settings "--build-option=--verbose"
790+
foo --config-settings "--build-option=--quiet"
791+
simple1 --config-settings "--build-option=--verbose"
792+
simple2
793+
"""
794+
)
795+
)
796+
797+
script.pip(
798+
"install",
799+
"--no-index",
800+
"-f",
801+
script.scratch_path,
802+
"-f",
803+
common_wheels,
804+
"-r",
805+
reqs_file,
806+
)
807+
808+
simple0_args = simple0_sdist.args()
809+
assert "--verbose" in simple0_args
810+
foo_args = foo_sdist.args()
811+
assert "--quiet" in foo_args
812+
simple1_args = simple1_sdist.args()
813+
assert "--verbose" in simple1_args
814+
bar_args = bar_sdist.args()
815+
assert "--verbose" not in bar_args
816+
simple3_args = simple3_sdist.args()
817+
assert "--verbose" not in simple3_args
818+
simple2_args = simple2_sdist.args()
819+
assert "--verbose" not in simple2_args

tests/unit/test_req_file.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,13 @@ def parse_reqfile(
7373
options=options,
7474
constraint=constraint,
7575
):
76-
yield install_req_from_parsed_requirement(parsed_req, isolated=isolated)
76+
yield install_req_from_parsed_requirement(
77+
parsed_req,
78+
isolated=isolated,
79+
config_settings=parsed_req.options.get("config_settings")
80+
if parsed_req.options
81+
else None,
82+
)
7783

7884

7985
def test_read_file_url(tmp_path: Path, session: PipSession) -> None:
@@ -343,10 +349,14 @@ def test_nested_constraints_file(
343349
assert reqs[0].constraint
344350

345351
def test_options_on_a_requirement_line(self, line_processor: LineProcessor) -> None:
346-
line = 'SomeProject --global-option="yo3" --global-option "yo4"'
352+
line = (
353+
'SomeProject --global-option="yo3" --global-option "yo4" '
354+
'--config-settings="yo3=yo4" --config-settings "yo1=yo2"'
355+
)
347356
filename = "filename"
348357
req = line_processor(line, filename, 1)[0]
349358
assert req.global_options == ["yo3", "yo4"]
359+
assert req.config_settings == {"yo3": "yo4", "yo1": "yo2"}
350360

351361
def test_hash_options(self, line_processor: LineProcessor) -> None:
352362
"""Test the --hash option: mostly its value storage.

0 commit comments

Comments
 (0)