diff --git a/piptools/utils.py b/piptools/utils.py index a1711a507..5e3e17260 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -591,16 +591,23 @@ def _validate_config( :raises click.NoSuchOption: if config contains unknown keys. :raises click.BadOptionUsage: if config contains invalid values. """ - cli_params = { - param.name: param - for param in click_context.command.params - if param.name is not None + from piptools.scripts.compile import cli as compile_cli + from piptools.scripts.sync import cli as sync_cli + + compile_cli_params = { + param.name: param for param in compile_cli.params if param.name is not None + } + + sync_cli_params = { + param.name: param for param in sync_cli.params if param.name is not None } + all_keys = set(compile_cli_params) | set(sync_cli_params) + for key, value in config.items(): - # Validate unknown keys - if key not in cli_params: - possibilities = difflib.get_close_matches(key, cli_params.keys()) + # Validate unknown keys in both compile and sync + if key not in all_keys: + possibilities = difflib.get_close_matches(key, all_keys) raise click.NoSuchOption( option_name=key, message=f"No such config key {key!r}.", @@ -608,19 +615,26 @@ def _validate_config( ctx=click_context, ) - # Validate invalid values - param = cli_params[key] - try: - param.type_cast_value(value=value, ctx=click_context) - except Exception as e: - raise click.BadOptionUsage( - option_name=key, - message=( - f"Invalid value for config key {key!r}: {value!r}.{os.linesep}" - f"Details: {e}" - ), - ctx=click_context, - ) from e + # Get all params associated with this key in both compile and sync + associated_params = ( + cli_params[key] + for cli_params in (compile_cli_params, sync_cli_params) + if key in cli_params + ) + + # Validate value against types of all associated params + for param in associated_params: + try: + param.type_cast_value(value=value, ctx=click_context) + except Exception as e: + raise click.BadOptionUsage( + option_name=key, + message=( + f"Invalid value for config key {key!r}: {value!r}.{os.linesep}" + f"Details: {e}" + ), + ctx=click_context, + ) from e def select_config_file(src_files: tuple[str, ...]) -> Path | None: diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py index 90267ea6a..a0f03137b 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -3019,6 +3019,20 @@ def test_raise_error_on_invalid_config_option( assert "Invalid value for config key 'dry_run': ['invalid', 'value']" in out.stderr +def test_allow_in_config_pip_sync_option(pip_conf, runner, tmp_path, make_config_file): + config_file = make_config_file("--ask", True) # pip-sync's option + + req_in = tmp_path / "requirements.in" + req_in.touch() + + out = runner.invoke( + cli, [req_in.as_posix(), "--verbose", "--config", config_file.as_posix()] + ) + + assert out.exit_code == 0 + assert "Using pip-tools configuration defaults found" in out.stderr + + def test_cli_boolean_flag_config_option_has_valid_context( pip_conf, runner, tmp_path, make_config_file ): diff --git a/tests/test_cli_sync.py b/tests/test_cli_sync.py index 9eca78082..88457bda9 100644 --- a/tests/test_cli_sync.py +++ b/tests/test_cli_sync.py @@ -424,3 +424,16 @@ def test_raise_error_on_invalid_config_option(run, runner, tmp_path, make_config assert out.exit_code == 2 assert "Invalid value for config key 'dry_run': ['invalid', 'value']" in out.stderr + + +@mock.patch("piptools.sync.run") +def test_allow_in_config_pip_compile_option(run, runner, tmp_path, make_config_file): + config_file = make_config_file("generate-hashes", True) # pip-compile's option + + with open(sync.DEFAULT_REQUIREMENTS_FILE, "w") as reqs_txt: + reqs_txt.write("six==1.10.0") + + out = runner.invoke(cli, ["--verbose", "--config", config_file.as_posix()]) + + assert out.exit_code == 0 + assert "Using pip-tools configuration defaults found" in out.stderr