"""Unit tests for ``openbb_sec.utils.xbrl_render``."""
import pytest
from openbb_sec.utils.xbrl_render import (
_clean_member,
_dims_str,
_format_value,
_header_line,
_is_text_block,
_looks_xbrl,
_period_label,
_period_sort_key,
_strip_prefix,
render_xbrl_facts,
)
_XBRL_BYTES = b"data"
_TEXT_BLOCK_HTML = "
"
def _facts():
"""A representative ``parse_instance`` facts mapping."""
return {
"us-gaap:Revenues": [
{
"start": "2023-01-01",
"end": "2023-12-31",
"period_type": "duration",
"label": "Revenues",
"tag": "us-gaap:Revenues",
"dimensions": {},
"value": 1000,
"unit": "usd",
"presentation": [{"table": "Statements Of Operations", "order": 2.0}],
},
{
"start": "2022-01-01",
"end": "2022-12-31",
"period_type": "duration",
"label": "Revenues",
"tag": "us-gaap:Revenues",
"dimensions": {},
"value": 900.5,
"unit": "usd",
"presentation": [{"table": "Statements Of Operations", "order": 2.0}],
},
],
"us-gaap:GrossProfit": [
{
"start": "2023-01-01",
"end": "2023-12-31",
"period_type": "duration",
"label": "Gross Profit",
"tag": "us-gaap:GrossProfit",
"dimensions": {"axis1": {"label": "Segment A [Member]"}},
"value": 500,
"unit": "usd",
"presentation": [{"table": "Statements Of Operations", "order": 1.0}],
},
],
"us-gaap:SharesOutstanding": [
{
"start": "2023-01-01",
"end": "2023-12-31",
"period_type": "duration",
"label": "Shares",
"tag": "us-gaap:SharesOutstanding",
"dimensions": {},
"value": 42,
"unit": None,
"presentation": [{"table": "Statements Of Operations", "order": 3.0}],
},
],
"dei:EntityRegistrantName": [
{
"end": "2023-12-31",
"period_type": "instant",
"label": "Entity Registrant Name",
"tag": "dei:EntityRegistrantName",
"dimensions": {},
"value": "Apple Inc",
"unit": None,
"presentation": [{"table": "Cover", "order": 1.0}],
},
],
"dei:DocumentType": [
{
"end": "2023-12-31",
"period_type": "instant",
"label": "Document Type",
"tag": "dei:DocumentType",
"dimensions": {},
"value": "10-K",
"unit": None,
"presentation": [{"table": "Cover", "order": 2.0}],
},
],
"dei:DocumentPeriodEndDate": [
{
"end": "2023-12-31",
"period_type": "instant",
"label": "Document Period End Date",
"tag": "dei:DocumentPeriodEndDate",
"dimensions": {},
"value": "2023-12-31",
"unit": None,
"presentation": [{"table": "Cover", "order": 3.0}],
},
],
"us-gaap:PoliciesTextBlock": [
{
"end": "2023-12-31",
"period_type": "instant",
"label": "Policies",
"tag": "us-gaap:SignificantAccountingPoliciesTextBlock",
"dimensions": {},
"value": _TEXT_BLOCK_HTML,
"unit": None,
"presentation": [{"table": "Notes", "order": 1.0}],
},
{
"end": "2023-12-31",
"period_type": "instant",
"label": "Policies",
"tag": "us-gaap:SignificantAccountingPoliciesTextBlock",
"dimensions": {},
"value": _TEXT_BLOCK_HTML,
"unit": None,
"presentation": [{"table": "Notes", "order": 1.0}],
},
{
"end": "2023-12-31",
"period_type": "instant",
"label": "Empty",
"tag": "us-gaap:EmptyTextBlock",
"dimensions": {},
"value": "",
"unit": None,
"presentation": [{"table": "Notes", "order": 2.0}],
},
],
}
def _patch_parser(
monkeypatch,
*,
contexts=None,
units=None,
facts=None,
exc=None,
linkbase_errors=None,
):
"""Replace ``XBRLParser`` with a fake returning fixed parse_instance output."""
class _FakeXBRLParser:
def __init__(self):
self.linkbase_errors = linkbase_errors or {}
def parse_instance(self, stream, base_url=None):
if exc is not None:
raise exc
return (contexts or {}, units or {}, facts if facts is not None else {})
monkeypatch.setattr(
"openbb_sec.utils.xbrl_taxonomy_helper.XBRLParser", _FakeXBRLParser
)
class TestStringHelpers:
"""Prefix-stripping and dimension-member cleaning."""
def test_strip_prefix(self):
assert _strip_prefix("us-gaap:Assets") == "Assets"
assert _strip_prefix("Assets") == "Assets"
assert _strip_prefix("") == ""
assert _strip_prefix(None) == ""
def test_clean_member_strips_member_suffixes(self):
assert _clean_member("us-gaap:SegmentA [Member]") == "Segment A"
assert _clean_member("FooBarMember") == "Foo Bar"
assert _clean_member("prefix:Plain") == "Plain"
def test_dims_str(self):
assert _dims_str(None) == ""
assert _dims_str({}) == ""
assert _dims_str({"a": {"label": "X [Member]"}}) == "X"
assert _dims_str({"a": {"member": "ns:BetaMember"}}) == "Beta"
assert _dims_str({"a": {}}) == ""
assert _dims_str({"a": "ns:ZetaMember"}) == "Zeta"
assert _dims_str({"a": {"label": "Alpha"}, "b": "Beta"}) == "Alpha; Beta"
class TestPeriodHelpers:
"""Period labels and sort keys."""
def test_period_label_instant(self):
assert (
_period_label({"period_type": "instant", "start": "", "end": "2023-12-31"})
== "2023-12-31"
)
def test_period_label_duration_range(self):
assert (
_period_label(
{
"period_type": "duration",
"start": "2023-01-01",
"end": "2023-12-31",
}
)
== "2023-01-01 – 2023-12-31"
)
def test_period_label_duration_without_start(self):
assert (
_period_label({"period_type": "duration", "start": "", "end": "2023-12-31"})
== "2023-12-31"
)
def test_period_sort_key(self):
assert _period_sort_key("2023-01-01 – 2023-12-31") == "2023-12-31"
assert _period_sort_key("short") == "short"
class TestFactClassifiers:
"""Text-block detection and value formatting."""
def test_is_text_block(self):
assert _is_text_block({"tag": "us-gaap:FooTextBlock", "value": 1}) is True
assert _is_text_block({"tag": "us-gaap:Foo", "value": " x"}) is True
assert _is_text_block({"tag": "us-gaap:Foo", "value": 1000}) is False
assert _is_text_block({"tag": "us-gaap:Foo", "value": "plain"}) is False
def test_format_value(self):
assert _format_value(None) == ""
assert _format_value("") == ""
assert _format_value(1000) == "1,000"
assert _format_value(1000.0) == "1,000"
assert _format_value(1234.5) == "1,234.5"
assert _format_value("n/a") == "n/a"
def test_header_line(self):
assert _header_line(_facts()) == "Apple Inc · 10-K · 2023-12-31"
assert _header_line({}) == ""
class TestRenderXbrlFacts:
"""End-to-end rendering of an XBRL instance via a patched parser."""
def test_renders_statements_cover_and_notes(self, monkeypatch):
_patch_parser(monkeypatch, units={"usd": "USD"}, facts=_facts())
html = render_xbrl_facts(_XBRL_BYTES, source_url="https://www.sec.gov/d/x.xml")
assert html is not None
assert "
XBRL Facts
" in html
assert "Apple Inc · 10-K · 2023-12-31" in html
assert "1,000" in html and "900.5" in html
assert "Gross Profit — Segment A" in html
assert "USD" in html
# Cover is ordered ahead of the other statement tables.
assert html.index('data-name="Cover"') < html.index(
'data-name="Statements Of Operations"'
)
# The duplicated text block is rendered exactly once.
assert html.count(_TEXT_BLOCK_HTML) == 1
def test_truncated_preview_with_source_url(self, monkeypatch):
_patch_parser(monkeypatch, units={"usd": "USD"}, facts=_facts())
html = render_xbrl_facts(
_XBRL_BYTES, source_url="https://www.sec.gov/d/x.xml", truncated=True
)
assert "open the full document" in html
assert "https://www.sec.gov/d/x.xml" in html
def test_truncated_preview_without_source_url(self, monkeypatch):
_patch_parser(monkeypatch, units={"usd": "USD"}, facts=_facts())
html = render_xbrl_facts(_XBRL_BYTES, truncated=True)
assert "Showing a preview of a large file." in html
def test_not_xbrl_returns_none(self):
assert render_xbrl_facts(b"not xbrl") is None
def test_parse_error_returns_none(self, monkeypatch):
_patch_parser(monkeypatch, exc=ValueError("bad"))
assert render_xbrl_facts(_XBRL_BYTES) is None
def test_no_facts_returns_none(self, monkeypatch):
_patch_parser(monkeypatch, facts={})
assert render_xbrl_facts(_XBRL_BYTES) is None
def test_no_presentation_tables_returns_none(self, monkeypatch):
facts = {
"us-gaap:Revenues": [
{
"end": "2023-12-31",
"period_type": "instant",
"label": "Revenues",
"tag": "us-gaap:Revenues",
"dimensions": {},
"value": 1,
"unit": None,
"presentation": None,
}
]
}
_patch_parser(monkeypatch, facts=facts)
assert render_xbrl_facts(_XBRL_BYTES) is None
class TestUnavailablePresentation:
"""A presentation document that failed to load is reported, not hidden."""
_FACTS = {
"us-gaap:Revenues": [
{
"end": "2023-12-31",
"period_type": "instant",
"label": "Revenues",
"tag": "us-gaap:Revenues",
"dimensions": {},
"value": 1,
"unit": None,
"presentation": None,
}
]
}
@pytest.mark.parametrize("kind", ["presentation", "schema"])
def test_failed_document_is_explained(self, monkeypatch, kind):
_patch_parser(
monkeypatch,
facts=self._FACTS,
linkbase_errors={kind: "HTTPError: 429 "},
)
html = render_xbrl_facts(_XBRL_BYTES, source_url="https://www.sec.gov/d/x.xml")
assert f"filing's {kind} document could not be loaded" in html
assert "HTTPError: 429 <Too Many>" in html
assert 'href="https://www.sec.gov/d/x.xml"' in html
def test_failed_document_without_source_url(self, monkeypatch):
_patch_parser(
monkeypatch,
facts=self._FACTS,
linkbase_errors={"presentation": "Timeout: slow"},
)
html = render_xbrl_facts(_XBRL_BYTES)
assert "could not be loaded (Timeout: slow)." in html
assert "Open the XBRL document" not in html
def test_label_failure_alone_is_not_reported(self, monkeypatch):
_patch_parser(
monkeypatch,
facts=self._FACTS,
linkbase_errors={"label": "Timeout: slow"},
)
assert render_xbrl_facts(_XBRL_BYTES) is None
def test_looks_xbrl():
"""``_looks_xbrl`` matches the instance namespace or root tag."""
assert _looks_xbrl(b"") is True
assert (
_looks_xbrl(b"") is True
)
assert _looks_xbrl(b"") is False