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

Validate parsed config against CLI options #1910

Merged
merged 6 commits into from
Jul 16, 2023

Conversation

atugushev
Copy link
Member

@atugushev atugushev commented Jul 13, 2023

Refs: #1909.

Config key validation

$ cat .pip-tools.toml
[tool.pip-tools]
annotation = false

$ pip-compile
Error: No such config key: annotation. (Possible options: annotate, annotation_style)

Config value validation

$ cat .pip-tools.toml
[tool.pip-tools]
dry-run = ["foo", "bar"]

$ pip-compile
Error: Invalid value for config key 'dry-run': ['foo', 'bar']
Contributor checklist
  • Provided the tests for the changes.
  • Assure PR title is short, clear, and good to be included in the user-oriented changelog
Maintainer checklist
  • Assure one of these labels is present: backwards incompatible, feature, enhancement, deprecation, bug, dependency, docs or skip-changelog as they determine changelog listing.
  • Assign the PR to an existing or new milestone for the target version (following Semantic Versioning).

@atugushev atugushev marked this pull request as ready for review July 13, 2023 23:02
@@ -573,21 +573,16 @@ def test_get_sys_path_for_python_executable():
@pytest.mark.parametrize(
("pyproject_param", "new_default"),
(
# From sync
("ask", True),
Copy link
Member Author

@atugushev atugushev Jul 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I have removed the options for pip-sync (which led to test failures) because we only test the pip-compile CLI here. We could add a separate test for pip-sync with its own options, but it seems redundant to me.

@atugushev atugushev added the enhancement Improvements to functionality label Jul 13, 2023
Copy link
Contributor

@chrysle chrysle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add the below condition? It would convert all negating boolean flags to their valid equivalents, e.g. no-annotate = true -> annotate = false, but error out in case these equivalents or the given values do not exist.

Of course the suggestions for non-valid flags would have to be adapted...

Comment on lines +592 to +612
for key, value in config.items():
# Validate unknown keys
if key not in cli_params:
possibilities = difflib.get_close_matches(key, cli_params.keys())
raise click.NoSuchOption(
option_name=key,
message=f"No such config key {key!r}.",
possibilities=possibilities,
ctx=click_context,
)

# Validate invalid values
param = cli_params[key]
try:
param.type.convert(value=value, param=param, ctx=click_context)
except Exception as e:
raise click.BadOptionUsage(
option_name=key,
message=f"Invalid value for config key {key!r}: {value!r}.",
ctx=click_context,
) from e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for key, value in config.items():
# Validate unknown keys
if key not in cli_params:
possibilities = difflib.get_close_matches(key, cli_params.keys())
raise click.NoSuchOption(
option_name=key,
message=f"No such config key {key!r}.",
possibilities=possibilities,
ctx=click_context,
)
# Validate invalid values
param = cli_params[key]
try:
param.type.convert(value=value, param=param, ctx=click_context)
except Exception as e:
raise click.BadOptionUsage(
option_name=key,
message=f"Invalid value for config key {key!r}: {value!r}.",
ctx=click_context,
) from e
swap_keys = False
new_items = {}
old_items = {}
for key, value in config.items():
# Validate unknown keys
if key not in cli_params:
# Replace boolean flag contexts like ``no_annotate`` with their equivalents
if key.startswith("no_"):
old_items[key] = value
key = key[3:]
value = not value
swap_keys = True
new_items[key] = value
else:
possibilities = difflib.get_close_matches(key, cli_params.keys())
raise click.NoSuchOption(
option_name=key,
message=f"No such config key {key!r}.",
possibilities=possibilities,
ctx=click_context,
)
# Validate invalid values
param = cli_params[key]
try:
param.type.convert(value=value, param=param, ctx=click_context)
except Exception as e:
raise click.BadOptionUsage(
option_name=key,
message=f"Invalid value for config key {key!r}: {value!r}.",
ctx=click_context,
) from e
if swap_keys:
for key, value in old_items.items():
del config[key]
for key, value in new_items.items():
config[key] = value

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense indeed. I would prefer to implement this in the parse_config_file() function though. The _validate_config() function does not intend a config mutation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that function, you might be able to implement it like this:

    swap_items = False
    new_items = {}
    old_items = {}

    for key, value in piptools_config.items(): 
        # Replace boolean flag contexts like ``no_annotate`` with their equivalents
        if key.startswith("no_"):
            swap_items = True
            old_items[key] = value
            if isinstance(value, bool):
                value = not value
            new_items[key[3:]] = value

    if swap_items:
        for key, value in old_items.items():
            del piptools_config[key]
        for key, value in new_items.items():
            piptools_config[key] = value


    return piptools_config

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like to address this in a following up PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks 👍🏻

Copy link
Member Author

@atugushev atugushev Jul 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a reminder to myself: I think we can refactor parse_config_file to parse options using the technique from the following snippet.

Snippet
(Pdb++) opts = {
    opt: option 
    for option in ctx.command.params
    for opt in itertools.chain(option.opts, option.secondary_opts)
}

(Pdb++) pp opts
{'--all-extras': <Option all_extras>,
 '--allow-unsafe': <Option allow_unsafe>,
 '--annotate': <Option annotate>,
 '--no-annotate': <Option annotate >,
 ...
}
 
(Pdb++) opts['--pip-args'].name
'pip_args_str'

(Pdb++) opts['--annotate'].name
'annotate'

(Pdb++) opts['--no-annotate'].name
'annotate'

(Pdb++) opts['--extra'].name
'extras'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I guess this would enable us to rationalise the function get_click_dest_for_option() away. Would you like me to address that too, while I'm at it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, feel free to!

@AndydeCleyre
Copy link
Contributor

This looks good to me, thanks!

Can you include the exception types to expect, click.BadOptionUsage and click.NoSuchOption, in the _validate_config docstring?

@atugushev
Copy link
Member Author

atugushev commented Jul 15, 2023

Can you include the exception types to expect, click.BadOptionUsage and click.NoSuchOption, in the _validate_config docstring?

@AndydeCleyre thanks! Addressed in 278d8df.

@atugushev atugushev enabled auto-merge (squash) July 16, 2023 09:54
@atugushev
Copy link
Member Author

Thanks @AndydeCleyre and @chrysle for the review! 🙏🏻

@atugushev atugushev merged commit 9636428 into jazzband:main Jul 16, 2023
34 checks passed
@atugushev atugushev added the config Related to pip-tools' configuration label Jul 16, 2023
@atugushev atugushev deleted the validate-config branch July 17, 2023 15:44
@atugushev atugushev added this to the 7.1.0 milestone Jul 17, 2023
@atugushev atugushev changed the title Validate parsed config against click command params Validate parsed config against CLI options Jul 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
config Related to pip-tools' configuration enhancement Improvements to functionality
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants