diff --git a/README.md b/README.md index b126d34b..666dec23 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ By default, both `pip-compile` and `pip-sync` will look first for a `.pip-tools.toml` file and then in your `pyproject.toml`. You can also specify an alternate TOML configuration file with the `--config` option. +It is possible to specify configuration values both globally and command-specific. For example, to by default generate `pip` hashes in the resulting requirements file output, you can specify in a configuration file: @@ -311,6 +312,28 @@ so the above could also be specified in this format: generate_hashes = true ``` +Configuration defaults specific to `pip-compile` and `pip-sync` can be put beneath +separate sections. For example, to by default perform a dry-run with `pip-compile`: + +```toml +[tool.pip-tools.compile] # "sync" for pip-sync +dry-run = true +``` + +This does not affect the `pip-sync` command, which also has a `--dry-run` option. +Note that local settings take preference over the global ones of the same name, +whenever both are declared, thus this would also make `pip-compile` generate hashes, +but discard the global dry-run setting: + +```toml +[tool.pip-tools] +generate-hashes = true +dry-run = true + +[tool.pip-tools.compile] +dry-run = false +``` + You might be wrapping the `pip-compile` command in another script. To avoid confusing consumers of your custom script you can override the update command generated at the top of requirements files by setting the diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py index 462215f4..a2fb6300 100755 --- a/piptools/scripts/compile.py +++ b/piptools/scripts/compile.py @@ -68,7 +68,10 @@ def _determine_linesep( }[strategy] -@click.command(context_settings={"help_option_names": options.help_option_names}) +@click.command( + name="pip-compile", + context_settings={"help_option_names": options.help_option_names}, +) @click.pass_context @options.version @options.verbose diff --git a/piptools/scripts/sync.py b/piptools/scripts/sync.py index acddf927..b83e811c 100755 --- a/piptools/scripts/sync.py +++ b/piptools/scripts/sync.py @@ -30,7 +30,9 @@ DEFAULT_REQUIREMENTS_FILE = "requirements.txt" -@click.command(context_settings={"help_option_names": options.help_option_names}) +@click.command( + name="pip-sync", context_settings={"help_option_names": options.help_option_names} +) @options.version @options.ask @options.dry_run diff --git a/piptools/utils.py b/piptools/utils.py index de4399be..3b2061f7 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -532,7 +532,7 @@ def override_defaults_from_config_file( ``None`` is returned if no such file is found. ``pip-tools`` will use the first config file found, searching in this order: - an explicitly given config file, a d``.pip-tools.toml``, a ``pyproject.toml`` + an explicitly given config file, a ``.pip-tools.toml``, a ``pyproject.toml`` file. Those files are searched for in the same directory as the requirements input file, or the current working directory if requirements come via stdin. """ @@ -547,9 +547,9 @@ def override_defaults_from_config_file( config_file = Path(value) config = parse_config_file(ctx, config_file) - if config: - _validate_config(ctx, config) - _assign_config_to_cli_context(ctx, config) + + _validate_config(ctx, config) + _assign_config_to_cli_context(ctx, config) return config_file @@ -678,13 +678,24 @@ def parse_config_file( hint=f"Could not parse '{config_file !s}': {value_err !s}", ) - # In a TOML file, we expect the config to be under `[tool.pip-tools]` + # In a TOML file, we expect the config to be under `[tool.pip-tools]`, + # `[tool.pip-tools.compile]` or `[tool.pip-tools.sync]` piptools_config: dict[str, Any] = config.get("tool", {}).get("pip-tools", {}) + + assert click_context.command.name is not None + # TODO: Replace with `str.removeprefix()` once dropped 3.8 + config_section_name = click_context.command.name[len("pip-") :] + + piptools_config.update(piptools_config.pop(config_section_name, {})) + piptools_config.pop("compile", {}) + piptools_config.pop("sync", {}) + piptools_config = _normalize_keys_in_config(piptools_config) piptools_config = _invert_negative_bool_options_in_config( ctx=click_context, config=piptools_config, ) + return piptools_config diff --git a/tests/conftest.py b/tests/conftest.py index 1f380383..cb463229 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -501,6 +501,8 @@ def _maker( pyproject_param: str, new_default: Any, config_file_name: str = DEFAULT_CONFIG_FILE_NAMES[0], + section: str = "pip-tools", + subsection: str | None = None, ) -> Path: # Create a nested directory structure if config_file_name includes directories config_dir = (tmpdir_cwd / config_file_name).parent @@ -508,7 +510,11 @@ def _maker( # Make a config file with this one config default override config_file = tmpdir_cwd / config_file_name - config_to_dump = {"tool": {"pip-tools": {pyproject_param: new_default}}} + + nested_config = {pyproject_param: new_default} + if subsection: + nested_config = {subsection: nested_config} + config_to_dump = {"tool": {section: nested_config}} config_file.write_text(tomli_w.dumps(config_to_dump)) return cast(Path, config_file.relative_to(tmpdir_cwd)) diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py index 52188af4..77cf21f8 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -3562,3 +3562,17 @@ def test_origin_of_extra_requirement_not_written_to_annotations( ) == out.stdout ) + + +def test_tool_specific_config_option(pip_conf, runner, tmp_path, make_config_file): + config_file = make_config_file( + "dry-run", True, section="pip-tools", subsection="compile" + ) + + req_in = tmp_path / "requirements.in" + req_in.touch() + + out = runner.invoke(cli, [req_in.as_posix(), "--config", config_file.as_posix()]) + + assert out.exit_code == 0 + assert "Dry-run, so nothing updated" in out.stderr diff --git a/tests/test_cli_sync.py b/tests/test_cli_sync.py index 20e9689e..5e69770f 100644 --- a/tests/test_cli_sync.py +++ b/tests/test_cli_sync.py @@ -450,3 +450,18 @@ def test_allow_in_config_pip_compile_option(run, runner, tmp_path, make_config_f assert out.exit_code == 0 assert "Using pip-tools configuration defaults found" in out.stderr + + +@mock.patch("piptools.sync.run") +def test_tool_specific_config_option(run, runner, make_config_file): + config_file = make_config_file( + "dry-run", True, section="pip-tools", subsection="sync" + ) + + with open(sync.DEFAULT_REQUIREMENTS_FILE, "w") as reqs_txt: + reqs_txt.write("six==1.10.0") + + out = runner.invoke(cli, ["--config", config_file.as_posix()]) + + assert out.exit_code == 1 + assert "Would install:" in out.stdout