I can validate formatting in editor
This commit is contained in:
@@ -5,23 +5,19 @@ Tests the parsing of DSL text into ScopedRule objects.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from myfasthtml.core.formatting.dsl import (
|
||||
parse_dsl,
|
||||
ColumnScope,
|
||||
RowScope,
|
||||
CellScope,
|
||||
ScopedRule,
|
||||
DSLSyntaxError,
|
||||
)
|
||||
from myfasthtml.core.formatting.dataclasses import (
|
||||
Condition,
|
||||
Style,
|
||||
FormatRule,
|
||||
NumberFormatter,
|
||||
DateFormatter,
|
||||
BooleanFormatter,
|
||||
TextFormatter,
|
||||
EnumFormatter,
|
||||
NumberFormatter,
|
||||
DateFormatter,
|
||||
BooleanFormatter,
|
||||
TextFormatter,
|
||||
EnumFormatter,
|
||||
)
|
||||
from myfasthtml.core.formatting.dsl import (
|
||||
parse_dsl,
|
||||
ColumnScope,
|
||||
RowScope,
|
||||
CellScope,
|
||||
DSLSyntaxError,
|
||||
)
|
||||
|
||||
|
||||
@@ -31,102 +27,102 @@ from myfasthtml.core.formatting.dataclasses import (
|
||||
|
||||
|
||||
class TestColumnScope:
|
||||
"""Tests for column scope parsing."""
|
||||
|
||||
def test_i_can_parse_column_scope(self):
|
||||
"""Test parsing a simple column scope."""
|
||||
dsl = """
|
||||
"""Tests for column scope parsing."""
|
||||
|
||||
def test_i_can_parse_column_scope(self):
|
||||
"""Test parsing a simple column scope."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
|
||||
def test_i_can_parse_column_scope_with_quoted_name(self):
|
||||
"""Test parsing a column scope with quoted name containing spaces."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
|
||||
def test_i_can_parse_column_scope_with_quoted_name(self):
|
||||
"""Test parsing a column scope with quoted name containing spaces."""
|
||||
dsl = """
|
||||
column "total amount":
|
||||
style("error")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "total amount"
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "total amount"
|
||||
|
||||
|
||||
class TestRowScope:
|
||||
"""Tests for row scope parsing."""
|
||||
|
||||
def test_i_can_parse_row_scope(self):
|
||||
"""Test parsing a row scope."""
|
||||
dsl = """
|
||||
"""Tests for row scope parsing."""
|
||||
|
||||
def test_i_can_parse_row_scope(self):
|
||||
"""Test parsing a row scope."""
|
||||
dsl = """
|
||||
row 0:
|
||||
style("neutral")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, RowScope)
|
||||
assert rules[0].scope.row == 0
|
||||
|
||||
def test_i_can_parse_row_scope_with_large_index(self):
|
||||
"""Test parsing a row scope with a large index."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, RowScope)
|
||||
assert rules[0].scope.row == 0
|
||||
|
||||
def test_i_can_parse_row_scope_with_large_index(self):
|
||||
"""Test parsing a row scope with a large index."""
|
||||
dsl = """
|
||||
row 999:
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].scope.row == 999
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].scope.row == 999
|
||||
|
||||
|
||||
class TestCellScope:
|
||||
"""Tests for cell scope parsing."""
|
||||
|
||||
def test_i_can_parse_cell_scope_with_coords(self):
|
||||
"""Test parsing a cell scope with coordinates."""
|
||||
dsl = """
|
||||
"""Tests for cell scope parsing."""
|
||||
|
||||
def test_i_can_parse_cell_scope_with_coords(self):
|
||||
"""Test parsing a cell scope with coordinates."""
|
||||
dsl = """
|
||||
cell (amount, 3):
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, CellScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
assert rules[0].scope.row == 3
|
||||
assert rules[0].scope.cell_id is None
|
||||
|
||||
def test_i_can_parse_cell_scope_with_quoted_column(self):
|
||||
"""Test parsing a cell scope with quoted column name."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, CellScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
assert rules[0].scope.row == 3
|
||||
assert rules[0].scope.cell_id is None
|
||||
|
||||
def test_i_can_parse_cell_scope_with_quoted_column(self):
|
||||
"""Test parsing a cell scope with quoted column name."""
|
||||
dsl = """
|
||||
cell ("total amount", 5):
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].scope.column == "total amount"
|
||||
assert rules[0].scope.row == 5
|
||||
|
||||
def test_i_can_parse_cell_scope_with_id(self):
|
||||
"""Test parsing a cell scope with cell ID."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].scope.column == "total amount"
|
||||
assert rules[0].scope.row == 5
|
||||
|
||||
def test_i_can_parse_cell_scope_with_id(self):
|
||||
"""Test parsing a cell scope with cell ID."""
|
||||
dsl = """
|
||||
cell tcell_grid1-3-2:
|
||||
style("highlight")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, CellScope)
|
||||
assert rules[0].scope.cell_id == "tcell_grid1-3-2"
|
||||
assert rules[0].scope.column is None
|
||||
assert rules[0].scope.row is None
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert isinstance(rules[0].scope, CellScope)
|
||||
assert rules[0].scope.cell_id == "tcell_grid1-3-2"
|
||||
assert rules[0].scope.column is None
|
||||
assert rules[0].scope.row is None
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -135,61 +131,61 @@ cell tcell_grid1-3-2:
|
||||
|
||||
|
||||
class TestStyleParsing:
|
||||
"""Tests for style expression parsing."""
|
||||
|
||||
def test_i_can_parse_style_with_preset(self):
|
||||
"""Test parsing style with preset only."""
|
||||
dsl = """
|
||||
"""Tests for style expression parsing."""
|
||||
|
||||
def test_i_can_parse_style_with_preset(self):
|
||||
"""Test parsing style with preset only."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert rules[0].rule.style is not None
|
||||
assert rules[0].rule.style.preset == "error"
|
||||
|
||||
def test_i_can_parse_style_without_preset(self):
|
||||
"""Test parsing style without preset, with direct properties."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert rules[0].rule.style is not None
|
||||
assert rules[0].rule.style.preset == "error"
|
||||
|
||||
def test_i_can_parse_style_without_preset(self):
|
||||
"""Test parsing style without preset, with direct properties."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style(color="red", bold=True)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert style.preset is None
|
||||
assert style.color == "red"
|
||||
assert style.font_weight == "bold"
|
||||
|
||||
def test_i_can_parse_style_with_preset_and_options(self):
|
||||
"""Test parsing style with preset and additional options."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert style.preset is None
|
||||
assert style.color == "red"
|
||||
assert style.font_weight == "bold"
|
||||
|
||||
def test_i_can_parse_style_with_preset_and_options(self):
|
||||
"""Test parsing style with preset and additional options."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error", bold=True, italic=True)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert style.preset == "error"
|
||||
assert style.font_weight == "bold"
|
||||
assert style.font_style == "italic"
|
||||
|
||||
@pytest.mark.parametrize("option,attr_name,attr_value", [
|
||||
("bold=True", "font_weight", "bold"),
|
||||
("italic=True", "font_style", "italic"),
|
||||
("underline=True", "text_decoration", "underline"),
|
||||
("strikethrough=True", "text_decoration", "line-through"),
|
||||
])
|
||||
def test_i_can_parse_style_options(self, option, attr_name, attr_value):
|
||||
"""Test parsing individual style options."""
|
||||
dsl = f"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert style.preset == "error"
|
||||
assert style.font_weight == "bold"
|
||||
assert style.font_style == "italic"
|
||||
|
||||
@pytest.mark.parametrize("option,attr_name,attr_value", [
|
||||
("bold=True", "font_weight", "bold"),
|
||||
("italic=True", "font_style", "italic"),
|
||||
("underline=True", "text_decoration", "underline"),
|
||||
("strikethrough=True", "text_decoration", "line-through"),
|
||||
])
|
||||
def test_i_can_parse_style_options(self, option, attr_name, attr_value):
|
||||
"""Test parsing individual style options."""
|
||||
dsl = f"""
|
||||
column amount:
|
||||
style({option})
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert getattr(style, attr_name) == attr_value
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
style = rules[0].rule.style
|
||||
assert getattr(style, attr_name) == attr_value
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -198,100 +194,100 @@ column amount:
|
||||
|
||||
|
||||
class TestFormatParsing:
|
||||
"""Tests for format expression parsing."""
|
||||
|
||||
def test_i_can_parse_format_preset(self):
|
||||
"""Test parsing format with preset."""
|
||||
dsl = """
|
||||
"""Tests for format expression parsing."""
|
||||
|
||||
def test_i_can_parse_format_preset(self):
|
||||
"""Test parsing format with preset."""
|
||||
dsl = """
|
||||
column amount:
|
||||
format("EUR")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert formatter is not None
|
||||
assert formatter.preset == "EUR"
|
||||
|
||||
def test_i_can_parse_format_preset_with_options(self):
|
||||
"""Test parsing format preset with options."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert formatter is not None
|
||||
assert formatter.preset == "EUR"
|
||||
|
||||
def test_i_can_parse_format_preset_with_options(self):
|
||||
"""Test parsing format preset with options."""
|
||||
dsl = """
|
||||
column amount:
|
||||
format("EUR", precision=3)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert formatter.preset == "EUR"
|
||||
assert formatter.precision == 3
|
||||
|
||||
@pytest.mark.parametrize("format_type,formatter_class", [
|
||||
("number", NumberFormatter),
|
||||
("date", DateFormatter),
|
||||
("boolean", BooleanFormatter),
|
||||
("text", TextFormatter),
|
||||
("enum", EnumFormatter),
|
||||
])
|
||||
def test_i_can_parse_format_types(self, format_type, formatter_class):
|
||||
"""Test parsing explicit format types."""
|
||||
dsl = f"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert formatter.preset == "EUR"
|
||||
assert formatter.precision == 3
|
||||
|
||||
@pytest.mark.parametrize("format_type,formatter_class", [
|
||||
("number", NumberFormatter),
|
||||
("date", DateFormatter),
|
||||
("boolean", BooleanFormatter),
|
||||
("text", TextFormatter),
|
||||
("enum", EnumFormatter),
|
||||
])
|
||||
def test_i_can_parse_format_types(self, format_type, formatter_class):
|
||||
"""Test parsing explicit format types."""
|
||||
dsl = f"""
|
||||
column amount:
|
||||
format.{format_type}()
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, formatter_class)
|
||||
|
||||
def test_i_can_parse_format_number_with_options(self):
|
||||
"""Test parsing format.number with all options."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, formatter_class)
|
||||
|
||||
def test_i_can_parse_format_number_with_options(self):
|
||||
"""Test parsing format.number with all options."""
|
||||
dsl = """
|
||||
column amount:
|
||||
format.number(precision=2, suffix=" EUR", thousands_sep=" ")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, NumberFormatter)
|
||||
assert formatter.precision == 2
|
||||
assert formatter.suffix == " EUR"
|
||||
assert formatter.thousands_sep == " "
|
||||
|
||||
def test_i_can_parse_format_date_with_options(self):
|
||||
"""Test parsing format.date with format option."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, NumberFormatter)
|
||||
assert formatter.precision == 2
|
||||
assert formatter.suffix == " EUR"
|
||||
assert formatter.thousands_sep == " "
|
||||
|
||||
def test_i_can_parse_format_date_with_options(self):
|
||||
"""Test parsing format.date with format option."""
|
||||
dsl = """
|
||||
column created_at:
|
||||
format.date(format="%d/%m/%Y")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, DateFormatter)
|
||||
assert formatter.format == "%d/%m/%Y"
|
||||
|
||||
def test_i_can_parse_format_boolean_with_options(self):
|
||||
"""Test parsing format.boolean with all options."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, DateFormatter)
|
||||
assert formatter.format == "%d/%m/%Y"
|
||||
|
||||
def test_i_can_parse_format_boolean_with_options(self):
|
||||
"""Test parsing format.boolean with all options."""
|
||||
dsl = """
|
||||
column active:
|
||||
format.boolean(true_value="Oui", false_value="Non")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, BooleanFormatter)
|
||||
assert formatter.true_value == "Oui"
|
||||
assert formatter.false_value == "Non"
|
||||
|
||||
def test_i_can_parse_format_enum_with_source(self):
|
||||
"""Test parsing format.enum with source mapping."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, BooleanFormatter)
|
||||
assert formatter.true_value == "Oui"
|
||||
assert formatter.false_value == "Non"
|
||||
|
||||
def test_i_can_parse_format_enum_with_source(self):
|
||||
"""Test parsing format.enum with source mapping."""
|
||||
dsl = """
|
||||
column status:
|
||||
format.enum(source={"draft": "Brouillon", "published": "Publie"})
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, EnumFormatter)
|
||||
assert formatter.source == {"draft": "Brouillon", "published": "Publie"}
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
formatter = rules[0].rule.formatter
|
||||
assert isinstance(formatter, EnumFormatter)
|
||||
assert formatter.source == {"draft": "Brouillon", "published": "Publie"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -300,89 +296,89 @@ column status:
|
||||
|
||||
|
||||
class TestConditionParsing:
|
||||
"""Tests for condition parsing."""
|
||||
|
||||
@pytest.mark.parametrize("operator,dsl_op", [
|
||||
("==", "=="),
|
||||
("!=", "!="),
|
||||
("<", "<"),
|
||||
("<=", "<="),
|
||||
(">", ">"),
|
||||
(">=", ">="),
|
||||
("contains", "contains"),
|
||||
("startswith", "startswith"),
|
||||
("endswith", "endswith"),
|
||||
])
|
||||
def test_i_can_parse_comparison_operators(self, operator, dsl_op):
|
||||
"""Test parsing all comparison operators."""
|
||||
dsl = f"""
|
||||
"""Tests for condition parsing."""
|
||||
|
||||
@pytest.mark.parametrize("operator,dsl_op", [
|
||||
("==", "=="),
|
||||
("!=", "!="),
|
||||
("<", "<"),
|
||||
("<=", "<="),
|
||||
(">", ">"),
|
||||
(">=", ">="),
|
||||
("contains", "contains"),
|
||||
("startswith", "startswith"),
|
||||
("endswith", "endswith"),
|
||||
])
|
||||
def test_i_can_parse_comparison_operators(self, operator, dsl_op):
|
||||
"""Test parsing all comparison operators."""
|
||||
dsl = f"""
|
||||
column amount:
|
||||
style("error") if value {dsl_op} 0
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition is not None
|
||||
assert condition.operator == operator
|
||||
|
||||
@pytest.mark.parametrize("unary_op", ["isempty", "isnotempty"])
|
||||
def test_i_can_parse_unary_conditions(self, unary_op):
|
||||
"""Test parsing unary conditions (isempty, isnotempty)."""
|
||||
dsl = f"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition is not None
|
||||
assert condition.operator == operator
|
||||
|
||||
@pytest.mark.parametrize("unary_op", ["isempty", "isnotempty"])
|
||||
def test_i_can_parse_unary_conditions(self, unary_op):
|
||||
"""Test parsing unary conditions (isempty, isnotempty)."""
|
||||
dsl = f"""
|
||||
column name:
|
||||
style("neutral") if value {unary_op}
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == unary_op
|
||||
|
||||
def test_i_can_parse_condition_in(self):
|
||||
"""Test parsing 'in' condition with list."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == unary_op
|
||||
|
||||
def test_i_can_parse_condition_in(self):
|
||||
"""Test parsing 'in' condition with list."""
|
||||
dsl = """
|
||||
column status:
|
||||
style("success") if value in ["approved", "validated"]
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == "in"
|
||||
assert condition.value == ["approved", "validated"]
|
||||
|
||||
def test_i_can_parse_condition_between(self):
|
||||
"""Test parsing 'between' condition."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == "in"
|
||||
assert condition.value == ["approved", "validated"]
|
||||
|
||||
def test_i_can_parse_condition_between(self):
|
||||
"""Test parsing 'between' condition."""
|
||||
dsl = """
|
||||
column score:
|
||||
style("warning") if value between 30 and 70
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == "between"
|
||||
assert condition.value == [30, 70]
|
||||
|
||||
def test_i_can_parse_condition_negation(self):
|
||||
"""Test parsing negated condition."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.operator == "between"
|
||||
assert condition.value == [30, 70]
|
||||
|
||||
def test_i_can_parse_condition_negation(self):
|
||||
"""Test parsing negated condition."""
|
||||
dsl = """
|
||||
column status:
|
||||
style("error") if not value == "approved"
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.negate is True
|
||||
assert condition.operator == "=="
|
||||
|
||||
def test_i_can_parse_condition_case_sensitive(self):
|
||||
"""Test parsing case-sensitive condition."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.negate is True
|
||||
assert condition.operator == "=="
|
||||
|
||||
def test_i_can_parse_condition_case_sensitive(self):
|
||||
"""Test parsing case-sensitive condition."""
|
||||
dsl = """
|
||||
column name:
|
||||
style("error") if value == "Error" (case)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.case_sensitive is True
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.case_sensitive is True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -391,31 +387,31 @@ column name:
|
||||
|
||||
|
||||
class TestLiteralParsing:
|
||||
"""Tests for literal value parsing in conditions."""
|
||||
|
||||
@pytest.mark.parametrize("literal,expected_value,expected_type", [
|
||||
('"hello"', "hello", str),
|
||||
("'world'", "world", str),
|
||||
("42", 42, int),
|
||||
("-10", -10, int),
|
||||
("3.14", 3.14, float),
|
||||
("-2.5", -2.5, float),
|
||||
("True", True, bool),
|
||||
("False", False, bool),
|
||||
("true", True, bool),
|
||||
("false", False, bool),
|
||||
])
|
||||
def test_i_can_parse_literals(self, literal, expected_value, expected_type):
|
||||
"""Test parsing various literal types in conditions."""
|
||||
dsl = f"""
|
||||
"""Tests for literal value parsing in conditions."""
|
||||
|
||||
@pytest.mark.parametrize("literal,expected_value,expected_type", [
|
||||
('"hello"', "hello", str),
|
||||
("'world'", "world", str),
|
||||
("42", 42, int),
|
||||
("-10", -10, int),
|
||||
("3.14", 3.14, float),
|
||||
("-2.5", -2.5, float),
|
||||
("True", True, bool),
|
||||
("False", False, bool),
|
||||
("true", True, bool),
|
||||
("false", False, bool),
|
||||
])
|
||||
def test_i_can_parse_literals(self, literal, expected_value, expected_type):
|
||||
"""Test parsing various literal types in conditions."""
|
||||
dsl = f"""
|
||||
column amount:
|
||||
style("error") if value == {literal}
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == expected_value
|
||||
assert isinstance(condition.value, expected_type)
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == expected_value
|
||||
assert isinstance(condition.value, expected_type)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -424,29 +420,29 @@ column amount:
|
||||
|
||||
|
||||
class TestReferenceParsing:
|
||||
"""Tests for cell reference parsing in conditions."""
|
||||
|
||||
def test_i_can_parse_column_reference(self):
|
||||
"""Test parsing column reference in condition."""
|
||||
dsl = """
|
||||
"""Tests for cell reference parsing in conditions."""
|
||||
|
||||
def test_i_can_parse_column_reference(self):
|
||||
"""Test parsing column reference in condition."""
|
||||
dsl = """
|
||||
column actual:
|
||||
style("error") if value > col.budget
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == {"col": "budget"}
|
||||
|
||||
def test_i_can_parse_column_reference_with_quoted_name(self):
|
||||
"""Test parsing column reference with quoted name."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == {"col": "budget"}
|
||||
|
||||
def test_i_can_parse_column_reference_with_quoted_name(self):
|
||||
"""Test parsing column reference with quoted name."""
|
||||
dsl = """
|
||||
column actual:
|
||||
style("error") if value > col."max budget"
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == {"col": "max budget"}
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
condition = rules[0].rule.condition
|
||||
assert condition.value == {"col": "max budget"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -455,27 +451,27 @@ column actual:
|
||||
|
||||
|
||||
class TestComplexStructures:
|
||||
"""Tests for complex DSL structures."""
|
||||
|
||||
def test_i_can_parse_multiple_rules_in_scope(self):
|
||||
"""Test parsing multiple rules under one scope."""
|
||||
dsl = """
|
||||
"""Tests for complex DSL structures."""
|
||||
|
||||
def test_i_can_parse_multiple_rules_in_scope(self):
|
||||
"""Test parsing multiple rules under one scope."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error") if value < 0
|
||||
style("success") if value > 1000
|
||||
format("EUR")
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 3
|
||||
# All rules share the same scope
|
||||
for rule in rules:
|
||||
assert isinstance(rule.scope, ColumnScope)
|
||||
assert rule.scope.column == "amount"
|
||||
|
||||
def test_i_can_parse_multiple_scopes(self):
|
||||
"""Test parsing multiple scopes."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 3
|
||||
# All rules share the same scope
|
||||
for rule in rules:
|
||||
assert isinstance(rule.scope, ColumnScope)
|
||||
assert rule.scope.column == "amount"
|
||||
|
||||
def test_i_can_parse_multiple_scopes(self):
|
||||
"""Test parsing multiple scopes."""
|
||||
dsl = """
|
||||
column amount:
|
||||
format("EUR")
|
||||
|
||||
@@ -485,51 +481,51 @@ column status:
|
||||
row 0:
|
||||
style("neutral", bold=True)
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 3
|
||||
|
||||
# First rule: column amount
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
|
||||
# Second rule: column status
|
||||
assert isinstance(rules[1].scope, ColumnScope)
|
||||
assert rules[1].scope.column == "status"
|
||||
|
||||
# Third rule: row 0
|
||||
assert isinstance(rules[2].scope, RowScope)
|
||||
assert rules[2].scope.row == 0
|
||||
|
||||
def test_i_can_parse_style_and_format_combined(self):
|
||||
"""Test parsing style and format on same line."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 3
|
||||
|
||||
# First rule: column amount
|
||||
assert isinstance(rules[0].scope, ColumnScope)
|
||||
assert rules[0].scope.column == "amount"
|
||||
|
||||
# Second rule: column status
|
||||
assert isinstance(rules[1].scope, ColumnScope)
|
||||
assert rules[1].scope.column == "status"
|
||||
|
||||
# Third rule: row 0
|
||||
assert isinstance(rules[2].scope, RowScope)
|
||||
assert rules[2].scope.row == 0
|
||||
|
||||
def test_i_can_parse_style_and_format_combined(self):
|
||||
"""Test parsing style and format on same line."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error") format("EUR") if value < 0
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
rule = rules[0].rule
|
||||
assert rule.style is not None
|
||||
assert rule.style.preset == "error"
|
||||
assert rule.formatter is not None
|
||||
assert rule.formatter.preset == "EUR"
|
||||
assert rule.condition is not None
|
||||
assert rule.condition.operator == "<"
|
||||
|
||||
def test_i_can_parse_comments(self):
|
||||
"""Test that comments are ignored."""
|
||||
dsl = """
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
rule = rules[0].rule
|
||||
assert rule.style is not None
|
||||
assert rule.style.preset == "error"
|
||||
assert rule.formatter is not None
|
||||
assert rule.formatter.preset == "EUR"
|
||||
assert rule.condition is not None
|
||||
assert rule.condition.operator == "<"
|
||||
|
||||
def test_i_can_parse_comments(self):
|
||||
"""Test that comments are ignored."""
|
||||
dsl = """
|
||||
# This is a comment
|
||||
column amount:
|
||||
# Another comment
|
||||
style("error") if value < 0
|
||||
"""
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].rule.style.preset == "error"
|
||||
rules = parse_dsl(dsl)
|
||||
|
||||
assert len(rules) == 1
|
||||
assert rules[0].rule.style.preset == "error"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -538,39 +534,39 @@ column amount:
|
||||
|
||||
|
||||
class TestSyntaxErrors:
|
||||
"""Tests for syntax error handling."""
|
||||
|
||||
def test_i_cannot_parse_invalid_syntax(self):
|
||||
"""Test that invalid syntax raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
"""Tests for syntax error handling."""
|
||||
|
||||
def test_i_cannot_parse_invalid_syntax(self):
|
||||
"""Test that invalid syntax raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
column amount
|
||||
style("error")
|
||||
"""
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_missing_indent(self):
|
||||
"""Test that missing indentation raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_missing_indent(self):
|
||||
"""Test that missing indentation raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error")
|
||||
"""
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_empty_scope(self):
|
||||
"""Test that empty scope raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_empty_scope(self):
|
||||
"""Test that empty scope raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
column amount:
|
||||
"""
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_invalid_operator(self):
|
||||
"""Test that invalid operator raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
def test_i_cannot_parse_invalid_operator(self):
|
||||
"""Test that invalid operator raises DSLSyntaxError."""
|
||||
dsl = """
|
||||
column amount:
|
||||
style("error") if value <> 0
|
||||
"""
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
with pytest.raises(DSLSyntaxError):
|
||||
parse_dsl(dsl)
|
||||
|
||||
Reference in New Issue
Block a user