From c77560ebcd830c1a765ea0838779118d32fff3a6 Mon Sep 17 00:00:00 2001 From: Chavithra Date: Thu, 17 Nov 2022 12:44:16 +0100 Subject: [PATCH] Add choices generation (#3441) * Add choices generation * Update forex_controller * Remove raised exception * Linting * Linting * Linting * Tests --- openbb_terminal/core/completer/choices.py | 207 ++++++++++++++++++ openbb_terminal/forex/forex_controller.py | 48 +--- openbb_terminal/parent_classes.py | 13 ++ .../stocks/test_stocks_controller.py | 4 +- 4 files changed, 233 insertions(+), 39 deletions(-) create mode 100644 openbb_terminal/core/completer/choices.py diff --git a/openbb_terminal/core/completer/choices.py b/openbb_terminal/core/completer/choices.py new file mode 100644 index 00000000000..132b5b241f3 --- /dev/null +++ b/openbb_terminal/core/completer/choices.py @@ -0,0 +1,207 @@ +from argparse import ArgumentParser +from inspect import unwrap +from os import environ +from types import MethodType +from typing import List +from unittest.mock import patch +from openbb_terminal.helper_funcs import check_file_type_saved, check_positive +from openbb_terminal.rich_config import get_ordered_list_sources + + +def __mock_parse_known_args_and_warn( + controller, + parser: ArgumentParser, + other_args: List[str], + export_allowed: int = 0, + raw: bool = False, + limit: int = 0, +) -> None: + """Add the arguments that would have normally added by `parse_known_args_and_warn`. + + Parameters + ---------- + parser: argparse.ArgumentParser + Parser with predefined arguments + other_args: List[str] + list of arguments to parse + export_allowed: int + Choose from 0, 1, + 2 and EXPORT_BOTH_RAW_DATA_AND_FIGURES + raw: bool + Add the --raw flag + limit: int + Add a --limit flag with this number default + """ + + _ = other_args + parser.add_argument( + "-h", "--help", action="store_true", help="show this help message" + ) + if export_allowed > 0: + choices_export = [] + help_export = "Does not export!" + + if export_allowed == 1: + choices_export = ["csv", "json", "xlsx"] + help_export = "Export raw data into csv, json, xlsx" + elif export_allowed == 2: + choices_export = ["png", "jpg", "pdf", "svg"] + help_export = "Export figure into png, jpg, pdf, svg " + else: + choices_export = ["csv", "json", "xlsx", "png", "jpg", "pdf", "svg"] + help_export = "Export raw data into csv, json, xlsx and figure into png, jpg, pdf, svg " + + parser.add_argument( + "--export", + default="", + type=check_file_type_saved(choices_export), + dest="export", + help=help_export, + choices=choices_export, + ) + + if raw: + parser.add_argument( + "--raw", + dest="raw", + action="store_true", + default=False, + help="Flag to display raw data", + ) + if limit > 0: + parser.add_argument( + "-l", + "--limit", + dest="limit", + default=limit, + help="Number of entries to show in data.", + type=check_positive, + ) + sources = get_ordered_list_sources(f"{controller.PATH}{parser.prog}") + # Allow to change source if there is more than one + if len(sources) > 1: + parser.add_argument( + "--source", + action="store", + dest="source", + choices=sources, + default=sources[0], # the first source from the list is the default + help="Data source to select from", + ) + return None + + +def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> None: + parser.add_argument( + "-h", "--help", action="store_true", help="show this help message" + ) + _ = other_args + + return None + + +def _get_argument_parser( + controller, + command: str, +) -> ArgumentParser: + """Intercept the ArgumentParser instance from the command function. + + A command function being a function like : call_help, call_overview + + Args: + controller (BaseController): Instance of the Terminal Controller. + command (str): A name from controller.CHOICES_COMMANDS + + Returns: + ArgumentParser: ArgumentParser instance from the command function. + """ + + if command not in controller.CHOICES_COMMANDS: + raise AttributeError(f"Invalid command : '{command}'") + + bound_mock_parse_known_args_and_warn = MethodType( + __mock_parse_known_args_and_warn, controller + ) + + patch_parse_simple_args = patch( + target="openbb_terminal.helper_funcs.parse_simple_args", + side_effect=__mock_parse_simple_args, + return_value=None, + ) + patch_parse_known_args_and_warn = patch.object( + target=controller, + attribute="parse_known_args_and_warn", + side_effect=bound_mock_parse_known_args_and_warn, + return_value=None, + ) + + parse_simple_args = patch_parse_simple_args.start() + parse_known_args_and_warn = patch_parse_known_args_and_warn.start() + + command = "call_" + command + command_func = getattr(controller, command) + command_func = unwrap(command_func) + command_func(controller, []) + + if parse_known_args_and_warn.call_count == 1: + args = parse_known_args_and_warn.call_args.args + argument_parser = args[0] + elif parse_simple_args.call_count == 1: + args = parse_simple_args.call_args.args + argument_parser = args[0] + else: + raise AssertionError( + "One of these functions should be called once:\n" + " - parse_known_args_and_warn\n" + " - parse_known_args_and_warn\n" + ) + + patch_parse_simple_args.stop() + patch_parse_known_args_and_warn.stop() + + return argument_parser + + +def _build_command_choice_map(argument_parser: ArgumentParser) -> dict: + choice_map: dict = {} + for action in argument_parser._actions: # pylint: disable=protected-access + if len(action.option_strings) == 1: + long_name = action.option_strings[0] + short_name = "" + elif len(action.option_strings) == 2: + short_name = action.option_strings[0] + long_name = action.option_strings[1] + else: + raise AttributeError(f"Invalid argument_parser: {argument_parser}") + + if action.choices: + choice_map[long_name] = {c: {} for c in action.choices} + else: + choice_map[long_name] = {} + + if short_name and long_name: + choice_map[short_name] = long_name + + return choice_map + + +def build_controller_choice_map(controller) -> dict: + command_list = controller.CHOICES_COMMANDS + controller_choice_map: dict = {c: {} for c in controller.controller_choices} + controller_choice_map["support"] = controller.SUPPORT_CHOICES + controller_choice_map["about"] = controller.ABOUT_CHOICES + + try: + for command in command_list: + argument_parser = _get_argument_parser( + controller=controller, + command=command, + ) + controller_choice_map[command] = _build_command_choice_map( + argument_parser=argument_parser + ) + except Exception as exception: + if environ["DEBUG_MODE"] == "true": + raise exception + + return controller_choice_map diff --git a/openbb_terminal/forex/forex_controller.py b/openbb_terminal/forex/forex_controller.py index d57ac2e82d3..0f63c4ae67d 100644 --- a/openbb_terminal/forex/forex_controller.py +++ b/openbb_terminal/forex/forex_controller.py @@ -46,19 +46,22 @@ class ForexController(BaseController): """Forex Controller class.""" CHOICES_COMMANDS = [ + "fwd", + "candle", "load", "quote", - "candle", - "resources", - "fwd", - "forecast", - "oanda", ] - CHOICES_MENUS = ["ta", "qa", "Oanda"] + CHOICES_MENUS = [ + "forecast", + "qa", + "oanda", + "ta", + ] RESOLUTION = ["i", "d", "w", "m"] PATH = "/forex/" FILE_PATH = os.path.join(os.path.dirname(__file__), "README.md") + CHOICES_GENERATION = True def __init__(self, queue: List[str] = None): """Construct Data.""" @@ -71,39 +74,10 @@ class ForexController(BaseController): self.data = pd.DataFrame() if session and obbff.USE_PROMPT_TOOLKIT: - one_to_hundred: dict = {str(c): {} for c in range(1, 100)} + choices: dict = self.choices - choices: dict = {c: {} for c in self.controller_choices} - choices["load"] = {c: {} for c in FX_TICKERS} + choices["load"].update({c: {} for c in FX_TICKERS}) choices["load"]["--ticker"] = {c: {} for c in FX_TICKERS} - choices["load"]["-t"] = "--ticker" - choices["load"]["--resolution"] = {c: {} for c in self.RESOLUTION} - choices["load"]["-r"] = "--resolution" - choices["load"]["--interval"] = { - c: {} for c in SOURCES_INTERVALS["YahooFinance"] - } - choices["load"]["--start"] = None - choices["load"]["-s"] = "--start" - choices["load"]["--source"] = {c: {} for c in FOREX_SOURCES} - choices["quote"]["--source"] = { - c: {} for c in get_ordered_list_sources(f"{self.PATH}quote") - } - choices["candle"] = { - "--sort": {c: {} for c in forex_helper.CANDLE_SORT}, - "--plotly": {}, - "-p": "--plotly", - "--reverse": {}, - "-r": "--reverse", - "--raw": {}, - "--trend": {}, - "-t": "--trend", - "--ma": None, - "--limit": one_to_hundred, - "-l": "--limit", - } - - choices["support"] = self.SUPPORT_CHOICES - choices["about"] = self.ABOUT_CHOICES self.completer = NestedCompleter.from_nested_dict(choices) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 4c30a635c25..7162a6b6b0d 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -48,6 +48,7 @@ from openbb_terminal.rich_config import console, get_ordered_list_sources from openbb_terminal.stocks import stocks_helper from openbb_terminal.terminal_helper import open_openbb_documentation from openbb_terminal.cryptocurrency import cryptocurrency_helpers +from openbb_terminal.core.completer.choices import build_controller_choice_map logger = logging.getLogger(__name__) @@ -107,6 +108,7 @@ class BaseController(metaclass=ABCMeta): TRY_RELOAD = False PATH: str = "" FILE_PATH: str = "" + CHOICES_GENERATION = False def __init__(self, queue: List[str] = None) -> None: """Create the base class for any controller in the codebase. @@ -166,6 +168,16 @@ class BaseController(metaclass=ABCMeta): self.SUPPORT_CHOICES = support_choices + self.choices = self.build_choices() + + def build_choices(self): + if self.CHOICES_GENERATION: + choices = build_controller_choice_map(controller=self) + else: + choices = {} + + return choices + def check_path(self) -> None: """Check if command path is valid.""" path = self.PATH @@ -719,6 +731,7 @@ class BaseController(metaclass=ABCMeta): type=check_file_type_saved(choices_export), dest="export", help=help_export, + choices=choices_export, ) if raw: diff --git a/tests/openbb_terminal/stocks/test_stocks_controller.py b/tests/openbb_terminal/stocks/test_stocks_controller.py index 1ca24e7508e..a1bbc23bade 100644 --- a/tests/openbb_terminal/stocks/test_stocks_controller.py +++ b/tests/openbb_terminal/stocks/test_stocks_controller.py @@ -251,7 +251,7 @@ def test_call_func_expect_queue(expected_queue, func, queue): [ "--query=mock_query", "--limit=1", - "--export=''", + "--export=csv", ], "stocks_helper.search", [], @@ -262,7 +262,7 @@ def test_call_func_expect_queue(expected_queue, func, queue): sector="", industry="", exchange_country="", - export="", + export="csv", ), ), (