Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
595 changes: 595 additions & 0 deletions notebooks/how_to/tests/custom_tests/style_custom_test_tables.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "validmind"
version = "2.13.14"
version = "2.14.0"
description = "ValidMind Library"
readme = "README.pypi.md"
requires-python = ">=3.9,<3.15"
Expand Down
2 changes: 1 addition & 1 deletion r/validmind/DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: validmind
Type: Package
Title: Interface to the 'ValidMind' Platform
Version: 2.13.14
Version: 2.14.0
Authors@R: c(person("Andres", "Rodriguez", role = c("aut", "cre","cph"),
email = "andres@validmind.ai"))
Maintainer: Andres Rodriguez <andres@validmind.ai>
Expand Down
2 changes: 2 additions & 0 deletions scripts/run_e2e_notebooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- notebooks/use_cases/regression/quickstart_regression_full_suite.ipynb
- notebooks/how_to/tests/custom_tests/integrate_external_test_providers.ipynb
- notebooks/how_to/tests/custom_tests/implement_custom_tests.ipynb
- notebooks/how_to/tests/custom_tests/style_custom_test_tables.ipynb

To add more notebooks to the list, simply add the path to the `NOTEBOOKS_TO_RUN` list.
This will use the default project id for the notebook. If you want to use a different
Expand Down Expand Up @@ -45,6 +46,7 @@
"notebooks/how_to/metrics/run_unit_metrics.ipynb",
"notebooks/how_to/tests/custom_tests/integrate_external_test_providers.ipynb",
"notebooks/how_to/tests/custom_tests/implement_custom_tests.ipynb",
"notebooks/how_to/tests/custom_tests/style_custom_test_tables.ipynb",
"notebooks/how_to/tests/explore_tests/explore_tests.ipynb",
]

Expand Down
131 changes: 118 additions & 13 deletions tests/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from validmind.vm_models.figure import Figure
from validmind.errors import InvalidParameterError
from validmind.tests.output import TableOutputHandler
from validmind.tests.run import run_test
from validmind.utils import md_to_html

Expand Down Expand Up @@ -55,7 +56,7 @@ def run_async(self, func, *args, **kwargs):

def test_raw_data_initialization(self):
"""Test RawData initialization and methods"""
raw_data = RawData(log=True, dataset_duplicates=pd.DataFrame({'col1': [1, 2]}))
raw_data = RawData(log=True, dataset_duplicates=pd.DataFrame({"col1": [1, 2]}))

self.assertTrue(raw_data.log)
self.assertIsInstance(raw_data.dataset_duplicates, pd.DataFrame)
Expand Down Expand Up @@ -134,6 +135,88 @@ def test_test_result_add_table(self):
self.assertEqual(len(test_result.tables), 1)
self.assertEqual(test_result.tables[0].title, "Test Table")

def test_table_output_handler_plain_dataframe_is_unchanged(self):
"""Plain DataFrame tables serialize and render exactly as before Styler support."""
test_result = TestResult(result_id="test_1")
df = pd.DataFrame(
{"Check": ["Missing values", "Outliers"], "Score": [0.91234, 1.0]}
)

TableOutputHandler().process(df, test_result)

self.assertEqual(
test_result.tables[0].serialize()["data"],
[
{"Check": "Missing values", "Score": 0.9123},
{"Check": "Outliers", "Score": 1.0},
],
)
html = test_result.to_html()
self.assertIn(">Missing values<", html)
self.assertIn(">0.9123<", html)
self.assertNotIn("<div style=", html)

def test_table_output_handler_converts_pandas_styler(self):
"""Test pandas Styler table outputs preserve portable cell styles."""
test_result = TestResult(result_id="test_1")
df = pd.DataFrame(
{
"Check": ["Missing values", "Outlier rate"],
"Observed": [0.008, 0.027],
}
)
styler = df.style.format({"Observed": "{:.1%}"}).map(
lambda value: (
"background-color: #EAF4FF; color: #083E44; "
"font-weight: 600; text-align: center"
if isinstance(value, float)
else ""
)
)

TableOutputHandler().process(styler, test_result)

serialized_table = test_result.tables[0].serialize()["data"]
self.assertEqual(serialized_table[0]["Check"], "Missing values")
self.assertEqual(
serialized_table[0]["Observed"],
{
"value": "0.8%",
"bgcolor": "#EAF4FF",
"color": "#083E44",
"fontWeight": "600",
"textAlign": "center",
},
)
html = test_result.to_html()
self.assertIn("background-color: #EAF4FF", html)
self.assertIn("font-weight: 600", html)
self.assertNotIn("{'value': '0.8%'", html)

def test_table_output_handler_styler_keeps_rounding(self):
"""Styler tables keep the 4-decimal rounding, styled or not."""
test_result = TestResult(result_id="test_1")
df = pd.DataFrame(
{
"styled_col": [1.23456789, 2.3456789],
"untouched_col": [10.111111, 20.222222],
}
)
styler = df.style.map(
lambda value: "background-color: red" if value > 2 else "",
subset=["styled_col"],
)

TableOutputHandler().process(styler, test_result)

rows = test_result.tables[0].serialize()["data"]
self.assertEqual(rows[0], {"styled_col": 1.2346, "untouched_col": 10.1111})
self.assertEqual(rows[1]["untouched_col"], 20.2222)
self.assertEqual(rows[1]["styled_col"], {"value": 2.3457, "bgcolor": "red"})
html = test_result.to_html()
self.assertIn(">1.2346<", html)
self.assertIn("background-color: red", html)

def test_test_result_add_figure(self):
"""Test adding figures to TestResult"""
test_result = TestResult(result_id="test_1")
Expand Down Expand Up @@ -326,7 +409,9 @@ def test_text_generation_result_logs_markdown_source(
)

@patch("validmind.vm_models.result.result.api_client.alog_text")
async def test_text_generation_result_log_async_with_section_id(self, mock_log_text):
async def test_text_generation_result_log_async_with_section_id(
self, mock_log_text
):
"""Test async logging of TextGenerationResult forwards section_id"""
text_result = TextGenerationResult(
result_id="text_1",
Expand Down Expand Up @@ -387,7 +472,9 @@ async def test_text_generation_result_log_async_uses_ai_revision_name(
section_id=None,
)

async def test_text_generation_result_log_async_requires_section_id_for_new_block(self):
async def test_text_generation_result_log_async_requires_section_id_for_new_block(
self,
):
"""Test new generated text requires a section_id for placement"""
text_result = TextGenerationResult(
result_id="text_1",
Expand Down Expand Up @@ -579,26 +666,44 @@ def test_figure_interactive_toggle_plotly(self):
try:
html = figure.to_html()
self.assertIsInstance(html, str)
self.assertIn("vm-plotly-data", html, "Default should include plotly data")
self.assertIn(
"vm-plotly-data", html, "Default should include plotly data"
)
self.assertIn("vm-plotly-test_key", html)
finally:
if env_backup is not None:
os.environ["VALIDMIND_INTERACTIVE_FIGURES"] = env_backup
else:
with patch.dict(os.environ, {"VALIDMIND_INTERACTIVE_FIGURES": value}, clear=False):
with patch.dict(
os.environ, {"VALIDMIND_INTERACTIVE_FIGURES": value}, clear=False
):
html = figure.to_html()
self.assertIsInstance(html, str)
self.assertIn("vm-plotly-data", html, f"Should include plotly data for value: {value}")
self.assertIn(
"vm-plotly-data",
html,
f"Should include plotly data for value: {value}",
)
self.assertIn("vm-plotly-test_key", html)

# Test disabled values
disabled_values = ["false", "False", "FALSE", "0", "no", "No", "NO"]
for value in disabled_values:
with patch.dict(os.environ, {"VALIDMIND_INTERACTIVE_FIGURES": value}, clear=False):
with patch.dict(
os.environ, {"VALIDMIND_INTERACTIVE_FIGURES": value}, clear=False
):
html = figure.to_html()
self.assertIsInstance(html, str)
self.assertNotIn("vm-plotly-data", html, f"Should exclude plotly data for value: {value}")
self.assertNotIn("vm-plotly-test_key", html, f"Should exclude plotly container for value: {value}")
self.assertNotIn(
"vm-plotly-data",
html,
f"Should exclude plotly data for value: {value}",
)
self.assertNotIn(
"vm-plotly-test_key",
html,
f"Should exclude plotly container for value: {value}",
)
# Should still contain the static image
self.assertIn("data:image/png;base64", html)
self.assertIn("vm-img-test_key", html)
Expand All @@ -611,7 +716,9 @@ def test_figure_interactive_toggle_matplotlib_unaffected(self):

# Test that matplotlib figures never include plotly data regardless of setting
# Only need to test once since behavior is identical for all values
with patch.dict(os.environ, {"VALIDMIND_INTERACTIVE_FIGURES": "true"}, clear=False):
with patch.dict(
os.environ, {"VALIDMIND_INTERACTIVE_FIGURES": "true"}, clear=False
):
html = figure.to_html()
self.assertIsInstance(html, str)
self.assertNotIn("vm-plotly-data", html)
Expand All @@ -627,9 +734,7 @@ def test_figure_title_serializes_as_caption(self):
plotly_fig = go.Figure(data=go.Scatter(x=[1, 2, 3], y=[4, 5, 6]))

# With a title -> metadata.caption is set
titled = Figure(
key="k", figure=plotly_fig, ref_id="r1", title="My Cool Chart"
)
titled = Figure(key="k", figure=plotly_fig, ref_id="r1", title="My Cool Chart")
payload = titled.serialize()
meta = _json.loads(payload["metadata"])
self.assertEqual(meta["_ref_id"], "r1")
Expand Down
Loading
Loading