summaryrefslogtreecommitdiffstats
path: root/cli/openbb_cli/controllers/choices.py
diff options
context:
space:
mode:
Diffstat (limited to 'cli/openbb_cli/controllers/choices.py')
-rw-r--r--cli/openbb_cli/controllers/choices.py322
1 files changed, 322 insertions, 0 deletions
diff --git a/cli/openbb_cli/controllers/choices.py b/cli/openbb_cli/controllers/choices.py
new file mode 100644
index 00000000000..7e66660ff79
--- /dev/null
+++ b/cli/openbb_cli/controllers/choices.py
@@ -0,0 +1,322 @@
+"""This module contains functions to build the choice map for the controllers."""
+
+from argparse import SUPPRESS, ArgumentParser
+from contextlib import contextmanager
+from inspect import isfunction, unwrap
+from types import MethodType
+from typing import Callable, List, Literal
+from unittest.mock import patch
+
+from openbb_cli.controllers.utils import (
+ check_file_type_saved,
+ check_positive,
+)
+from openbb_cli.session import Session
+
+
+def __mock_parse_known_args_and_warn(
+ controller, # pylint: disable=unused-argument
+ parser: ArgumentParser,
+ other_args: List[str],
+ export_allowed: Literal[
+ "no_export", "raw_data_only", "figures_only", "raw_data_and_figures"
+ ] = "no_export",
+ raw: bool = False,
+ limit: int = 0,
+) -> None:
+ """Add arguments.
+
+ Add the arguments that would have normally added by :
+ - openbb_cli.base_controller.BaseController.parse_known_args_and_warn
+
+ Parameters
+ ----------
+ parser: argparse.ArgumentParser
+ Parser with predefined arguments
+ other_args: List[str]
+ list of arguments to parse
+ export_allowed: Literal["no_export", "raw_data_only", "figures_only", "raw_data_and_figures"]
+ Export options
+ 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 != "no_export":
+ choices_export = []
+ help_export = "Does not export!"
+
+ if export_allowed == "raw_data_only":
+ choices_export = ["csv", "json", "xlsx"]
+ help_export = "Export raw data into csv, json, xlsx"
+ elif export_allowed == "figures_only":
+ 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,
+ )
+
+
+def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> None:
+ """Add arguments.
+
+ Add the arguments that would have normally added by:
+ - openbb_cli.parent_classes.BaseController.parse_simple_args
+
+ Parameters
+ ----------
+ parser: argparse.ArgumentParser
+ Parser with predefined arguments
+ other_args: List[str]
+ List of arguments to parse
+ """
+ parser.add_argument(
+ "-h", "--help", action="store_true", help="show this help message"
+ )
+ _ = other_args
+
+
+def __get_command_func(controller, command: str):
+ """Get the function with the name `f"call_{command}"` from controller object.
+
+ Parameters
+ ----------
+ controller: BaseController
+ Instance of the CLI Controller.
+ command: str
+ A name from controller.CHOICES_COMMANDS
+
+ Returns
+ -------
+ Callable: Command function.
+ """
+ if command not in controller.CHOICES_COMMANDS:
+ raise AttributeError(
+ f"The following command is not inside `CHOICES_COMMANDS` : '{command}'"
+ )
+
+ command = f"call_{command}"
+ command_func = getattr(controller, command)
+ command_func = unwrap(func=command_func)
+
+ if isfunction(command_func):
+ command_func = MethodType(command_func, controller)
+
+ return command_func
+
+
+def contains_functions_to_patch(command_func: Callable) -> bool:
+ """Check command function.
+
+ Check if a `command_func` actually contains the functions we want to mock, i.e.:
+ - parse_simple_args
+ - parse_known_args_and_warn
+
+ Parameters
+ ----------
+ command_func: Callable
+ Function to check.
+
+ Returns
+ -------
+ bool: Whether or not `command_func` contains the mocked functions.
+ """
+ co_names = command_func.__code__.co_names
+
+ return bool(
+ "parse_simple_args" in co_names or "parse_known_args_and_warn" in co_names
+ )
+
+
+@contextmanager
+def __patch_controller_functions(controller):
+ """Patch controller functions.
+
+ Patch the following function from a BaseController instance:
+ - parse_simple_args
+ - parse_known_args_and_warn
+
+ These functions take an 'argparse.ArgumentParser' object as parameter.
+ We want to intercept this 'argparse.ArgumentParser' object.
+
+ Parameters
+ ----------
+ controller: BaseController
+ BaseController object that needs to be patched.
+
+ Returns
+ -------
+ List[Callable]: List of mocked functions.
+ """
+ bound_mock_parse_known_args_and_warn = MethodType(
+ __mock_parse_known_args_and_warn,
+ controller,
+ )
+
+ rich = patch(
+ target="openbb_cli.config.console.Console.print",
+ return_value=None,
+ )
+
+ patcher_list = [
+ patch.object(
+ target=controller,
+ attribute="parse_simple_args",
+ side_effect=__mock_parse_simple_args,
+ return_value=None,
+ ),
+ patch.object(
+ target=controller,
+ attribute="parse_known_args_and_warn",
+ side_effect=bound_mock_parse_known_args_and_warn,
+ return_value=None,
+ ),
+ ]
+
+ if not Session().settings.DEBUG_MODE:
+ rich.start()
+ patched_function_list = []
+ for patcher in patcher_list:
+ patched_function_list.append(patcher.start())
+
+ yield patched_function_list
+
+ if not Session().settings.DEBUG_MODE:
+ rich.stop()
+ for patcher in patcher_list:
+ patcher.stop()
+
+
+def _get_argument_parser(
+ controller,
+ command: str,
+) -> ArgumentParser:
+ """Intercept the ArgumentParser instance from the command function.
+
+ A command function being a function starting with `call_`, like:
+ - call_help
+ - call_overview
+ - call_load
+
+ Parameters
+ ----------
+ controller: BaseController
+ Instance of the CLI Controller.
+ command: str
+ A name from `controller.CHOICES_COMMANDS`.
+
+ Returns
+ -------
+ ArgumentParser: ArgumentParser instance from the command function.
+ """
+ command_func: Callable = __get_command_func(controller=controller, command=command)
+
+ if not contains_functions_to_patch(command_func=command_func):
+ raise AssertionError(
+ f"One of these functions should be inside `call_{command}`:\n"
+ " - parse_simple_args\n"
+ " - parse_known_args_and_warn\n"
+ )
+
+ with __patch_controller_functions(controller=controller) as patched_function_list:
+ command_func([])
+
+ call_count = 0
+ for patched_function in patched_function_list:
+ call_count += patched_function.call_count
+ if patched_function.call_count == 1:
+ args, kwargs = patched_function.call_args
+ argument_parser = (
+ kwargs["parser"] if kwargs.get("parser", None) else args[0]
+ )
+
+ if call_count != 1:
+ raise AssertionError(
+ f"One of these functions should be called once inside `call_{command}`:\n"
+ " - parse_simple_args\n"
+ " - parse_known_args_and_warn\n"
+ )
+
+ return argument_parser
+
+
+def _build_command_choice_map(argument_parser: ArgumentParser) -> dict:
+ """Build the choice map for a command."""
+ choice_map: dict = {}
+ for action in argument_parser._actions: # pylint: disable=protected-access
+ if action.help == SUPPRESS:
+ continue
+ 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 hasattr(action, "choices") and action.choices:
+ choice_map[long_name] = {str(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:
+ """Build the choice map for a controller."""
+ command_list = controller.CHOICES_COMMANDS
+ controller_choice_map: dict = {c: {} for c in controller.controller_choices}
+
+ for command in command_list:
+ try:
+ 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 Session().settings.DEBUG_MODE:
+ raise Exception(
+ f"On command : `{command}`.\n{str(exception)}"
+ ) from exception
+
+ return controller_choice_map