Skip to content

Commit

Permalink
Add support for tool-specific configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
chrysle committed Jan 2, 2024
1 parent e02d186 commit 8ba0253
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 16 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 tool-specific.
For example, to by default generate `pip` hashes in the resulting
requirements file output, you can specify in a configuration file:

Expand All @@ -311,6 +312,27 @@ 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-compile]
dry-run = true
```

This does not affect the `pip-sync` command, which also has a `--dry-run` option.
Note that tool-specific configuration overrides global settings, 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-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
Expand Down
5 changes: 4 additions & 1 deletion piptools/scripts/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion piptools/scripts/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 34 additions & 13 deletions piptools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -546,10 +546,21 @@ def override_defaults_from_config_file(
else:
config_file = Path(value)

config = parse_config_file(ctx, config_file)
if config:
_validate_config(ctx, config)
_assign_config_to_cli_context(ctx, config)
piptools_config, pipcompile_config, pipsync_config = parse_config_file(
ctx, config_file
)

configs = [piptools_config]

if ctx.command.name == "pip-compile":
configs.append(pipcompile_config)
elif ctx.command.name == "pip-sync":
configs.append(pipsync_config)

for config in configs:
if config:
_validate_config(ctx, config)
_assign_config_to_cli_context(ctx, config)

return config_file

Expand Down Expand Up @@ -664,7 +675,7 @@ def get_cli_options(ctx: click.Context) -> dict[str, click.Parameter]:

def parse_config_file(
click_context: click.Context, config_file: Path
) -> dict[str, Any]:
) -> tuple[dict[str, Any], ...]:
try:
config = tomllib.loads(config_file.read_text(encoding="utf-8"))
except OSError as os_err:
Expand All @@ -678,14 +689,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-compile]` or `[tool.pip-sync]`
piptools_config: dict[str, Any] = config.get("tool", {}).get("pip-tools", {})
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
pipcompile_config: dict[str, Any] = config.get("tool", {}).get("pip-compile", {})
pipsync_config: dict[str, Any] = config.get("tool", {}).get("pip-sync", {})

configs = []

for config in (piptools_config, pipcompile_config, pipsync_config):
if config:
config = _normalize_keys_in_config(config)
config = _invert_negative_bool_options_in_config(
ctx=click_context,
config=config,
)
configs.append(config)

return tuple(configs)


def _normalize_keys_in_config(config: dict[str, Any]) -> dict[str, Any]:
Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,14 +501,15 @@ def _maker(
pyproject_param: str,
new_default: Any,
config_file_name: str = DEFAULT_CONFIG_FILE_NAMES[0],
section: str = "pip-tools",
) -> Path:
# Create a nested directory structure if config_file_name includes directories
config_dir = (tmpdir_cwd / config_file_name).parent
config_dir.mkdir(exist_ok=True, parents=True)

# 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}}}
config_to_dump = {"tool": {section: {pyproject_param: new_default}}}
config_file.write_text(tomli_w.dumps(config_to_dump))
return cast(Path, config_file.relative_to(tmpdir_cwd))

Expand Down
11 changes: 11 additions & 0 deletions tests/test_cli_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3562,3 +3562,14 @@ 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-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
13 changes: 13 additions & 0 deletions tests/test_cli_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,3 +450,16 @@ 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-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

0 comments on commit 8ba0253

Please sign in to comment.