Files
OpenBB/cli/tests/test_session.py
Danglewood f6867c2b18 [V5] openbb-charting: Refactor for V5 and Decouple (#7545)
* refactor charting for v5 patterns and decouple entirely

* coveragerc

* lint and tests

* fix tests

* configure dev group with openbb-technical

* ty

* fix cli test

* remove 3.15 from test matrix - dependencies are unstable

---------

Co-authored-by: deeleeramone <>
2026-07-01 09:37:01 -07:00

231 lines
7.7 KiB
Python

"""Test the Session class."""
from unittest.mock import MagicMock, patch
import pytest
from openbb_cli.models.settings import Settings
from openbb_cli.session import _UNSET, Session
def _fresh_session() -> Session:
"""Return a Session that's not the cached singleton from a prior test.
Settings load from the user's ``~/.openbb_platform/.cli.env`` env file,
which can leave ``OPENBB_OUTPUT_MODE`` set from prior interactive runs.
Explicitly reset to the documented baseline so tests are deterministic.
"""
Session._instances.pop(Session, None)
s = Session()
s.settings.OUTPUT_MODE = "tsv"
s.settings.USE_INTERACTIVE_DF = False
s._output_adapter = _UNSET
return s
@pytest.fixture
def session():
"""Provide a fresh Session per-test."""
return _fresh_session()
def test_session_initialization(session):
"""Heavy attrs are not materialized on construction."""
assert session.settings is not None
assert session.style is not None
assert session.console is not None
assert session.obbject_registry is not None
assert isinstance(session.settings, Settings)
assert session._prompt_session is _UNSET
assert session._backend is _UNSET
assert session._output_adapter is _UNSET
@patch("openbb_cli.session.sys.stdin.isatty", return_value=True)
def test_prompt_session_when_tty(mock_isatty, session):
"""Returns a PromptSession instance when stdin is a TTY."""
with patch("prompt_toolkit.PromptSession") as mock_ps:
mock_ps.return_value = MagicMock(name="PromptSession")
prompt = session.prompt_session
assert prompt is not None
@patch("openbb_cli.session.sys.stdin.isatty", return_value=False)
def test_prompt_session_when_not_tty(mock_isatty, session):
"""Returns None when stdin is not a TTY (CI / agent / pipe)."""
assert session.prompt_session is None
@patch(
"openbb_cli.session.sys.stdin.isatty",
side_effect=ValueError("detached"),
)
def test_prompt_session_swallows_isatty_failure(mock_isatty, session):
"""Detached stdin (ValueError on isatty) is treated as non-interactive."""
assert session.is_interactive is False
assert session.prompt_session is None
def test_prompt_session_cached(session):
"""Subsequent accesses return the cached value."""
with patch.object(session, "_build_prompt_session", return_value="X") as build:
first = session.prompt_session
second = session.prompt_session
assert first == second == "X"
build.assert_called_once()
@pytest.mark.parametrize(
"mode, expected_module",
[
("tsv", "openbb_cli.outputs.tsv"),
("rich", "openbb_cli.outputs.rich"),
("json", "openbb_cli.outputs.json"),
("html", "openbb_cli.outputs.html"),
],
)
def test_output_adapter_modes(mode, expected_module):
"""OUTPUT_MODE setting selects the matching adapter."""
s = _fresh_session()
s.settings.OUTPUT_MODE = mode
s._output_adapter = _UNSET
adapter = s.output_adapter
assert adapter.__class__.__module__ == expected_module
def test_output_adapter_default_is_tsv(session):
"""Default OUTPUT_MODE is 'tsv' — adapter is TsvOutput."""
from openbb_cli.outputs.tsv import TsvOutput
assert isinstance(session.output_adapter, TsvOutput)
def test_output_adapter_cached_when_mode_unchanged(session):
"""Subsequent accesses return the same instance when ``OUTPUT_MODE`` is stable."""
a1 = session.output_adapter
a2 = session.output_adapter
assert a1 is a2
def test_output_adapter_rebuilds_on_mode_change(session):
"""Changing ``OUTPUT_MODE`` via ``/settings/`` swaps the adapter — must
not stay frozen at first-access mode."""
from openbb_cli.outputs.json import JsonOutput
from openbb_cli.outputs.tsv import TsvOutput
session.settings.OUTPUT_MODE = "tsv"
assert isinstance(session.output_adapter, TsvOutput)
session.settings.OUTPUT_MODE = "json"
assert isinstance(session.output_adapter, JsonOutput)
def test_backend_cached(session):
"""Backend property caches the first-built value."""
sentinel = object()
with patch.object(session, "_build_backend", return_value=sentinel) as build:
first = session.backend
second = session.backend
assert first is second is sentinel
build.assert_called_once()
def test_build_backend_swallows_import_failure(session):
"""``_build_backend`` returns None when openbb_charting import fails.
Patches ``sys.modules`` to inject a None entry that triggers ImportError on
the dynamic import, exercising the except-Exception branch of the real
builder rather than the mocked-out version.
"""
import sys
with patch.dict(sys.modules, {"openbb_charting.core.backend": None}):
assert session._build_backend() is None
def test_build_backend_returns_real_backend_on_success(session):
"""Happy path: openbb_charting and ChartingSettings both import → Backend instance."""
sentinel = MagicMock(name="Backend instance")
fake_backend_module = MagicMock()
fake_backend_module.Backend.return_value = sentinel
fake_settings_module = MagicMock()
fake_settings_module.ChartingSettings.return_value = MagicMock(name="settings")
import sys
with patch.dict(
sys.modules,
{
"openbb_charting.core.backend": fake_backend_module,
"openbb_core.app.model.charts.charting_settings": fake_settings_module,
},
):
assert session._build_backend() is sentinel
def test_resolve_backend_class_prefers_registered_backend():
"""A backend registered via the entry-point group is returned directly."""
sentinel = object()
with patch(
"openbb_core.app.charting.get_charting_backend_class",
return_value=sentinel,
):
assert Session._resolve_backend_class() is sentinel
def test_build_backend_returns_none_when_no_backend_resolves(session):
"""``_build_backend`` returns None when no backend class resolves."""
import sys
with (
patch.dict(
sys.modules,
{"openbb_core.app.model.charts.charting_settings": MagicMock()},
),
patch.object(session, "_resolve_backend_class", return_value=None),
):
assert session._build_backend() is None
def test_build_prompt_session_swallows_failure(session):
"""Constructing PromptSession may fail (e.g. terminal init); session degrades to None."""
import sys
with (
patch("openbb_cli.session.sys.stdin.isatty", return_value=True),
patch.dict(sys.modules, {"prompt_toolkit": None}),
):
assert session._build_prompt_session() is None
def test_max_obbjects_not_exceeded(session):
"""Returns False under the registry size limit."""
session.settings.N_TO_KEEP_OBBJECT_REGISTRY = 10
assert session.max_obbjects_exceeded() is False
def test_max_obbjects_exceeded_at_limit():
"""At the limit (0), the empty registry counts as exceeded."""
s = _fresh_session()
s.settings.N_TO_KEEP_OBBJECT_REGISTRY = 0
assert s.max_obbjects_exceeded() is True
def test_obb_swallows_user_styles_directory_failure():
"""If reading ``obb.user.preferences.user_styles_directory`` raises, the
session still returns the fake obb (the styling apply is best-effort)."""
import sys
import types
fake = types.SimpleNamespace()
fake.user = MagicMock()
type(fake.user).preferences = property(
lambda self: (_ for _ in ()).throw(RuntimeError("preferences blew up"))
)
fake_mod = types.ModuleType("openbb")
fake_mod.obb = fake
s = _fresh_session()
with patch.dict(sys.modules, {"openbb": fake_mod}):
assert s._obb is fake
with patch.dict(sys.modules, {"openbb": None}):
assert s._obb is fake