Skip to content

Commit 7c9e549

Browse files
new: Add a No Default option when running the configure command (#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`
1 parent bcac1ce commit 7c9e549

File tree

3 files changed

+230
-21
lines changed

3 files changed

+230
-21
lines changed

linodecli/configuration/__init__.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from .helpers import (
1717
_check_browsers,
18+
_config_get_with_default,
1819
_default_thing_input,
1920
_get_config,
2021
_get_config_path,
@@ -382,34 +383,49 @@ def configure(
382383
regions,
383384
"Default Region (Optional): ",
384385
"Please select a valid Region, or press Enter to skip",
386+
current_value=_config_get_with_default(
387+
self.config, username, "region"
388+
),
385389
)
386390

387391
config["type"] = _default_thing_input(
388392
"Default Type of Linode to deploy.",
389393
types,
390394
"Default Type of Linode (Optional): ",
391395
"Please select a valid Type, or press Enter to skip",
396+
current_value=_config_get_with_default(
397+
self.config, username, "type"
398+
),
392399
)
393400

394401
config["image"] = _default_thing_input(
395402
"Default Image to deploy to new Linodes.",
396403
images,
397404
"Default Image (Optional): ",
398405
"Please select a valid Image, or press Enter to skip",
406+
current_value=_config_get_with_default(
407+
self.config, username, "image"
408+
),
399409
)
400410

401411
config["mysql_engine"] = _default_thing_input(
402412
"Default Engine to create a Managed MySQL Database.",
403413
mysql_engines,
404414
"Default Engine (Optional): ",
405415
"Please select a valid MySQL Database Engine, or press Enter to skip",
416+
current_value=_config_get_with_default(
417+
self.config, username, "mysql_engine"
418+
),
406419
)
407420

408421
config["postgresql_engine"] = _default_thing_input(
409422
"Default Engine to create a Managed PostgreSQL Database.",
410423
postgresql_engines,
411424
"Default Engine (Optional): ",
412425
"Please select a valid PostgreSQL Database Engine, or press Enter to skip",
426+
current_value=_config_get_with_default(
427+
self.config, username, "postgresql_engine"
428+
),
413429
)
414430

415431
if auth_users:
@@ -418,6 +434,9 @@ def configure(
418434
auth_users,
419435
"Default Option (Optional): ",
420436
"Please select a valid Option, or press Enter to skip",
437+
current_value=_config_get_with_default(
438+
self.config, username, "authorized_users"
439+
),
421440
)
422441

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

448467
for k, v in config.items():
449-
if v:
450-
self.config.set(username, k, v)
468+
if v is None:
469+
if self.config.has_option(username, k):
470+
self.config.remove_option(username, k)
471+
472+
continue
473+
474+
self.config.set(username, k, v)
451475

452476
self.write_config()
453477
os.chmod(_get_config_path(), 0o600)

linodecli/configuration/helpers.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import configparser
66
import os
77
import webbrowser
8+
from typing import Any, Optional
89

910
from .auth import _do_get_request
1011

@@ -84,38 +85,72 @@ def _check_browsers():
8485

8586

8687
def _default_thing_input(
87-
ask, things, prompt, error, optional=True
88+
ask, things, prompt, error, optional=True, current_value=None
8889
): # pylint: disable=too-many-arguments
8990
"""
9091
Requests the user choose from a list of things with the given prompt and
9192
error if they choose something invalid. If optional, the user may hit
9293
enter to not configure this option.
9394
"""
9495
print(f"\n{ask} Choices are:")
96+
97+
exists = current_value is not None
98+
99+
idx_offset = int(exists) + 1
100+
101+
# If there is a current value, users should have the option to clear it
102+
if exists:
103+
print(" 1 - No Default")
104+
95105
for ind, thing in enumerate(things):
96-
print(f" {ind + 1} - {thing}")
106+
print(f" {ind + idx_offset} - {thing}")
97107
print()
98108

99-
ret = ""
100109
while True:
101-
choice = input(prompt)
102-
103-
if choice:
104-
try:
105-
choice = int(choice)
106-
choice = things[choice - 1]
107-
except:
108-
pass
109-
110-
if choice in list(things):
111-
ret = choice
112-
break
113-
print(error)
114-
else:
110+
choice_idx = input(prompt)
111+
112+
if not choice_idx:
113+
# The user wants to skip this config option
115114
if optional:
116-
break
115+
return current_value
116+
117+
print(error)
118+
continue
119+
120+
try:
121+
choice_idx = int(choice_idx)
122+
except:
123+
# Re-prompt if invalid value
124+
continue
125+
126+
# The user wants to drop this default
127+
if exists and choice_idx == 1:
128+
return None
129+
130+
# We need to shift the index to account for the "No Default" option
131+
choice_idx -= idx_offset
132+
133+
# Validate index
134+
if choice_idx >= len(things) or choice_idx < 0:
117135
print(error)
118-
return ret
136+
continue
137+
138+
# Choice was valid; return
139+
return things[choice_idx]
140+
141+
142+
def _config_get_with_default(
143+
config: configparser.ConfigParser,
144+
user: str,
145+
field: str,
146+
default: Any = None,
147+
) -> Optional[Any]:
148+
"""
149+
Gets a ConfigParser value and returns a default value if the key isn't found.
150+
"""
151+
return (
152+
config.get(user, field) if config.has_option(user, field) else default
153+
)
119154

120155

121156
def _handle_no_default_user(self): # pylint: disable=too-many-branches

tests/unit/test_configuration.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import requests_mock
1414

1515
from linodecli import configuration
16+
from linodecli.configuration import _default_thing_input
1617

1718

1819
class TestConfiguration:
@@ -371,3 +372,152 @@ def mock_input(prompt):
371372
# make sure that we set the default engine value according to type of database
372373
assert conf.get_value("mysql_engine") == "mysql/test-engine"
373374
assert conf.get_value("postgresql_engine") == "postgresql/test-engine"
375+
376+
def test_default_thing_input_no_current(self, monkeypatch):
377+
stdout_buf = io.StringIO()
378+
monkeypatch.setattr("sys.stdin", io.StringIO("1\n"))
379+
380+
with contextlib.redirect_stdout(stdout_buf):
381+
result = _default_thing_input(
382+
"foo\n", ["foo", "bar"], "prompt text", "error text"
383+
)
384+
385+
output_lines = stdout_buf.getvalue().splitlines()
386+
387+
assert output_lines == [
388+
"",
389+
"foo",
390+
" Choices are:",
391+
" 1 - foo",
392+
" 2 - bar",
393+
"",
394+
"prompt text",
395+
]
396+
397+
assert result == "foo"
398+
399+
def test_default_thing_input_skip(self, monkeypatch):
400+
stdout_buf = io.StringIO()
401+
monkeypatch.setattr("sys.stdin", io.StringIO("\n"))
402+
403+
with contextlib.redirect_stdout(stdout_buf):
404+
result = _default_thing_input(
405+
"foo\n",
406+
["foo", "bar"],
407+
"prompt text",
408+
"error text",
409+
current_value="foo",
410+
)
411+
412+
output_lines = stdout_buf.getvalue().splitlines()
413+
414+
print(output_lines)
415+
416+
assert output_lines == [
417+
"",
418+
"foo",
419+
" Choices are:",
420+
" 1 - No Default",
421+
" 2 - foo",
422+
" 3 - bar",
423+
"",
424+
"prompt text",
425+
]
426+
427+
assert result == "foo"
428+
429+
def test_default_thing_input_no_default(self, monkeypatch):
430+
stdout_buf = io.StringIO()
431+
monkeypatch.setattr("sys.stdin", io.StringIO("1\n"))
432+
433+
with contextlib.redirect_stdout(stdout_buf):
434+
result = _default_thing_input(
435+
"foo\n",
436+
["foo", "bar"],
437+
"prompt text",
438+
"error text",
439+
current_value="foo",
440+
)
441+
442+
output_lines = stdout_buf.getvalue().splitlines()
443+
444+
print(output_lines)
445+
446+
assert output_lines == [
447+
"",
448+
"foo",
449+
" Choices are:",
450+
" 1 - No Default",
451+
" 2 - foo",
452+
" 3 - bar",
453+
"",
454+
"prompt text",
455+
]
456+
457+
assert result is None
458+
459+
def test_default_thing_input_valid(self, monkeypatch):
460+
stdout_buf = io.StringIO()
461+
monkeypatch.setattr("sys.stdin", io.StringIO("3\n"))
462+
463+
with contextlib.redirect_stdout(stdout_buf):
464+
result = _default_thing_input(
465+
"foo\n",
466+
["foo", "bar"],
467+
"prompt text",
468+
"error text",
469+
current_value="foo",
470+
)
471+
472+
output_lines = stdout_buf.getvalue().splitlines()
473+
474+
print(output_lines)
475+
476+
assert output_lines == [
477+
"",
478+
"foo",
479+
" Choices are:",
480+
" 1 - No Default",
481+
" 2 - foo",
482+
" 3 - bar",
483+
"",
484+
"prompt text",
485+
]
486+
487+
assert result == "bar"
488+
489+
def test_default_thing_input_valid_no_current(self, monkeypatch):
490+
stdout_buf = io.StringIO()
491+
monkeypatch.setattr("sys.stdin", io.StringIO("3\n1\n"))
492+
493+
with contextlib.redirect_stdout(stdout_buf):
494+
result = _default_thing_input(
495+
"foo\n",
496+
["foo", "bar"],
497+
"prompt text",
498+
"error text",
499+
)
500+
501+
output = stdout_buf.getvalue()
502+
503+
assert "error text" in output
504+
505+
assert result == "foo"
506+
507+
def test_default_thing_input_out_of_range(self, monkeypatch):
508+
stdout_buf = io.StringIO()
509+
monkeypatch.setattr("sys.stdin", io.StringIO("4\n2\n"))
510+
511+
with contextlib.redirect_stdout(stdout_buf):
512+
result = _default_thing_input(
513+
"foo\n",
514+
["foo", "bar"],
515+
"prompt text",
516+
"error text",
517+
current_value="foo",
518+
)
519+
520+
output = stdout_buf.getvalue()
521+
assert "error text" in output
522+
523+
assert result == "foo"

0 commit comments

Comments
 (0)