Skip to content

Commit

Permalink
Added support for booleans (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
fpgmaas authored Mar 21, 2023
1 parent 58c95e3 commit e03a780
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 11 deletions.
72 changes: 67 additions & 5 deletions ckit/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,34 @@ class Argument:
default: str = None


@dataclass
class Boolean:
"""
Class to hold a boolean option, that will prompt the user for yes/no before executing the command.
Args:
name: Name of the boolean. Should match $name in the command.
prompt: The prompt to show to the user.
if_true: Value that will replace $name if user chooses `Y`.
if_true: Value that will replace $name if user chooses `N`. Defaults to "".
default: Default value (True/False corresponding to `Y`/`N`) if the user presses Enter instead of making a choice.
"""

name: str
prompt: str
if_true: str
if_false: str = ""
default: bool = True


class Command:
def __init__(
self, name: str, cmd: str | list(str), echo: bool = True, args: list[str | dict[str, any]] = None
self,
name: str,
cmd: str | list(str),
echo: bool = True,
args: list[str | dict[str, any]] = None,
booleans: list[dict[str, any]] = None,
) -> None:
"""
A command object.
Expand All @@ -25,6 +50,7 @@ def __init__(
self.cmd = [cmd] if isinstance(cmd, str) else cmd
self.echo = echo
self.arguments = self._parse_arguments(args) if args else None
self.booleans = self._parse_booleans(booleans) if booleans else None

if self.arguments:
self._validate_arguments()
Expand All @@ -47,6 +73,21 @@ def _parse_arguments(self, args: list[str | dict[str, any]]) -> list[Argument]:
raise ValueError(f"Argument should be type dict or string, but found {type(arg)}: {arg}")
return arguments

def _parse_booleans(self, booleans: list[dict[str, any]]) -> list[Argument]:
"""
Parse a list of boolean definitions into Boolean objects.
Args:
args: A list of dictonaries, where the keys are the names of the Boolean object, and the values are dicts with key-value pairs for the other
properties. Valid values for these key-value paris are the class attributes of the Boolean class, except for 'name'.
"""
parsed_booleans = []
for boolean in booleans:
name = list(boolean.keys())[0]
value = list(boolean.values())[0]
parsed_booleans.append(Boolean(name=name, **value))
return parsed_booleans

def _validate_arguments(self):
"""
Validate that the arguments are actually used in the commands.
Expand All @@ -61,8 +102,8 @@ def _validate_arguments(self):
def run(self):
cmd = self.cmd

if self.arguments:
cmd = self._prompt_and_replace_arguments(cmd)
if self.arguments or self.booleans:
cmd = self._prompt_and_replace_arguments_and_booleans(cmd)

for command in cmd:
if self.echo:
Expand All @@ -80,19 +121,40 @@ def _expand_env_vars(command):
"""
return os.path.expandvars(command)

def _prompt_and_replace_arguments_and_booleans(self, cmd):
"""
For each argument or boolean, prompt the user for input, and then replace the matching string in the cmd.
"""
commands_formatted_to_print = "\n".join(cmd)
click.echo(f"Command{'s' if len(cmd) > 1 else ''} to be run:\n\n{commands_formatted_to_print}\n")
if self.arguments:
cmd = self._prompt_and_replace_arguments(cmd)
if self.booleans:
cmd = self._prompt_and_replace_booleans(cmd)
return cmd

def _prompt_and_replace_arguments(self, cmd):
"""
For each argument, prompt the user for input, and then replace the matching string in the cmd.
"""
commands_formatted_to_print = "\n".join(self.cmd)
click.echo(f"Command{'s' if len(self.cmd) > 1 else ''} to be run:\n\n{commands_formatted_to_print}\n")
for argument in self.arguments:
value = click.prompt(
f"Please enter a value for argument `{argument.name}`", type=str, default=argument.default
)
cmd = [command.replace(f"${argument.name}", value) for command in cmd]
return cmd

def _prompt_and_replace_booleans(self, cmd):
"""
For each boolean, prompt the user for input, and then replace the matching string in the cmd.
"""
for boolean in self.booleans:
value = click.confirm(f"{boolean.prompt}", default=boolean.default)
replacement = boolean.if_true if value else boolean.if_false
cmd = [command.replace(f"${boolean.name}", replacement) for command in cmd]

return cmd

def __repr__(self):
return f"Command `{self.name}`"

Expand Down
8 changes: 8 additions & 0 deletions ckit/config/config_files_initiatior.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
- name
- fruit: apple
command-with-boolean:
cmd: "ls $detailed"
echo: false
booleans:
- detailed:
prompt: Show details?
if_true: -lh
one-long-command:
cmd: "echo Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
Expand Down
29 changes: 28 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,38 @@ command:
- name
```

Arguments can be given a defalt value as follows:
Arguments can be given a default value as follows:

```yaml
command:
cmd: "echo My name is $name"
args:
- name: "Calvin"
```

### booleans (Optional)

A list of boolean arguments for the command, where the value in the command will depend on a `Y/N` prompt before the command(s) specified in `cmd` will be run. The value of a boolean argument named `name` will replace `$name` in the `cmd`. For example:

```yaml
command-with-boolean:
cmd: "ls $detailed"
booleans:
- detailed:
prompt: "Show details?"
if_true: -lh
```

By default, the value that is passed when the user chooses `N` is `""`. This can be changed with the `if_false` argument. The default choice can be changed from `Y` to `N` with the `default` argument. See the following example:

```yaml
command-with-boolean:
cmd: "echo I like $fruit"
echo: false
booleans:
- fruit:
prompt: "Do you like apples (y) or pears (n)?"
if_true: apples
if_false: pears
default: false
```
10 changes: 7 additions & 3 deletions tests/config/test_yaml_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ def test_yaml_parser(tmp_path: Path) -> None:
echo:
cmd: "echo Hello World!"
group2:
echo:
cmd: "echo Hello World!"
subgroup-a:
echo:
cmd: "echo Hello World!"
subgroup-b:
echo:
cmd: "echo Hello World!"
"""

with run_within_dir(tmp_path):
Expand All @@ -21,4 +25,4 @@ def test_yaml_parser(tmp_path: Path) -> None:

parsed_yaml = YamlParser().parse(filepath)
assert parsed_yaml.get("group1").get_names() == ["echo"]
assert parsed_yaml.get("group2").get_names() == ["echo"]
assert parsed_yaml.get("group2").get_names() == ["subgroup-a", "subgroup-b"]
36 changes: 34 additions & 2 deletions tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
def test_command_with_argument(mock_prompt, capsys):
mock_prompt.return_value = "Calvin"
command = Command(name="test", cmd="echo Hello $name!", args=["name"])
command.arguments[0].name = "name"
command.arguments[0].default = None
assert command.arguments[0].name == "name"
assert command.arguments[0].default is None

output = command.run()
assert "echo Hello Calvin" in capsys.readouterr().out
Expand All @@ -21,6 +21,38 @@ def test_command_with_argument_with_default():
command.arguments[0].default = "Joe"


@mock.patch("click.confirm")
def test_command_with_boolean_true(mock_confirm, capsys):
mock_confirm.return_value = True
boolean = {"message": {"prompt": "Print message?", "if_true": "Hello!"}}
command = Command(name="test", cmd="echo $message", booleans=[boolean])
assert command.booleans[0].name == "message"
assert command.booleans[0].prompt == "Print message?"
assert command.booleans[0].if_true == "Hello!"
assert command.booleans[0].if_false == ""
assert command.booleans[0].default

output = command.run()
assert "echo Hello!" in capsys.readouterr().out
assert output.returncode == 0


@mock.patch("click.confirm")
def test_command_with_boolean_false(mock_confirm, capsys):
mock_confirm.return_value = False
boolean = {"message": {"prompt": "Print message?", "if_true": "Hello!", "if_false": "Hobbes"}}
command = Command(name="test", cmd="echo $message", booleans=[boolean])
assert command.booleans[0].name == "message"
assert command.booleans[0].prompt == "Print message?"
assert command.booleans[0].if_true == "Hello!"
assert command.booleans[0].if_false == "Hobbes"
assert command.booleans[0].default

output = command.run()
assert "echo Hobbes" in capsys.readouterr().out
assert output.returncode == 0


# Causes issue with tox in GH Action

# def test_command_stops_running_on_error(capsys):
Expand Down

0 comments on commit e03a780

Please sign in to comment.