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 support for command-specific configuration sections #1966

Merged
merged 6 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 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 command-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,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
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
21 changes: 16 additions & 5 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 @@ -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

Expand Down Expand Up @@ -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


Expand Down
8 changes: 7 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,14 +501,20 @@ 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
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}}}

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))

Expand Down
14 changes: 14 additions & 0 deletions tests/test_cli_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions tests/test_cli_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
webknjaz marked this conversation as resolved.
Show resolved Hide resolved
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