|
|
"""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, Tuple |
|
|
from unittest.mock import patch |
|
|
|
|
|
from openbb_cli.controllers.utils import ( |
|
|
check_file_type_saved, |
|
|
check_positive, |
|
|
validate_register_key, |
|
|
) |
|
|
from openbb_cli.session import Session |
|
|
|
|
|
session = Session() |
|
|
|
|
|
|
|
|
def __mock_parse_known_args_and_warn( |
|
|
controller, |
|
|
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 or xlsx." |
|
|
elif export_allowed == "figures_only": |
|
|
choices_export = ["png", "jpg"] |
|
|
help_export = "Export figure into png or jpg." |
|
|
else: |
|
|
choices_export = ["csv", "json", "xlsx", "png", "jpg"] |
|
|
help_export = ( |
|
|
"Export raw data into csv, json, xlsx and figure into png or jpg." |
|
|
) |
|
|
|
|
|
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, |
|
|
) |
|
|
|
|
|
parser.add_argument( |
|
|
"--register_obbject", |
|
|
dest="register_obbject", |
|
|
action="store_false", |
|
|
default=True, |
|
|
help="Flag to store data in the OBBject registry, True by default.", |
|
|
) |
|
|
parser.add_argument( |
|
|
"--register_key", |
|
|
dest="register_key", |
|
|
default="", |
|
|
help="Key to reference data in the OBBject registry.", |
|
|
type=validate_register_key, |
|
|
) |
|
|
|
|
|
|
|
|
def __mock_parse_simple_args(parser: ArgumentParser, other_args: List[str]) -> Tuple: |
|
|
"""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 |
|
|
return None, None |
|
|
|
|
|
|
|
|
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, 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: |
|
|
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 |
|
|
|