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