Skip to content

Commit 4a36d90

Browse files
authored
[BugFix] openbb-core: Fix Package Builder In no_validate Path & Handle Lazy Annotations (#7423)
* fix unresolved string annotations in no_validate path * add regression test * bump and regenerate locks --------- Co-authored-by: deeleeramone <>
1 parent ff75000 commit 4a36d90

7 files changed

Lines changed: 162 additions & 60 deletions

File tree

openbb_platform/core/openbb/assets/reference.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"openbb": "1.6.4core",
2+
"openbb": "1.6.5core",
33
"info": {
44
"title": "OpenBB Platform (Python)",
55
"description": "Investment research for everyone, anywhere.",
6-
"core": "1.6.4",
6+
"core": "1.6.5",
77
"extensions": {
88
"openbb_core_extension": [],
99
"openbb_provider_extension": [],

openbb_platform/core/openbb_core/app/static/package_builder.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,9 @@ def build(cls, path: str) -> str:
557557
code += "\nfrom fastapi import Depends"
558558

559559
module_list = [
560-
hint_type.__module__ if hasattr(hint_type, "__module__") else hint_type
560+
hint_type.__module__
561561
for hint_type in hint_type_list
562+
if hasattr(hint_type, "__module__")
562563
]
563564
module_list = list(set(module_list))
564565
module_list.sort() # type: ignore
@@ -1531,6 +1532,11 @@ def get_type_repr(type_hint: Any) -> str:
15311532
if isinstance(type_hint, type):
15321533
return type_hint.__name__
15331534

1535+
# Unwrap ForwardRef to its inner string so we don't emit
1536+
# ForwardRef('int') in generated signatures.
1537+
if hasattr(type_hint, "__forward_arg__"):
1538+
return type_hint.__forward_arg__
1539+
15341540
s = str(type_hint)
15351541
if s.startswith("typing."):
15361542
s = s[7:]
@@ -1615,7 +1621,7 @@ def build_func_returns(return_type: type) -> str:
16151621
if return_type == _empty:
16161622
func_returns = "Any"
16171623
elif isinstance(return_type, str):
1618-
func_returns = f"ForwardRef('{return_type}')"
1624+
func_returns = return_type
16191625
elif isclass(return_type) and issubclass(return_type, OBBject):
16201626
func_returns = "OBBject"
16211627
else:
@@ -2042,6 +2048,10 @@ def get_field_type(
20422048
try:
20432049
_type = field_type
20442050

2051+
# Unwrap ForwardRef to its inner string
2052+
if hasattr(_type, "__forward_arg__"):
2053+
_type = _type.__forward_arg__
2054+
20452055
if "BeforeValidator" in str(_type):
20462056
_type = "Optional[int]" if is_optional else "int" # type: ignore
20472057

openbb_platform/core/poetry.lock

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openbb_platform/core/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "openbb-core"
3-
version = "1.6.4"
3+
version = "1.6.5"
44
description = "OpenBB package with core functionality."
55
authors = ["OpenBB Team <hello@openbb.co>"]
66
license = "AGPL-3.0-only"

openbb_platform/core/tests/app/static/test_package_builder.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,3 +896,95 @@ def optional_return_dependency(optional: str = "value") -> MockDep | None:
896896
assert not method_definition._is_safe_dependency(optional_request_dependency)
897897
assert not method_definition._is_safe_dependency(none_return_dependency)
898898
assert method_definition._is_safe_dependency(optional_return_dependency)
899+
900+
901+
def test_build_func_params_unwraps_forward_ref(method_definition):
902+
"""Test that ForwardRef annotations are unwrapped in generated function params.
903+
904+
Regression test: when extensions use `from __future__ import annotations`
905+
with `no_validate=True`, annotations remain as strings at runtime.
906+
Annotated["int", ...] auto-wraps "int" into ForwardRef("int").
907+
The builder must unwrap these to plain type strings.
908+
"""
909+
# pylint: disable=import-outside-toplevel
910+
from collections import OrderedDict
911+
from typing import ForwardRef
912+
913+
from openbb_core.app.model.field import OpenBBField
914+
915+
param_map = OrderedDict(
916+
{
917+
"symbol": Parameter(
918+
name="symbol",
919+
kind=Parameter.POSITIONAL_OR_KEYWORD,
920+
annotation=Annotated[
921+
ForwardRef("str"), OpenBBField(description="")
922+
],
923+
),
924+
"days": Parameter(
925+
name="days",
926+
kind=Parameter.POSITIONAL_OR_KEYWORD,
927+
annotation=Annotated[
928+
ForwardRef("int"), OpenBBField(description="")
929+
],
930+
default=7,
931+
),
932+
"asset_type": Parameter(
933+
name="asset_type",
934+
kind=Parameter.POSITIONAL_OR_KEYWORD,
935+
annotation=Annotated[
936+
ForwardRef("Literal['stock', 'etf', 'all'] | None"),
937+
OpenBBField(description=""),
938+
],
939+
default=None,
940+
),
941+
}
942+
)
943+
944+
output = method_definition.build_func_params(param_map)
945+
946+
assert "ForwardRef" not in output, (
947+
f"ForwardRef should be unwrapped in generated params, got:\n{output}"
948+
)
949+
assert "str," in output
950+
assert "int," in output
951+
assert "Literal['stock', 'etf', 'all']" in output
952+
953+
954+
def test_build_func_returns_string_annotation(method_definition):
955+
"""Test that string return types are emitted directly, not wrapped in ForwardRef.
956+
957+
Regression test: `from __future__ import annotations` causes return annotations
958+
to be strings. The builder should emit the string directly (e.g., "OBBject")
959+
rather than wrapping it as ForwardRef('OBBject').
960+
"""
961+
output = method_definition.build_func_returns(return_type="OBBject")
962+
assert output == "OBBject"
963+
assert "ForwardRef" not in output
964+
965+
output2 = method_definition.build_func_returns(return_type="Any")
966+
assert output2 == "Any"
967+
assert "ForwardRef" not in output2
968+
969+
970+
def test_get_field_type_unwraps_forward_ref(docstring_generator):
971+
"""Test that ForwardRef is unwrapped in docstring type formatting.
972+
973+
Regression test: ForwardRef('int') should render as 'int' in docstrings,
974+
not as the literal string "ForwardRef('int')".
975+
"""
976+
# pylint: disable=import-outside-toplevel
977+
from typing import ForwardRef
978+
979+
result = docstring_generator.get_field_type(ForwardRef("int"), is_required=True)
980+
assert "ForwardRef" not in result, (
981+
f"ForwardRef should be unwrapped in docstring types, got: {result}"
982+
)
983+
assert "int" in result
984+
985+
result2 = docstring_generator.get_field_type(
986+
ForwardRef("Literal['stock', 'etf', 'all'] | None"), is_required=False
987+
)
988+
assert "ForwardRef" not in result2, (
989+
f"ForwardRef should be unwrapped in docstring types, got: {result2}"
990+
)

0 commit comments

Comments
 (0)