From e8b6b1b334fffe5c5de8c2951a974b0126ffd2b0 Mon Sep 17 00:00:00 2001 From: Werner Llácer Date: Wed, 12 Jan 2022 23:24:38 +0100 Subject: 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 --- archinstall/lib/menu/menu.py | 33 ++++++++++++-- archinstall/lib/user_interaction.py | 89 ++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 9 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/menu/menu.py b/archinstall/lib/menu/menu.py index 65be4956..dfd47a7a 100644 --- a/archinstall/lib/menu/menu.py +++ b/archinstall/lib/menu/menu.py @@ -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(p_options,dict): + options = list(p_options.keys()) + else: + options = list(p_options) - if isinstance(options, dict): - options = list(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) diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index 11ce4072..c213a941 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -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) -- cgit v1.2.3-70-g09d2