-
Notifications
You must be signed in to change notification settings - Fork 916
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
Use click features for project creation prompts #4387
base: main
Are you sure you want to change the base?
Changes from 24 commits
9bb3361
10dbea5
9b4a9a7
73180ca
48934d5
3f184a8
75456b7
601b02f
8305cae
05b7479
7b71418
d0af3d3
7aade5b
0ce0219
6d37aed
893415d
f8cc6c3
c1e96df
eae9d5a
1d7af53
0ac3d2b
d8a6b6f
f9534ee
f777d09
da38e1a
6cc3e15
e71e015
5828415
0923ca4
8eec43b
20e5fe7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -167,6 +167,7 @@ def _kedro_version_equal_or_lower_to_starters(version: str) -> bool: | |
"pyspark": "6", | ||
"viz": "7", | ||
} | ||
|
||
NUMBER_TO_TOOLS_NAME = { | ||
"1": "Linting", | ||
"2": "Testing", | ||
|
@@ -311,8 +312,18 @@ def starter() -> None: | |
@click.option("--starter", "-s", "starter_alias", help=STARTER_ARG_HELP) | ||
@click.option("--checkout", help=CHECKOUT_ARG_HELP) | ||
@click.option("--directory", help=DIRECTORY_ARG_HELP) | ||
@click.option("--tools", "-t", "selected_tools", help=TOOLS_ARG_HELP) | ||
@click.option("--name", "-n", "project_name", help=NAME_ARG_HELP) | ||
@click.option( | ||
"--name", | ||
"-n", | ||
"project_name", | ||
help=NAME_ARG_HELP, | ||
) | ||
@click.option( | ||
"--tools", | ||
"-t", | ||
"selected_tools", | ||
help=TOOLS_ARG_HELP, | ||
) | ||
@click.option("--example", "-e", "example_pipeline", help=EXAMPLE_ARG_HELP) | ||
@click.option("--telemetry", "-tc", "telemetry_consent", help=TELEMETRY_ARG_HELP) | ||
def new( # noqa: PLR0913 | ||
|
@@ -337,6 +348,7 @@ def new( # noqa: PLR0913 | |
"example": example_pipeline, | ||
"telemetry_consent": telemetry_consent, | ||
} | ||
|
||
_validate_flag_inputs(flag_inputs) | ||
starters_dict = _get_starters_dict() | ||
|
||
|
@@ -381,6 +393,7 @@ def new( # noqa: PLR0913 | |
shutil.rmtree(tmpdir, onerror=_remove_readonly) # type: ignore[arg-type] | ||
|
||
# Obtain config, either from a file or from interactive user prompts. | ||
|
||
extra_context = _get_extra_context( | ||
prompts_required=prompts_required, | ||
config_path=config_path, | ||
|
@@ -727,7 +740,8 @@ def _fetch_validate_parse_config_from_file( | |
|
||
|
||
def _fetch_validate_parse_config_from_user_prompts( | ||
prompts: dict[str, Any], cookiecutter_context: OrderedDict | None | ||
prompts: dict[str, Any], | ||
cookiecutter_context: OrderedDict | None, | ||
) -> dict[str, str]: | ||
"""Interactively obtains information from user prompts. | ||
|
||
|
@@ -739,9 +753,6 @@ def _fetch_validate_parse_config_from_user_prompts( | |
Configuration for starting a new project. This is passed as ``extra_context`` | ||
to cookiecutter and will overwrite the cookiecutter.json defaults. | ||
""" | ||
from cookiecutter.environment import StrictEnvironment | ||
from cookiecutter.prompt import read_user_variable, render_variable | ||
|
||
if not cookiecutter_context: | ||
raise Exception("No cookiecutter context available.") | ||
|
||
|
@@ -751,14 +762,16 @@ def _fetch_validate_parse_config_from_user_prompts( | |
prompt = _Prompt(**prompt_dict) | ||
|
||
# render the variable on the command line | ||
cookiecutter_variable = render_variable( | ||
env=StrictEnvironment(context=cookiecutter_context), | ||
raw=cookiecutter_context.get(variable_name), | ||
cookiecutter_dict=config, | ||
) | ||
default_value = cookiecutter_context.get(variable_name) or "" | ||
|
||
# read the user's input for the variable | ||
user_input = read_user_variable(str(prompt), cookiecutter_variable) | ||
user_input = click.prompt( | ||
str(prompt), | ||
default=default_value, | ||
show_default=True, | ||
type=str, | ||
).strip() | ||
|
||
if user_input: | ||
prompt.validate(user_input) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we still need to use our own custom validation or is it possible to use click for that as well? I saw that was one of the suggestions on the original ticket #3878 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Especially lower down in the code we have pretty complex parsing/validation methods e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm looking into this right now. From what I had seen I think the options that click has might be a bit unspecific for what we need, but there's an option to apply custom validation logic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes for the more complicated options it might not be possible, but for
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That does work for yes/no options. One minor issue though is that the error message is not customizable though. On top of that, the example pipeline option received through a config file would still need to have some validation since it doesn't go through the CLI options, so we'd still need the function. This is what the error message from As opposed to the one we have right now, from For interactive prompts, that are happening inside a loop, there would need to be some sort of logic to pass the click.Choice value for the different prompts. Could be done through the prompts.yml file maybe. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For that specific (Also, could we maybe move this user_input parameter to be inside the _Prompt class and validate it as we set it? The function is called only once, I don't think it necessarily needs to be a function) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another possibility is using the callback parameter in click for custom validation. For example, this: def _validate_yes_no(ctx, param, value) -> None:
if not re.match(r"(?i)^\s*(y|yes|n|no)\s*$", value, flags=re.X):
raise click.BadParameter(f"'{value}' is an invalid value for {param}. It must contain only y, n, YES, or NO (case insensitive).")
return value
@click.option("--telemetry", "-tc", "telemetry_consent", help=TELEMETRY_ARG_HELP, callback=_validate_yes_no) Results in this: Does require writing the callback functions though. And the output is the click Exception output. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally think the default click exception ("Invalid value for..") is fine. But maybe @astrojuanlu has a different view on that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree the default click exception is fine, considering the tradeoffs |
||
config[variable_name] = user_input | ||
|
@@ -997,9 +1010,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: | |
self.error_message = kwargs.get("error_message", "") | ||
|
||
def __str__(self) -> str: | ||
# Format the title with optional color | ||
title = self.title.strip().title() | ||
title = click.style(title + "\n" + "=" * len(title), bold=True) | ||
prompt_lines = [title, self.text] | ||
title = click.style(title, fg="cyan", bold=True) | ||
title_line = "=" * len(self.title) | ||
title_line = click.style(title_line, fg="cyan", bold=True) | ||
|
||
# Format the main text | ||
text = self.text.strip() | ||
|
||
# Combine everything | ||
prompt_lines = [title, title_line, text] | ||
prompt_text = "\n".join(str(line).strip() for line in prompt_lines) | ||
return f"\n{prompt_text}\n" | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use Click choice options here https://click.palletsprojects.com/en/stable/options/#choice-options to simplify the prompting and validation logic for example project?