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 4 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.
uranusjr marked this conversation as resolved.
Show resolved Hide resolved
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
1 change: 1 addition & 0 deletions src/pip/_internal/req/req_file.py
Expand Up @@ -72,6 +72,7 @@
cmdoptions.install_options,
cmdoptions.global_options,
cmdoptions.hash,
cmdoptions.config_settings,
]

# the 'dest' string values
Expand Down
2 changes: 2 additions & 0 deletions src/pip/_internal/req/req_install.py
Expand Up @@ -150,6 +150,8 @@ 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
if isinstance(comes_from, InstallRequirement) and comes_from.config_settings:
q0w marked this conversation as resolved.
Show resolved Hide resolved
self.config_settings = comes_from.config_settings
q0w marked this conversation as resolved.
Show resolved Hide resolved
# Set to True after successful preparation of this requirement
self.prepared = False
# User supplied requirement are explicitly requested for installation
Expand Down
45 changes: 44 additions & 1 deletion tests/functional/test_config_settings.py
Expand Up @@ -3,7 +3,7 @@
from typing import Tuple
from zipfile import ZipFile

from tests.lib import PipTestEnvironment
from tests.lib import PipTestEnvironment, create_basic_sdist_for_package

PYPROJECT_TOML = """\
[build-system]
Expand Down Expand Up @@ -112,6 +112,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("config.json")
assert json.loads(output) == {"FOO": "Hello"}


def test_install_sees_config(script: PipTestEnvironment) -> None:
_, _, project_dir = make_project(script.scratch_path)
script.pip(
Expand All @@ -125,6 +139,17 @@ def test_install_sees_config(script: PipTestEnvironment) -> None:
assert json.load(f) == {"FOO": "Hello"}


def test_install_sees_config_reqs(script: PipTestEnvironment) -> None:
_, _, 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 / "config.json"
with open(config, "rb") as f:
assert json.load(f) == {"FOO": "Hello"}


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


def test_install_config_reqs(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"
)
script.pip("install", "--no-index", "-f", str(a_sdist.parent), "-r", "reqs.txt")
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"}
71 changes: 64 additions & 7 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, Dict, Optional

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,10 +33,17 @@ def args(self) -> Any:
return json.loads(self._args_path.read_text())


class ArgRecordingSdistMaker(Protocol):
def __call__(
self, name: str, extra_files: Optional[Dict[str, str]] = None
) -> ArgRecordingSdist:
...


@pytest.fixture()
def arg_recording_sdist_maker(
script: PipTestEnvironment,
) -> Callable[[str], ArgRecordingSdist]:
) -> ArgRecordingSdistMaker:
arg_writing_setup_py = textwrap.dedent(
"""
import io
Expand All @@ -52,9 +64,13 @@ def arg_recording_sdist_maker(
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, extra_files: Optional[Dict[str, str]] = None
) -> ArgRecordingSdist:
_extra_files = {"setup.py": arg_writing_setup_py.format(name=name)}
if extra_files is not None:
_extra_files.update(extra_files)
sdist_path = create_basic_sdist_for_package(script, name, "0.1.0", _extra_files)
args_path = output_dir / f"{name}.json"
return ArgRecordingSdist(sdist_path, args_path)

Expand Down Expand Up @@ -334,7 +350,7 @@ def test_wheel_user_with_prefix_in_pydistutils_cfg(

def test_install_option_in_requirements_file_overrides_cli(
script: PipTestEnvironment,
arg_recording_sdist_maker: Callable[[str], ArgRecordingSdist],
arg_recording_sdist_maker: ArgRecordingSdistMaker,
) -> None:
simple_sdist = arg_recording_sdist_maker("simple")

Expand Down Expand Up @@ -763,7 +779,7 @@ def test_install_unsupported_wheel_file(

def test_install_options_local_to_package(
script: PipTestEnvironment,
arg_recording_sdist_maker: Callable[[str], ArgRecordingSdist],
arg_recording_sdist_maker: ArgRecordingSdistMaker,
) -> None:
"""Make sure --install-options does not leak across packages.

Expand Down Expand Up @@ -817,3 +833,44 @@ def test_location_related_install_option_fails(script: PipTestEnvironment) -> No
expect_error=True,
)
assert "['--home'] from simple" in result.stderr


@pytest.mark.network
def test_config_settings_local_to_package(
script: PipTestEnvironment, arg_recording_sdist_maker: ArgRecordingSdistMaker
) -> None:
pyproject_toml = textwrap.dedent(
"""
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
"""
)
simple1_sdist = arg_recording_sdist_maker(
"simple1", {"pyproject.toml": pyproject_toml}
)
simple2_sdist = arg_recording_sdist_maker(
"simple2", {"pyproject.toml": pyproject_toml}
)

reqs_file = script.scratch_path.joinpath("reqs.txt")
reqs_file.write_text(
textwrap.dedent(
"""
simple1 --config-settings "--build-option=--verbose"
simple2
"""
)
)
script.pip(
"install",
"-f",
str(simple1_sdist.sdist_path.parent),
"-r",
reqs_file,
)

simple1_args = simple1_sdist.args()
assert "--verbose" in simple1_args
simple2_args = simple2_sdist.args()
assert "--verbose" not in simple2_args
12 changes: 10 additions & 2 deletions tests/unit/test_req_file.py
Expand Up @@ -74,7 +74,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 @@ -346,12 +352,14 @@ def test_nested_constraints_file(
def test_options_on_a_requirement_line(self, line_processor: LineProcessor) -> None:
line = (
"SomeProject --install-option=yo1 --install-option yo2 "
'--global-option="yo3" --global-option "yo4"'
'--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.install_options == ["yo1", "yo2"]
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