diff --git a/news/12480.feature.rst b/news/12480.feature.rst new file mode 100644 index 00000000000..1e9d5531996 --- /dev/null +++ b/news/12480.feature.rst @@ -0,0 +1 @@ +Support per requirement options for editable installs. diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index f717c1ccc79..1ef3d5ef6e7 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -75,8 +75,16 @@ cmdoptions.config_settings, ] +SUPPORTED_OPTIONS_EDITABLE_REQ: List[Callable[..., optparse.Option]] = [ + cmdoptions.config_settings, +] + + # the 'dest' string values SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ] +SUPPORTED_OPTIONS_EDITABLE_REQ_DEST = [ + str(o().dest) for o in SUPPORTED_OPTIONS_EDITABLE_REQ +] logger = logging.getLogger(__name__) @@ -178,31 +186,25 @@ def handle_requirement_line( assert line.is_requirement + # get the options that apply to requirements if line.is_editable: - # For editable requirements, we don't support per-requirement - # options, so just return the parsed requirement. - return ParsedRequirement( - requirement=line.requirement, - is_editable=line.is_editable, - comes_from=line_comes_from, - constraint=line.constraint, - ) + supported_dest = SUPPORTED_OPTIONS_EDITABLE_REQ_DEST else: - # get the options that apply to requirements - req_options = {} - for dest in SUPPORTED_OPTIONS_REQ_DEST: - if dest in line.opts.__dict__ and line.opts.__dict__[dest]: - req_options[dest] = line.opts.__dict__[dest] - - line_source = f"line {line.lineno} of {line.filename}" - return ParsedRequirement( - requirement=line.requirement, - is_editable=line.is_editable, - comes_from=line_comes_from, - constraint=line.constraint, - options=req_options, - line_source=line_source, - ) + supported_dest = SUPPORTED_OPTIONS_REQ_DEST + req_options = {} + for dest in supported_dest: + if dest in line.opts.__dict__ and line.opts.__dict__[dest]: + req_options[dest] = line.opts.__dict__[dest] + + line_source = f"line {line.lineno} of {line.filename}" + return ParsedRequirement( + requirement=line.requirement, + is_editable=line.is_editable, + comes_from=line_comes_from, + constraint=line.constraint, + options=req_options, + line_source=line_source, + ) def handle_option_line( diff --git a/tests/functional/test_pep660.py b/tests/functional/test_pep660.py index 8418b26894c..d562d0750db 100644 --- a/tests/functional/test_pep660.py +++ b/tests/functional/test_pep660.py @@ -37,7 +37,7 @@ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): with open("log.txt", "a") as f: - print(":build_wheel called", file=f) + print(f":build_wheel called with config_settings={config_settings}", file=f) return _build_wheel(wheel_directory, config_settings, metadata_directory) """ @@ -55,7 +55,7 @@ def prepare_metadata_for_build_editable(metadata_directory, config_settings=None def build_editable(wheel_directory, config_settings=None, metadata_directory=None): with open("log.txt", "a") as f: - print(":build_editable called", file=f) + print(f":build_editable called with config_settings={config_settings}", file=f) return _build_wheel(wheel_directory, config_settings, metadata_directory) """ # fmt: on @@ -88,6 +88,16 @@ def _assert_hook_called(project_dir: Path, hook: str) -> None: assert f":{hook} called" in log, f"{hook} has not been called" +def _assert_hook_called_with_config_settings( + project_dir: Path, hook: str, config_settings: Dict[str, str] +) -> None: + log = project_dir.joinpath("log.txt").read_text() + assert f":{hook} called" in log, f"{hook} has not been called" + assert ( + f":{hook} called with config_settings={config_settings}" in log + ), f"{hook} has not been called with the expected config settings:\n{log}" + + def _assert_hook_not_called(project_dir: Path, hook: str) -> None: log = project_dir.joinpath("log.txt").read_text() assert f":{hook} called" not in log, f"{hook} should not have been called" @@ -119,9 +129,35 @@ def test_install_pep660_basic(tmpdir: Path, script: PipTestEnvironment) -> None: "--no-build-isolation", "--editable", project_dir, + "--config-setting", + "x=y", + ) + _assert_hook_called(project_dir, "prepare_metadata_for_build_editable") + _assert_hook_called_with_config_settings(project_dir, "build_editable", {"x": "y"}) + assert ( + result.test_env.site_packages.joinpath("project.egg-link") + not in result.files_created + ), "a .egg-link file should not have been created" + + +def test_install_pep660_from_reqs_file( + tmpdir: Path, script: PipTestEnvironment +) -> None: + """ + Test with backend that supports build_editable. + """ + project_dir = _make_project(tmpdir, BACKEND_WITH_PEP660, with_setup_py=False) + reqs_file = tmpdir / "requirements.txt" + reqs_file.write_text(f"-e {project_dir.as_uri()} --config-setting x=y\n") + result = script.pip( + "install", + "--no-index", + "--no-build-isolation", + "-r", + reqs_file, ) _assert_hook_called(project_dir, "prepare_metadata_for_build_editable") - _assert_hook_called(project_dir, "build_editable") + _assert_hook_called_with_config_settings(project_dir, "build_editable", {"x": "y"}) assert ( result.test_env.site_packages.joinpath("project.egg-link") not in result.files_created