Test static package_builder

This commit is contained in:
Igor Radovanovic
2023-07-26 17:26:56 +02:00
parent 5f905b2105
commit 1457d1bd6c
4 changed files with 485 additions and 5 deletions

View File

@@ -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"

View File

@@ -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"])

View File

@@ -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"]

View File

@@ -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.