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 mutually exclusive group #422

Merged
merged 12 commits into from
Aug 16, 2024

Conversation

fran-tirapu
Copy link
Contributor

@fran-tirapu fran-tirapu commented Aug 5, 2024

As discussed on #421 we should support mutually exclusive groups ,
this PR aims to add this functionality to our cli arguments.

QA

New keyword mutually_exclusive_required_group can be added to any cli.Argument and expected XOR validation is executed by argparse library.

@fran-tirapu fran-tirapu marked this pull request as ready for review August 5, 2024 13:25
Copy link
Contributor

@priitlatt priitlatt left a comment

Choose a reason for hiding this comment

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

We currently already have plenty of mutually exclusive arguments, but all of them are optional. So this feature would benefit us a lot more if both required and optional mutually exclusive groups were supported.

Please also track the changes in CHANGELOG.md. This is useful for easily and publicly backtracking what and when was modified, but it is also later used to generate release notes.

src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_properties.py Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
def _get_argument_group(self, argument) -> ArgumentGroup:
if argument.argument_group_name is None:
if argument.argument_group_name is None and argument.mutually_exclusive_required_group is None:
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be great if something somewhere validated that argument is not assigned both to a named group and a mutually exclusive group.

src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
@fran-tirapu fran-tirapu requested a review from priitlatt August 5, 2024 17:00
Copy link
Contributor

@priitlatt priitlatt left a comment

Choose a reason for hiding this comment

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

In addition what is pointed out in the code comments I like to bring out that so far we've fully mirrored CLI help messages in Markdown docs. Now that arguments in mutually exclusive groups are all explicitly separated from other arguments, this will not be reflected in Markdown documents. As such, I think we need changes to doc.py too.

src/codemagic/cli/argument/argument_properties.py Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_parser_builder.py Outdated Show resolved Hide resolved
src/codemagic/cli/argument/argument_properties.py Outdated Show resolved Hide resolved
@fran-tirapu fran-tirapu requested a review from priitlatt August 7, 2024 09:44
@fran-tirapu fran-tirapu changed the title Add mutually exclusive required group Add mutually exclusive group Aug 7, 2024
Copy link
Contributor

@priitlatt priitlatt left a comment

Choose a reason for hiding this comment

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

Sorry, while the functionality is there in the most part, it really bothers me that the args in help messages are in rather unexpected order. I created a test app to illustrate it

Test app code
from typing import Optional

from codemagic import cli
from codemagic.cli import Colors

yes_no_group = cli.MutuallyExclusiveGroup(
    name="agree or disagree",
    required=False,
)

project_number_group = cli.MutuallyExclusiveGroup(
    name="specify project number",
    required=True,
)


class Argument(cli.Argument):
    PROJECT_NUMBER = cli.ArgumentProperties(
        key="project_number",
        flags=("--project-number", "-p"),
        description="Project number in Firebase",
        mutually_exclusive_group=project_number_group,
    )
    PROJECT_ID = cli.ArgumentProperties(
        key="project_id",
        flags=("--project-id",),
        description=(
            f"{Colors.BOLD('Deprecated in version x.y.z')} and will be removed in a future release, "
            f"use `{Colors.BRIGHT_BLUE(cli.ArgumentProperties.get_flag(PROJECT_NUMBER))}` instead. "
            "Project number in Firebase"
        ),
        mutually_exclusive_group=project_number_group,
    )

    YES = cli.ArgumentProperties(
        flags=("--yes", "-y"),
        key="yes",
        type=bool,
        description="Agree",
        argparse_kwargs={"action": "store_true", "default": None},
        mutually_exclusive_group=yes_no_group,
    )
    NO = cli.ArgumentProperties(
        flags=("--no", "-n"),
        key="no",
        type=bool,
        description="Disagree",
        argparse_kwargs={"action": "store_false", "default": None},
        mutually_exclusive_group=yes_no_group,
    )

    TOOL_OPTIONAL = cli.ArgumentProperties(
        flags=("--tool-optional",),
        key="tool_optional",
        description="This value is optional for the tool",
        argparse_kwargs={"required": False},
    )

    TOOL_REQUIRED = cli.ArgumentProperties(
        flags=("--tool-required",),
        key="tool_required",
        description="This value is required for the tool",
        argparse_kwargs={"required": True},
    )

    TOOL_POSITIONAL = cli.ArgumentProperties(
        key="tool_positional",
        description="Positional value for tool",
    )

    ACTION_OPTIONAL = cli.ArgumentProperties(
        flags=("--action-optional",),
        key="action_optional",
        description="This value is optional for the action",
        argparse_kwargs={"required": False},
    )

    ACTION_REQUIRED = cli.ArgumentProperties(
        flags=("--action-required",),
        key="action_required",
        description="This value is required for the action",
        argparse_kwargs={"required": True},
    )

    ACTION_POSITIONAL = cli.ArgumentProperties(
        key="action_positional",
        description="Positional value for action",
    )


@cli.common_arguments(
    Argument.TOOL_POSITIONAL,
    Argument.TOOL_REQUIRED,
    Argument.TOOL_OPTIONAL,
    Argument.PROJECT_ID,
    Argument.PROJECT_NUMBER,
)
class MutuallyExclusiveArguments(cli.CliApp):
    """
    Help message for mutually-exclusive-arguments tool
    """

    def __init__(
        self,
        *,
        tool_positional: str,
        tool_required: str,
        tool_optional: Optional[str],
        project_id: Optional[str] = None,
        project_number: Optional[str] = None,
        **kwargs,
    ):
        super().__init__(**kwargs)
        if not project_number and not project_id:
            raise ValueError("Either project ID or number must be supplied")
        self.tool_positional = tool_positional
        self.tool_required = tool_required
        self.tool_optional = tool_optional
        self.project_id = project_id
        self.project_number = project_number

    @cli.action(
        "show-values",
        Argument.ACTION_POSITIONAL,
        Argument.ACTION_REQUIRED,
        Argument.ACTION_OPTIONAL,
        Argument.YES,
        Argument.NO,
    )
    def show_values(
        self,
        action_positional: str,
        action_required: str,
        action_optional: Optional[str],
        yes: Optional[bool] = None,
        no: Optional[bool] = None,
    ):
        """
        Help message for action show-values
        """

        self.echo("-" * 20)
        self.echo("Tool arguments:")
        self.echo(f"{self.tool_positional=}")
        self.echo(f"{self.tool_required=}")
        self.echo(f"{self.tool_optional=}")
        self.echo(f"{self.project_id=}")
        self.echo(f"{self.project_number=}")
        self.echo("-" * 20)
        self.echo("Action arguments:")
        self.echo(f"{action_positional=}")
        self.echo(f"{action_required=}")
        self.echo(f"{action_optional=}")
        self.echo(f"{yes=}")
        self.echo(f"{no=}")
        self.echo("-" * 20)


if __name__ == "__main__":
    MutuallyExclusiveArguments.invoke_cli()

Help message for this in the terminal is as follows:

Screenshot

Screenshot 2024-08-09 at 12 24 36

Note the order in which options are shown:

  1. required actions args,
  2. optional actions args,
  3. common optional args for all tools,
  4. optional mutually exclusive action args,
  5. required args for tool,
  6. optional args for tool,
  7. required mutually exclusive action tool.

The mix of optional and required things is rather difficult to follow and can be only source for confusion. I find that either action or tool level args should come first (in logical order), then tool or action (whatever was not used in previous part) args in the same order as the previous. Finally the options that apply to all tools can follow.

So, one way to clean things up would be to show args in the following order:

  1. required args for tool (common for all actions in tool),
  2. required mutually exlusive args for tool (common for all actions in tool),
  3. optional args for tool (common for all actions in tool),
  4. optional mutually exlcusive args for tool (common for all actions in tool),
  5. required args for action,
  6. required mutually exclusive args for action,
  7. optional args for action,
  8. optional mutually exclusive args for action,
  9. common optional args for all tools.

Couple of more things:

  • tool's common mutually exclusive arguments are described as action arguments, which is contradicting with how other common args are described in help messages,
  • optional mutually exclusive groups are not documented in generated markdown docs.

@fran-tirapu fran-tirapu requested a review from priitlatt August 13, 2024 14:17
Copy link
Contributor

@priitlatt priitlatt left a comment

Choose a reason for hiding this comment

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

Overall looks great! 🙌
Currently still requesting changes because of merge conflicts in changelog.
Apart from that, there are some really minor comments on markdown docs generation script.

doc.py Outdated Show resolved Hide resolved
doc.py Outdated Show resolved Hide resolved
doc.py Outdated Show resolved Hide resolved
Copy link
Contributor

@priitlatt priitlatt left a comment

Choose a reason for hiding this comment

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

:shipit:

@fran-tirapu fran-tirapu merged commit 56a37bf into master Aug 16, 2024
11 checks passed
@fran-tirapu fran-tirapu deleted the feature/add-mutually_exclusive_required_group branch August 16, 2024 08:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants