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:
parent
c6fdf775c8
commit
e8b6b1b334
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue