Skip to content

Commit eccec2f

Browse files
ref: refactoring and unit testing config (#348)
## 📝 Description **What does this PR do and why is this change necessary?** Refactor all code relevant to generating the linode-cli config file Add unit tests to the config generation
1 parent f80a2f9 commit eccec2f

File tree

6 files changed

+410
-180
lines changed

6 files changed

+410
-180
lines changed

linodecli/configuration/__init__.py

Lines changed: 89 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@
33
used elsewhere.
44
"""
55

6-
import argparse
76
import os
87
import sys
9-
import webbrowser
8+
import argparse
109

1110
from .auth import (
11+
_get_token_web,
1212
_check_full_access,
1313
_do_get_request,
1414
_get_token_terminal,
15-
_get_token_web,
1615
)
1716
from .helpers import (
17+
_check_browsers,
1818
_default_thing_input,
1919
_get_config,
2020
_get_config_path,
@@ -23,22 +23,6 @@
2323

2424
ENV_TOKEN_NAME = "LINODE_CLI_TOKEN"
2525

26-
# this is a list of browser that _should_ work for web-based auth. This is mostly
27-
# intended to exclude lynx and other terminal browsers which could be opened, but
28-
# won't work.
29-
KNOWN_GOOD_BROWSERS = {
30-
"chrome",
31-
"firefox",
32-
"mozilla",
33-
"netscape",
34-
"opera",
35-
"safari",
36-
"chromium",
37-
"chromium-browser",
38-
"epiphany",
39-
}
40-
41-
4226
class CLIConfig:
4327
"""
4428
Generates the necessary config for the Linode CLI
@@ -73,17 +57,6 @@ def __init__(self, base_url, username=None, skip_config=False):
7357
elif environ_token is not None:
7458
self.used_env_token = True
7559

76-
def set_user(self, username):
77-
"""
78-
Sets the acting username. If this username is not in the config, this is
79-
an error. This overrides the default username
80-
"""
81-
if not self.config.has_section(username):
82-
print(f"User {username} is not configured!")
83-
sys.exit(1)
84-
85-
self.username = username
86-
8760
def default_username(self):
8861
"""
8962
Returns the default-user Username
@@ -92,83 +65,16 @@ def default_username(self):
9265
return self.config.get("DEFAULT", "default-user")
9366
return ""
9467

95-
def update_namespace(self, namespace, new_dict):
96-
"""
97-
In order to update the namespace, we need to turn it into a dict, modify it there,
98-
then reconstruct it with the exploded dict.
99-
"""
100-
ns_dict = vars(namespace)
101-
warn_dict = {}
102-
for k in new_dict:
103-
if k.startswith("plugin-"):
104-
# plugins set config options that start with 'plugin-' - these don't
105-
# get included in the updated namespace
106-
continue
107-
if k in ns_dict and isinstance(k, list):
108-
ns_dict[k].append(new_dict[k])
109-
if k in ns_dict and ns_dict[k] is None:
110-
warn_dict[k] = new_dict[k]
111-
ns_dict[k] = new_dict[k]
112-
if not any(
113-
x in ["--suppress-warnings", "--no-headers"] for x in sys.argv
114-
):
115-
print(
116-
f"using default values: {warn_dict}, use --no-defaults flag to disable defaults"
117-
)
118-
return argparse.Namespace(**ns_dict)
119-
120-
def update(self, namespace, allowed_defaults):
68+
def set_user(self, username):
12169
"""
122-
This updates a Namespace (as returned by ArgumentParser) with config values
123-
if they aren't present in the Namespace already.
70+
Sets the acting username. If this username is not in the config, this is
71+
an error. This overrides the default username
12472
"""
125-
if self.used_env_token and self.config is None:
126-
# the CLI is using a token defined in the environment; as such, we may
127-
# not have actually loaded a config file. That's fine, there are just
128-
# no defaults
129-
return None
130-
131-
username = self.username or self.default_username()
132-
133-
if not self.config.has_option(username, "token") and not os.environ.get(
134-
ENV_TOKEN_NAME, None
135-
):
136-
print(f"User {username} is not configured.")
73+
if not self.config.has_section(username):
74+
print(f"User {username} is not configured!")
13775
sys.exit(1)
13876

139-
if self.config.has_section(username) and allowed_defaults:
140-
# update_dicts = {
141-
# default_key: self.config.get(username, default_key)
142-
# for default_key in allowed_defaults
143-
# if self.config.has_option(username, default_key)
144-
# }
145-
update_dicts = {}
146-
for default_key in allowed_defaults:
147-
if not self.config.has_option(username, default_key):
148-
continue
149-
value = self.config.get(username, default_key)
150-
if default_key == "authorized_users":
151-
update_dicts[default_key] = [value]
152-
else:
153-
update_dicts[default_key] = value
154-
155-
return self.update_namespace(namespace, update_dicts)
156-
return namespace
157-
158-
def get_token(self):
159-
"""
160-
Returns the token for a configured user
161-
"""
162-
if self.used_env_token:
163-
return os.environ.get(ENV_TOKEN_NAME, None)
164-
165-
if self.config.has_option(
166-
self.username or self.default_username(), "token"
167-
):
168-
return self.config.get(
169-
self.username or self.default_username(), "token"
170-
)
171-
return ""
77+
self.username = username
17278

17379
def remove_user(self, username):
17480
"""
@@ -177,7 +83,7 @@ def remove_user(self, username):
17783
"""
17884
if self.default_username() == username:
17985
print(
180-
f"Cannot remote {username} as they are the default user! You can "
86+
f"Cannot remove {username} as they are the default user! You can "
18187
"change the default user with: `linode-cli set-user USERNAME`"
18288
)
18389
sys.exit(1)
@@ -210,8 +116,17 @@ def set_default_user(self, username):
210116
self.config.set("DEFAULT", "default-user", username)
211117
self.write_config()
212118

213-
# plugin methods - these are intended for plugins to utilize to store their
214-
# own persistent config information
119+
def get_token(self):
120+
"""
121+
Returns the token for a configured user
122+
"""
123+
if self.used_env_token:
124+
return os.environ.get(ENV_TOKEN_NAME, None)
125+
126+
if self.config.has_option(self.username or self.default_username(), "token"):
127+
return self.config.get(self.username or self.default_username(), "token")
128+
return ""
129+
215130
def get_value(self, key):
216131
"""
217132
Retrieves and returns an existing config value for the current user. This
@@ -236,6 +151,8 @@ def get_value(self, key):
236151

237152
return self.config.get(username, key)
238153

154+
# plugin methods - these are intended for plugins to utilize to store their
155+
# own persistent config information
239156
def plugin_set_value(self, key, value):
240157
"""
241158
Sets a new config value for a plugin for the current user. Plugin config
@@ -286,31 +203,65 @@ def plugin_get_value(self, key):
286203

287204
return self.config.get(username, full_key)
288205

289-
def write_config(self, silent=False):
206+
# TODO: this is more of an argparsing function than it is a config function
207+
# might be better to move this to argparsing during refactor and just have
208+
# configuration return defaults or keys or something
209+
def update(self, namespace, allowed_defaults):
210+
"""
211+
This updates a Namespace (as returned by ArgumentParser) with config values
212+
if they aren't present in the Namespace already.
213+
"""
214+
if self.used_env_token and self.config is None:
215+
return None
216+
username = self.username or self.default_username()
217+
if (not self.config.has_option(username, "token")
218+
and not os.environ.get(ENV_TOKEN_NAME, None)):
219+
print(f"User {username} is not configured.")
220+
sys.exit(1)
221+
if (not self.config.has_section(username)
222+
or allowed_defaults is None):
223+
return namespace
224+
225+
warn_dict = {}
226+
ns_dict = vars(namespace)
227+
for key in allowed_defaults:
228+
if key not in ns_dict:
229+
continue
230+
if ns_dict[key] is not None:
231+
continue
232+
# plugins set config options that start with 'plugin-'
233+
# these don't get included in the updated namespace
234+
if key.startswith("plugin-"):
235+
continue
236+
if self.config.has_option(username, key):
237+
value = self.config.get(username, key)
238+
else:
239+
value = allowed_defaults[key]
240+
if key == "authorized_users":
241+
ns_dict[key] = [value]
242+
warn_dict[key] = [value]
243+
else:
244+
ns_dict[key] = value
245+
warn_dict[key] = value
246+
247+
if not any(x in ["--suppress-warnings", "--no-headers"] for x in sys.argv):
248+
print(f"using default values: {warn_dict}, "
249+
"use --no-defaults flag to disable defaults")
250+
return argparse.Namespace(**ns_dict)
251+
252+
def write_config(self):
290253
"""
291254
Saves the config file as it is right now. This can be used by plugins
292255
to save values they've set, and is used internally to update the config
293256
on disk when a new user if configured.
294-
295-
:param silent: If True, does not print a message noting the config file
296-
has been updated. This is primarily intended for silently
297-
updated the config file from one version to another.
298-
:type silent: bool
299257
"""
300-
301-
# Create the ~/.config directory if it does not exist
302258
if not os.path.exists(f"{os.path.expanduser('~')}/.config"):
303259
os.makedirs(f"{os.path.expanduser('~')}/.config")
304-
305260
with open(_get_config_path(), "w", encoding="utf-8") as f:
306261
self.config.write(f)
307262

308-
if not silent:
309-
print(f"\nConfig written to {_get_config_path()}")
310263

311-
def configure(
312-
self,
313-
): # pylint: disable=too-many-branches,too-many-statements
264+
def configure(self): # pylint: disable=too-many-branches,too-many-statements
314265
"""
315266
This assumes we're running interactively, and prompts the user
316267
for a series of defaults in order to make future CLI calls
@@ -326,67 +277,36 @@ def configure(
326277
username = None
327278
token = None
328279

329-
print(
330-
"""Welcome to the Linode CLI. This will walk you through some initial setup."""
331-
)
280+
print("Welcome to the Linode CLI. This will walk you through some initial setup.")
332281

333282
if ENV_TOKEN_NAME in os.environ:
334283
print(
335-
"""Using token from {env_token_name}.
284+
f"""Using token from {ENV_TOKEN_NAME}.
336285
Note that no token will be saved in your configuration file.
337-
* If you lose or remove {env_token_name}.
338-
* All profiles will use {env_token_name}.""".format(
339-
env_token_name=ENV_TOKEN_NAME
340-
)
286+
* If you lose or remove {ENV_TOKEN_NAME}.
287+
* All profiles will use {ENV_TOKEN_NAME}."""
341288
)
342289
username = "DEFAULT"
343290
token = os.getenv(ENV_TOKEN_NAME)
344291

345292
else:
346-
# let's see if we _can_ use web
347-
can_use_browser = True
348-
try:
349-
webbrowser.get()
350-
except webbrowser.Error:
351-
# there are no browsers installed
352-
can_use_browser = False
353-
354-
if can_use_browser and not KNOWN_GOOD_BROWSERS.intersection(
355-
webbrowser._tryorder # pylint: disable=protected-access
356-
):
357-
print()
358-
print(
359-
"This tool defaults to web-based authentication, however "
360-
"no known-working browsers were found."
361-
)
362-
363-
while True:
364-
r = input("Try it anyway? [y/N]: ")
365-
if r.lower() in "yn ":
366-
can_use_browser = r.lower() == "y"
367-
break
368-
369-
if self.configure_with_pat or not can_use_browser:
370-
username, config["token"] = _get_token_terminal(self.base_url)
371-
else:
372-
# pylint: disable=line-too-long
373-
print()
374-
print(
375-
"The CLI will use its web-based authentication to log you in. "
376-
"If you prefer to supply a Personal Access Token, use `linode-cli configure --token`. "
377-
)
378-
print()
293+
if _check_browsers() and not self.configure_with_pat:
294+
print("""
295+
The CLI will use its web-based authentication to log you in.
296+
If you prefer to supply a Personal Access Token, use `linode-cli configure --token`.
297+
""")
379298
input(
380-
"Press enter to continue. This will open a browser and proceed with authentication."
299+
"Press enter to continue. "
300+
"This will open a browser and proceed with authentication."
381301
)
382302
username, config["token"] = _get_token_web(self.base_url)
383-
# pylint: enable=line-too-long
384-
303+
else:
304+
username, config["token"] = _get_token_terminal(self.base_url)
385305
token = config["token"]
386306

387-
print()
388-
print(f"Configuring {username}")
389-
print()
307+
print(f"\nConfiguring {username}\n")
308+
309+
# Configuring Defaults
390310

391311
regions = [
392312
r["id"] for r in _do_get_request(self.base_url, "/regions")["data"]
@@ -452,20 +372,14 @@ def configure(
452372
if not is_default:
453373
if username != self.default_username():
454374
while True:
455-
value = input(
456-
"Make this user the default when using the CLI? [y/N]: "
457-
)
458-
375+
value = input("Make this user the default when using the CLI? [y/N]: ")
459376
if value.lower() in "yn":
460377
is_default = value.lower() == "y"
461378
break
462379
if not value.strip():
463380
break
464-
465381
if not is_default: # they didn't change the default user
466-
print(
467-
f"Active user will remain {self.config.get('DEFAULT', 'default-user')}"
468-
)
382+
print(f"Active user will remain {self.config.get('DEFAULT', 'default-user')}")
469383

470384
if is_default:
471385
# if this is the default user, make it so

0 commit comments

Comments
 (0)