Restore generic_select function (#857)

* recreate generic_select and generic_multi_select functions

* flake8 complains

* Addressed some review issues
-> Options checks propagated to Menu(()
-> Options parameter inmutable at Menu()
-> Some text adapted
-> Sort will be handled by Menu()
-> Better handling of default value

* Solved the two problems found:
lack of list(dict.[keys/values] and impact in copy()
sideffects of renaming menu parameter options into p_options

* Now the problem of the copy was with a generator

* Add a log message whenever an "strange" object type is sent into Menu

* Validation of types has been streamlined.
Default values are now accesible to generic_select without restriction
This commit is contained in:
Werner Llácer 2022-01-12 23:24:38 +01:00 committed by GitHub
parent c6fdf775c8
commit e8b6b1b334
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 113 additions and 9 deletions

View File

@ -1,15 +1,20 @@
from archinstall.lib.menu.simple_menu import TerminalMenu
from ..exceptions import RequirementError
from ..output import log
from collections.abc import Iterable
import sys
import logging
class Menu(TerminalMenu):
def __init__(self, title, options, skip=True, multi=False, default_option=None, sort=True):
def __init__(self, title, p_options, skip=True, multi=False, default_option=None, sort=True):
"""
Creates a new menu
:param title: Text that will be displayed above the menu
:type title: str
:param options: Options to be displayed in the menu to chose from;
:param p_options: Options to be displayed in the menu to chose from;
if dict is specified then the keys of such will be used as options
:type options: list, dict
@ -25,9 +30,29 @@ class Menu(TerminalMenu):
:param sort: Indicate if the options should be sorted alphabetically before displaying
:type sort: bool
"""
# we guarantee the inmutability of the options outside the class.
# an unknown number of iterables (.keys(),.values(),generator,...) can't be directly copied, in this case
# we recourse to make them lists before, but thru an exceptions
# this is the old code, which is not maintenable with more types
# options = copy(list(p_options) if isinstance(p_options,(type({}.keys()),type({}.values()))) else p_options)
# We check that the options are iterable. If not we abort. Else we copy them to lists
# it options is a dictionary we use the values as entries of the list
# if options is a string object, each character becomes an entry
# if options is a list, we implictily build a copy to mantain immutability
if not isinstance(p_options,Iterable):
log(f"Objects of type {type(p_options)} is not iterable, and are not supported at Menu",fg="red")
log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING)
raise RequirementError("Menu() requires an iterable as option.")
if isinstance(options, dict):
options = list(options)
if isinstance(p_options,dict):
options = list(p_options.keys())
else:
options = list(p_options)
if not options:
log(" * Menu didn't find any options to choose from * ", fg='red')
log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING)
raise RequirementError('Menu.__init__() requires at least one option to proceed.')
if sort:
options = sorted(options)

View File

@ -8,6 +8,7 @@ import shutil
import signal
import sys
import time
from collections.abc import Iterable
from typing import List, Any, Optional, Dict, Union, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
@ -325,7 +326,7 @@ def ask_for_a_timezone() -> str:
selected_tz = Menu(
f'Select a timezone or leave blank to use default "{default}"',
timezones,
list(timezones),
skip=False,
default_option=default
).run()
@ -404,7 +405,7 @@ def ask_to_configure_network() -> Dict[str, Any]:
**list_interfaces()
}
nic = Menu('Select one network interface to configure', interfaces.values()).run()
nic = Menu('Select one network interface to configure', list(interfaces.values())).run()
if nic and nic != 'Copy ISO network configuration to installation':
if nic == 'Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)':
@ -787,7 +788,7 @@ def select_profile() -> Optional[str]:
title = 'This is a list of pre-programmed profiles, ' \
'they might make it easier to install things like desktop environments'
selection = Menu(title=title, options=options.keys()).run()
selection = Menu(title=title, p_options=list(options.keys())).run()
if selection is not None:
return options[selection]
@ -825,7 +826,7 @@ def select_mirror_regions() -> Dict[str, Any]:
mirrors = list_mirrors()
selected_mirror = Menu(
'Select one of the regions to download packages from',
mirrors.keys(),
list(mirrors.keys()),
multi=True
).run()
@ -847,7 +848,7 @@ def select_harddrives() -> Optional[str]:
selected_harddrive = Menu(
'Select one or more hard drives to use and configure',
options.keys(),
list(options.keys()),
multi=True
).run()
@ -939,3 +940,81 @@ def select_locale_enc(default):
).run()
return selected_locale
def generic_select(p_options :Union[list,dict],
input_text :str = "Select one of the values shown below: ",
allow_empty_input :bool = True,
options_output :bool = True, # function not available
sort :bool = False,
multi :bool = False,
default :Any = None) -> Any:
"""
A generic select function that does not output anything
other than the options and their indexes. As an example:
generic_select(["first", "second", "third option"])
> first
second
third option
When the user has entered the option correctly,
this function returns an item from list, a string, or None
Options can be any iterable.
Duplicate entries are not checked, but the results with them are unreliable. Which element to choose from the duplicates depends on the return of the index()
Default value if not on the list of options will be added as the first element
sort will be handled by Menu()
"""
# We check that the options are iterable. If not we abort. Else we copy them to lists
# it options is a dictionary we use the values as entries of the list
# if options is a string object, each character becomes an entry
# if options is a list, we implictily build a copy to mantain immutability
if not isinstance(p_options,Iterable):
log(f"Objects of type {type(p_options)} is not iterable, and are not supported at generic_select",fg="red")
log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING)
raise RequirementError("generic_select() requires an iterable as option.")
if isinstance(p_options,dict):
options = list(p_options.values())
else:
options = list(p_options)
# check that the default value is in the list. If not it will become the first entry
if default and default not in options:
options.insert(0,default)
# one of the drawbacks of the new interface is that in only allows string like options, so we do a conversion
# also for the default value if it exists
soptions = list(map(str,options))
default_value = options[options.index(default)] if default else None
selected_option = Menu(
input_text,
soptions,
skip=allow_empty_input,
multi=multi,
default_option=default_value,
sort=sort
).run()
# we return the original objects, not the strings.
# options is the list with the original objects and soptions the list with the string values
# thru the map, we get from the value selected in soptions it index, and thu it the original object
if not selected_option:
return selected_option
elif isinstance(selected_option,list): # for multi True
selected_option = list(map(lambda x: options[soptions.index(x)],selected_option))
else: # for multi False
selected_option = options[soptions.index(selected_option)]
return selected_option
def generic_multi_select(p_options :Union[list,dict],
text :str = "Select one or more of the options below: ",
sort :bool = False,
default :Any = None,
allow_empty :bool = False) -> Any:
return generic_select(p_options,
input_text=text,
allow_empty_input=allow_empty,
sort=sort,
multi=True,
default=default)