"""Unit tests for DataService.""" import pandas as pd import pytest from myfasthtml.core.constants import ColumnType from myfasthtml.core.data.ColumnDefinition import ColumnDefinition class TestDataInitialisation: """Tests for the Data initialisation section of DataService.""" @pytest.fixture def service(self, dsm): return dsm.create_service("ns.tbl", save_state=False) def test_i_can_load_a_dataframe(self, service): """load_dataframe() populates the store and column definitions.""" df = pd.DataFrame({"name": ["Alice", "Bob"], "age": [30, 25]}) service.load_dataframe(df) assert service.get_store().ne_df is not None assert service.get_store().ns_total_rows == 2 assert len(service.columns) == 2 def test_i_can_load_an_empty_dataframe(self, service): """load_dataframe() with empty DataFrame sets total_rows to 0.""" service.load_dataframe(pd.DataFrame()) assert service.get_store().ns_total_rows == 0 assert service.columns == [] def test_i_can_load_dataframe_without_reinitializing_columns(self, service): """load_dataframe(init_columns=False) preserves existing column definitions.""" df = pd.DataFrame({"a": [1]}) service.load_dataframe(df) original_columns = list(service.columns) df2 = pd.DataFrame({"a": [1, 2], "b": [3, 4]}) service.load_dataframe(df2, init_columns=False) assert service.columns == original_columns def test_i_can_load_none_dataframe_without_error(self, service): """load_dataframe(None) is a no-op and does not raise. Why this matters: - Early return on None protects against uninitialized callers. - ne_df must remain None (no side effects on the store). """ service.load_dataframe(None) assert service.get_store().ne_df is None def test_i_can_load_dataframe_with_column_name_normalization(self, service): """load_dataframe() normalizes column names to safe IDs via make_safe_id. Why this matters: - Columns with spaces or special characters must be accessible as safe IDs. - make_safe_id lowercases and replaces non-safe characters with underscores. """ df = pd.DataFrame({"First Name": ["Alice"], "Last Name": ["Smith"]}) service.load_dataframe(df) col_ids = [c.col_id for c in service.columns] assert col_ids == ["first_name", "last_name"] class TestMutations: """Tests for the Mutations section of DataService.""" @pytest.fixture def service(self, dsm): svc = dsm.create_service("ns.mutations", save_state=False) svc.load_dataframe(pd.DataFrame({"value": [1, 2, 3]})) return svc def test_i_can_add_a_row(self, service): """add_row() appends a row with default values and updates the caches.""" service.add_row() assert service.get_store().ns_total_rows == 4 assert len(service.get_store().ne_df) == 4 def test_i_can_add_a_row_with_custom_data(self, service): """add_row() with explicit data stores the provided values.""" service.add_row(row_data={"value": 99}) assert service.get_store().ne_df.iloc[-1]["value"] == 99 def test_i_can_set_data(self, service): """set_data() updates the cell in the DataFrame, fast-access cache, and row data.""" service.set_data("value", 1, 99) assert service.get_store().ne_df.at[1, "value"] == 99 assert service.get_store().ns_fast_access["value"][1] == 99 assert service.get_store().ns_row_data[1]["value"] == 99 @pytest.mark.parametrize("col_type, expected_default", [ (ColumnType.Text, ""), (ColumnType.Number, 0), (ColumnType.Bool, False), (ColumnType.Datetime, pd.NaT), (ColumnType.Choice, ""), (ColumnType.Enum, ""), (ColumnType.RowSelection_, ""), ]) def test_i_can_add_column_with_correct_default_value(self, service, col_type, expected_default): """add_column() creates a DataFrame column with the type-appropriate default value. Why these assertions matter: - col_id in ne_df.columns: Confirms the column is materialized in the DataFrame. - len(columns) == 2: Confirms the column is registered in the metadata. - default value: Each type has a specific sentinel value; wrong defaults corrupt data. - pd.isna() for Datetime: pd.NaT does not support equality comparison. """ col_def = ColumnDefinition(col_id="__new__", col_index=-1, title="New Col", type=col_type) service.add_column(col_def) assert col_def.col_id in service.get_store().ne_df.columns assert len(service.columns) == 2 actual = service.get_store().ne_df[col_def.col_id].iloc[0] if pd.isna(expected_default): assert pd.isna(actual) else: assert actual == expected_default @pytest.mark.parametrize("col_type", [ColumnType.Formula, ColumnType.RowIndex]) def test_i_can_add_virtual_column_without_dataframe_column(self, service, col_type): """add_column() with virtual types does not create a DataFrame column. Why these assertions matter: - col_id not in ne_df.columns: Virtual columns are computed, not stored in the DataFrame. - col_index == -1: Sentinel value marking virtual columns. - len(columns) == 2: Column is registered in the state metadata despite being virtual. """ col_def = ColumnDefinition(col_id="__new__", col_index=-1, title="Virtual", type=col_type) service.add_column(col_def) assert col_def.col_id not in service.get_store().ne_df.columns assert col_def.col_index == -1 assert len(service.columns) == 2 def test_i_can_add_row_without_loaded_dataframe_without_error(self, dsm): """add_row() is a no-op and does not raise when no DataFrame is loaded.""" service = dsm.create_service("ns.nodf_row", save_state=False) service.add_row() assert service.get_store().ne_df is None def test_i_can_set_data_without_loaded_dataframe_without_error(self, dsm): """set_data() is a no-op and does not raise when no DataFrame is loaded.""" service = dsm.create_service("ns.nodf_set", save_state=False) service.set_data("x", 0, 42) assert service.get_store().ne_df is None class TestFormulaManagement: """Tests for the Formula management section of DataService.""" @pytest.fixture def service(self, dsm): svc = dsm.create_service("ns.formula", save_state=False) svc.load_dataframe(pd.DataFrame({"a": [1, 2, 3]})) return svc def test_i_can_get_table_name(self, service): """table_name property returns the value set at creation.""" assert service.table_name == "ns.formula" def test_i_can_update_table_name(self, service): """set_table_name() updates the table name.""" service.set_table_name("ns.new_name") assert service.table_name == "ns.new_name" def test_i_can_register_formula(self, service): """register_formula() registers a formula in the shared FormulaEngine. Why these assertions matter: - has_formula: Confirms the formula was registered in the engine's DAG. - get_formula_text: Confirms the source expression is stored as-is. """ service.register_formula("computed", "{a} + 1") engine = service.get_formula_engine() assert engine.has_formula("ns.formula", "computed") assert engine.get_formula_text("ns.formula", "computed") == "{a} + 1" def test_i_can_remove_formula(self, service): """remove_formula() unregisters a formula from the FormulaEngine.""" service.register_formula("computed", "{a} + 1") service.remove_formula("computed") engine = service.get_formula_engine() assert not engine.has_formula("ns.formula", "computed") def test_i_cannot_register_invalid_formula(self, service): """register_formula() with invalid DSL syntax does not register the formula. Why this matters: - parse_formula() raises DSLSyntaxError when it cannot parse the expression. - register_formula() catches the exception to protect the caller, but the formula must remain absent from the engine — not silently removed. """ service.register_formula("computed", "invalid syntax without braces") engine = service.get_formula_engine() assert not engine.has_formula("ns.formula", "computed")