From 1457d1bd6cbcabf01e046e31355202b904f3eafd Mon Sep 17 00:00:00 2001 From: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:26:56 +0200 Subject: [PATCH] Test static package_builder --- openbb_sdk/pyproject.toml | 4 + .../openbb_core/app/static/package_builder.py | 48 +- openbb_sdk/sdk/core/pyproject.toml | 1 - .../core/tests/test_static_package_builder.py | 437 ++++++++++++++++++ 4 files changed, 485 insertions(+), 5 deletions(-) create mode 100644 openbb_sdk/sdk/core/tests/test_static_package_builder.py diff --git a/openbb_sdk/pyproject.toml b/openbb_sdk/pyproject.toml index 13487a2e048..15f9bdd8616 100644 --- a/openbb_sdk/pyproject.toml +++ b/openbb_sdk/pyproject.toml @@ -22,6 +22,10 @@ openbb-fmp = { path = "./providers/fmp" } openbb-polygon = { path = "./providers/polygon" } openbb-fred = { path = "./providers/fred" } + +[tool.poetry.group.dev.dependencies] +pytest = "^6.2.2" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/openbb_sdk/sdk/core/openbb_core/app/static/package_builder.py b/openbb_sdk/sdk/core/openbb_core/app/static/package_builder.py index 5be1fb330fb..d39df854f3d 100644 --- a/openbb_sdk/sdk/core/openbb_core/app/static/package_builder.py +++ b/openbb_sdk/sdk/core/openbb_core/app/static/package_builder.py @@ -106,8 +106,11 @@ class PackageBuilder: class ModuleBuilder: + """Build the module for the SDK.""" + @staticmethod def build(path: str) -> str: + """Build the module.""" code = "### THIS FILE IS AUTO-GENERATED. DO NOT EDIT. ###\n" code += ImportDefinition.build(path=path) code += ClassDefinition.build(path=path) @@ -116,8 +119,11 @@ class ModuleBuilder: class ImportDefinition: + """Build the import definition for the SDK.""" + @staticmethod def filter_hint_type_list(hint_type_list: List[Type]) -> List[Type]: + """Filter the hint type list.""" new_hint_type_list = [] for hint_type in hint_type_list: if hint_type != _empty and hint_type.__module__ != "builtins": @@ -129,6 +135,7 @@ class ImportDefinition: @classmethod def get_function_hint_type_list(cls, func: Callable) -> List[Type]: + """Get the hint type list from the function.""" sig = signature(func) parameter_map = sig.parameters return_type = sig.return_annotation @@ -148,6 +155,7 @@ class ImportDefinition: @classmethod def get_path_hint_type_list(cls, path: str) -> List[Type]: + """Get the hint type list from the path.""" route_map = PathHandler.build_route_map() path_list = PathHandler.build_path_list(route_map=route_map) child_path_list = PathHandler.get_child_path_list( @@ -165,6 +173,7 @@ class ImportDefinition: @classmethod def build(cls, path: str) -> str: + """Build the import definition.""" hint_type_list = cls.get_path_hint_type_list(path=path) code = "\nfrom openbb_core.app.static.container import Container" code += "\nfrom openbb_core.app.model.command_output import CommandOutput" @@ -195,8 +204,11 @@ class ImportDefinition: class ClassDefinition: + """Build the class definition for the SDK.""" + @staticmethod def build(path: str) -> str: + """Build the class definition.""" class_name = PathHandler.build_module_class(path=path) code = f"\nclass {class_name}(Container):\n" route_map = PathHandler.build_route_map() @@ -343,8 +355,11 @@ class DocstringGenerator: class MethodDefinition: + """Build the method definition for the SDK.""" + @staticmethod def build_class_loader_method(path: str) -> str: + """Build the class loader method.""" module_name = PathHandler.build_module_name(path=path) class_name = PathHandler.build_module_class(path=path) function_name = path.rsplit("/", maxsplit=1)[-1].strip("/") @@ -358,6 +373,7 @@ class MethodDefinition: @staticmethod def get_type(field: Any) -> type: + """Get the type of the field.""" field_type = getattr(field, "type", Parameter.empty) if isclass(field_type): name = field_type.__name__ @@ -369,24 +385,26 @@ class MethodDefinition: @staticmethod def get_default(field: Any): + """Get the default value of the field.""" field_default = getattr(field, "default", None) if field_default is None or field_default is MISSING: return Parameter.empty - default_default = getattr(field_default, "default", None) - if default_default is MISSING or default_default is Ellipsis: - return Parameter.empty + if field_default is not Ellipsis: + return field_default - return default_default + return Parameter.empty @staticmethod def is_annotated_dc(annotation) -> bool: + """Check if the annotation is an annotated dataclass.""" return get_origin(annotation) == Annotated and hasattr( annotation.__args__[0], "__dataclass_fields__" ) @staticmethod def reorder_params(params: Dict[str, Parameter]) -> OrderedDict[str, Parameter]: + """Reorder the params.""" formatted_keys = list(params.keys()) for k in ["provider", "extra_params"]: if k in formatted_keys: @@ -403,6 +421,7 @@ class MethodDefinition: def format_params( parameter_map: Dict[str, Parameter] ) -> OrderedDict[str, Parameter]: + """Format the params.""" # These are types we want to expand. # For example, start_date is always a 'date', but we also accept 'str' as input. # Be careful, if the type is not coercible by pydantic to the original type, you @@ -466,6 +485,7 @@ class MethodDefinition: @staticmethod def build_func_params(parameter_map: Dict[str, Parameter]) -> str: + """Build the function parameters.""" od = MethodDefinition.format_params(parameter_map=parameter_map) func_params = ", ".join(str(param) for param in od.values()) func_params = func_params.replace("NoneType", "None") @@ -477,6 +497,7 @@ class MethodDefinition: @staticmethod def build_func_returns(return_type: type) -> str: + """Build the function returns.""" if return_type == _empty: func_returns = "None" elif return_type.__module__ == "builtins": @@ -501,6 +522,7 @@ class MethodDefinition: def build_command_method_signature( func_name: str, parameter_map: Dict[str, Parameter], return_type: type ) -> str: + """Build the command method signature.""" func_params = MethodDefinition.build_func_params(parameter_map) func_returns = MethodDefinition.build_func_returns(return_type) code = "\n @filter_call" @@ -517,12 +539,14 @@ class MethodDefinition: @staticmethod def build_command_method_doc(func: Callable): + """Build the command method docstring.""" code = f' """{func.__doc__}"""\n' if func.__doc__ else "" return code @staticmethod def build_command_method_implementation(path: str, func: Callable): + """Build the command method implementation.""" sig = signature(func) parameter_map = dict(sig.parameters) parameter_map.pop("cc", None) @@ -560,6 +584,7 @@ class MethodDefinition: def build_command_method( cls, path: str, func: Callable, model_name: Optional[str] ) -> str: + """Build the command method.""" func_name = func.__name__ sig = signature(func) @@ -582,8 +607,11 @@ class MethodDefinition: class PathHandler: + """Handle the paths for the SDK.""" + @staticmethod def build_route_map() -> Dict[str, BaseRoute]: + """Build the route map.""" router = RouterLoader.from_extensions() route_map = {route.path: route for route in router.api_router.routes} # type: ignore @@ -591,6 +619,7 @@ class PathHandler: @staticmethod def build_path_list(route_map: Dict[str, BaseRoute]) -> List[str]: + """Build the path list.""" path_list = [] for route_path in route_map: if route_path not in path_list: @@ -607,10 +636,12 @@ class PathHandler: @staticmethod def get_route(path: str, route_map: Dict[str, BaseRoute]): + """Get the route from the path.""" return route_map.get(path, None) @staticmethod def get_child_path_list(path: str, path_list: List[str]) -> List[str]: + """Get the child path list.""" direct_children = [] for p in path_list: if p.startswith(path): @@ -622,28 +653,34 @@ class PathHandler: @staticmethod def clean_path(path: str) -> str: + """Clean the path.""" if path.startswith("/"): path = path[1:] return path.replace("-", "_").replace("/", "_") @classmethod def build_module_name(cls, path: str) -> str: + """Build the module name.""" if path == "": return "__commands__" return cls.clean_path(path=path) @classmethod def build_module_class(cls, path: str) -> str: + """Build the module class.""" if path == "": return "Commands" return f"CLASS_{cls.clean_path(path=path)}" class Linters: + """Run the linters for the SDK.""" + current_folder = str(Path(Path(__file__).parent, "package").absolute()) @staticmethod def print_separator(symbol: str, length: int = 160): + """Print a separator.""" print(symbol * length) @staticmethod @@ -665,12 +702,15 @@ class Linters: @classmethod def black(cls): + """Run black.""" cls.run(linter="black") @classmethod def ruff(cls): + """Run ruff.""" cls.run(linter="ruff", flags=["--fix"]) @classmethod def mypy(cls): + """Run mypy.""" cls.run(linter="mypy", flags=["--ignore-missing-imports"]) diff --git a/openbb_sdk/sdk/core/pyproject.toml b/openbb_sdk/sdk/core/pyproject.toml index 753b43804c4..99d6bdbf9ee 100644 --- a/openbb_sdk/sdk/core/pyproject.toml +++ b/openbb_sdk/sdk/core/pyproject.toml @@ -26,7 +26,6 @@ openbb-provider = { path = "../provider" } [tool.poetry.group.dev.dependencies] openbb-provider = { path = "../provider" } -pytest = "^6.2.2" [build-system] requires = ["poetry-core"] diff --git a/openbb_sdk/sdk/core/tests/test_static_package_builder.py b/openbb_sdk/sdk/core/tests/test_static_package_builder.py new file mode 100644 index 00000000000..6cbb16f4a07 --- /dev/null +++ b/openbb_sdk/sdk/core/tests/test_static_package_builder.py @@ -0,0 +1,437 @@ +"""Test the app/static/package_builder.py file.""" +# pylint: disable=redefined-outer-name + +from dataclasses import dataclass +from inspect import _empty +from typing import Annotated + +import pytest +from openbb_core.app.static.package_builder import ( + ClassDefinition, + ImportDefinition, + Linters, + MethodDefinition, + ModuleBuilder, + PackageBuilder, + Parameter, + PathHandler, +) + + +@pytest.fixture(scope="module") +def package_builder(): + """Return package builder.""" + return PackageBuilder() + + +def test_package_builder_init(package_builder): + """Test package builder init.""" + assert package_builder + + +@pytest.mark.skip("Rebuilds the packages which takes some time.") +def test_package_builder_build(package_builder): + """Test package builder build.""" + package_builder.build() + + +@pytest.mark.skip() +def test_save_module_map(package_builder): + """Test save module map.""" + package_builder.save_module_map() + + +@pytest.mark.skip() +def test_save_modules(package_builder): + """Test save module.""" + package_builder.save_modules() + + +@pytest.mark.skip() +def test_save_package(package_builder): + """Test save package.""" + package_builder.save_package() + + +def test_run_linters(package_builder): + """Test run linters.""" + package_builder.run_linters() + + +@pytest.mark.skip("We avoid writing to the package.") +def test_write_to_package(package_builder): + """Test save to package.""" + package_builder.write_to_package() + + +@pytest.fixture(scope="module") +def module_builder(): + """Return module builder.""" + return ModuleBuilder() + + +def test_module_builder_init(module_builder): + """Test module builder init.""" + assert module_builder + + +@pytest.fixture(scope="module") +def class_definition(): + """Return class definition.""" + return ClassDefinition() + + +def test_class_definition_init(class_definition): + """Test class definition init.""" + assert class_definition + + +def test_build(class_definition): + """Test build.""" + code = class_definition.build("openbb_core.app.static.container.Container") + assert code + + +@pytest.fixture(scope="module") +def method_definition(): + """Return method definition.""" + return MethodDefinition() + + +def test_method_definition_init(method_definition): + """Test method definition init.""" + assert method_definition + + +def test_build_class_loader_method(method_definition): + """Test build class loader method.""" + code = method_definition.build_class_loader_method( + "openbb_core.app.static.container.Container" + ) + assert code + + +def test_get_type(method_definition): + """Test get type.""" + type_ = method_definition.get_type(field=Parameter.empty) + assert type_ + assert isinstance(type_, type) + + +def test_get_type_hint(method_definition): + """Test get type hint.""" + + class TestField: + type = int + + field = TestField() + result = method_definition.get_type(field) + assert result == int + + +def test_field_with_type_attribute_missing_type(method_definition): + """Test field with type attribute missing type.""" + + class TestField: + type = Parameter.empty + + field = TestField() + result = method_definition.get_type(field) + assert result is Parameter.empty + + +def test_get_default(method_definition): + """Test get default.""" + + class TestField: + default = 42 + + field = TestField() + result = method_definition.get_default(field) + assert result == 42 + + +def test_get_default_default_value(method_definition): + """Test get default default value.""" + + class TestField: + default = type(Ellipsis)() + + field = TestField() + result = method_definition.get_default(field) + assert result == _empty + + +def test_get_default_no_default(method_definition): + """Test get default no default.""" + + class TestField: + pass + + field = TestField() + result = method_definition.get_default(field) + assert result == _empty + + +def test_is_annotated_dc(method_definition): + """Test is annotated dc.""" + result = method_definition.is_annotated_dc(annotation=Parameter.empty) + assert not result + + +def test_is_annotated_dc_annotated(method_definition): + """Test is annotated dc annotated.""" + + @dataclass + class TestAnnotatedDataClass: + """Test annotated data class.""" + + value: int + + annotated_dataclass = Annotated[TestAnnotatedDataClass, "test_annotation"] + result = method_definition.is_annotated_dc(annotation=annotated_dataclass) + assert result + + +def test_reorder_params(method_definition): + """Test reorder params.""" + params = { + "provider": Parameter.empty, + "extra_params": Parameter.empty, + "param1": Parameter.empty, + "param2": Parameter.empty, + } + result = method_definition.reorder_params(params=params) + assert result + assert list(result.keys()) == ["param1", "param2", "provider", "extra_params"] + + +def test_build_func_params(method_definition): + """Test build func params.""" + param_map = { + "param1": Parameter("NoneType", kind=Parameter.POSITIONAL_OR_KEYWORD), + "param2": Parameter("int", kind=Parameter.POSITIONAL_OR_KEYWORD), + } + + expected_output = "param1, param2, chart: bool = False" + output = method_definition.build_func_params(param_map) + + assert output == expected_output + + +@pytest.mark.parametrize( + "return_type, expected_output", + [ + (_empty, "None"), + (int, "int"), + ], +) +def test_build_func_returns(method_definition, return_type, expected_output): + """Test build func returns.""" + output = method_definition.build_func_returns(return_type=return_type) + assert output == expected_output + + +def test_build_command_method_signature(method_definition): + """Test build command method signature.""" + parameter_map = { + "param1": Parameter("NoneType", kind=Parameter.POSITIONAL_OR_KEYWORD), + "param2": Parameter("int", kind=Parameter.POSITIONAL_OR_KEYWORD), + } + return_type = int + output = method_definition.build_command_method_signature( + func_name="test_func", parameter_map=parameter_map, return_type=return_type + ) + assert output + + +def test_build_command_method_doc(method_definition): + """Test build command method doc.""" + + def some_func(): + """Do some func doc.""" + + output = method_definition.build_command_method_doc(func=some_func) + assert output + assert isinstance(output, str) + + +def test_build_command_method_implementation(method_definition): + """Test build command method implementation.""" + + def some_func(): + """Do some func doc.""" + return 42 + + output = method_definition.build_command_method_implementation( + path="openbb_core.app.static.container.Container", func=some_func + ) + + assert output + assert isinstance(output, str) + + +def test_build_command_method(method_definition): + """Test build command method.""" + + def some_func(): + """Do some func doc.""" + return 42 + + output = method_definition.build_command_method( + path="openbb_core.app.static.container.Container", + func=some_func, + model_name=None, + ) + + assert output + assert isinstance(output, str) + + +@pytest.fixture(scope="module") +def import_definition(): + """Return import definition.""" + return ImportDefinition() + + +def test_import_definition_init(import_definition): + """Test import definition init.""" + assert import_definition + + +def test_filter_hint_type_list(import_definition): + """Test filter type hint list.""" + output = import_definition.filter_hint_type_list( + hint_type_list=[int, str, float, bool, _empty, _empty, _empty, _empty] + ) + assert output == [] + + +def test_import_definition_get_path_hint_type_list(import_definition): + """Test import definition get path hint type list.""" + hint_type_list = import_definition.get_path_hint_type_list( + path="openbb_core.app.static.container.Container" + ) + assert hint_type_list == [] + + +def test_import_definition_build(import_definition): + """Test import definition build.""" + code = import_definition.build(path="openbb_core.app.static.container.Container") + assert code + + +@pytest.fixture(scope="module") +def path_handler(): + """Return path handler.""" + return PathHandler() + + +def test_path_handler_init(path_handler): + """Test path handler init.""" + assert path_handler + + +@pytest.fixture(scope="module") +def route_map(path_handler): + """Return route map.""" + return path_handler.build_route_map() + + +def test_build_route_map(route_map): + """Test build route map.""" + assert route_map + assert isinstance(route_map, dict) + + +@pytest.fixture(scope="module") +def path_list(path_handler, route_map): + """Return path list.""" + return path_handler.build_path_list(route_map=route_map) + + +def test_build_path_list(path_list): + """Test build path list.""" + assert path_list + assert isinstance(path_list, list) + + +def test_get_route(path_handler, route_map): + """Test get route.""" + route = path_handler.get_route(route_map=route_map, path="/stocks/load") + + assert route + + +def test_get_child_path_list(path_handler, path_list): + """Test get child path list.""" + child_path_list = path_handler.get_child_path_list( + path="/stocks", path_list=path_list + ) + + assert child_path_list + assert isinstance(child_path_list, list) + + +def test_clean_path(path_handler): + """Test clean path.""" + path = "/stocks/load" + result = path_handler.clean_path(path=path) + assert result == "stocks_load" + + +def test_build_module_name(path_handler): + """Test build module name.""" + module_name = path_handler.build_module_name(path="") + assert module_name == "__commands__" + + module_name = path_handler.build_module_name(path="/stocks/load") + assert module_name == "stocks_load" + + +def test_build_module_class(path_handler): + """Test build module class.""" + module_class = path_handler.build_module_class(path="") + assert module_class == "Commands" + + module_class = path_handler.build_module_class(path="/stocks/load") + assert module_class == "CLASS_stocks_load" + + +@pytest.fixture(scope="module") +def linters(): + """Return linters.""" + return Linters() + + +def test_linters_init(linters): + """Test linters init.""" + assert linters + + +def test_print_separator(linters): + """Test print separator.""" + linters.print_separator(symbol="AAPL") + + +def test_run(linters): + """Test run.""" + linters.run(linter="ruff") + + +def test_ruff(linters): + """Test ruff.""" + linters.ruff() + + +def test_black(linters): + """Test black.""" + linters.black() + + +def test_mypy(linters): + """Test mypy.""" + linters.mypy() + + +# TODO: Implement unit test for the DocstringGenerator class once refactored.