From 8ba0253d23ca6ee0b55a1a94c5e87690d3af453f Mon Sep 17 00:00:00 2001 From: chrysle Date: Mon, 14 Aug 2023 09:12:58 +0200 Subject: [PATCH 1/6] Add support for tool-specific configuration --- README.md | 22 +++++++++++++++++ piptools/scripts/compile.py | 5 +++- piptools/scripts/sync.py | 4 +++- piptools/utils.py | 47 +++++++++++++++++++++++++++---------- tests/conftest.py | 3 ++- tests/test_cli_compile.py | 11 +++++++++ tests/test_cli_sync.py | 13 ++++++++++ 7 files changed, 89 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index b126d34b7..4a4f02661 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 tool-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,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 diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py index 462215f4d..a2fb6300a 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 acddf9279..b83e811cb 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 de4399be4..270c304fc 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. """ @@ -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 @@ -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: @@ -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]: diff --git a/tests/conftest.py b/tests/conftest.py index 1f380383b..7bfd5ffb4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -501,6 +501,7 @@ 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 @@ -508,7 +509,7 @@ 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}}} + 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)) diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py index 52188af4f..0e1e66a96 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -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 diff --git a/tests/test_cli_sync.py b/tests/test_cli_sync.py index 20e9689ec..48bf12da5 100644 --- a/tests/test_cli_sync.py +++ b/tests/test_cli_sync.py @@ -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 From ccc6f2e1ab24d3eff0b05c4ad5f08022cf81b61c Mon Sep 17 00:00:00 2001 From: chrysle Date: Thu, 23 Nov 2023 18:27:36 +0100 Subject: [PATCH 2/6] Rename config sections and refine detection logic Also tweak the wording in the configuration section of the README. Co-authored-by: Sviatoslav Sydorenko --- README.md | 11 +++++---- piptools/utils.py | 52 +++++++++++++++++++-------------------- tests/test_cli_compile.py | 3 ++- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 4a4f02661..8b6e192af 100644 --- a/README.md +++ b/README.md @@ -290,7 +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. +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: @@ -316,20 +316,21 @@ Configuration defaults specific to `pip-compile` and `pip-sync` can be put benea separate sections. For example, to by default perform a dry-run with `pip-compile`: ```toml -[tool.pip-compile] +[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 tool-specific configuration overrides global settings, thus this would -also make `pip-compile` generate hashes, but discard the global dry-run setting: +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-compile] +[tool.pip-tools.compile] dry-run = false ``` diff --git a/piptools/utils.py b/piptools/utils.py index 270c304fc..7fc825b1a 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -546,21 +546,12 @@ def override_defaults_from_config_file( else: config_file = Path(value) - piptools_config, pipcompile_config, pipsync_config = parse_config_file( + 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) + _validate_config(ctx, config) + _assign_config_to_cli_context(ctx, config) return config_file @@ -675,7 +666,7 @@ def get_cli_options(ctx: click.Context) -> dict[str, click.Parameter]: def parse_config_file( click_context: click.Context, config_file: Path -) -> tuple[dict[str, Any], ...]: +) -> dict[str, Any]: try: config = tomllib.loads(config_file.read_text(encoding="utf-8")) except OSError as os_err: @@ -692,21 +683,28 @@ def parse_config_file( # 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", {}) - 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) + pipcompile_config: dict[str, Any] = config.get("tool", {}).get("pip-tools", {}).get("compile", {}) + pipsync_config: dict[str, Any] = config.get("tool", {}).get("pip-tools", {}).get("sync", {}) + + config = piptools_config + + if click_context.command.name == "pip-compile": + config.pop("compile") + config.update(pipcompile_config) + elif click_context.command.name == "pip-sync": + config.pop("sync") + config.update(pipsync_config) + + print(config) + + if config: + config = _normalize_keys_in_config(config) + config = _invert_negative_bool_options_in_config( + ctx=click_context, + config=config, + ) - return tuple(configs) + return config def _normalize_keys_in_config(config: dict[str, Any]) -> dict[str, Any]: diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py index 0e1e66a96..a8b4c29b9 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -3563,8 +3563,9 @@ 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") + config_file = make_config_file("dry-run", True, section="pip-tools.compile") req_in = tmp_path / "requirements.in" req_in.touch() From 2443d815fa1c48a4d9404bf33381f26712aa6b76 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:32:10 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- README.md | 4 ++-- piptools/utils.py | 39 ++++++++++++++++++++------------------- tests/conftest.py | 9 ++++++++- tests/test_cli_compile.py | 7 ++++++- tests/test_cli_sync.py | 2 +- 5 files changed, 37 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 8b6e192af..666dec237 100644 --- a/README.md +++ b/README.md @@ -321,8 +321,8 @@ 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, +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 diff --git a/piptools/utils.py b/piptools/utils.py index 7fc825b1a..0c841bacf 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -546,9 +546,7 @@ def override_defaults_from_config_file( else: config_file = Path(value) - config = parse_config_file( - ctx, config_file - ) + config = parse_config_file(ctx, config_file) _validate_config(ctx, config) _assign_config_to_cli_context(ctx, config) @@ -683,26 +681,29 @@ def parse_config_file( # 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", {}) - pipcompile_config: dict[str, Any] = config.get("tool", {}).get("pip-tools", {}).get("compile", {}) - pipsync_config: dict[str, Any] = config.get("tool", {}).get("pip-tools", {}).get("sync", {}) + pipcompile_config: dict[str, Any] = ( + config.get("tool", {}).get("pip-tools", {}).get("compile", {}) + ) + pipsync_config: dict[str, Any] = ( + config.get("tool", {}).get("pip-tools", {}).get("sync", {}) + ) config = piptools_config - + if click_context.command.name == "pip-compile": - config.pop("compile") - config.update(pipcompile_config) + if pipcompile_config: + config.pop("compile") + config.update(pipcompile_config) elif click_context.command.name == "pip-sync": - config.pop("sync") - config.update(pipsync_config) - - print(config) - - if config: - config = _normalize_keys_in_config(config) - config = _invert_negative_bool_options_in_config( - ctx=click_context, - config=config, - ) + if pipsync_config: + config.pop("sync") + config.update(pipsync_config) + + config = _normalize_keys_in_config(config) + config = _invert_negative_bool_options_in_config( + ctx=click_context, + config=config, + ) return config diff --git a/tests/conftest.py b/tests/conftest.py index 7bfd5ffb4..4da135004 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -502,6 +502,7 @@ def _maker( 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 @@ -509,7 +510,13 @@ def _maker( # Make a config file with this one config default override config_file = tmpdir_cwd / config_file_name - config_to_dump = {"tool": {section: {pyproject_param: new_default}}} + + if subsection: + config_to_dump = { + "tool": {section: {subsection: {pyproject_param: new_default}}} + } + else: + 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)) diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py index a8b4c29b9..bcc778b7a 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -3565,7 +3565,12 @@ def test_origin_of_extra_requirement_not_written_to_annotations( 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.compile") + config_file = make_config_file( + "dry-run", True, section="pip-tools", subsection="compile" + ) + + with open(config_file.as_posix()) as f: + print(f.read()) req_in = tmp_path / "requirements.in" req_in.touch() diff --git a/tests/test_cli_sync.py b/tests/test_cli_sync.py index 48bf12da5..20688beea 100644 --- a/tests/test_cli_sync.py +++ b/tests/test_cli_sync.py @@ -454,7 +454,7 @@ def test_allow_in_config_pip_compile_option(run, runner, tmp_path, make_config_f @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") + 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") From ec50d5e8dcf0ce7529b98b26d228763c4f97b124 Mon Sep 17 00:00:00 2001 From: chrysle Date: Tue, 2 Jan 2024 15:16:07 +0100 Subject: [PATCH 4/6] Simplify code and update comment Co-authored-by: Sviatoslav Sydorenko --- piptools/utils.py | 31 +++++++++++-------------------- tests/conftest.py | 8 +++----- tests/test_cli_compile.py | 3 --- tests/test_cli_sync.py | 4 +++- 4 files changed, 17 insertions(+), 29 deletions(-) diff --git a/piptools/utils.py b/piptools/utils.py index 0c841bacf..575c413ed 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -679,33 +679,24 @@ def parse_config_file( ) # In a TOML file, we expect the config to be under `[tool.pip-tools]`, - # `[tool.pip-compile]` or `[tool.pip-sync]` + # `[tool.pip-tools.compile]` or `[tool.pip-tools.sync]` piptools_config: dict[str, Any] = config.get("tool", {}).get("pip-tools", {}) - pipcompile_config: dict[str, Any] = ( - config.get("tool", {}).get("pip-tools", {}).get("compile", {}) - ) - pipsync_config: dict[str, Any] = ( - config.get("tool", {}).get("pip-tools", {}).get("sync", {}) - ) - config = piptools_config + # TODO: Replace with `str.removeprefix()` once dropped 3.8 + assert click_context.command.name is not None + config_section_name = click_context.command.name[len("pip-"):] - if click_context.command.name == "pip-compile": - if pipcompile_config: - config.pop("compile") - config.update(pipcompile_config) - elif click_context.command.name == "pip-sync": - if pipsync_config: - config.pop("sync") - config.update(pipsync_config) + piptools_config.update(piptools_config.pop(config_section_name, {})) + piptools_config.pop("compile", {}) + piptools_config.pop("sync", {}) - config = _normalize_keys_in_config(config) - config = _invert_negative_bool_options_in_config( + piptools_config = _normalize_keys_in_config(piptools_config) + piptools_config = _invert_negative_bool_options_in_config( ctx=click_context, - config=config, + config=piptools_config, ) - return config + return piptools_config def _normalize_keys_in_config(config: dict[str, Any]) -> dict[str, Any]: diff --git a/tests/conftest.py b/tests/conftest.py index 4da135004..cb4632293 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -511,12 +511,10 @@ def _maker( # Make a config file with this one config default override config_file = tmpdir_cwd / config_file_name + nested_config = {pyproject_param: new_default} if subsection: - config_to_dump = { - "tool": {section: {subsection: {pyproject_param: new_default}}} - } - else: - config_to_dump = {"tool": {section: {pyproject_param: new_default}}} + 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 bcc778b7a..77cf21f87 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -3569,9 +3569,6 @@ def test_tool_specific_config_option(pip_conf, runner, tmp_path, make_config_fil "dry-run", True, section="pip-tools", subsection="compile" ) - with open(config_file.as_posix()) as f: - print(f.read()) - req_in = tmp_path / "requirements.in" req_in.touch() diff --git a/tests/test_cli_sync.py b/tests/test_cli_sync.py index 20688beea..5e69770fe 100644 --- a/tests/test_cli_sync.py +++ b/tests/test_cli_sync.py @@ -454,7 +454,9 @@ def test_allow_in_config_pip_compile_option(run, runner, tmp_path, make_config_f @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") + 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") From af73d2ada490d9baaba59e710443748195b8a28b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 2 Jan 2024 14:35:23 +0000 Subject: [PATCH 5/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- piptools/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piptools/utils.py b/piptools/utils.py index 575c413ed..87390d7bb 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -684,7 +684,7 @@ def parse_config_file( # TODO: Replace with `str.removeprefix()` once dropped 3.8 assert click_context.command.name is not None - config_section_name = click_context.command.name[len("pip-"):] + config_section_name = click_context.command.name[len("pip-") :] piptools_config.update(piptools_config.pop(config_section_name, {})) piptools_config.pop("compile", {}) From b23438507f83a49173e1aa0d752f11273993a3bf Mon Sep 17 00:00:00 2001 From: chrysle Date: Tue, 2 Jan 2024 15:50:57 +0100 Subject: [PATCH 6/6] Update piptools/utils.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sviatoslav Sydorenko (Святослав Сидоренко) --- piptools/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piptools/utils.py b/piptools/utils.py index 87390d7bb..3b2061f7a 100644 --- a/piptools/utils.py +++ b/piptools/utils.py @@ -682,8 +682,8 @@ def parse_config_file( # `[tool.pip-tools.compile]` or `[tool.pip-tools.sync]` piptools_config: dict[str, Any] = config.get("tool", {}).get("pip-tools", {}) - # TODO: Replace with `str.removeprefix()` once dropped 3.8 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, {}))