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 --config-settings #11634

Merged
merged 26 commits into from Apr 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions news/11325.feature.rst
@@ -0,0 +1 @@
Support a per-requirement ``--config-settings`` option in requirements files.
3 changes: 3 additions & 0 deletions src/pip/_internal/cli/req_command.py
Expand Up @@ -438,6 +438,9 @@ def get_requirements(
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,
uranusjr marked this conversation as resolved.
Show resolved Hide resolved
)
requirements.append(req_to_add)

Expand Down
3 changes: 3 additions & 0 deletions src/pip/_internal/req/constructors.py
Expand Up @@ -453,6 +453,7 @@ def install_req_from_parsed_requirement(
isolated: bool = False,
use_pep517: Optional[bool] = None,
user_supplied: bool = False,
config_settings: Optional[Dict[str, Union[str, List[str]]]] = None,
) -> InstallRequirement:
if parsed_req.is_editable:
req = install_req_from_editable(
Expand All @@ -462,6 +463,7 @@ def install_req_from_parsed_requirement(
constraint=parsed_req.constraint,
isolated=isolated,
user_supplied=user_supplied,
config_settings=config_settings,
)

else:
Expand All @@ -481,6 +483,7 @@ def install_req_from_parsed_requirement(
constraint=parsed_req.constraint,
line_source=parsed_req.line_source,
user_supplied=user_supplied,
config_settings=config_settings,
)
return req

Expand Down
1 change: 1 addition & 0 deletions src/pip/_internal/req/req_file.py
Expand Up @@ -71,6 +71,7 @@
SUPPORTED_OPTIONS_REQ: List[Callable[..., optparse.Option]] = [
cmdoptions.global_options,
cmdoptions.hash,
cmdoptions.config_settings,
]

# the 'dest' string values
Expand Down
47 changes: 46 additions & 1 deletion tests/functional/test_config_settings.py
Expand Up @@ -5,7 +5,7 @@
from zipfile import ZipFile

from pip._internal.utils.urls import path_to_url
from tests.lib import PipTestEnvironment
from tests.lib import PipTestEnvironment, create_basic_sdist_for_package

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


def test_backend_sees_config_reqs(script: PipTestEnvironment) -> None:
name, version, project_dir = make_project(script.scratch_path)
script.scratch_path.joinpath("reqs.txt").write_text(
f"{project_dir} --config-settings FOO=Hello"
)
script.pip("wheel", "-r", "reqs.txt")
wheel_file_name = f"{name}-{version}-py3-none-any.whl"
wheel_file_path = script.cwd / wheel_file_name
with open(wheel_file_path, "rb") as f:
with ZipFile(f) as z:
output = z.read(f"{name}-config.json")
assert json.loads(output) == {"FOO": "Hello"}


def test_backend_sees_config_via_constraint(script: PipTestEnvironment) -> None:
name, version, project_dir = make_project(script.scratch_path)
constraints_file = script.scratch_path / "constraints.txt"
Expand Down Expand Up @@ -244,6 +258,17 @@ def test_install_sees_config(script: PipTestEnvironment) -> None:
assert json.load(f) == {"FOO": "Hello"}


def test_install_sees_config_reqs(script: PipTestEnvironment) -> None:
name, _, project_dir = make_project(script.scratch_path)
script.scratch_path.joinpath("reqs.txt").write_text(
f"{project_dir} --config-settings FOO=Hello"
)
script.pip("install", "-r", "reqs.txt")
config = script.site_packages_path / f"{name}-config.json"
with open(config, "rb") as f:
assert json.load(f) == {"FOO": "Hello"}


def test_install_editable_sees_config(script: PipTestEnvironment) -> None:
name, _, project_dir = make_project(script.scratch_path)
script.pip(
Expand All @@ -256,3 +281,23 @@ def test_install_editable_sees_config(script: PipTestEnvironment) -> None:
config = script.site_packages_path / f"{name}-config.json"
with open(config, "rb") as f:
assert json.load(f) == {"FOO": "Hello"}


def test_install_config_reqs(script: PipTestEnvironment) -> None:
name, _, 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(
f'{project_dir} --config-settings "--build-option=--cffi" '
'--config-settings "--build-option=--avx2" '
"--config-settings FOO=BAR"
)
script.pip("install", "--no-index", "-f", str(a_sdist.parent), "-r", "reqs.txt")
script.assert_installed(foo="1.0")
config = script.site_packages_path / f"{name}-config.json"
with open(config, "rb") as f:
assert json.load(f) == {"--build-option": ["--cffi", "--avx2"], "FOO": "BAR"}
110 changes: 100 additions & 10 deletions tests/functional/test_install_reqs.py
Expand Up @@ -2,7 +2,7 @@
import os
import textwrap
from pathlib import Path
from typing import Any, Callable
from typing import TYPE_CHECKING, Any

import pytest

Expand All @@ -18,6 +18,11 @@
)
from tests.lib.local_repos import local_checkout

if TYPE_CHECKING:
from typing import Protocol
else:
Protocol = object


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


class ArgRecordingSdistMaker(Protocol):
def __call__(self, name: str, **kwargs: Any) -> ArgRecordingSdist:
...


@pytest.fixture()
def arg_recording_sdist_maker(
script: PipTestEnvironment,
) -> Callable[[str], ArgRecordingSdist]:
arg_writing_setup_py = textwrap.dedent(
) -> ArgRecordingSdistMaker:
arg_writing_setup_py_prelude = textwrap.dedent(
"""
import io
import json
import os
import sys

from setuptools import setup

args_path = os.path.join(os.environ["OUTPUT_DIR"], "{name}.json")
with open(args_path, 'w') as f:
json.dump(sys.argv, f)

setup(name={name!r}, version="0.1.0")
"""
)
output_dir = script.scratch_path.joinpath("args_recording_sdist_maker_output")
output_dir.mkdir(parents=True)
script.environ["OUTPUT_DIR"] = str(output_dir)

def _arg_recording_sdist_maker(name: str) -> ArgRecordingSdist:
extra_files = {"setup.py": arg_writing_setup_py.format(name=name)}
sdist_path = create_basic_sdist_for_package(script, name, "0.1.0", extra_files)
def _arg_recording_sdist_maker(
name: str,
**kwargs: Any,
) -> ArgRecordingSdist:
sdist_path = create_basic_sdist_for_package(
script,
name,
"0.1.0",
setup_py_prelude=arg_writing_setup_py_prelude.format(name=name),
**kwargs,
)
args_path = output_dir / f"{name}.json"
return ArgRecordingSdist(sdist_path, args_path)

Expand Down Expand Up @@ -727,3 +741,79 @@ def test_install_unsupported_wheel_file(
in result.stderr
)
assert len(result.files_created) == 0


def test_config_settings_local_to_package(
script: PipTestEnvironment,
common_wheels: Path,
arg_recording_sdist_maker: ArgRecordingSdistMaker,
) -> None:
pyproject_toml = textwrap.dedent(
"""
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
"""
)
simple0_sdist = arg_recording_sdist_maker(
"simple0",
extra_files={"pyproject.toml": pyproject_toml},
depends=["foo"],
)
foo_sdist = arg_recording_sdist_maker(
"foo",
extra_files={"pyproject.toml": pyproject_toml},
)
simple1_sdist = arg_recording_sdist_maker(
"simple1",
extra_files={"pyproject.toml": pyproject_toml},
depends=["bar"],
)
bar_sdist = arg_recording_sdist_maker(
"bar",
extra_files={"pyproject.toml": pyproject_toml},
depends=["simple3"],
)
simple3_sdist = arg_recording_sdist_maker(
"simple3", extra_files={"pyproject.toml": pyproject_toml}
)
simple2_sdist = arg_recording_sdist_maker(
"simple2",
extra_files={"pyproject.toml": pyproject_toml},
)

reqs_file = script.scratch_path.joinpath("reqs.txt")
reqs_file.write_text(
textwrap.dedent(
"""
simple0 --config-settings "--build-option=--verbose"
foo --config-settings "--build-option=--quiet"
simple1 --config-settings "--build-option=--verbose"
simple2
"""
)
)

script.pip(
"install",
"--no-index",
"-f",
script.scratch_path,
"-f",
common_wheels,
"-r",
reqs_file,
)

simple0_args = simple0_sdist.args()
assert "--verbose" in simple0_args
foo_args = foo_sdist.args()
assert "--quiet" in foo_args
simple1_args = simple1_sdist.args()
assert "--verbose" in simple1_args
bar_args = bar_sdist.args()
assert "--verbose" not in bar_args
simple3_args = simple3_sdist.args()
assert "--verbose" not in simple3_args
simple2_args = simple2_sdist.args()
assert "--verbose" not in simple2_args
14 changes: 12 additions & 2 deletions tests/unit/test_req_file.py
Expand Up @@ -73,7 +73,13 @@ def parse_reqfile(
options=options,
constraint=constraint,
):
yield install_req_from_parsed_requirement(parsed_req, isolated=isolated)
yield install_req_from_parsed_requirement(
parsed_req,
isolated=isolated,
config_settings=parsed_req.options.get("config_settings")
if parsed_req.options
else None,
)


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

def test_options_on_a_requirement_line(self, line_processor: LineProcessor) -> None:
line = 'SomeProject --global-option="yo3" --global-option "yo4"'
line = (
'SomeProject --global-option="yo3" --global-option "yo4" '
'--config-settings="yo3=yo4" --config-settings "yo1=yo2"'
)
filename = "filename"
req = line_processor(line, filename, 1)[0]
assert req.global_options == ["yo3", "yo4"]
assert req.config_settings == {"yo3": "yo4", "yo1": "yo2"}

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