Fixed Syntax validation and autocompletion. Fixed unit tests

This commit is contained in:
2026-02-15 18:32:34 +01:00
parent 789c06b842
commit 27f12b2c32
13 changed files with 532 additions and 640 deletions

View File

@@ -10,6 +10,9 @@ from myfasthtml.controls.datagrid_objects import DataGridColumnState
from myfasthtml.controls.helpers import icons, mk from myfasthtml.controls.helpers import icons, mk
from myfasthtml.core.commands import Command from myfasthtml.core.commands import Command
from myfasthtml.core.constants import ColumnType 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.core.instances import MultipleInstance
from myfasthtml.icons.fluent_p1 import chevron_right20_regular, chevron_left20_regular from myfasthtml.icons.fluent_p1 import chevron_right20_regular, chevron_left20_regular
@@ -37,11 +40,11 @@ class Commands(BaseCommands):
self._owner, self._owner,
self._owner.show_all_columns).htmx(target=f"#{self._id}", swap="innerHTML") 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", return Command(f"UpdateColumn",
f"Update column {col_id}", f"Update column {col_id}",
self._owner, self._owner,
self._owner.update_column, self._owner.save_column_details,
kwargs={"col_id": col_id} kwargs={"col_id": col_id}
).htmx(target=f"#{self._id}", swap="innerHTML") ).htmx(target=f"#{self._id}", swap="innerHTML")
@@ -64,41 +67,36 @@ class DataGridColumnsManager(MultipleInstance):
self.commands = Commands(self) self.commands = Commands(self)
self._new_column = False 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") self._formula_editor = DataGridFormulaEditor(self, conf=conf, _id=f"{self._id}-formula-editor")
DslsManager.register(completion_engine, FormulaParser())
@property @property
def columns(self): def columns(self):
return self._parent.get_state().columns 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] cols_defs = [c for c in self.columns if c.col_id == col_id]
if not cols_defs: if not cols_defs:
return None
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) col_def = DataGridColumnState(col_id, -1)
else:
col_def = cols_defs[0].copy() if copy else cols_defs[0]
if updates: 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)]: for k, v in [(k, v) for k, v in updates.items() if hasattr(col_def, k)]:
if k == "type":
if k == "visible":
col_def.visible = v == "on"
elif k == "type":
col_def.type = ColumnType(v) col_def.type = ColumnType(v)
elif k == "width": elif k == "width":
col_def.width = int(v) col_def.width = int(v)
@@ -112,7 +110,7 @@ class DataGridColumnsManager(MultipleInstance):
def toggle_column(self, col_id): def toggle_column(self, col_id):
logger.debug(f"toggle_column {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: if col_def is None:
logger.debug(f" column '{col_id}' is not found.") logger.debug(f" column '{col_id}' is not found.")
return Div(f"Column '{col_id}' not found") return Div(f"Column '{col_id}' not found")
@@ -123,7 +121,7 @@ class DataGridColumnsManager(MultipleInstance):
def show_column_details(self, col_id): def show_column_details(self, col_id):
logger.debug(f"show_column_details {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: if col_def is None:
logger.debug(f" column '{col_id}' is not found.") logger.debug(f" column '{col_id}' is not found.")
return Div(f"Column '{col_id}' not found") return Div(f"Column '{col_id}' not found")
@@ -133,25 +131,21 @@ class DataGridColumnsManager(MultipleInstance):
def show_all_columns(self): def show_all_columns(self):
return self._mk_inner_content() return self._mk_inner_content()
def update_column(self, col_id, client_response): def save_column_details(self, col_id, client_response):
logger.debug(f"update_column {col_id=}, {client_response=}") logger.debug(f"save_column_details {col_id=}, {client_response=}")
col_def = self._get_col_def_from_col_id(col_id, client_response, copy=False) self._get_updated_col_def_from_col_id(col_id, client_response, copy=False)
if col_def is None: self._parent.save_state()
logger.debug(f" column '{col_id}' is not found.")
else:
# save the new values
self._parent.save_state()
return self._mk_inner_content() return self._mk_inner_content()
def on_new_column(self): def on_new_column(self):
self._new_column = True 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) return self.mk_column_details(col_def)
def on_column_type_changed(self, col_id, client_response): def on_column_type_changed(self, col_id, client_response):
logger.debug(f"on_column_type_changed {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) return self.mk_column_details(col_def)
def _register_formula(self, col_def) -> None: def _register_formula(self, col_def) -> None:
@@ -240,7 +234,7 @@ class DataGridColumnsManager(MultipleInstance):
legend="Column details", legend="Column details",
cls="fieldset border-base-300 rounded-box" 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()), on_cancel=self.commands.show_all_columns()),
cls="mb-1", cls="mb-1",
), ),

View File

@@ -128,8 +128,8 @@ class BaseCompletionEngine(ABC):
Generate suggestions for the given context. Generate suggestions for the given context.
Args: Args:
context: The detected completion context context: The detected completion context (from the parser)
scope: The detected scope scope: The detected scope (table, column, row, cell...)
prefix: The current word prefix (for filtering) prefix: The current word prefix (for filtering)
Returns: Returns:

View File

@@ -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 ""))

View File

@@ -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
__all__ = ["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 ""))

View File

@@ -8,6 +8,7 @@ Provides context-aware suggestions for:
- Keywords: ``if``, ``else``, ``and``, ``or``, ``not``, ``WHERE`` - Keywords: ``if``, ``else``, ``and``, ``or``, ``not``, ``WHERE``
""" """
import re import re
from dataclasses import dataclass, field
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
from myfasthtml.core.dsl.types import Position, Suggestion 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): class FormulaCompletionEngine(BaseCompletionEngine):
""" """
Context-aware completion engine for formula expressions. Context-aware completion engine for formula expressions.
@@ -42,7 +58,7 @@ class FormulaCompletionEngine(BaseCompletionEngine):
"""Formula has no scope — always the same single-expression scope.""" """Formula has no scope — always the same single-expression scope."""
return None 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. Detect completion context based on cursor position in formula text.
@@ -52,129 +68,80 @@ class FormulaCompletionEngine(BaseCompletionEngine):
scope: Unused (formulas have no scopes). scope: Unused (formulas have no scopes).
Returns: Returns:
Context string: ``"column_ref"``, ``"cross_table"``, FormulaContext describing the kind of completion expected.
``"function"``, ``"keyword"``, or ``"general"``.
""" """
# Get text up to cursor
lines = text.split("\n") lines = text.split("\n")
line_idx = min(cursor.line, len(lines) - 1) line_text = lines[min(cursor.line, len(lines) - 1)]
line_text = lines[line_idx]
text_before = line_text[:cursor.ch] text_before = line_text[:cursor.ch]
# Check if we are inside a { ... } reference
last_brace = text_before.rfind("{") last_brace = text_before.rfind("{")
if last_brace >= 0: if last_brace >= 0:
inside = text_before[last_brace + 1:] inside = text_before[last_brace + 1:]
if "}" not in inside: if "}" not in inside:
if "." in inside: if "." in inside:
return "cross_table" cross_table_name = inside[:inside.rfind(".")]
return "column_ref" return FormulaContext("cross_table", cross_table_name)
return FormulaContext("column_ref")
# Check if we are typing a function name (alphanumeric at word start) if re.search(r"[a-z_][a-z0-9_]*$", text_before, re.IGNORECASE):
word_match = re.search(r"[a-z_][a-z0-9_]*$", text_before, re.IGNORECASE) return FormulaContext("function_or_keyword")
if word_match:
return "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. Generate suggestions based on the detected context.
Args: Args:
text: The full formula text. context: FormulaContext from ``detect_context``.
cursor: Cursor position. scope: Unused (formulas have no scopes).
scope: Unused. prefix: Current word prefix (filtering handled by base class).
context: String from ``detect_context``.
Returns: Returns:
List of Suggestion objects. List of Suggestion objects.
""" """
suggestions = [] match context.kind:
case "column_ref":
return self._column_suggestions(self.table_name)
if context == "column_ref": case "cross_table":
# Suggest columns from the current table return self._column_suggestions(context.cross_table_name)
suggestions += self._column_suggestions(self.table_name)
elif context == "cross_table": case "function_or_keyword":
# Get the table name prefix from text_before return self._function_suggestions() + self._keyword_suggestions()
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 ""
# Suggest columns from the referenced table case _: # general
if table_prefix: return (
suggestions += self._column_suggestions(table_prefix) self._function_suggestions()
else: + self._keyword_suggestions()
suggestions += self._table_suggestions() + [Suggestion("{", "Column reference", "keyword")]
)
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
# ==================== Private helpers ==================== # ==================== 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.""" """Get column name suggestions for a table."""
try: try:
columns = self.provider.list_columns(table_name) columns = self.provider.list_columns(table_name)
return [ return [Suggestion(col, f"Column from {table_name}", "column") for col in (columns or [])]
Suggestion(
label=col,
detail=f"Column from {table_name}",
insert_text=col,
)
for col in (columns or [])
]
except Exception: except Exception:
return [] return []
def _table_suggestions(self) -> list: def _table_suggestions(self) -> list[Suggestion]:
"""Get table name suggestions.""" """Get table name suggestions."""
try: try:
tables = self.provider.list_tables() tables = self.provider.list_tables()
return [ return [Suggestion(t, "Table", "table") for t in (tables or [])]
Suggestion(
label=t,
detail="Table",
insert_text=t,
)
for t in (tables or [])
]
except Exception: except Exception:
return [] return []
def _function_suggestions(self) -> list: def _function_suggestions(self) -> list[Suggestion]:
"""Get built-in function name suggestions.""" """Get built-in function name suggestions."""
return [ return [
Suggestion( Suggestion(f"{name}(", name, "function")
label=name,
detail="Function",
insert_text=f"{name}(",
)
for name in sorted(BUILTIN_FUNCTIONS.keys()) for name in sorted(BUILTIN_FUNCTIONS.keys())
] ]
def _keyword_suggestions(self) -> list: def _keyword_suggestions(self) -> list[Suggestion]:
"""Get keyword suggestions.""" """Get keyword suggestions."""
return [ return [Suggestion(kw, "Keyword", "keyword") for kw in FORMULA_KEYWORDS]
Suggestion(label=kw, detail="Keyword", insert_text=kw)
for kw in FORMULA_KEYWORDS
]

View File

@@ -3,25 +3,6 @@ class FormulaError(Exception):
pass 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): class FormulaValidationError(FormulaError):
"""Raised when the formula is syntactically correct but semantically invalid.""" """Raised when the formula is syntactically correct but semantically invalid."""
pass pass

View File

@@ -6,7 +6,7 @@ No indentation handling needed — formulas are single-line expressions.
""" """
from lark import Lark, UnexpectedInput from lark import Lark, UnexpectedInput
from .exceptions import FormulaSyntaxError from myfasthtml.core.dsl.exceptions import DSLSyntaxError
from .grammar import FORMULA_GRAMMAR from .grammar import FORMULA_GRAMMAR
@@ -39,7 +39,7 @@ class FormulaParser:
lark.Tree: The parsed AST. lark.Tree: The parsed AST.
Raises: Raises:
FormulaSyntaxError: If the text has syntax errors. DSLSyntaxError: If the text has syntax errors.
""" """
text = text.strip() text = text.strip()
if not text: if not text:
@@ -52,7 +52,7 @@ class FormulaParser:
if hasattr(e, "get_context"): if hasattr(e, "get_context"):
context = e.get_context(text) context = e.get_context(text)
raise FormulaSyntaxError( raise DSLSyntaxError(
message=self._format_error_message(e), message=self._format_error_message(e),
line=getattr(e, "line", None), line=getattr(e, "line", None),
column=getattr(e, "column", None), column=getattr(e, "column", None),

View File

@@ -34,7 +34,7 @@ def parse_formula(text: str) -> FormulaDefinition | None:
FormulaDefinition on success, None if text is empty. FormulaDefinition on success, None if text is empty.
Raises: Raises:
FormulaSyntaxError: If the formula text is syntactically invalid. DSLSyntaxError: If the formula text is syntactically invalid.
""" """
text = text.strip() if text else "" text = text.strip() if text else ""
if not text: if not text:
@@ -80,7 +80,7 @@ class FormulaEngine:
formula_text: The formula expression string. formula_text: The formula expression string.
Raises: Raises:
FormulaSyntaxError: If the formula is syntactically invalid. DSLSyntaxError: If the formula is syntactically invalid.
FormulaCycleError: If the formula would create a circular dependency. FormulaCycleError: If the formula would create a circular dependency.
""" """
formula_text = formula_text.strip() if formula_text else "" formula_text = formula_text.strip() if formula_text else ""

View File

@@ -12,7 +12,7 @@ from starlette.routing import Mount
from myfasthtml.core.constants import Routes, ROUTE_ROOT from myfasthtml.core.constants import Routes, ROUTE_ROOT
from myfasthtml.core.dsl.types import Position from myfasthtml.core.dsl.types import Position
from myfasthtml.core.dsls import DslsManager 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 from myfasthtml.test.MyFT import MyFT
utils_app, utils_rt = fast_app() utils_app, utils_rt = fast_app()

View File

@@ -1,469 +1,415 @@
import shutil import shutil
from dataclasses import dataclass, field from dataclasses import dataclass, field
from unittest.mock import Mock
import pytest 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.Search import Search
from myfasthtml.controls.datagrid_objects import DataGridColumnState from myfasthtml.controls.datagrid_objects import DataGridColumnState
from myfasthtml.core.constants import ColumnType from myfasthtml.core.constants import ColumnType
from myfasthtml.core.instances import InstancesManager, MultipleInstance from myfasthtml.core.instances import InstancesManager, MultipleInstance
from myfasthtml.test.matcher import ( from myfasthtml.test.matcher import (
matches, find_one, find, Contains, TestIcon, TestObject matches, find_one, find, Contains, TestIcon, TestObject
) )
@dataclass @dataclass
class MockDatagridState: class MockDatagridState:
"""Mock state object that mimics DatagridState.""" """Mock state object that mimics DatagridState."""
columns: list = field(default_factory=list) columns: list = field(default_factory=list)
class MockDataGrid(MultipleInstance): class MockDataGrid(MultipleInstance):
"""Mock DataGrid parent for testing DataGridColumnsManager.""" """Mock DataGrid parent for testing DataGridColumnsManager."""
def __init__(self, parent, columns=None, _id=None): def __init__(self, parent, columns=None, _id=None):
super().__init__(parent, _id=_id) super().__init__(parent, _id=_id)
self._state = MockDatagridState(columns=columns or []) self._state = MockDatagridState(columns=columns or [])
self._save_state_called = False self._save_state_called = False
def get_state(self): def get_state(self):
return self._state return self._state
def save_state(self): def save_state(self):
self._save_state_called = True self._save_state_called = True
def get_table_name(self):
return "mock_table"
@pytest.fixture @pytest.fixture
def mock_datagrid(root_instance): def mock_datagrid(root_instance):
"""Create a mock DataGrid with sample columns.""" """Create a mock DataGrid with sample columns."""
columns = [ columns = [
DataGridColumnState(col_id="name", col_index=0, title="Name", type=ColumnType.Text, visible=True), 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="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), DataGridColumnState(col_id="email", col_index=2, title="Email", type=ColumnType.Text, visible=False),
] ]
yield MockDataGrid(root_instance, columns=columns, _id="test-datagrid") yield MockDataGrid(root_instance, columns=columns, _id="test-datagrid")
InstancesManager.reset() InstancesManager.reset()
@pytest.fixture @pytest.fixture
def columns_manager(mock_datagrid): def columns_manager(mock_datagrid):
"""Create a DataGridColumnsManager instance for testing.""" """Create a DataGridColumnsManager instance for testing."""
shutil.rmtree(".myFastHtmlDb", ignore_errors=True) shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
yield DataGridColumnsManager(mock_datagrid) yield DataGridColumnsManager(mock_datagrid)
shutil.rmtree(".myFastHtmlDb", ignore_errors=True) shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
class TestDataGridColumnsManagerBehaviour: class TestDataGridColumnsManagerBehaviour:
"""Tests for DataGridColumnsManager behavior and logic.""" """Tests for DataGridColumnsManager behavior and logic."""
# ========================================================================= # =========================================================================
# Initialization # Get Column Definition
# ========================================================================= # =========================================================================
def test_i_can_create_columns_manager(self, mock_datagrid): def test_i_can_get_existing_column_by_id(self, columns_manager):
"""Test that DataGridColumnsManager can be created with a DataGrid parent.""" """Test finding an existing column by its ID."""
cm = DataGridColumnsManager(mock_datagrid) col_def = columns_manager._get_col_def_from_col_id("name")
assert cm is not None assert col_def is not None
assert cm._parent == mock_datagrid assert col_def.col_id == "name"
assert isinstance(cm.commands, Commands) assert col_def.title == "Name"
# ========================================================================= def test_i_cannot_get_nonexistent_column(self, columns_manager):
# Columns Property """Test that getting a nonexistent column returns None."""
# ========================================================================= col_def = columns_manager._get_col_def_from_col_id("nonexistent")
def test_columns_property_returns_parent_state_columns(self, columns_manager, mock_datagrid): assert col_def is None
"""Test that columns property returns columns from parent's state."""
columns = columns_manager.columns
assert columns == mock_datagrid.get_state().columns def test_i_can_get_and_update_column_state(self, columns_manager):
assert len(columns) == 3 """Test that get_col_def_from_col_id updates the column state."""
assert columns[0].col_id == "name" 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):
# Get Column Definition """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)
def test_i_can_get_existing_column_by_id(self, columns_manager): assert col_def.visible is False
"""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" # Toggle Column Visibility
assert col_def.title == "Name" # =========================================================================
def test_i_cannot_get_nonexistent_column(self, columns_manager): @pytest.mark.parametrize("col_id, initial_visible, expected_visible", [
"""Test that getting a nonexistent column returns None.""" ("name", True, False), # visible -> hidden
col_def = columns_manager._get_col_def_from_col_id("nonexistent") ("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
assert col_def is None columns_manager.toggle_column(col_id)
# ========================================================================= col_def = columns_manager._get_col_def_from_col_id(col_id)
# Toggle Column Visibility assert col_def.visible == expected_visible
# =========================================================================
@pytest.mark.parametrize("col_id, initial_visible, expected_visible", [ def test_toggle_column_saves_state(self, columns_manager, mock_datagrid):
("name", True, False), # visible -> hidden """Test that toggle_column calls save_state on parent."""
("email", False, True), # hidden -> visible mock_datagrid._save_state_called = False
])
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) columns_manager.toggle_column("name")
assert col_def.visible == expected_visible assert mock_datagrid._save_state_called is True
def test_toggle_column_saves_state(self, columns_manager, mock_datagrid): def test_toggle_column_returns_column_label(self, columns_manager):
"""Test that toggle_column calls save_state on parent.""" """Test that toggle_column returns an HTML element."""
mock_datagrid._save_state_called = False result = columns_manager.toggle_column("name")
columns_manager.toggle_column("name") assert isinstance(result, FT)
assert mock_datagrid._save_state_called is True 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")
def test_toggle_column_returns_column_label(self, columns_manager): expected = Div("Column 'nonexistent' not found")
"""Test that toggle_column returns the updated column label.""" assert matches(result, expected)
result = columns_manager.toggle_column("name")
# Result should be a Div with the column label structure # =========================================================================
assert result is not None # Show All Columns
assert hasattr(result, 'tag') # =========================================================================
def test_i_cannot_toggle_nonexistent_column(self, columns_manager): def test_show_all_columns_returns_search_component(self, columns_manager):
"""Test that toggling a nonexistent column returns an error message.""" """Test that mk_all_columns returns a Search component."""
result = columns_manager.toggle_column("nonexistent") result = columns_manager.mk_all_columns()
expected = Div("Column 'nonexistent' not found") assert isinstance(result, Search)
assert matches(result, expected)
# ========================================================================= def test_show_all_columns_returns_configured_search(self, columns_manager):
# Show Column Details """Test that mk_all_columns returns a correctly configured Search component."""
# ========================================================================= result = columns_manager.mk_all_columns()
def test_i_can_show_column_details_for_existing_column(self, columns_manager): assert result.items_names == "Columns"
"""Test that show_column_details returns the details form for an existing column.""" assert len(result.items) == 3
result = columns_manager.show_column_details("name") col_def = result.items[0]
assert result.get_attr(col_def) == col_def.col_id
# Should contain a Form - check by finding form tag in children # =========================================================================
expected = Form() # Update Column
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): def test_i_can_update_column_title(self, columns_manager):
"""Test that showing details for nonexistent column returns error message.""" """Test updating a column's title via client_response."""
result = columns_manager.show_column_details("nonexistent") columns_manager.save_column_details("name", {"title": "New Name"})
expected = Div("Column 'nonexistent' not found") col_def = columns_manager._get_col_def_from_col_id("name")
assert matches(result, expected) assert col_def.title == "New Name"
# ========================================================================= def test_i_can_update_column_visibility_via_form(self, columns_manager):
# Show All Columns """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
def test_show_all_columns_returns_search_component(self, columns_manager): # Unchecked checkbox sends nothing, checked sends "on"
"""Test that show_all_columns returns a Search component.""" columns_manager.save_column_details("name", {"visible": "off"}) # Not "on" means unchecked
result = columns_manager.show_all_columns() col_def = columns_manager._get_col_def_from_col_id("name")
assert col_def.visible is False
assert isinstance(result, Search) # 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_show_all_columns_contains_all_columns(self, columns_manager): def test_i_can_update_column_type(self, columns_manager):
"""Test that show_all_columns Search contains all columns.""" """Test updating a column's type."""
result = columns_manager.show_all_columns() columns_manager.save_column_details("name", {"type": "Number"})
assert len(result.items) == 3 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):
# Update Column """Test updating a column's width."""
# ========================================================================= columns_manager.save_column_details("name", {"width": "200"})
def test_i_can_update_column_title(self, columns_manager): col_def = columns_manager._get_col_def_from_col_id("name")
"""Test updating a column's title via client_response.""" assert col_def.width == 200
client_response = {"title": "New Name"}
columns_manager.update_column("name", client_response) 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
col_def = columns_manager._get_col_def_from_col_id("name") columns_manager.save_column_details("name", {"title": "Updated"})
assert col_def.title == "New Name"
def test_i_can_update_column_visibility_via_form(self, columns_manager): assert mock_datagrid._save_state_called is True
"""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" def test_update_column_ignores_unknown_attributes(self, columns_manager):
client_response = {"visible": "off"} # Not "on" means unchecked """Test that save_column_details ignores attributes not in DataGridColumnState."""
columns_manager.update_column("name", client_response) columns_manager.save_column_details("name", {"unknown_attr": "value", "title": "New Title"})
assert col_def.visible is False col_def = columns_manager._get_col_def_from_col_id("name")
assert col_def.title == "New Title"
# Check it back on assert not hasattr(col_def, "unknown_attr")
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)
class TestDataGridColumnsManagerRender: class TestDataGridColumnsManagerRender:
"""Tests for DataGridColumnsManager HTML rendering.""" """Tests for DataGridColumnsManager HTML rendering."""
@pytest.fixture @pytest.fixture
def columns_manager(self, mock_datagrid): def columns_manager(self, mock_datagrid):
"""Create a fresh DataGridColumnsManager for render tests.""" """Create a fresh DataGridColumnsManager for render tests."""
shutil.rmtree(".myFastHtmlDb", ignore_errors=True) shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
cm = DataGridColumnsManager(mock_datagrid) cm = DataGridColumnsManager(mock_datagrid)
yield cm yield cm
shutil.rmtree(".myFastHtmlDb", ignore_errors=True) shutil.rmtree(".myFastHtmlDb", ignore_errors=True)
# ========================================================================= # =========================================================================
# Global Structure # Global Structure
# ========================================================================= # =========================================================================
def test_i_can_render_columns_manager_with_columns(self, columns_manager): def test_i_can_render_columns_manager_with_columns(self, columns_manager):
"""Test that DataGridColumnsManager renders with correct global structure. """Test that DataGridColumnsManager renders with correct global structure.
Why these elements matter: Why these elements matter:
- id: Required for HTMX targeting in commands - id: Required for HTMX targeting in commands
- Contains Search component: Main content for column list - Contains Search component: Main content for column list
""" """
html = columns_manager.render() html = columns_manager.render()
expected = Div( expected = Div(
TestObject(Search), # Search component TestObject(Search), # Search component (column list)
id=columns_manager._id, Div(), # New column button
) id=columns_manager._id,
)
assert matches(html, expected) assert matches(html, expected)
# ========================================================================= # =========================================================================
# mk_column_label # mk_column_label
# ========================================================================= # =========================================================================
def test_column_label_has_checkbox_and_details_navigation(self, columns_manager): def test_column_label_has_checkbox_and_details_navigation(self, columns_manager):
"""Test that column label contains checkbox and navigation to details. """Test that column label contains checkbox and navigation to details.
Why these elements matter: Why these elements matter:
- Checkbox (Input type=checkbox): Controls column visibility - Checkbox (Input type=checkbox): Controls column visibility
- Label with column ID: Identifies the column - Label with column ID: Identifies the column
- Chevron icon: Indicates navigation to details - Chevron icon: Indicates navigation to details
- id with tcolman_ prefix: Required for HTMX swap targeting - id with tcolman_ prefix: Required for HTMX swap targeting
""" """
col_def = columns_manager._get_col_def_from_col_id("name") col_def = columns_manager._get_col_def_from_col_id("name")
label = columns_manager.mk_column_label(col_def) label = columns_manager.mk_column_label(col_def)
# Should have the correct ID pattern # Should have the correct ID pattern
expected = Div( expected = Div(
id=f"tcolman_{columns_manager._id}-name", id=f"tcolman_{columns_manager._id}-name",
cls=Contains("flex"), cls=Contains("flex"),
) )
assert matches(label, expected) assert matches(label, expected)
# Should contain a checkbox # Should contain a checkbox
checkbox = find_one(label, Input(type="checkbox")) checkbox = find_one(label, Input(type="checkbox"))
assert checkbox is not None assert checkbox is not None
# Should contain chevron icon for navigation # Should contain chevron icon for navigation
chevron = find_one(label, TestIcon("chevron_right20_regular")) chevron = find_one(label, TestIcon("chevron_right20_regular"))
assert chevron is not None assert chevron is not None
def test_column_label_checkbox_is_checked_when_visible(self, columns_manager): def test_column_label_checkbox_is_checked_when_visible(self, columns_manager):
"""Test that checkbox is checked when column is visible. """Test that checkbox is checked when column is visible.
Why this matters: Why this matters:
- checked attribute: Reflects current visibility state - checked attribute: Reflects current visibility state
- User can see which columns are visible - User can see which columns are visible
""" """
col_def = columns_manager._get_col_def_from_col_id("name") col_def = columns_manager._get_col_def_from_col_id("name")
assert col_def.visible is True assert col_def.visible is True
label = columns_manager.mk_column_label(col_def) label = columns_manager.mk_column_label(col_def)
checkbox = find_one(label, Input(type="checkbox")) checkbox = find_one(label, Input(type="checkbox"))
# Checkbox should have checked attribute # Checkbox should have checked attribute
assert checkbox.attrs.get("checked") is True assert checkbox.attrs.get("checked") is True
def test_column_label_checkbox_is_unchecked_when_hidden(self, columns_manager): def test_column_label_checkbox_is_unchecked_when_hidden(self, columns_manager):
"""Test that checkbox is unchecked when column is hidden. """Test that checkbox is unchecked when column is hidden.
Why this matters: Why this matters:
- No checked attribute: Indicates column is hidden - No checked attribute: Indicates column is hidden
- Visual feedback for user - Visual feedback for user
""" """
col_def = columns_manager._get_col_def_from_col_id("email") col_def = columns_manager._get_col_def_from_col_id("email")
assert col_def.visible is False assert col_def.visible is False
label = columns_manager.mk_column_label(col_def) label = columns_manager.mk_column_label(col_def)
checkbox = find_one(label, Input(type="checkbox")) checkbox = find_one(label, Input(type="checkbox"))
# Checkbox should not have checked attribute (or it should be False/None) # Checkbox should not have checked attribute (or it should be False/None)
checked = checkbox.attrs.get("checked") checked = checkbox.attrs.get("checked")
assert checked is None or checked is False assert checked is None or checked is False
# ========================================================================= # =========================================================================
# mk_column_details # mk_column_details
# ========================================================================= # =========================================================================
def test_column_details_contains_all_form_fields(self, columns_manager): def test_column_details_contains_all_form_fields(self, columns_manager):
"""Test that column details form contains all required fields. """Test that column details form contains all required fields.
Why these elements matter: Why these elements matter:
- col_id field (readonly): Shows column identifier - col_id field (readonly): Shows column identifier
- title field: Editable column display name - title field: Editable column display name
- visible checkbox: Toggle visibility - visible checkbox: Toggle visibility
- type select: Change column type - type select: Change column type
- width input: Set column width - width input: Set column width
""" """
col_def = columns_manager._get_col_def_from_col_id("name") col_def = columns_manager._get_col_def_from_col_id("name")
details = columns_manager.mk_column_details(col_def) details = columns_manager.mk_column_details(col_def)
# Should contain Form # Should contain Form
form = Form() form = Form()
del form.attrs["enctype"] del form.attrs["enctype"]
form = find_one(details, form) form = find_one(details, form)
assert form is not None assert form is not None
# Should contain all required input fields # Should contain all required input fields
col_id_input = find_one(form, Input(name="col_id")) col_id_input = find_one(form, Input(name="col_id"))
assert col_id_input is not None assert col_id_input is not None
assert col_id_input.attrs.get("readonly") is True assert col_id_input.attrs.get("readonly") is True
title_input = find_one(form, Input(name="title")) title_input = find_one(form, Input(name="title"))
assert title_input is not None assert title_input is not None
visible_checkbox = find_one(form, Input(name="visible", type="checkbox")) visible_checkbox = find_one(form, Input(name="visible", type="checkbox"))
assert visible_checkbox is not None assert visible_checkbox is not None
type_select = find_one(form, Select(name="type")) type_select = find_one(form, Select(name="type"))
assert type_select is not None assert type_select is not None
width_input = find_one(form, Input(name="width", type="number")) width_input = find_one(form, Input(name="width", type="number"))
assert width_input is not None assert width_input is not None
def test_column_details_has_back_button(self, columns_manager): def test_column_details_has_back_button(self, columns_manager):
"""Test that column details has a back button to return to all columns. """Test that column details has a back button to return to all columns.
Why this matters: Why this matters:
- Back navigation: User can return to column list - Back navigation: User can return to column list
- Chevron left icon: Visual indicator of back action - Chevron left icon: Visual indicator of back action
""" """
col_def = columns_manager._get_col_def_from_col_id("name") col_def = columns_manager._get_col_def_from_col_id("name")
details = columns_manager.mk_column_details(col_def) details = columns_manager.mk_column_details(col_def)
# Should contain back chevron icon # Should contain back chevron icon
back_icon = find_one(details, TestIcon("chevron_left20_regular", wrapper="span")) back_icon = find_one(details, TestIcon("chevron_left20_regular", wrapper="span"))
assert back_icon is not None assert back_icon is not None
def test_column_details_form_has_fieldset_with_legend(self, columns_manager): def test_column_details_form_has_fieldset_with_legend(self, columns_manager):
"""Test that column details form has a fieldset with legend. """Test that column details form has a fieldset with legend.
Why this matters: Why this matters:
- Fieldset groups related fields - Fieldset groups related fields
- Legend provides context ("Column details") - Legend provides context ("Column details")
""" """
col_def = columns_manager._get_col_def_from_col_id("name") col_def = columns_manager._get_col_def_from_col_id("name")
details = columns_manager.mk_column_details(col_def) details = columns_manager.mk_column_details(col_def)
fieldset = find_one(details, Fieldset(legend="Column details")) fieldset = find_one(details, Fieldset(legend="Column details"))
assert fieldset is not None assert fieldset is not None
# ========================================================================= # =========================================================================
# mk_all_columns # show_column_details
# ========================================================================= # =========================================================================
def test_all_columns_uses_search_component(self, columns_manager): def test_i_can_show_column_details_for_existing_column(self, columns_manager):
"""Test that mk_all_columns returns a Search component. """Test that show_column_details returns a form-based view for an existing column.
Why this matters: Why these elements matter:
- Search component: Enables filtering columns by name - Form element: show_column_details must return an editable form view
- items_names="Columns": Labels the search appropriately - Exactly one form: Ensures the response is unambiguous (not multiple forms)
""" """
result = columns_manager.mk_all_columns() result = columns_manager.show_column_details("name")
assert isinstance(result, Search) expected = Form()
assert result.items_names == "Columns" del expected.attrs["enctype"]
forms = find(result, expected)
assert len(forms) == 1, "Should contain exactly one form"
def test_all_columns_search_has_correct_configuration(self, columns_manager): # =========================================================================
"""Test that Search component is configured correctly. # mk_all_columns
# =========================================================================
Why these elements matter: def test_all_columns_renders_all_column_labels(self, columns_manager):
- items: Contains all column definitions """Test that all columns render produces labels for all columns.
- 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 Why this matters:
assert len(result.items) == 3 - All columns visible in list
- Each column has its label rendered
"""
search = columns_manager.mk_all_columns()
rendered = search.render()
# get_attr should return col_id # Should find 3 column labels in the results
col_def = result.items[0] results_div = find_one(rendered, Div(id=f"{search._id}-results"))
assert result.get_attr(col_def) == col_def.col_id assert results_div is not None
def test_all_columns_renders_all_column_labels(self, columns_manager): # Each column should have a label with tcolman_ prefix
"""Test that all columns render produces labels for all columns. for col_id in ["name", "age", "email"]:
label = find_one(results_div, Div(id=f"tcolman_{columns_manager._id}-{col_id}"))
Why this matters: assert label is not None, f"Column label for '{col_id}' should be present"
- 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"

View File

@@ -4,7 +4,8 @@ Tests for the FormulaEngine facade.
import numpy as np import numpy as np
import pytest 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 from myfasthtml.core.formula.engine import FormulaEngine
@@ -72,9 +73,9 @@ class TestSetFormula:
assert totals[2] == 50 assert totals[2] == 50
def test_i_cannot_set_invalid_formula(self): 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() engine = make_engine()
with pytest.raises(FormulaSyntaxError): with pytest.raises(DSLSyntaxError):
engine.set_formula("t", "col", "{Price} * * {Qty}") engine.set_formula("t", "col", "{Price} * * {Qty}")
def test_i_cannot_set_formula_with_cycle(self): def test_i_cannot_set_formula_with_cycle(self):

View File

@@ -11,7 +11,7 @@ from myfasthtml.core.formula.dataclasses import (
LiteralNode, LiteralNode,
FormulaDefinition, FormulaDefinition,
) )
from myfasthtml.core.formula.dsl.exceptions import FormulaSyntaxError from myfasthtml.core.dsl.exceptions import DSLSyntaxError
from myfasthtml.core.formula.engine import parse_formula from myfasthtml.core.formula.engine import parse_formula
@@ -177,12 +177,12 @@ def test_i_can_parse_nested_functions():
"123 + + 456", # double operator "123 + + 456", # double operator
]) ])
def test_i_cannot_parse_invalid_syntax(formula_text): def test_i_cannot_parse_invalid_syntax(formula_text):
"""Test that invalid syntax raises FormulaSyntaxError.""" """Test that invalid syntax raises DSLSyntaxError."""
with pytest.raises(FormulaSyntaxError): with pytest.raises(DSLSyntaxError):
parse_formula(formula_text) parse_formula(formula_text)
def test_i_cannot_parse_unclosed_brace(): def test_i_cannot_parse_unclosed_brace():
"""Test that an unclosed brace raises FormulaSyntaxError.""" """Test that an unclosed brace raises DSLSyntaxError."""
with pytest.raises(FormulaSyntaxError): with pytest.raises(DSLSyntaxError):
parse_formula("{Price") parse_formula("{Price")

View File

@@ -30,7 +30,9 @@ def session():
@pytest.fixture @pytest.fixture
def parent(session): 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 @pytest.fixture