Skip to content

Commit

Permalink
new: Add a No Default option when running the configure command (#…
Browse files Browse the repository at this point in the history
…472)

## 📝 Description

This change adds a `No Default` option to the `linode-cli configure`
command that allows users to optionally clear a default if it is already
defined in the existing config. This differs from the skip (enter)
functionality in that the skip functionality retains the previous config
value and the `No Default` option drops the config value.

## ✔️ How to Test

`make testunit`
  • Loading branch information
lgarber-akamai authored Jun 14, 2023
1 parent bcac1ce commit 7c9e549
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 21 deletions.
28 changes: 26 additions & 2 deletions linodecli/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)
from .helpers import (
_check_browsers,
_config_get_with_default,
_default_thing_input,
_get_config,
_get_config_path,
Expand Down Expand Up @@ -382,34 +383,49 @@ def configure(
regions,
"Default Region (Optional): ",
"Please select a valid Region, or press Enter to skip",
current_value=_config_get_with_default(
self.config, username, "region"
),
)

config["type"] = _default_thing_input(
"Default Type of Linode to deploy.",
types,
"Default Type of Linode (Optional): ",
"Please select a valid Type, or press Enter to skip",
current_value=_config_get_with_default(
self.config, username, "type"
),
)

config["image"] = _default_thing_input(
"Default Image to deploy to new Linodes.",
images,
"Default Image (Optional): ",
"Please select a valid Image, or press Enter to skip",
current_value=_config_get_with_default(
self.config, username, "image"
),
)

config["mysql_engine"] = _default_thing_input(
"Default Engine to create a Managed MySQL Database.",
mysql_engines,
"Default Engine (Optional): ",
"Please select a valid MySQL Database Engine, or press Enter to skip",
current_value=_config_get_with_default(
self.config, username, "mysql_engine"
),
)

config["postgresql_engine"] = _default_thing_input(
"Default Engine to create a Managed PostgreSQL Database.",
postgresql_engines,
"Default Engine (Optional): ",
"Please select a valid PostgreSQL Database Engine, or press Enter to skip",
current_value=_config_get_with_default(
self.config, username, "postgresql_engine"
),
)

if auth_users:
Expand All @@ -418,6 +434,9 @@ def configure(
auth_users,
"Default Option (Optional): ",
"Please select a valid Option, or press Enter to skip",
current_value=_config_get_with_default(
self.config, username, "authorized_users"
),
)

# save off the new configuration
Expand Down Expand Up @@ -446,8 +465,13 @@ def configure(
print(f"Active user is now {username}")

for k, v in config.items():
if v:
self.config.set(username, k, v)
if v is None:
if self.config.has_option(username, k):
self.config.remove_option(username, k)

continue

self.config.set(username, k, v)

self.write_config()
os.chmod(_get_config_path(), 0o600)
Expand Down
73 changes: 54 additions & 19 deletions linodecli/configuration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import configparser
import os
import webbrowser
from typing import Any, Optional

from .auth import _do_get_request

Expand Down Expand Up @@ -84,38 +85,72 @@ def _check_browsers():


def _default_thing_input(
ask, things, prompt, error, optional=True
ask, things, prompt, error, optional=True, current_value=None
): # pylint: disable=too-many-arguments
"""
Requests the user choose from a list of things with the given prompt and
error if they choose something invalid. If optional, the user may hit
enter to not configure this option.
"""
print(f"\n{ask} Choices are:")

exists = current_value is not None

idx_offset = int(exists) + 1

# If there is a current value, users should have the option to clear it
if exists:
print(" 1 - No Default")

for ind, thing in enumerate(things):
print(f" {ind + 1} - {thing}")
print(f" {ind + idx_offset} - {thing}")
print()

ret = ""
while True:
choice = input(prompt)

if choice:
try:
choice = int(choice)
choice = things[choice - 1]
except:
pass

if choice in list(things):
ret = choice
break
print(error)
else:
choice_idx = input(prompt)

if not choice_idx:
# The user wants to skip this config option
if optional:
break
return current_value

print(error)
continue

try:
choice_idx = int(choice_idx)
except:
# Re-prompt if invalid value
continue

# The user wants to drop this default
if exists and choice_idx == 1:
return None

# We need to shift the index to account for the "No Default" option
choice_idx -= idx_offset

# Validate index
if choice_idx >= len(things) or choice_idx < 0:
print(error)
return ret
continue

# Choice was valid; return
return things[choice_idx]


def _config_get_with_default(
config: configparser.ConfigParser,
user: str,
field: str,
default: Any = None,
) -> Optional[Any]:
"""
Gets a ConfigParser value and returns a default value if the key isn't found.
"""
return (
config.get(user, field) if config.has_option(user, field) else default
)


def _handle_no_default_user(self): # pylint: disable=too-many-branches
Expand Down
150 changes: 150 additions & 0 deletions tests/unit/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import requests_mock

from linodecli import configuration
from linodecli.configuration import _default_thing_input


class TestConfiguration:
Expand Down Expand Up @@ -371,3 +372,152 @@ def mock_input(prompt):
# make sure that we set the default engine value according to type of database
assert conf.get_value("mysql_engine") == "mysql/test-engine"
assert conf.get_value("postgresql_engine") == "postgresql/test-engine"

def test_default_thing_input_no_current(self, monkeypatch):
stdout_buf = io.StringIO()
monkeypatch.setattr("sys.stdin", io.StringIO("1\n"))

with contextlib.redirect_stdout(stdout_buf):
result = _default_thing_input(
"foo\n", ["foo", "bar"], "prompt text", "error text"
)

output_lines = stdout_buf.getvalue().splitlines()

assert output_lines == [
"",
"foo",
" Choices are:",
" 1 - foo",
" 2 - bar",
"",
"prompt text",
]

assert result == "foo"

def test_default_thing_input_skip(self, monkeypatch):
stdout_buf = io.StringIO()
monkeypatch.setattr("sys.stdin", io.StringIO("\n"))

with contextlib.redirect_stdout(stdout_buf):
result = _default_thing_input(
"foo\n",
["foo", "bar"],
"prompt text",
"error text",
current_value="foo",
)

output_lines = stdout_buf.getvalue().splitlines()

print(output_lines)

assert output_lines == [
"",
"foo",
" Choices are:",
" 1 - No Default",
" 2 - foo",
" 3 - bar",
"",
"prompt text",
]

assert result == "foo"

def test_default_thing_input_no_default(self, monkeypatch):
stdout_buf = io.StringIO()
monkeypatch.setattr("sys.stdin", io.StringIO("1\n"))

with contextlib.redirect_stdout(stdout_buf):
result = _default_thing_input(
"foo\n",
["foo", "bar"],
"prompt text",
"error text",
current_value="foo",
)

output_lines = stdout_buf.getvalue().splitlines()

print(output_lines)

assert output_lines == [
"",
"foo",
" Choices are:",
" 1 - No Default",
" 2 - foo",
" 3 - bar",
"",
"prompt text",
]

assert result is None

def test_default_thing_input_valid(self, monkeypatch):
stdout_buf = io.StringIO()
monkeypatch.setattr("sys.stdin", io.StringIO("3\n"))

with contextlib.redirect_stdout(stdout_buf):
result = _default_thing_input(
"foo\n",
["foo", "bar"],
"prompt text",
"error text",
current_value="foo",
)

output_lines = stdout_buf.getvalue().splitlines()

print(output_lines)

assert output_lines == [
"",
"foo",
" Choices are:",
" 1 - No Default",
" 2 - foo",
" 3 - bar",
"",
"prompt text",
]

assert result == "bar"

def test_default_thing_input_valid_no_current(self, monkeypatch):
stdout_buf = io.StringIO()
monkeypatch.setattr("sys.stdin", io.StringIO("3\n1\n"))

with contextlib.redirect_stdout(stdout_buf):
result = _default_thing_input(
"foo\n",
["foo", "bar"],
"prompt text",
"error text",
)

output = stdout_buf.getvalue()

assert "error text" in output

assert result == "foo"

def test_default_thing_input_out_of_range(self, monkeypatch):
stdout_buf = io.StringIO()
monkeypatch.setattr("sys.stdin", io.StringIO("4\n2\n"))

with contextlib.redirect_stdout(stdout_buf):
result = _default_thing_input(
"foo\n",
["foo", "bar"],
"prompt text",
"error text",
current_value="foo",
)

output = stdout_buf.getvalue()
assert "error text" in output

assert result == "foo"

0 comments on commit 7c9e549

Please sign in to comment.