mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 20:42:26 +00:00
* stash some changes * add more robust testing * mypy * point PR at V5 * introduce spec file * codespell * test fix * fix workflow environment setup * fix workflow environment setup * fix workflow environment setup * add pyyaml to dependencies * split lint jobs * fix workflow environment setup * fix workflow environment setup * workflow env setup * workflow env setup * clean up code comments * add auth hook entrypoints * codespell * add codegen feature * codespell * move _unpack into dispatchers for consistency with codegen packages * surface nested models in the response * fix missing coverage in CI * socrata updates * test fix * detect plotly output * add --include and --exclude flags from generate-extension command * cap test matrix at python 3.14 * no useless comments * platform controller command description split * merge URL overloads from path params * exclude none and unset from model dump --------- Co-authored-by: deeleeramone <> Co-authored-by: Copilot <copilot@github.com>
308 lines
9.8 KiB
Python
308 lines
9.8 KiB
Python
"""Test OBBject Registry."""
|
|
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from openbb_core.app.model.obbject import OBBject
|
|
|
|
from openbb_cli.argparse_translator.obbject_registry import Registry
|
|
|
|
|
|
@pytest.fixture
|
|
def registry():
|
|
"""Fixture to create a Registry instance for testing."""
|
|
return Registry()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_obbject():
|
|
"""Fixture to create a mock OBBject for testing."""
|
|
|
|
class MockModel:
|
|
"""Mock model for testing."""
|
|
|
|
def __init__(self, value):
|
|
self.mock_value = value
|
|
self._model_json_schema = "mock_json_schema"
|
|
|
|
def model_json_schema(self):
|
|
return self._model_json_schema
|
|
|
|
obb = Mock(spec=OBBject)
|
|
obb.id = "123"
|
|
obb.provider = "test_provider"
|
|
obb.extra = {"command": "test_command"}
|
|
obb._route = "/test/route"
|
|
obb._standard_params = Mock()
|
|
obb._standard_params = {}
|
|
obb.results = [MockModel(1), MockModel(2)]
|
|
return obb
|
|
|
|
|
|
def test_listing_all_obbjects(registry, mock_obbject):
|
|
"""Registry surfaces the entire OBBject sans ``results`` — every metadata
|
|
field rides along, including ``provider`` at top level and
|
|
``command`` nested under ``extra``. No synthesized duplicates of
|
|
private attrs.
|
|
"""
|
|
registry.register(mock_obbject)
|
|
|
|
all_obbjects = registry.all
|
|
assert len(all_obbjects) == 1
|
|
assert all_obbjects[0]["provider"] == "test_provider"
|
|
# ``command`` lives where the OBBject keeps it: under ``extra``.
|
|
assert all_obbjects[0]["extra"]["command"] == "test_command"
|
|
# Private attrs (_route, _standard_params) are NOT duplicated as
|
|
# synthetic columns — callers read them off the OBBject directly,
|
|
# or off ``extra.metadata`` which carries the canonical values.
|
|
assert "_route" not in all_obbjects[0]
|
|
assert "_standard_params" not in all_obbjects[0]
|
|
assert "route" not in all_obbjects[0]
|
|
assert "standard params" not in all_obbjects[0]
|
|
|
|
|
|
def test_registry_initialization(registry):
|
|
"""Test the Registry is initialized correctly."""
|
|
assert registry.obbjects == []
|
|
|
|
|
|
def test_register_new_obbject(registry, mock_obbject):
|
|
"""Test registering a new OBBject."""
|
|
registry.register(mock_obbject)
|
|
assert mock_obbject in registry.obbjects
|
|
|
|
|
|
def test_register_duplicate_obbject(registry, mock_obbject):
|
|
"""Test that duplicate OBBjects are not added."""
|
|
registry.register(mock_obbject)
|
|
registry.register(mock_obbject)
|
|
assert len(registry.obbjects) == 1
|
|
|
|
|
|
def test_get_obbject_by_index(registry, mock_obbject):
|
|
"""Test retrieving an obbject by its index."""
|
|
registry.register(mock_obbject)
|
|
retrieved = registry.get(0)
|
|
assert retrieved == mock_obbject
|
|
|
|
|
|
def test_remove_obbject_by_index(registry, mock_obbject):
|
|
"""Test removing an obbject by index."""
|
|
registry.register(mock_obbject)
|
|
registry.remove(0)
|
|
assert mock_obbject not in registry.obbjects
|
|
|
|
|
|
def test_remove_last_obbject_by_default(registry, mock_obbject):
|
|
"""Test removing the last obbject by default."""
|
|
registry.register(mock_obbject)
|
|
registry.remove()
|
|
assert not registry.obbjects
|
|
|
|
|
|
def test_get_by_key_found(registry, mock_obbject):
|
|
"""Test retrieving obbject by its register_key."""
|
|
mock_obbject.extra = {"command": "test_command", "register_key": "my_data"}
|
|
registry.register(mock_obbject)
|
|
result = registry.get("my_data")
|
|
assert result == mock_obbject
|
|
|
|
|
|
def test_get_by_key_not_found(registry, mock_obbject):
|
|
"""Test get returns None when key does not exist."""
|
|
mock_obbject.extra = {"command": "test_command", "register_key": "other"}
|
|
registry.register(mock_obbject)
|
|
result = registry.get("missing")
|
|
assert result is None
|
|
|
|
|
|
def test_get_by_key_no_register_key(registry, mock_obbject):
|
|
"""Test get by key when no register_key set."""
|
|
mock_obbject.extra = {"command": "test_command"}
|
|
registry.register(mock_obbject)
|
|
result = registry.get("anything")
|
|
assert result is None
|
|
|
|
|
|
def test_obbject_keys_empty(registry):
|
|
"""Test obbject_keys returns empty list when registry is empty."""
|
|
assert registry.obbject_keys == []
|
|
|
|
|
|
def test_obbject_keys_with_keys(registry, mock_obbject):
|
|
"""Test obbject_keys returns register_keys."""
|
|
mock_obbject.extra = {"command": "test", "register_key": "my_key"}
|
|
registry.register(mock_obbject)
|
|
assert registry.obbject_keys == ["my_key"]
|
|
|
|
|
|
def test_obbject_keys_skips_without_key(registry):
|
|
"""Test obbject_keys skips obbjects without register_key."""
|
|
obj = Mock(spec=OBBject)
|
|
obj.id = "456"
|
|
obj.extra = {"command": "test"}
|
|
obj.results = [{"data": 1}]
|
|
registry.register(obj)
|
|
assert registry.obbject_keys == []
|
|
|
|
|
|
def test_get_invalid_type_raises(registry):
|
|
"""Test get raises ValueError for invalid arg type."""
|
|
with pytest.raises(ValueError, match="Couldn't get"):
|
|
registry.get(3.14)
|
|
|
|
|
|
def test_all_empty_registry(registry):
|
|
"""Test all returns empty dict for empty registry."""
|
|
assert registry.all == {}
|
|
|
|
|
|
def test_all_index_as_stack_order(registry):
|
|
"""Test that all uses stack order (most recent = index 0)."""
|
|
obj1 = Mock(spec=OBBject)
|
|
obj1.id = "1"
|
|
obj1.provider = "p1"
|
|
obj1.extra = {"command": "cmd1"}
|
|
obj1._route = "/r1"
|
|
obj1._standard_params = {}
|
|
obj1.results = [{"data": 1}]
|
|
|
|
obj2 = Mock(spec=OBBject)
|
|
obj2.id = "2"
|
|
obj2.provider = "p2"
|
|
obj2.extra = {"command": "cmd2"}
|
|
obj2._route = "/r2"
|
|
obj2._standard_params = {}
|
|
obj2.results = [{"data": 2}]
|
|
|
|
registry.register(obj1)
|
|
registry.register(obj2)
|
|
|
|
all_items = registry.all
|
|
assert all_items[0]["extra"]["command"] == "cmd2"
|
|
assert all_items[1]["extra"]["command"] == "cmd1"
|
|
|
|
|
|
def test_get_out_of_bounds(registry, mock_obbject):
|
|
"""Test get returns None for out-of-bounds index."""
|
|
registry.register(mock_obbject)
|
|
assert registry.get(99) is None
|
|
|
|
|
|
def test_all_does_not_duplicate_private_attrs_into_synthetic_columns(registry):
|
|
"""Private attrs (``_route``, ``_standard_params``) are NOT promoted to
|
|
synthetic table columns — that would duplicate ``extra.metadata.route``
|
|
and ``extra.metadata.arguments.standard_params`` for any OBBject that
|
|
came through ``command_runner`` (or our spec-mode dispatcher), which is
|
|
exactly the duplication users complained about.
|
|
"""
|
|
obj = Mock(spec=OBBject)
|
|
obj.id = "1"
|
|
obj.provider = "p"
|
|
obj.extra = {
|
|
"command": "cmd",
|
|
"metadata": {
|
|
"route": "/r",
|
|
"arguments": {
|
|
"provider_choices": {},
|
|
"standard_params": {"symbol": "AAPL", "limit": 10},
|
|
"extra_params": {},
|
|
},
|
|
},
|
|
}
|
|
obj._route = "/r"
|
|
obj._standard_params = {"symbol": "AAPL", "limit": 10}
|
|
obj.results = []
|
|
registry.register(obj)
|
|
row = registry.all[0]
|
|
# The metadata is in ``extra.metadata`` — not duplicated.
|
|
assert row["extra"]["metadata"]["arguments"]["standard_params"] == {
|
|
"symbol": "AAPL",
|
|
"limit": 10,
|
|
}
|
|
assert row["extra"]["metadata"]["route"] == "/r"
|
|
# No mirror columns.
|
|
assert "_route" not in row
|
|
assert "_standard_params" not in row
|
|
assert "route" not in row
|
|
assert "standard params" not in row
|
|
|
|
|
|
def test_all_falls_back_when_model_dump_raises(registry):
|
|
"""When ``OBBject.model_dump`` itself raises (corrupted state, custom
|
|
pydantic config, etc.) the registry must not blow up — fall back to
|
|
a manual ``getattr``-based row so recall still works.
|
|
"""
|
|
obj = Mock(spec=OBBject)
|
|
obj.id = "x"
|
|
obj.provider = "p"
|
|
obj.warnings = None
|
|
obj.chart = None
|
|
obj.extra = {"command": "c"}
|
|
obj._route = "/x"
|
|
obj._standard_params = {}
|
|
obj.results = [{"a": 1}]
|
|
# Force model_dump to blow up — exercises the manual-fallback branch.
|
|
obj.model_dump = Mock(side_effect=RuntimeError("boom"))
|
|
registry.register(obj)
|
|
row = registry.all[0]
|
|
# Manual fallback still produces the canonical row keys.
|
|
assert row["id"] == "x"
|
|
assert row["provider"] == "p"
|
|
assert row["extra"]["command"] == "c"
|
|
|
|
|
|
def test_all_dumps_full_obbject_minus_results(registry):
|
|
"""The recall row is ``OBBject.model_dump(exclude={'results'})`` plus the
|
|
private attrs — every metadata field on the OBBject rides along untouched.
|
|
Smoke-checks that ``extra.metadata`` and ``extra.results_metadata`` survive.
|
|
"""
|
|
obj = Mock(spec=OBBject)
|
|
obj.id = "abc"
|
|
obj.provider = None
|
|
obj.warnings = None
|
|
obj.chart = None
|
|
obj.extra = {
|
|
"command": "/foo --bar 1",
|
|
"results_metadata": {"columns": {"price": {"description": "$/ton"}}},
|
|
"metadata": {
|
|
"route": "/foo",
|
|
"duration": 12345,
|
|
"arguments": {
|
|
"provider_choices": {},
|
|
"standard_params": {"bar": 1},
|
|
"extra_params": {},
|
|
},
|
|
"timestamp": "2026-05-04T00:00:00+00:00",
|
|
},
|
|
}
|
|
obj._route = "/foo"
|
|
obj._standard_params = {"bar": 1}
|
|
obj.results = [{"price": 12.5}]
|
|
obj.model_dump = Mock(
|
|
return_value={
|
|
"id": "abc",
|
|
"provider": None,
|
|
"warnings": None,
|
|
"chart": None,
|
|
"extra": obj.extra,
|
|
}
|
|
)
|
|
registry.register(obj)
|
|
row = registry.all[0]
|
|
# Every OBBject field except ``results`` is present.
|
|
assert row["id"] == "abc"
|
|
assert row["provider"] is None
|
|
assert row["chart"] is None
|
|
# ``extra`` carries the rich metadata, intact.
|
|
assert (
|
|
row["extra"]["results_metadata"]["columns"]["price"]["description"] == "$/ton"
|
|
)
|
|
assert row["extra"]["metadata"]["route"] == "/foo"
|
|
assert row["extra"]["metadata"]["duration"] == 12345
|
|
assert row["extra"]["command"] == "/foo --bar 1"
|
|
# Private attrs are NOT promoted to synthetic top-level keys — that
|
|
# would duplicate ``extra.metadata.route`` etc.
|
|
assert "_route" not in row
|
|
assert "_standard_params" not in row
|