From 27f12b2c32dd0bf277bbe0785c6b65e5c94cfbc2 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Sun, 15 Feb 2026 18:32:34 +0100 Subject: [PATCH] Fixed Syntax validation and autocompletion. Fixed unit tests --- .../controls/DataGridColumnsManager.py | 70 +- src/myfasthtml/core/dsl/base_completion.py | 4 +- src/myfasthtml/core/dsl/exceptions.py | 50 ++ .../core/formatting/dsl/exceptions.py | 55 +- .../dsl/completion/FormulaCompletionEngine.py | 137 ++- src/myfasthtml/core/formula/dsl/exceptions.py | 21 +- src/myfasthtml/core/formula/dsl/parser.py | 6 +- src/myfasthtml/core/formula/engine.py | 4 +- src/myfasthtml/core/utils.py | 2 +- .../controls/test_datagrid_columns_manager.py | 802 ++++++++---------- tests/core/formula/test_formula_engine.py | 7 +- tests/core/formula/test_formula_parser.py | 10 +- tests/core/test_datagrid_registry.py | 4 +- 13 files changed, 532 insertions(+), 640 deletions(-) create mode 100644 src/myfasthtml/core/dsl/exceptions.py diff --git a/src/myfasthtml/controls/DataGridColumnsManager.py b/src/myfasthtml/controls/DataGridColumnsManager.py index 88b2b9f..9c3d437 100644 --- a/src/myfasthtml/controls/DataGridColumnsManager.py +++ b/src/myfasthtml/controls/DataGridColumnsManager.py @@ -10,6 +10,9 @@ from myfasthtml.controls.datagrid_objects import DataGridColumnState from myfasthtml.controls.helpers import icons, mk from myfasthtml.core.commands import Command from myfasthtml.core.constants import ColumnType +from myfasthtml.core.dsls import DslsManager +from myfasthtml.core.formula.dsl.completion.FormulaCompletionEngine import FormulaCompletionEngine +from myfasthtml.core.formula.dsl.parser import FormulaParser from myfasthtml.core.instances import MultipleInstance from myfasthtml.icons.fluent_p1 import chevron_right20_regular, chevron_left20_regular @@ -37,11 +40,11 @@ class Commands(BaseCommands): self._owner, self._owner.show_all_columns).htmx(target=f"#{self._id}", swap="innerHTML") - def update_column(self, col_id): + def save_column_details(self, col_id): return Command(f"UpdateColumn", f"Update column {col_id}", self._owner, - self._owner.update_column, + self._owner.save_column_details, kwargs={"col_id": col_id} ).htmx(target=f"#{self._id}", swap="innerHTML") @@ -64,41 +67,36 @@ class DataGridColumnsManager(MultipleInstance): self.commands = Commands(self) self._new_column = False - conf = DslEditorConf(save_button=False, placeholder="{Column} * {OtherColumn}", line_numbers=False) + completion_engine = FormulaCompletionEngine( + self._parent._parent, + self._parent.get_table_name(), + ) + conf = DslEditorConf(save_button=False, line_numbers=False, engine_id=completion_engine.get_id()) self._formula_editor = DataGridFormulaEditor(self, conf=conf, _id=f"{self._id}-formula-editor") + DslsManager.register(completion_engine, FormulaParser()) @property def columns(self): return self._parent.get_state().columns - def _get_col_def_from_col_id(self, col_id, updates=None, copy=True): + def _get_col_def_from_col_id(self, col_id, copy=True): """ - Retrieves a column definition from the column ID, with optional updates - made using information provided by the client response. - - :param col_id: The unique identifier of the column to retrieve. - :param updates: Optional dictionary containing updated values for - the column attributes provided by the client. If specified, the - attributes of the column definition will be updated accordingly. - :return: A copy of the column definition object with any updates applied, - or None if no column matching the provided ID is found. - :rtype: ColumnDefinition | None """ - if updates: - updates["visible"] = "visible" in updates and updates["visible"] == "on" - cols_defs = [c for c in self.columns if c.col_id == col_id] if not cols_defs: - col_def = DataGridColumnState(col_id, -1) - else: - col_def = cols_defs[0].copy() if copy else cols_defs[0] + return None - if updates: + return cols_defs[0].copy() if copy else cols_defs[0] + + def _get_updated_col_def_from_col_id(self, col_id, updates=None, copy=True): + col_def = self._get_col_def_from_col_id(col_id, copy=copy) + if col_def is None: + col_def = DataGridColumnState(col_id, -1) + + if updates is not None: + updates["visible"] = "visible" in updates and updates["visible"] == "on" for k, v in [(k, v) for k, v in updates.items() if hasattr(col_def, k)]: - - if k == "visible": - col_def.visible = v == "on" - elif k == "type": + if k == "type": col_def.type = ColumnType(v) elif k == "width": col_def.width = int(v) @@ -112,7 +110,7 @@ class DataGridColumnsManager(MultipleInstance): def toggle_column(self, col_id): logger.debug(f"toggle_column {col_id=}") - col_def = self._get_col_def_from_col_id(col_id) + col_def = self._get_col_def_from_col_id(col_id, copy=False) if col_def is None: logger.debug(f" column '{col_id}' is not found.") return Div(f"Column '{col_id}' not found") @@ -123,7 +121,7 @@ class DataGridColumnsManager(MultipleInstance): def show_column_details(self, col_id): logger.debug(f"show_column_details {col_id=}") - col_def = self._get_col_def_from_col_id(col_id) + col_def = self._get_updated_col_def_from_col_id(col_id) if col_def is None: logger.debug(f" column '{col_id}' is not found.") return Div(f"Column '{col_id}' not found") @@ -133,25 +131,21 @@ class DataGridColumnsManager(MultipleInstance): def show_all_columns(self): return self._mk_inner_content() - def update_column(self, col_id, client_response): - logger.debug(f"update_column {col_id=}, {client_response=}") - col_def = self._get_col_def_from_col_id(col_id, client_response, copy=False) - if col_def is None: - logger.debug(f" column '{col_id}' is not found.") - else: - # save the new values - self._parent.save_state() + def save_column_details(self, col_id, client_response): + logger.debug(f"save_column_details {col_id=}, {client_response=}") + self._get_updated_col_def_from_col_id(col_id, client_response, copy=False) + self._parent.save_state() return self._mk_inner_content() def on_new_column(self): self._new_column = True - col_def = self._get_col_def_from_col_id("__new__") + col_def = self._get_updated_col_def_from_col_id("__new__") return self.mk_column_details(col_def) def on_column_type_changed(self, col_id, client_response): logger.debug(f"on_column_type_changed {col_id=}, {client_response=}") - col_def = self._get_col_def_from_col_id(col_id, client_response) + col_def = self._get_updated_col_def_from_col_id(col_id, client_response) return self.mk_column_details(col_def) def _register_formula(self, col_def) -> None: @@ -240,7 +234,7 @@ class DataGridColumnsManager(MultipleInstance): legend="Column details", cls="fieldset border-base-300 rounded-box" ), - mk.dialog_buttons(on_ok=self.commands.update_column(col_def.col_id), + mk.dialog_buttons(on_ok=self.commands.save_column_details(col_def.col_id), on_cancel=self.commands.show_all_columns()), cls="mb-1", ), diff --git a/src/myfasthtml/core/dsl/base_completion.py b/src/myfasthtml/core/dsl/base_completion.py index 94e3f99..748e366 100644 --- a/src/myfasthtml/core/dsl/base_completion.py +++ b/src/myfasthtml/core/dsl/base_completion.py @@ -128,8 +128,8 @@ class BaseCompletionEngine(ABC): Generate suggestions for the given context. Args: - context: The detected completion context - scope: The detected scope + context: The detected completion context (from the parser) + scope: The detected scope (table, column, row, cell...) prefix: The current word prefix (for filtering) Returns: diff --git a/src/myfasthtml/core/dsl/exceptions.py b/src/myfasthtml/core/dsl/exceptions.py new file mode 100644 index 0000000..d5e962f --- /dev/null +++ b/src/myfasthtml/core/dsl/exceptions.py @@ -0,0 +1,50 @@ +""" +Common DSL exceptions shared across all DSL implementations. +""" + + +class DSLError(Exception): + """Base exception for DSL errors.""" + pass + + +class DSLSyntaxError(DSLError): + """ + Raised when a DSL input has syntax errors. + + Attributes: + message: Error description + line: Line number where error occurred (1-based) + column: Column number where error occurred (1-based) + context: The problematic line or snippet + """ + + def __init__(self, message: str, line: int = None, column: int = None, context: str = None): + self.message = message + self.line = line + self.column = column + self.context = context + super().__init__(self._format_message()) + + def _format_message(self) -> str: + parts = [self.message] + if self.line is not None: + parts.append(f"at line {self.line}") + if self.column is not None: + parts[1] = f"at line {self.line}, column {self.column}" + if self.context: + parts.append(f"\n {self.context}") + if self.column is not None: + parts.append(f"\n {' ' * (self.column - 1)}^") + return " ".join(parts[:2]) + "".join(parts[2:]) + + +class DSLValidationError(DSLError): + """ + Raised when a DSL is syntactically correct but semantically invalid. + """ + + def __init__(self, message: str, line: int = None): + self.message = message + self.line = line + super().__init__(f"{message}" + (f" at line {line}" if line else "")) diff --git a/src/myfasthtml/core/formatting/dsl/exceptions.py b/src/myfasthtml/core/formatting/dsl/exceptions.py index d4d93fc..a6f382f 100644 --- a/src/myfasthtml/core/formatting/dsl/exceptions.py +++ b/src/myfasthtml/core/formatting/dsl/exceptions.py @@ -1,55 +1,6 @@ """ -DSL-specific exceptions. +DSL-specific exceptions — re-exported from the common location. """ +from myfasthtml.core.dsl.exceptions import DSLError, DSLSyntaxError, DSLValidationError - -class DSLError(Exception): - """Base exception for DSL errors.""" - pass - - -class DSLSyntaxError(DSLError): - """ - Raised when the DSL input has syntax errors. - - Attributes: - message: Error description - line: Line number where error occurred (1-based) - column: Column number where error occurred (1-based) - context: The problematic line or snippet - """ - - def __init__(self, message: str, line: int = None, column: int = None, context: str = None): - self.message = message - self.line = line - self.column = column - self.context = context - super().__init__(self._format_message()) - - def _format_message(self) -> str: - parts = [self.message] - if self.line is not None: - parts.append(f"at line {self.line}") - if self.column is not None: - parts[1] = f"at line {self.line}, column {self.column}" - if self.context: - parts.append(f"\n {self.context}") - if self.column is not None: - parts.append(f"\n {' ' * (self.column - 1)}^") - return " ".join(parts[:2]) + "".join(parts[2:]) - - -class DSLValidationError(DSLError): - """ - Raised when the DSL is syntactically correct but semantically invalid. - - Examples: - - Unknown preset name - - Invalid parameter for formatter type - - Missing required parameter - """ - - def __init__(self, message: str, line: int = None): - self.message = message - self.line = line - super().__init__(f"{message}" + (f" at line {line}" if line else "")) +__all__ = ["DSLError", "DSLSyntaxError", "DSLValidationError"] diff --git a/src/myfasthtml/core/formula/dsl/completion/FormulaCompletionEngine.py b/src/myfasthtml/core/formula/dsl/completion/FormulaCompletionEngine.py index d158734..1c3abd6 100644 --- a/src/myfasthtml/core/formula/dsl/completion/FormulaCompletionEngine.py +++ b/src/myfasthtml/core/formula/dsl/completion/FormulaCompletionEngine.py @@ -8,6 +8,7 @@ Provides context-aware suggestions for: - Keywords: ``if``, ``else``, ``and``, ``or``, ``not``, ``WHERE`` """ import re +from dataclasses import dataclass, field from myfasthtml.core.dsl.base_completion import BaseCompletionEngine from myfasthtml.core.dsl.types import Position, Suggestion @@ -22,6 +23,21 @@ FORMULA_KEYWORDS = [ ] +@dataclass +class FormulaContext: + """ + Completion context for a formula expression. + + Attributes: + kind: One of ``"column_ref"``, ``"cross_table"``, + ``"function_or_keyword"``, ``"general"``. + cross_table_name: Table name extracted from ``{TableName.`` syntax. + Only relevant when ``kind == "cross_table"``. + """ + kind: str + cross_table_name: str = field(default="") + + class FormulaCompletionEngine(BaseCompletionEngine): """ Context-aware completion engine for formula expressions. @@ -42,7 +58,7 @@ class FormulaCompletionEngine(BaseCompletionEngine): """Formula has no scope — always the same single-expression scope.""" return None - def detect_context(self, text: str, cursor: Position, scope): + def detect_context(self, text: str, cursor: Position, scope) -> FormulaContext: """ Detect completion context based on cursor position in formula text. @@ -52,129 +68,80 @@ class FormulaCompletionEngine(BaseCompletionEngine): scope: Unused (formulas have no scopes). Returns: - Context string: ``"column_ref"``, ``"cross_table"``, - ``"function"``, ``"keyword"``, or ``"general"``. + FormulaContext describing the kind of completion expected. """ - # Get text up to cursor lines = text.split("\n") - line_idx = min(cursor.line, len(lines) - 1) - line_text = lines[line_idx] + line_text = lines[min(cursor.line, len(lines) - 1)] text_before = line_text[:cursor.ch] - # Check if we are inside a { ... } reference last_brace = text_before.rfind("{") if last_brace >= 0: inside = text_before[last_brace + 1:] if "}" not in inside: if "." in inside: - return "cross_table" - return "column_ref" + cross_table_name = inside[:inside.rfind(".")] + return FormulaContext("cross_table", cross_table_name) + return FormulaContext("column_ref") - # Check if we are typing a function name (alphanumeric at word start) - word_match = re.search(r"[a-z_][a-z0-9_]*$", text_before, re.IGNORECASE) - if word_match: - return "function_or_keyword" + if re.search(r"[a-z_][a-z0-9_]*$", text_before, re.IGNORECASE): + return FormulaContext("function_or_keyword") - return "general" + return FormulaContext("general") - def get_suggestions(self, text: str, cursor: Position, scope, context) -> list: + def get_suggestions(self, context: FormulaContext, scope, prefix: str) -> list[Suggestion]: """ Generate suggestions based on the detected context. Args: - text: The full formula text. - cursor: Cursor position. - scope: Unused. - context: String from ``detect_context``. + context: FormulaContext from ``detect_context``. + scope: Unused (formulas have no scopes). + prefix: Current word prefix (filtering handled by base class). Returns: List of Suggestion objects. """ - suggestions = [] - - if context == "column_ref": - # Suggest columns from the current table - suggestions += self._column_suggestions(self.table_name) - - elif context == "cross_table": - # Get the table name prefix from text_before - lines = text.split("\n") - line_text = lines[min(cursor.line, len(lines) - 1)] - text_before = line_text[:cursor.ch] - last_brace = text_before.rfind("{") - inside = text_before[last_brace + 1:] if last_brace >= 0 else "" - dot_pos = inside.rfind(".") - table_prefix = inside[:dot_pos] if dot_pos >= 0 else "" + match context.kind: + case "column_ref": + return self._column_suggestions(self.table_name) - # Suggest columns from the referenced table - if table_prefix: - suggestions += self._column_suggestions(table_prefix) - else: - suggestions += self._table_suggestions() - - elif context == "function_or_keyword": - suggestions += self._function_suggestions() - suggestions += self._keyword_suggestions() - - else: # general - suggestions += self._function_suggestions() - suggestions += self._keyword_suggestions() - suggestions += [ - Suggestion( - label="{", - detail="Column reference", - insert_text="{", - ) - ] - - return suggestions + case "cross_table": + return self._column_suggestions(context.cross_table_name) + + case "function_or_keyword": + return self._function_suggestions() + self._keyword_suggestions() + + case _: # general + return ( + self._function_suggestions() + + self._keyword_suggestions() + + [Suggestion("{", "Column reference", "keyword")] + ) # ==================== Private helpers ==================== - def _column_suggestions(self, table_name: str) -> list: + def _column_suggestions(self, table_name: str) -> list[Suggestion]: """Get column name suggestions for a table.""" try: columns = self.provider.list_columns(table_name) - return [ - Suggestion( - label=col, - detail=f"Column from {table_name}", - insert_text=col, - ) - for col in (columns or []) - ] + return [Suggestion(col, f"Column from {table_name}", "column") for col in (columns or [])] except Exception: return [] - def _table_suggestions(self) -> list: + def _table_suggestions(self) -> list[Suggestion]: """Get table name suggestions.""" try: tables = self.provider.list_tables() - return [ - Suggestion( - label=t, - detail="Table", - insert_text=t, - ) - for t in (tables or []) - ] + return [Suggestion(t, "Table", "table") for t in (tables or [])] except Exception: return [] - def _function_suggestions(self) -> list: + def _function_suggestions(self) -> list[Suggestion]: """Get built-in function name suggestions.""" return [ - Suggestion( - label=name, - detail="Function", - insert_text=f"{name}(", - ) + Suggestion(f"{name}(", name, "function") for name in sorted(BUILTIN_FUNCTIONS.keys()) ] - def _keyword_suggestions(self) -> list: + def _keyword_suggestions(self) -> list[Suggestion]: """Get keyword suggestions.""" - return [ - Suggestion(label=kw, detail="Keyword", insert_text=kw) - for kw in FORMULA_KEYWORDS - ] + return [Suggestion(kw, "Keyword", "keyword") for kw in FORMULA_KEYWORDS] diff --git a/src/myfasthtml/core/formula/dsl/exceptions.py b/src/myfasthtml/core/formula/dsl/exceptions.py index 12878e5..5eabfad 100644 --- a/src/myfasthtml/core/formula/dsl/exceptions.py +++ b/src/myfasthtml/core/formula/dsl/exceptions.py @@ -3,25 +3,6 @@ class FormulaError(Exception): pass -class FormulaSyntaxError(FormulaError): - """Raised when the formula has syntax errors.""" - - def __init__(self, message, line=None, column=None, context=None): - self.message = message - self.line = line - self.column = column - self.context = context - super().__init__(self._format_message()) - - def _format_message(self): - parts = [self.message] - if self.line is not None: - parts.append(f"at line {self.line}") - if self.column is not None: - parts.append(f"col {self.column}") - return " ".join(parts) - - class FormulaValidationError(FormulaError): """Raised when the formula is syntactically correct but semantically invalid.""" pass @@ -29,7 +10,7 @@ class FormulaValidationError(FormulaError): class FormulaCycleError(FormulaError): """Raised when formula dependencies contain a cycle.""" - + def __init__(self, cycle_nodes): self.cycle_nodes = cycle_nodes super().__init__(f"Circular dependency detected involving: {', '.join(cycle_nodes)}") diff --git a/src/myfasthtml/core/formula/dsl/parser.py b/src/myfasthtml/core/formula/dsl/parser.py index 2f422f4..6d52dce 100644 --- a/src/myfasthtml/core/formula/dsl/parser.py +++ b/src/myfasthtml/core/formula/dsl/parser.py @@ -6,7 +6,7 @@ No indentation handling needed — formulas are single-line expressions. """ from lark import Lark, UnexpectedInput -from .exceptions import FormulaSyntaxError +from myfasthtml.core.dsl.exceptions import DSLSyntaxError from .grammar import FORMULA_GRAMMAR @@ -39,7 +39,7 @@ class FormulaParser: lark.Tree: The parsed AST. Raises: - FormulaSyntaxError: If the text has syntax errors. + DSLSyntaxError: If the text has syntax errors. """ text = text.strip() if not text: @@ -52,7 +52,7 @@ class FormulaParser: if hasattr(e, "get_context"): context = e.get_context(text) - raise FormulaSyntaxError( + raise DSLSyntaxError( message=self._format_error_message(e), line=getattr(e, "line", None), column=getattr(e, "column", None), diff --git a/src/myfasthtml/core/formula/engine.py b/src/myfasthtml/core/formula/engine.py index 5ba112f..69367ce 100644 --- a/src/myfasthtml/core/formula/engine.py +++ b/src/myfasthtml/core/formula/engine.py @@ -34,7 +34,7 @@ def parse_formula(text: str) -> FormulaDefinition | None: FormulaDefinition on success, None if text is empty. Raises: - FormulaSyntaxError: If the formula text is syntactically invalid. + DSLSyntaxError: If the formula text is syntactically invalid. """ text = text.strip() if text else "" if not text: @@ -80,7 +80,7 @@ class FormulaEngine: formula_text: The formula expression string. Raises: - FormulaSyntaxError: If the formula is syntactically invalid. + DSLSyntaxError: If the formula is syntactically invalid. FormulaCycleError: If the formula would create a circular dependency. """ formula_text = formula_text.strip() if formula_text else "" diff --git a/src/myfasthtml/core/utils.py b/src/myfasthtml/core/utils.py index 1f91021..e01a117 100644 --- a/src/myfasthtml/core/utils.py +++ b/src/myfasthtml/core/utils.py @@ -12,7 +12,7 @@ from starlette.routing import Mount from myfasthtml.core.constants import Routes, ROUTE_ROOT from myfasthtml.core.dsl.types import Position from myfasthtml.core.dsls import DslsManager -from myfasthtml.core.formatting.dsl import DSLSyntaxError +from myfasthtml.core.dsl.exceptions import DSLSyntaxError from myfasthtml.test.MyFT import MyFT utils_app, utils_rt = fast_app() diff --git a/tests/controls/test_datagrid_columns_manager.py b/tests/controls/test_datagrid_columns_manager.py index e0d3912..43f3699 100644 --- a/tests/controls/test_datagrid_columns_manager.py +++ b/tests/controls/test_datagrid_columns_manager.py @@ -1,469 +1,415 @@ import shutil from dataclasses import dataclass, field -from unittest.mock import Mock import pytest -from fasthtml.common import Div, Input, Label, Form, Fieldset, Select +from fasthtml.common import Div, FT, Input, Form, Fieldset, Select -from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager, Commands +from myfasthtml.controls.DataGridColumnsManager import DataGridColumnsManager from myfasthtml.controls.Search import Search from myfasthtml.controls.datagrid_objects import DataGridColumnState from myfasthtml.core.constants import ColumnType from myfasthtml.core.instances import InstancesManager, MultipleInstance from myfasthtml.test.matcher import ( - matches, find_one, find, Contains, TestIcon, TestObject + matches, find_one, find, Contains, TestIcon, TestObject ) @dataclass class MockDatagridState: - """Mock state object that mimics DatagridState.""" - columns: list = field(default_factory=list) + """Mock state object that mimics DatagridState.""" + columns: list = field(default_factory=list) class MockDataGrid(MultipleInstance): - """Mock DataGrid parent for testing DataGridColumnsManager.""" - - def __init__(self, parent, columns=None, _id=None): - super().__init__(parent, _id=_id) - self._state = MockDatagridState(columns=columns or []) - self._save_state_called = False - - def get_state(self): - return self._state - - def save_state(self): - self._save_state_called = True + """Mock DataGrid parent for testing DataGridColumnsManager.""" + + def __init__(self, parent, columns=None, _id=None): + super().__init__(parent, _id=_id) + self._state = MockDatagridState(columns=columns or []) + self._save_state_called = False + + def get_state(self): + return self._state + + def save_state(self): + self._save_state_called = True + + def get_table_name(self): + return "mock_table" @pytest.fixture def mock_datagrid(root_instance): - """Create a mock DataGrid with sample columns.""" - columns = [ - DataGridColumnState(col_id="name", col_index=0, title="Name", type=ColumnType.Text, visible=True), - DataGridColumnState(col_id="age", col_index=1, title="Age", type=ColumnType.Number, visible=True), - DataGridColumnState(col_id="email", col_index=2, title="Email", type=ColumnType.Text, visible=False), - ] - yield MockDataGrid(root_instance, columns=columns, _id="test-datagrid") - InstancesManager.reset() + """Create a mock DataGrid with sample columns.""" + columns = [ + DataGridColumnState(col_id="name", col_index=0, title="Name", type=ColumnType.Text, visible=True), + DataGridColumnState(col_id="age", col_index=1, title="Age", type=ColumnType.Number, visible=True), + DataGridColumnState(col_id="email", col_index=2, title="Email", type=ColumnType.Text, visible=False), + ] + yield MockDataGrid(root_instance, columns=columns, _id="test-datagrid") + InstancesManager.reset() @pytest.fixture def columns_manager(mock_datagrid): - """Create a DataGridColumnsManager instance for testing.""" - shutil.rmtree(".myFastHtmlDb", ignore_errors=True) - yield DataGridColumnsManager(mock_datagrid) - shutil.rmtree(".myFastHtmlDb", ignore_errors=True) + """Create a DataGridColumnsManager instance for testing.""" + shutil.rmtree(".myFastHtmlDb", ignore_errors=True) + yield DataGridColumnsManager(mock_datagrid) + shutil.rmtree(".myFastHtmlDb", ignore_errors=True) class TestDataGridColumnsManagerBehaviour: - """Tests for DataGridColumnsManager behavior and logic.""" - - # ========================================================================= - # Initialization - # ========================================================================= - - def test_i_can_create_columns_manager(self, mock_datagrid): - """Test that DataGridColumnsManager can be created with a DataGrid parent.""" - cm = DataGridColumnsManager(mock_datagrid) - - assert cm is not None - assert cm._parent == mock_datagrid - assert isinstance(cm.commands, Commands) - - # ========================================================================= - # Columns Property - # ========================================================================= - - def test_columns_property_returns_parent_state_columns(self, columns_manager, mock_datagrid): - """Test that columns property returns columns from parent's state.""" - columns = columns_manager.columns - - assert columns == mock_datagrid.get_state().columns - assert len(columns) == 3 - assert columns[0].col_id == "name" - - # ========================================================================= - # Get Column Definition - # ========================================================================= - - def test_i_can_get_existing_column_by_id(self, columns_manager): - """Test finding an existing column by its ID.""" - col_def = columns_manager._get_col_def_from_col_id("name") - - assert col_def is not None - assert col_def.col_id == "name" - assert col_def.title == "Name" - - def test_i_cannot_get_nonexistent_column(self, columns_manager): - """Test that getting a nonexistent column returns None.""" - col_def = columns_manager._get_col_def_from_col_id("nonexistent") - - assert col_def is None - - # ========================================================================= - # Toggle Column Visibility - # ========================================================================= - - @pytest.mark.parametrize("col_id, initial_visible, expected_visible", [ - ("name", True, False), # visible -> hidden - ("email", False, True), # hidden -> visible - ]) - def test_i_can_toggle_column_visibility(self, columns_manager, col_id, initial_visible, expected_visible): - """Test toggling column visibility from visible to hidden and vice versa.""" - col_def = columns_manager._get_col_def_from_col_id(col_id) - assert col_def.visible == initial_visible - - columns_manager.toggle_column(col_id) - - assert col_def.visible == expected_visible - - def test_toggle_column_saves_state(self, columns_manager, mock_datagrid): - """Test that toggle_column calls save_state on parent.""" - mock_datagrid._save_state_called = False - - columns_manager.toggle_column("name") - - assert mock_datagrid._save_state_called is True - - def test_toggle_column_returns_column_label(self, columns_manager): - """Test that toggle_column returns the updated column label.""" - result = columns_manager.toggle_column("name") - - # Result should be a Div with the column label structure - assert result is not None - assert hasattr(result, 'tag') - - def test_i_cannot_toggle_nonexistent_column(self, columns_manager): - """Test that toggling a nonexistent column returns an error message.""" - result = columns_manager.toggle_column("nonexistent") - - expected = Div("Column 'nonexistent' not found") - assert matches(result, expected) - - # ========================================================================= - # Show Column Details - # ========================================================================= - - def test_i_can_show_column_details_for_existing_column(self, columns_manager): - """Test that show_column_details returns the details form for an existing column.""" - result = columns_manager.show_column_details("name") - - # Should contain a Form - check by finding form tag in children - expected = Form() - del(expected.attrs["enctype"]) # hack. We don't know why enctype is added - forms = find(result, expected) - assert len(forms) == 1, "Should contain exactly one form" - - def test_i_cannot_show_details_for_nonexistent_column(self, columns_manager): - """Test that showing details for nonexistent column returns error message.""" - result = columns_manager.show_column_details("nonexistent") - - expected = Div("Column 'nonexistent' not found") - assert matches(result, expected) - - # ========================================================================= - # Show All Columns - # ========================================================================= - - def test_show_all_columns_returns_search_component(self, columns_manager): - """Test that show_all_columns returns a Search component.""" - result = columns_manager.show_all_columns() - - assert isinstance(result, Search) - - def test_show_all_columns_contains_all_columns(self, columns_manager): - """Test that show_all_columns Search contains all columns.""" - result = columns_manager.show_all_columns() - - assert len(result.items) == 3 - - # ========================================================================= - # Update Column - # ========================================================================= - - def test_i_can_update_column_title(self, columns_manager): - """Test updating a column's title via client_response.""" - client_response = {"title": "New Name"} - - columns_manager.update_column("name", client_response) - - col_def = columns_manager._get_col_def_from_col_id("name") - assert col_def.title == "New Name" - - def test_i_can_update_column_visibility_via_form(self, columns_manager): - """Test updating column visibility via checkbox form value.""" - col_def = columns_manager._get_col_def_from_col_id("name") - assert col_def.visible is True - - # Unchecked checkbox sends nothing, checked sends "on" - client_response = {"visible": "off"} # Not "on" means unchecked - columns_manager.update_column("name", client_response) - - assert col_def.visible is False - - # Check it back on - client_response = {"visible": "on"} - columns_manager.update_column("name", client_response) - - assert col_def.visible is True - - def test_i_can_update_column_type(self, columns_manager): - """Test updating a column's type.""" - client_response = {"type": "Number"} - - columns_manager.update_column("name", client_response) - - col_def = columns_manager._get_col_def_from_col_id("name") - assert col_def.type == ColumnType.Number - - def test_i_can_update_column_width(self, columns_manager): - """Test updating a column's width.""" - client_response = {"width": "200"} - - columns_manager.update_column("name", client_response) - - col_def = columns_manager._get_col_def_from_col_id("name") - assert col_def.width == 200 - - def test_update_column_saves_state(self, columns_manager, mock_datagrid): - """Test that update_column calls save_state on parent.""" - mock_datagrid._save_state_called = False - - columns_manager.update_column("name", {"title": "Updated"}) - - assert mock_datagrid._save_state_called is True - - def test_update_column_ignores_unknown_attributes(self, columns_manager): - """Test that update_column ignores attributes not in DataGridColumnState.""" - col_def = columns_manager._get_col_def_from_col_id("name") - original_title = col_def.title - - client_response = {"unknown_attr": "value", "title": "New Title"} - columns_manager.update_column("name", client_response) - - # unknown_attr should be ignored, title should be updated - assert col_def.title == "New Title" - assert not hasattr(col_def, "unknown_attr") - - def test_i_cannot_update_nonexistent_column(self, columns_manager): - """Test that updating nonexistent column returns mk_all_columns result.""" - result = columns_manager.update_column("nonexistent", {"title": "Test"}) - - # Should return the all columns view (Search component) - assert isinstance(result, Search) + """Tests for DataGridColumnsManager behavior and logic.""" + + # ========================================================================= + # Get Column Definition + # ========================================================================= + + def test_i_can_get_existing_column_by_id(self, columns_manager): + """Test finding an existing column by its ID.""" + col_def = columns_manager._get_col_def_from_col_id("name") + + assert col_def is not None + assert col_def.col_id == "name" + assert col_def.title == "Name" + + def test_i_cannot_get_nonexistent_column(self, columns_manager): + """Test that getting a nonexistent column returns None.""" + col_def = columns_manager._get_col_def_from_col_id("nonexistent") + + assert col_def is None + + def test_i_can_get_and_update_column_state(self, columns_manager): + """Test that get_col_def_from_col_id updates the column state.""" + updates = {"title": "New Name", "visible": "on", "type": "Number", "width": 200} + col_def = columns_manager._get_updated_col_def_from_col_id("name", updates) + assert col_def.title == "New Name" + assert col_def.visible is True + assert col_def.type == ColumnType.Number + assert col_def.width == 200 + + def test_i_can_get_and_update_column_state_visible_false(self, columns_manager): + """Test that get_col_def_from_col_id updates the column state.""" + updates = {} # visible is missing in the update => It must be set to False + col_def = columns_manager._get_updated_col_def_from_col_id("name", updates) + + assert col_def.visible is False + + # ========================================================================= + # Toggle Column Visibility + # ========================================================================= + + @pytest.mark.parametrize("col_id, initial_visible, expected_visible", [ + ("name", True, False), # visible -> hidden + ("email", False, True), # hidden -> visible + ]) + def test_i_can_toggle_column_visibility(self, columns_manager, col_id, initial_visible, expected_visible): + """Test toggling column visibility from visible to hidden and vice versa.""" + col_def = columns_manager._get_col_def_from_col_id(col_id) + assert col_def.visible == initial_visible + + columns_manager.toggle_column(col_id) + + col_def = columns_manager._get_col_def_from_col_id(col_id) + assert col_def.visible == expected_visible + + def test_toggle_column_saves_state(self, columns_manager, mock_datagrid): + """Test that toggle_column calls save_state on parent.""" + mock_datagrid._save_state_called = False + + columns_manager.toggle_column("name") + + assert mock_datagrid._save_state_called is True + + def test_toggle_column_returns_column_label(self, columns_manager): + """Test that toggle_column returns an HTML element.""" + result = columns_manager.toggle_column("name") + + assert isinstance(result, FT) + + def test_i_cannot_toggle_nonexistent_column(self, columns_manager): + """Test that toggling a nonexistent column returns an error message.""" + result = columns_manager.toggle_column("nonexistent") + + expected = Div("Column 'nonexistent' not found") + assert matches(result, expected) + + # ========================================================================= + # Show All Columns + # ========================================================================= + + def test_show_all_columns_returns_search_component(self, columns_manager): + """Test that mk_all_columns returns a Search component.""" + result = columns_manager.mk_all_columns() + + assert isinstance(result, Search) + + def test_show_all_columns_returns_configured_search(self, columns_manager): + """Test that mk_all_columns returns a correctly configured Search component.""" + result = columns_manager.mk_all_columns() + + assert result.items_names == "Columns" + assert len(result.items) == 3 + col_def = result.items[0] + assert result.get_attr(col_def) == col_def.col_id + + # ========================================================================= + # Update Column + # ========================================================================= + + def test_i_can_update_column_title(self, columns_manager): + """Test updating a column's title via client_response.""" + columns_manager.save_column_details("name", {"title": "New Name"}) + + col_def = columns_manager._get_col_def_from_col_id("name") + assert col_def.title == "New Name" + + def test_i_can_update_column_visibility_via_form(self, columns_manager): + """Test updating column visibility via checkbox form value.""" + col_def = columns_manager._get_col_def_from_col_id("name") + assert col_def.visible is True + + # Unchecked checkbox sends nothing, checked sends "on" + columns_manager.save_column_details("name", {"visible": "off"}) # Not "on" means unchecked + col_def = columns_manager._get_col_def_from_col_id("name") + assert col_def.visible is False + + # Check it back on + columns_manager.save_column_details("name", {"visible": "on"}) + col_def = columns_manager._get_col_def_from_col_id("name") + assert col_def.visible is True + + def test_i_can_update_column_type(self, columns_manager): + """Test updating a column's type.""" + columns_manager.save_column_details("name", {"type": "Number"}) + + col_def = columns_manager._get_col_def_from_col_id("name") + assert col_def.type == ColumnType.Number + + def test_i_can_update_column_width(self, columns_manager): + """Test updating a column's width.""" + columns_manager.save_column_details("name", {"width": "200"}) + + col_def = columns_manager._get_col_def_from_col_id("name") + assert col_def.width == 200 + + def test_update_column_saves_state(self, columns_manager, mock_datagrid): + """Test that save_column_details calls save_state on parent.""" + mock_datagrid._save_state_called = False + + columns_manager.save_column_details("name", {"title": "Updated"}) + + assert mock_datagrid._save_state_called is True + + def test_update_column_ignores_unknown_attributes(self, columns_manager): + """Test that save_column_details ignores attributes not in DataGridColumnState.""" + columns_manager.save_column_details("name", {"unknown_attr": "value", "title": "New Title"}) + + col_def = columns_manager._get_col_def_from_col_id("name") + assert col_def.title == "New Title" + assert not hasattr(col_def, "unknown_attr") class TestDataGridColumnsManagerRender: - """Tests for DataGridColumnsManager HTML rendering.""" + """Tests for DataGridColumnsManager HTML rendering.""" + + @pytest.fixture + def columns_manager(self, mock_datagrid): + """Create a fresh DataGridColumnsManager for render tests.""" + shutil.rmtree(".myFastHtmlDb", ignore_errors=True) + cm = DataGridColumnsManager(mock_datagrid) + yield cm + shutil.rmtree(".myFastHtmlDb", ignore_errors=True) + + # ========================================================================= + # Global Structure + # ========================================================================= + + def test_i_can_render_columns_manager_with_columns(self, columns_manager): + """Test that DataGridColumnsManager renders with correct global structure. - @pytest.fixture - def columns_manager(self, mock_datagrid): - """Create a fresh DataGridColumnsManager for render tests.""" - shutil.rmtree(".myFastHtmlDb", ignore_errors=True) - cm = DataGridColumnsManager(mock_datagrid) - yield cm - shutil.rmtree(".myFastHtmlDb", ignore_errors=True) + Why these elements matter: + - id: Required for HTMX targeting in commands + - Contains Search component: Main content for column list + """ + html = columns_manager.render() + + expected = Div( + TestObject(Search), # Search component (column list) + Div(), # New column button + id=columns_manager._id, + ) + + assert matches(html, expected) + + # ========================================================================= + # mk_column_label + # ========================================================================= + + def test_column_label_has_checkbox_and_details_navigation(self, columns_manager): + """Test that column label contains checkbox and navigation to details. - # ========================================================================= - # Global Structure - # ========================================================================= + Why these elements matter: + - Checkbox (Input type=checkbox): Controls column visibility + - Label with column ID: Identifies the column + - Chevron icon: Indicates navigation to details + - id with tcolman_ prefix: Required for HTMX swap targeting + """ + col_def = columns_manager._get_col_def_from_col_id("name") + label = columns_manager.mk_column_label(col_def) + + # Should have the correct ID pattern + expected = Div( + id=f"tcolman_{columns_manager._id}-name", + cls=Contains("flex"), + ) + assert matches(label, expected) + + # Should contain a checkbox + checkbox = find_one(label, Input(type="checkbox")) + assert checkbox is not None + + # Should contain chevron icon for navigation + chevron = find_one(label, TestIcon("chevron_right20_regular")) + assert chevron is not None + + def test_column_label_checkbox_is_checked_when_visible(self, columns_manager): + """Test that checkbox is checked when column is visible. - def test_i_can_render_columns_manager_with_columns(self, columns_manager): - """Test that DataGridColumnsManager renders with correct global structure. + Why this matters: + - checked attribute: Reflects current visibility state + - User can see which columns are visible + """ + col_def = columns_manager._get_col_def_from_col_id("name") + assert col_def.visible is True + + label = columns_manager.mk_column_label(col_def) + checkbox = find_one(label, Input(type="checkbox")) + + # Checkbox should have checked attribute + assert checkbox.attrs.get("checked") is True + + def test_column_label_checkbox_is_unchecked_when_hidden(self, columns_manager): + """Test that checkbox is unchecked when column is hidden. - Why these elements matter: - - id: Required for HTMX targeting in commands - - Contains Search component: Main content for column list - """ - html = columns_manager.render() + Why this matters: + - No checked attribute: Indicates column is hidden + - Visual feedback for user + """ + col_def = columns_manager._get_col_def_from_col_id("email") + assert col_def.visible is False + + label = columns_manager.mk_column_label(col_def) + checkbox = find_one(label, Input(type="checkbox")) + + # Checkbox should not have checked attribute (or it should be False/None) + checked = checkbox.attrs.get("checked") + assert checked is None or checked is False + + # ========================================================================= + # mk_column_details + # ========================================================================= + + def test_column_details_contains_all_form_fields(self, columns_manager): + """Test that column details form contains all required fields. - expected = Div( - TestObject(Search), # Search component - id=columns_manager._id, - ) + Why these elements matter: + - col_id field (readonly): Shows column identifier + - title field: Editable column display name + - visible checkbox: Toggle visibility + - type select: Change column type + - width input: Set column width + """ + col_def = columns_manager._get_col_def_from_col_id("name") + details = columns_manager.mk_column_details(col_def) + + # Should contain Form + form = Form() + del form.attrs["enctype"] + form = find_one(details, form) + assert form is not None + + # Should contain all required input fields + col_id_input = find_one(form, Input(name="col_id")) + assert col_id_input is not None + assert col_id_input.attrs.get("readonly") is True + + title_input = find_one(form, Input(name="title")) + assert title_input is not None + + visible_checkbox = find_one(form, Input(name="visible", type="checkbox")) + assert visible_checkbox is not None + + type_select = find_one(form, Select(name="type")) + assert type_select is not None + + width_input = find_one(form, Input(name="width", type="number")) + assert width_input is not None + + def test_column_details_has_back_button(self, columns_manager): + """Test that column details has a back button to return to all columns. - assert matches(html, expected) + Why this matters: + - Back navigation: User can return to column list + - Chevron left icon: Visual indicator of back action + """ + col_def = columns_manager._get_col_def_from_col_id("name") + details = columns_manager.mk_column_details(col_def) + + # Should contain back chevron icon + back_icon = find_one(details, TestIcon("chevron_left20_regular", wrapper="span")) + assert back_icon is not None + + def test_column_details_form_has_fieldset_with_legend(self, columns_manager): + """Test that column details form has a fieldset with legend. - # ========================================================================= - # mk_column_label - # ========================================================================= + Why this matters: + - Fieldset groups related fields + - Legend provides context ("Column details") + """ + col_def = columns_manager._get_col_def_from_col_id("name") + details = columns_manager.mk_column_details(col_def) + + fieldset = find_one(details, Fieldset(legend="Column details")) + assert fieldset is not None + + # ========================================================================= + # show_column_details + # ========================================================================= + + def test_i_can_show_column_details_for_existing_column(self, columns_manager): + """Test that show_column_details returns a form-based view for an existing column. - def test_column_label_has_checkbox_and_details_navigation(self, columns_manager): - """Test that column label contains checkbox and navigation to details. + Why these elements matter: + - Form element: show_column_details must return an editable form view + - Exactly one form: Ensures the response is unambiguous (not multiple forms) + """ + result = columns_manager.show_column_details("name") + + expected = Form() + del expected.attrs["enctype"] + forms = find(result, expected) + assert len(forms) == 1, "Should contain exactly one form" + + # ========================================================================= + # mk_all_columns + # ========================================================================= + + def test_all_columns_renders_all_column_labels(self, columns_manager): + """Test that all columns render produces labels for all columns. - Why these elements matter: - - Checkbox (Input type=checkbox): Controls column visibility - - Label with column ID: Identifies the column - - Chevron icon: Indicates navigation to details - - id with tcolman_ prefix: Required for HTMX swap targeting - """ - col_def = columns_manager._get_col_def_from_col_id("name") - label = columns_manager.mk_column_label(col_def) - - # Should have the correct ID pattern - expected = Div( - id=f"tcolman_{columns_manager._id}-name", - cls=Contains("flex"), - ) - assert matches(label, expected) - - # Should contain a checkbox - checkbox = find_one(label, Input(type="checkbox")) - assert checkbox is not None - - # Should contain chevron icon for navigation - chevron = find_one(label, TestIcon("chevron_right20_regular")) - assert chevron is not None - - def test_column_label_checkbox_is_checked_when_visible(self, columns_manager): - """Test that checkbox is checked when column is visible. - - Why this matters: - - checked attribute: Reflects current visibility state - - User can see which columns are visible - """ - col_def = columns_manager._get_col_def_from_col_id("name") - assert col_def.visible is True - - label = columns_manager.mk_column_label(col_def) - checkbox = find_one(label, Input(type="checkbox")) - - # Checkbox should have checked attribute - assert checkbox.attrs.get("checked") is True - - def test_column_label_checkbox_is_unchecked_when_hidden(self, columns_manager): - """Test that checkbox is unchecked when column is hidden. - - Why this matters: - - No checked attribute: Indicates column is hidden - - Visual feedback for user - """ - col_def = columns_manager._get_col_def_from_col_id("email") - assert col_def.visible is False - - label = columns_manager.mk_column_label(col_def) - checkbox = find_one(label, Input(type="checkbox")) - - # Checkbox should not have checked attribute (or it should be False/None) - checked = checkbox.attrs.get("checked") - assert checked is None or checked is False - - # ========================================================================= - # mk_column_details - # ========================================================================= - - def test_column_details_contains_all_form_fields(self, columns_manager): - """Test that column details form contains all required fields. - - Why these elements matter: - - col_id field (readonly): Shows column identifier - - title field: Editable column display name - - visible checkbox: Toggle visibility - - type select: Change column type - - width input: Set column width - """ - col_def = columns_manager._get_col_def_from_col_id("name") - details = columns_manager.mk_column_details(col_def) - - # Should contain Form - form = Form() - del form.attrs["enctype"] - form = find_one(details, form) - assert form is not None - - # Should contain all required input fields - col_id_input = find_one(form, Input(name="col_id")) - assert col_id_input is not None - assert col_id_input.attrs.get("readonly") is True - - title_input = find_one(form, Input(name="title")) - assert title_input is not None - - visible_checkbox = find_one(form, Input(name="visible", type="checkbox")) - assert visible_checkbox is not None - - type_select = find_one(form, Select(name="type")) - assert type_select is not None - - width_input = find_one(form, Input(name="width", type="number")) - assert width_input is not None - - def test_column_details_has_back_button(self, columns_manager): - """Test that column details has a back button to return to all columns. - - Why this matters: - - Back navigation: User can return to column list - - Chevron left icon: Visual indicator of back action - """ - col_def = columns_manager._get_col_def_from_col_id("name") - details = columns_manager.mk_column_details(col_def) - - # Should contain back chevron icon - back_icon = find_one(details, TestIcon("chevron_left20_regular", wrapper="span")) - assert back_icon is not None - - def test_column_details_form_has_fieldset_with_legend(self, columns_manager): - """Test that column details form has a fieldset with legend. - - Why this matters: - - Fieldset groups related fields - - Legend provides context ("Column details") - """ - col_def = columns_manager._get_col_def_from_col_id("name") - details = columns_manager.mk_column_details(col_def) - - fieldset = find_one(details, Fieldset(legend="Column details")) - assert fieldset is not None - - # ========================================================================= - # mk_all_columns - # ========================================================================= - - def test_all_columns_uses_search_component(self, columns_manager): - """Test that mk_all_columns returns a Search component. - - Why this matters: - - Search component: Enables filtering columns by name - - items_names="Columns": Labels the search appropriately - """ - result = columns_manager.mk_all_columns() - - assert isinstance(result, Search) - assert result.items_names == "Columns" - - def test_all_columns_search_has_correct_configuration(self, columns_manager): - """Test that Search component is configured correctly. - - Why these elements matter: - - items: Contains all column definitions - - get_attr: Extracts col_id for search matching - - template: Uses mk_column_label for rendering - """ - result = columns_manager.mk_all_columns() - - # Should have all 3 columns - assert len(result.items) == 3 - - # get_attr should return col_id - col_def = result.items[0] - assert result.get_attr(col_def) == col_def.col_id - - def test_all_columns_renders_all_column_labels(self, columns_manager): - """Test that all columns render produces labels for all columns. - - Why this matters: - - All columns visible in list - - Each column has its label rendered - """ - search = columns_manager.mk_all_columns() - rendered = search.render() - - # Should find 3 column labels in the results - results_div = find_one(rendered, Div(id=f"{search._id}-results")) - assert results_div is not None - - # Each column should have a label with tcolman_ prefix - for col_id in ["name", "age", "email"]: - label = find_one(results_div, Div(id=f"tcolman_{columns_manager._id}-{col_id}")) - assert label is not None, f"Column label for '{col_id}' should be present" + Why this matters: + - All columns visible in list + - Each column has its label rendered + """ + search = columns_manager.mk_all_columns() + rendered = search.render() + + # Should find 3 column labels in the results + results_div = find_one(rendered, Div(id=f"{search._id}-results")) + assert results_div is not None + + # Each column should have a label with tcolman_ prefix + for col_id in ["name", "age", "email"]: + label = find_one(results_div, Div(id=f"tcolman_{columns_manager._id}-{col_id}")) + assert label is not None, f"Column label for '{col_id}' should be present" diff --git a/tests/core/formula/test_formula_engine.py b/tests/core/formula/test_formula_engine.py index 875e4fd..58a0b0a 100644 --- a/tests/core/formula/test_formula_engine.py +++ b/tests/core/formula/test_formula_engine.py @@ -4,7 +4,8 @@ Tests for the FormulaEngine facade. import numpy as np import pytest -from myfasthtml.core.formula.dsl.exceptions import FormulaSyntaxError, FormulaCycleError +from myfasthtml.core.dsl.exceptions import DSLSyntaxError +from myfasthtml.core.formula.dsl.exceptions import FormulaCycleError from myfasthtml.core.formula.engine import FormulaEngine @@ -72,9 +73,9 @@ class TestSetFormula: assert totals[2] == 50 def test_i_cannot_set_invalid_formula(self): - """Test that invalid formula syntax raises FormulaSyntaxError.""" + """Test that invalid formula syntax raises DSLSyntaxError.""" engine = make_engine() - with pytest.raises(FormulaSyntaxError): + with pytest.raises(DSLSyntaxError): engine.set_formula("t", "col", "{Price} * * {Qty}") def test_i_cannot_set_formula_with_cycle(self): diff --git a/tests/core/formula/test_formula_parser.py b/tests/core/formula/test_formula_parser.py index 4b99f35..340c3e3 100644 --- a/tests/core/formula/test_formula_parser.py +++ b/tests/core/formula/test_formula_parser.py @@ -11,7 +11,7 @@ from myfasthtml.core.formula.dataclasses import ( LiteralNode, FormulaDefinition, ) -from myfasthtml.core.formula.dsl.exceptions import FormulaSyntaxError +from myfasthtml.core.dsl.exceptions import DSLSyntaxError from myfasthtml.core.formula.engine import parse_formula @@ -177,12 +177,12 @@ def test_i_can_parse_nested_functions(): "123 + + 456", # double operator ]) def test_i_cannot_parse_invalid_syntax(formula_text): - """Test that invalid syntax raises FormulaSyntaxError.""" - with pytest.raises(FormulaSyntaxError): + """Test that invalid syntax raises DSLSyntaxError.""" + with pytest.raises(DSLSyntaxError): parse_formula(formula_text) def test_i_cannot_parse_unclosed_brace(): - """Test that an unclosed brace raises FormulaSyntaxError.""" - with pytest.raises(FormulaSyntaxError): + """Test that an unclosed brace raises DSLSyntaxError.""" + with pytest.raises(DSLSyntaxError): parse_formula("{Price") diff --git a/tests/core/test_datagrid_registry.py b/tests/core/test_datagrid_registry.py index 614379a..609229d 100644 --- a/tests/core/test_datagrid_registry.py +++ b/tests/core/test_datagrid_registry.py @@ -30,7 +30,9 @@ def session(): @pytest.fixture def parent(session): - return SingleInstance(session=session, _id="test_parent_id") + instance = SingleInstance(session=session, _id="test_parent_id") + instance.get_formula_engine = lambda: None + return instance @pytest.fixture