mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 20:42:26 +00:00
* 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 <>
621 lines
24 KiB
Python
621 lines
24 KiB
Python
"""Tests for the ``openbb_charting.charting.Charting`` accessor class."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import warnings
|
|
from types import SimpleNamespace
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
from openbb_core.app.model.charts.chart import Chart
|
|
from openbb_core.app.model.obbject import OBBject
|
|
from openbb_core.provider.abstract.data import Data
|
|
|
|
from openbb_charting.charting import Charting
|
|
from openbb_charting.core.backend import Backend
|
|
from openbb_charting.core.openbb_figure import OpenBBFigure
|
|
from openbb_charting.query_params import EquityPriceHistoricalChartQueryParams
|
|
|
|
# pylint: disable=protected-access,redefined-outer-name
|
|
|
|
|
|
class LocalChartingViews:
|
|
"""A real charting-view class registered for the duration of the tests."""
|
|
|
|
@staticmethod
|
|
def equity_price_historical(**kwargs):
|
|
"""Render the real price-historical chart (returns a (fig, content) tuple)."""
|
|
from openbb_charting.charts.price_historical import price_historical
|
|
|
|
return price_historical(**kwargs)
|
|
|
|
@staticmethod
|
|
def custom_help_view(**kwargs):
|
|
"""A charted route with no ChartParams entry, to drive the help fallback."""
|
|
from openbb_charting.charts.price_historical import price_historical
|
|
|
|
return price_historical(**kwargs)
|
|
|
|
@staticmethod
|
|
def raise_runtime(**kwargs):
|
|
"""A view that raises ``RuntimeError`` to drive the re-raise branch."""
|
|
raise RuntimeError("charting does not support test")
|
|
|
|
|
|
class RecorderApp:
|
|
"""A real stand-in for the PyWry GUI app that records dispatched calls."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the recorder with empty call logs."""
|
|
self.calls: list[str] = []
|
|
self.theme = None
|
|
|
|
def emit(self, *args, **kwargs):
|
|
"""Record an emit call."""
|
|
self.calls.append("emit")
|
|
|
|
def show_plotly(self, **kwargs):
|
|
"""Record a ``show_plotly`` call."""
|
|
self.calls.append("show_plotly")
|
|
return "plotly"
|
|
|
|
def show_dataframe(self, **kwargs):
|
|
"""Record a ``show_dataframe`` call."""
|
|
self.calls.append("show_dataframe")
|
|
return "dataframe"
|
|
|
|
def show(self, **kwargs):
|
|
"""Record a ``show`` call."""
|
|
self.calls.append("show")
|
|
return "show"
|
|
|
|
def close(self, **kwargs):
|
|
"""Record a ``close`` call."""
|
|
self.calls.append("close")
|
|
|
|
|
|
class RecorderBackend:
|
|
"""A real stand-in for ``Backend.send_table`` / ``send_url`` delegation."""
|
|
|
|
def __init__(self):
|
|
"""Initialize the recorder with empty call logs."""
|
|
self.tables: list[dict] = []
|
|
self.urls: list[dict] = []
|
|
|
|
def send_table(self, **kwargs):
|
|
"""Record a table dispatch."""
|
|
self.tables.append(kwargs)
|
|
|
|
def send_url(self, **kwargs):
|
|
"""Record a URL dispatch."""
|
|
self.urls.append(kwargs)
|
|
|
|
|
|
class RaisingBackend:
|
|
"""A real backend whose dispatch methods raise, to drive warn branches."""
|
|
|
|
def send_table(self, **kwargs):
|
|
"""Raise to trigger the table warn branch."""
|
|
raise RuntimeError("table boom")
|
|
|
|
def send_url(self, **kwargs):
|
|
"""Raise to trigger the URL warn branch."""
|
|
raise RuntimeError("url boom")
|
|
|
|
|
|
def _make_ohlcv(seed: int = 7, periods: int = 60) -> pd.DataFrame:
|
|
"""Build a deterministic OHLCV frame indexed by date."""
|
|
rng = np.random.default_rng(seed)
|
|
idx = pd.date_range("2023-01-01", periods=periods, freq="D")
|
|
close = 100 + np.cumsum(rng.normal(0, 1, periods))
|
|
frame = pd.DataFrame(
|
|
{
|
|
"open": close + rng.normal(0, 0.5, periods),
|
|
"high": close + 1,
|
|
"low": close - 1,
|
|
"close": close,
|
|
"volume": rng.integers(1_000_000, 5_000_000, periods),
|
|
},
|
|
index=idx,
|
|
)
|
|
frame.index.name = "date"
|
|
return frame
|
|
|
|
|
|
def _df_to_data(frame: pd.DataFrame) -> list[Data]:
|
|
"""Convert an OHLCV frame to date-stamped ``Data`` records."""
|
|
records = frame.reset_index()
|
|
records["date"] = records["date"].dt.strftime("%Y-%m-%d")
|
|
return [Data.model_validate(row) for row in records.to_dict(orient="records")]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def local_views():
|
|
"""Register the local real view class for the duration of each test."""
|
|
original = Charting._extension_views_cache
|
|
Charting._extension_views_cache = [LocalChartingViews]
|
|
try:
|
|
yield
|
|
finally:
|
|
Charting._extension_views_cache = original
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_backend_singleton():
|
|
"""Reset the ``Backend`` singleton around each test to avoid state leakage."""
|
|
Backend.instance = None
|
|
yield
|
|
Backend.instance = None
|
|
|
|
|
|
def _obbject(route: str, results=None) -> OBBject:
|
|
"""Build a real ``OBBject`` with the given route and OHLCV results."""
|
|
obj = OBBject(
|
|
results=_df_to_data(_make_ohlcv()) if results is None else results,
|
|
provider="test",
|
|
)
|
|
obj._route = route
|
|
obj._standard_params = {"symbol": "AAA"}
|
|
obj._extra_params = {}
|
|
return obj
|
|
|
|
|
|
@pytest.fixture
|
|
def equity_obbject() -> OBBject:
|
|
"""A real ``OBBject`` shaped like equity price history with a resolvable view."""
|
|
return _obbject("/equity/price/historical")
|
|
|
|
|
|
class TestConstruction:
|
|
"""Tests for ``Charting`` construction and class accessors."""
|
|
|
|
def test_construct_from_real_obbject(self, equity_obbject):
|
|
"""It constructs from a real ``OBBject`` with a real backend."""
|
|
charting = Charting(equity_obbject)
|
|
assert isinstance(charting, Charting)
|
|
assert charting._functions
|
|
|
|
def test_get_backend_class(self):
|
|
"""``get_backend_class`` returns the built-in PyWry backend."""
|
|
assert Charting.get_backend_class() is Backend
|
|
|
|
def test_get_figure_class(self):
|
|
"""``get_figure_class`` returns ``OpenBBFigure``."""
|
|
assert Charting.get_figure_class() is OpenBBFigure
|
|
|
|
def test_functions_lists_registered_views(self):
|
|
"""``functions`` lists charting functions from the registered views."""
|
|
functions = Charting.functions()
|
|
assert "equity_price_historical" in functions
|
|
|
|
def test_extension_views_loaded_from_entry_points(self):
|
|
"""``_get_extension_views`` loads real entry points when uncached."""
|
|
Charting._extension_views_cache = None
|
|
views = Charting._get_extension_views()
|
|
assert isinstance(views, list)
|
|
|
|
def test_indicators_returns_params(self):
|
|
"""``indicators`` returns the aggregate indicators-params model."""
|
|
keys = list(Charting.indicators().model_dump().keys())
|
|
assert "sma" in keys
|
|
assert "macd" in keys
|
|
|
|
|
|
class TestChartFunctionResolution:
|
|
"""Tests for ``_get_chart_function`` route resolution."""
|
|
|
|
def test_resolves_known_route(self, equity_obbject):
|
|
"""It resolves a known route to a callable."""
|
|
charting = Charting(equity_obbject)
|
|
func = charting._get_chart_function("/equity/price/historical")
|
|
assert callable(func)
|
|
|
|
def test_none_route_raises(self, equity_obbject):
|
|
"""A ``None`` route raises a ``ValueError``."""
|
|
charting = Charting(equity_obbject)
|
|
with pytest.raises(ValueError, match="no function route"):
|
|
charting._get_chart_function(None)
|
|
|
|
def test_unknown_route_raises(self, equity_obbject):
|
|
"""An unknown route raises a ``ValueError``."""
|
|
charting = Charting(equity_obbject)
|
|
with pytest.raises(ValueError, match="Could not find the route"):
|
|
charting._get_chart_function("/does/not/exist")
|
|
|
|
|
|
class TestGetParams:
|
|
"""Tests for the ``get_params`` accessor."""
|
|
|
|
def test_known_route_returns_params_class(self, equity_obbject):
|
|
"""A registered route returns its instantiated query-params class."""
|
|
params = Charting(equity_obbject).get_params()
|
|
assert isinstance(params, EquityPriceHistoricalChartQueryParams)
|
|
|
|
def test_unregistered_route_returns_help(self):
|
|
"""A charted route with no ChartParams entry routes through ``help``."""
|
|
obj = _obbject("/custom/help/view")
|
|
obj.extra = {"metadata": SimpleNamespace(route="/custom/help/view")}
|
|
assert Charting(obj).get_params() is None
|
|
|
|
def test_none_route_raises(self, equity_obbject):
|
|
"""A ``None`` route raises a ``ValueError``."""
|
|
equity_obbject._route = None
|
|
with pytest.raises(ValueError, match="no function route"):
|
|
Charting(equity_obbject).get_params()
|
|
|
|
|
|
class TestPrepareDataAsDf:
|
|
"""Tests for ``_prepare_data_as_df``."""
|
|
|
|
def test_with_external_dataframe(self, equity_obbject):
|
|
"""It returns the supplied DataFrame and a True ``has_data`` flag."""
|
|
frame = _make_ohlcv()
|
|
data_df, has_data = Charting(equity_obbject)._prepare_data_as_df(frame)
|
|
assert has_data is True
|
|
assert "close" in data_df.columns
|
|
|
|
def test_without_data_uses_obbject(self, equity_obbject):
|
|
"""With no data it falls back to the OBBject contents."""
|
|
data_df, has_data = Charting(equity_obbject)._prepare_data_as_df(None)
|
|
assert has_data is False
|
|
assert "close" in data_df.columns
|
|
|
|
def test_drops_provider_column(self, equity_obbject):
|
|
"""A ``provider`` column present in the data is dropped."""
|
|
records = [
|
|
Data.model_validate({"date": "2023-01-01", "close": 1.0, "provider": "x"}),
|
|
Data.model_validate({"date": "2023-01-02", "close": 2.0, "provider": "x"}),
|
|
]
|
|
data_df, _ = Charting(equity_obbject)._prepare_data_as_df(records)
|
|
assert "provider" not in data_df.columns
|
|
|
|
|
|
class TestCreateCharts:
|
|
"""Tests for the generic chart constructors."""
|
|
|
|
def test_create_line_chart_returns_figure(self, equity_obbject):
|
|
"""``create_line_chart`` returns an ``OpenBBFigure`` when not rendering."""
|
|
frame = _make_ohlcv()
|
|
fig = Charting(equity_obbject).create_line_chart(frame, render=False)
|
|
assert isinstance(fig, OpenBBFigure)
|
|
|
|
def test_create_line_chart_render_dispatches(self, equity_obbject):
|
|
"""``create_line_chart`` with render dispatches through the GUI boundary."""
|
|
charting = Charting(equity_obbject)
|
|
charting._backend._app = RecorderApp()
|
|
frame = pd.DataFrame({"a": np.arange(10.0), "b": np.arange(10.0)})
|
|
charting.create_line_chart(frame, render=True)
|
|
assert "show_plotly" in charting._backend._app.calls
|
|
|
|
def test_create_bar_chart_returns_figure(self, equity_obbject):
|
|
"""``create_bar_chart`` returns an ``OpenBBFigure`` when not rendering."""
|
|
frame = pd.DataFrame(
|
|
{"label": ["a", "b", "c"], "value": [1, 2, 3], "other": [3, 2, 1]}
|
|
)
|
|
fig = Charting(equity_obbject).create_bar_chart(
|
|
frame, x="label", y=["value", "other"], render=False
|
|
)
|
|
assert isinstance(fig, OpenBBFigure)
|
|
|
|
def test_create_bar_chart_render_dispatches(self, equity_obbject):
|
|
"""``create_bar_chart`` with render dispatches through the GUI boundary."""
|
|
charting = Charting(equity_obbject)
|
|
charting._backend._app = RecorderApp()
|
|
frame = pd.DataFrame({"label": ["a", "b"], "value": [1, 2]})
|
|
charting.create_bar_chart(frame, x="label", y="value", render=True)
|
|
assert "show_plotly" in charting._backend._app.calls
|
|
|
|
def test_create_3d_surface(self, equity_obbject):
|
|
"""``create_3d_surface`` returns an ``OpenBBFigure``."""
|
|
x = pd.Series([1, 2, 3, 1, 2, 3])
|
|
y = pd.Series([1, 1, 1, 2, 2, 2])
|
|
z = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
|
|
fig = Charting(equity_obbject).create_3d_surface(x, y, z)
|
|
assert isinstance(fig, OpenBBFigure)
|
|
|
|
def test_create_correlation_matrix(self, equity_obbject):
|
|
"""``create_correlation_matrix`` returns an ``OpenBBFigure``."""
|
|
rng = np.random.default_rng(3)
|
|
frame = pd.DataFrame(
|
|
{
|
|
"a": rng.normal(0, 1, 30),
|
|
"b": rng.normal(0, 1, 30),
|
|
"c": rng.normal(0, 1, 30),
|
|
}
|
|
)
|
|
fig = Charting(equity_obbject).create_correlation_matrix(frame)
|
|
assert isinstance(fig, OpenBBFigure)
|
|
|
|
|
|
class TestNormalizeChartResponse:
|
|
"""Tests for ``_normalize_chart_response`` branch coverage."""
|
|
|
|
@pytest.fixture
|
|
def charting(self, equity_obbject) -> Charting:
|
|
"""A constructed ``Charting`` instance."""
|
|
return Charting(equity_obbject)
|
|
|
|
@pytest.fixture
|
|
def figure(self) -> OpenBBFigure:
|
|
"""A simple populated ``OpenBBFigure``."""
|
|
fig = OpenBBFigure()
|
|
fig.add_scatter(x=[1, 2, 3], y=[1, 2, 3])
|
|
return fig
|
|
|
|
def test_chart_passthrough(self, charting, figure):
|
|
"""A ``Chart`` instance is returned as-is."""
|
|
chart = Chart(fig=figure, content=None, format="plotly")
|
|
_, result = charting._normalize_chart_response(chart, {})
|
|
assert result is chart
|
|
|
|
def test_openbbfigure_serialized(self, charting, figure):
|
|
"""An ``OpenBBFigure`` is serialized into a plotly ``Chart``."""
|
|
fig, chart = charting._normalize_chart_response(figure, {})
|
|
assert fig is figure
|
|
assert chart.format == "plotly"
|
|
assert chart.content is not None
|
|
|
|
def test_tuple_with_openbbfigure(self, charting, figure):
|
|
"""A ``(OpenBBFigure, content)`` tuple is serialized to a plotly ``Chart``."""
|
|
_, chart = charting._normalize_chart_response((figure, {"x": 1}), {})
|
|
assert chart.format == "plotly"
|
|
|
|
def test_tuple_with_plain_figure(self, charting):
|
|
"""A ``(non-figure, content)`` tuple keeps the supplied content."""
|
|
_, chart = charting._normalize_chart_response(("not a fig", {"x": 1}), {})
|
|
assert chart.format == "str"
|
|
assert chart.content == {"x": 1}
|
|
|
|
def test_unknown_response(self, charting):
|
|
"""An unrecognized response yields an ``unknown`` format ``Chart``."""
|
|
_, chart = charting._normalize_chart_response("nonsense", {})
|
|
assert chart.format == "unknown"
|
|
|
|
|
|
class TestShow:
|
|
"""Tests for the ``show`` method."""
|
|
|
|
def test_show_resolves_real_view(self, equity_obbject):
|
|
"""``show`` resolves the registered view and stores a plotly chart."""
|
|
charting = Charting(equity_obbject)
|
|
charting.show(render=False)
|
|
assert equity_obbject.chart is not None
|
|
assert equity_obbject.chart.format == "plotly"
|
|
|
|
def test_show_render_dispatches(self, equity_obbject):
|
|
"""``show`` with render dispatches the figure through the GUI boundary."""
|
|
charting = Charting(equity_obbject)
|
|
charting._backend._app = RecorderApp()
|
|
charting.show(render=True)
|
|
assert "show_plotly" in charting._backend._app.calls
|
|
|
|
def test_show_applies_extra_params(self, equity_obbject):
|
|
"""``show`` merges OBBject extra params into the supplied extra_params."""
|
|
equity_obbject._extra_params = {"target": "close"}
|
|
charting = Charting(equity_obbject)
|
|
charting.show(render=False, extra_params={})
|
|
assert equity_obbject.chart is not None
|
|
|
|
def test_show_generic_fallback_for_unknown_route(self):
|
|
"""A route without a view falls back to the generic line chart."""
|
|
obj = _obbject("/made/up/route")
|
|
charting = Charting(obj)
|
|
charting.show(render=False)
|
|
assert obj.chart is not None
|
|
assert obj.chart.format == "plotly"
|
|
|
|
def test_show_reraises_runtime_error(self):
|
|
"""A ``RuntimeError`` raised by the view is re-raised unchanged."""
|
|
obj = _obbject("/raise/runtime")
|
|
charting = Charting(obj)
|
|
with pytest.raises(RuntimeError, match="does not support test"):
|
|
charting.show(render=False)
|
|
|
|
def test_show_fallback_failure_raises_runtime_error(self):
|
|
"""When even the generic fallback fails, a ``RuntimeError`` is raised."""
|
|
obj = _obbject("/made/up/route", results="not-a-time-series")
|
|
charting = Charting(obj)
|
|
with pytest.raises(RuntimeError, match="Failed to automatically create"):
|
|
charting.show(render=False)
|
|
|
|
def test_show_generic_fallback_render_dispatches(self):
|
|
"""The generic fallback with render dispatches through the GUI boundary."""
|
|
obj = _obbject("/made/up/route")
|
|
charting = Charting(obj)
|
|
charting._backend._app = RecorderApp()
|
|
charting.show(render=True)
|
|
assert "show_plotly" in charting._backend._app.calls
|
|
|
|
|
|
class TestHookContext:
|
|
"""Tests for hook-context construction."""
|
|
|
|
def test_make_hook_context_applies_overrides(self, equity_obbject):
|
|
"""Overrides supplied to ``_make_hook_context`` are set on the context."""
|
|
from openbb_core.app.charting import ChartLifecycle
|
|
|
|
charting = Charting(equity_obbject)
|
|
context = charting._make_hook_context(
|
|
ChartLifecycle.PRE_FIGURE, figure="FIG", content="CONTENT"
|
|
)
|
|
assert context.figure == "FIG"
|
|
assert context.content == "CONTENT"
|
|
|
|
|
|
class TestToChart:
|
|
"""Tests for the ``to_chart`` method."""
|
|
|
|
def test_to_chart_without_data(self, equity_obbject):
|
|
"""``to_chart`` with no data uses the OBBject contents."""
|
|
charting = Charting(equity_obbject)
|
|
charting.to_chart(render=False)
|
|
assert equity_obbject.chart is not None
|
|
assert equity_obbject.chart.format == "plotly"
|
|
|
|
def test_to_chart_with_external_data(self, equity_obbject):
|
|
"""``to_chart`` honors external data and a target column."""
|
|
charting = Charting(equity_obbject)
|
|
charting.to_chart(data=_make_ohlcv(), target="close", render=False)
|
|
assert equity_obbject.chart is not None
|
|
|
|
def test_to_chart_render_dispatches(self, equity_obbject):
|
|
"""``to_chart`` with render dispatches through the GUI boundary."""
|
|
charting = Charting(equity_obbject)
|
|
charting._backend._app = RecorderApp()
|
|
charting.to_chart(render=True)
|
|
assert "show_plotly" in charting._backend._app.calls
|
|
|
|
def test_to_chart_fallback(self):
|
|
"""``to_chart`` falls back to a generic line chart for a viewless route."""
|
|
obj = _obbject("/made/up/route")
|
|
charting = Charting(obj)
|
|
charting.to_chart(render=False)
|
|
assert obj.chart is not None
|
|
|
|
def test_to_chart_fallback_when_show_raises(self):
|
|
"""``to_chart`` recovers via its own generic chart when ``show`` raises."""
|
|
obj = _obbject("/raise/runtime")
|
|
charting = Charting(obj)
|
|
charting.to_chart(render=False)
|
|
assert obj.chart is not None
|
|
assert obj.chart.format == "plotly"
|
|
|
|
def test_to_chart_fallback_render_dispatches(self):
|
|
"""``to_chart`` fallback with render dispatches through the GUI boundary."""
|
|
obj = _obbject("/raise/runtime")
|
|
charting = Charting(obj)
|
|
charting._backend._app = RecorderApp()
|
|
charting.to_chart(render=True)
|
|
assert "show_plotly" in charting._backend._app.calls
|
|
|
|
def test_to_chart_fallback_failure_raises_runtime_error(self):
|
|
"""``to_chart`` raises a ``RuntimeError`` when its fallback also fails."""
|
|
obj = _obbject("/raise/runtime")
|
|
charting = Charting(obj)
|
|
with pytest.raises(RuntimeError, match="Failed to automatically create"):
|
|
charting.to_chart(
|
|
data=pd.DataFrame({"label": ["x", "y", "z"]}), render=False
|
|
)
|
|
|
|
|
|
class TestTable:
|
|
"""Tests for the ``table`` method."""
|
|
|
|
def test_table_with_query_toolbar(self, equity_obbject):
|
|
"""``table`` dispatches with the query toolbar enabled by default."""
|
|
charting = Charting(equity_obbject)
|
|
recorder = RecorderBackend()
|
|
charting._backend = recorder
|
|
charting.table()
|
|
assert len(recorder.tables) == 1
|
|
assert "include_query_toolbar" not in recorder.tables[0]
|
|
|
|
def test_table_without_query_toolbar(self, equity_obbject):
|
|
"""``table`` forwards the disabled query-toolbar flag."""
|
|
charting = Charting(equity_obbject)
|
|
recorder = RecorderBackend()
|
|
charting._backend = recorder
|
|
charting.table(include_query_toolbar=False)
|
|
assert recorder.tables[0]["include_query_toolbar"] is False
|
|
|
|
def test_table_with_named_index_and_title(self, equity_obbject):
|
|
"""``table`` resets a named index and uses the explicit title."""
|
|
charting = Charting(equity_obbject)
|
|
recorder = RecorderBackend()
|
|
charting._backend = recorder
|
|
charting.table(data=_make_ohlcv(), title="My Title")
|
|
assert recorder.tables[0]["title"] == "My Title"
|
|
|
|
def test_table_with_range_index(self, equity_obbject):
|
|
"""``table`` handles a default ``RangeIndex`` DataFrame."""
|
|
charting = Charting(equity_obbject)
|
|
recorder = RecorderBackend()
|
|
charting._backend = recorder
|
|
charting.table(data=pd.DataFrame({"a": [1, 2, 3]}))
|
|
assert len(recorder.tables) == 1
|
|
|
|
def test_table_warns_on_backend_failure(self, equity_obbject):
|
|
"""``table`` warns when the backend dispatch raises."""
|
|
charting = Charting(equity_obbject)
|
|
charting._backend = RaisingBackend()
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
charting.table()
|
|
assert any("Failed to show table" in str(w.message) for w in caught)
|
|
|
|
|
|
class TestUrl:
|
|
"""Tests for the ``url`` method."""
|
|
|
|
def test_url_delegates_to_backend(self, equity_obbject):
|
|
"""``url`` forwards its arguments to the backend dispatch."""
|
|
charting = Charting(equity_obbject)
|
|
recorder = RecorderBackend()
|
|
charting._backend = recorder
|
|
charting.url("https://example.com", title="Docs", width=100, height=200)
|
|
assert recorder.urls[0]["url"] == "https://example.com"
|
|
assert recorder.urls[0]["width"] == 100
|
|
|
|
def test_url_warns_on_backend_failure(self, equity_obbject):
|
|
"""``url`` warns when the backend dispatch raises."""
|
|
charting = Charting(equity_obbject)
|
|
charting._backend = RaisingBackend()
|
|
with warnings.catch_warnings(record=True) as caught:
|
|
warnings.simplefilter("always")
|
|
charting.url("https://example.com")
|
|
assert any("Failed to show figure" in str(w.message) for w in caught)
|
|
|
|
|
|
class TestToggleAndStyle:
|
|
"""Tests for chart-style helpers."""
|
|
|
|
def test_toggle_chart_style_without_chart_raises(self, equity_obbject):
|
|
"""Toggling style with no chart present raises a ``ValueError``."""
|
|
charting = Charting(equity_obbject)
|
|
with pytest.raises(ValueError, match="No chart has been created"):
|
|
charting.toggle_chart_style()
|
|
|
|
def test_toggle_chart_style_flips_style(self, equity_obbject):
|
|
"""Toggling style flips the configured chart style and updates the chart."""
|
|
charting = Charting(equity_obbject)
|
|
charting.show(render=False)
|
|
before = charting._charting_settings.chart_style
|
|
charting.toggle_chart_style()
|
|
assert charting._charting_settings.chart_style != before
|
|
assert equity_obbject.chart.content is not None
|
|
|
|
def test_set_chart_style_is_passthrough(self, equity_obbject):
|
|
"""``_set_chart_style`` returns the figure unchanged."""
|
|
charting = Charting(equity_obbject)
|
|
fig = OpenBBFigure()
|
|
assert charting._set_chart_style(fig) is fig
|
|
|
|
|
|
class TestConvertToString:
|
|
"""Tests for the static ``_convert_to_string`` sanitizer."""
|
|
|
|
def test_finite_number_passes_through(self):
|
|
"""A finite number is returned unchanged."""
|
|
assert Charting._convert_to_string(1.5) == 1.5
|
|
|
|
def test_nan_becomes_empty_string(self):
|
|
"""A NaN float renders as an empty string."""
|
|
assert Charting._convert_to_string(float("nan")) == ""
|
|
|
|
def test_dict_joins_values(self):
|
|
"""A dict is rendered as a comma-joined list of its values."""
|
|
assert Charting._convert_to_string({"a": 1, "b": 2}) == "1, 2"
|
|
|
|
def test_list_of_dicts_joins_values(self):
|
|
"""A list of dicts joins each dict's values."""
|
|
assert Charting._convert_to_string([{"a": 1}, {"b": 2}]) == "1, 2"
|
|
|
|
def test_plain_list_joins_items(self):
|
|
"""A plain list joins its items with commas."""
|
|
assert Charting._convert_to_string([1, 2, 3]) == "1, 2, 3"
|
|
|
|
def test_string_strips_brackets(self):
|
|
"""A string value strips bracket characters."""
|
|
assert Charting._convert_to_string("[hello]") == "hello"
|