Improved auto-completion engine for formatting parameters and added support for absolute value in number formatting.

This commit is contained in:
2026-03-11 22:39:52 +01:00
parent e704dad62c
commit 3105b72ac2
11 changed files with 438 additions and 39 deletions

5
.idea/MyFastHtml.iml generated
View File

@@ -4,6 +4,11 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/.myFastHtmlDb" />
<excludeFolder url="file://$MODULE_DIR$/benchmarks" />
<excludeFolder url="file://$MODULE_DIR$/examples" />
<excludeFolder url="file://$MODULE_DIR$/images" />
<excludeFolder url="file://$MODULE_DIR$/src/.myFastHtmlDb" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12.3 WSL (Ubuntu-24.04): (/home/kodjo/.virtualenvs/MyFastHtml/bin/python)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -330,6 +330,7 @@ Formats numbers, currencies, and percentages.
| `decimal_sep` | string | `"."` | Decimal separator (e.g., ".", ",") |
| `precision` | int | `0` | Number of decimal places |
| `multiplier` | number | `1` | Multiply value before display (e.g., 100 for %) |
| `absolute` | boolean | `False` | Display absolute value (no sign) |
**Examples:**
@@ -349,6 +350,10 @@ column rate:
# Large numbers with thousands separator
column population:
format.number(thousands_sep=",", precision=0)
# Absolute value - display variance without sign
column variance:
format.number(absolute=True, suffix=" €", precision=2)
```
#### Date Formatter
@@ -1085,6 +1090,14 @@ DaisyUI 5 color presets:
### Formatter Types
Possible formatter types :
- `number`
- `date`
- `boolean`
- `text`
- `enum`
- `constant`
#### Number Formatter
| Parameter | Type | Default | Description |
@@ -1095,6 +1108,7 @@ DaisyUI 5 color presets:
| `decimal_sep` | string | `"."` | Decimal separator |
| `precision` | int | `0` | Decimal places |
| `multiplier` | number | `1` | Multiply before display |
| `absolute` | boolean | `False` | Display absolute value (no sign) |
#### Date Formatter

View File

@@ -8,7 +8,7 @@ and other common operations used by completion engines.
from .types import Position, WordRange
# Delimiters used to detect word boundaries
DELIMITERS = set('"\' ()[]{}=,:<>!\t\n\r')
DELIMITERS = set('"\' ()[]{}=,:<>!.\t\n\r')
def get_line_at(text: str, line_number: int) -> str:

View File

@@ -79,6 +79,7 @@ class NumberFormatter(Formatter):
decimal_sep: str = "."
precision: int = 0
multiplier: float = 1.0
absolute: bool = False
@dataclass

View File

@@ -4,7 +4,9 @@ Completion engine for the formatting DSL.
Implements the BaseCompletionEngine for DataGrid formatting rules.
"""
import logging
import re
from myfasthtml.core.dsl import utils
from myfasthtml.core.dsl.base_completion import BaseCompletionEngine
from myfasthtml.core.dsl.types import Position, Suggestion, CompletionResult
from myfasthtml.core.utils import make_safe_id
@@ -12,6 +14,17 @@ from . import presets
from .contexts import Context, DetectedScope, detect_scope, detect_context
from .provider import DatagridMetadataProvider
_PARAM_CONTEXTS = {
Context.STYLE_ARGS,
Context.STYLE_PARAM,
Context.FORMAT_PARAM_NUMBER,
Context.FORMAT_PARAM_DATE,
Context.FORMAT_PARAM_BOOLEAN,
Context.FORMAT_PARAM_TEXT,
Context.FORMAT_PARAM_ENUM,
Context.FORMAT_PARAM_CONSTANT,
}
logger = logging.getLogger("FormattingCompletionEngine")
class FormattingCompletionEngine(BaseCompletionEngine):
@@ -37,6 +50,55 @@ class FormattingCompletionEngine(BaseCompletionEngine):
self.table_name: str = table_name # current table name
self._id = "formatting_completion_engine#" + make_safe_id(table_name)
def get_completions(self, text: str, cursor: Position) -> CompletionResult:
"""
Get completions with parameter deduplication.
Extends the base implementation by filtering out parameters
that are already present in the current style() or format.*() call.
Args:
text: The full DSL document text
cursor: Cursor position
Returns:
CompletionResult with already-used parameters removed
"""
result = super().get_completions(text, cursor)
scope = self.detect_scope(text, cursor.line)
context = self.detect_context(text, cursor, scope)
if context in _PARAM_CONTEXTS:
line_to_cursor = utils.get_line_up_to_cursor(text, cursor)
used_params = self._extract_used_params(line_to_cursor)
if used_params:
result.suggestions = [
s for s in result.suggestions
if s.label.rstrip("=") not in used_params
]
return result
def _extract_used_params(self, line_to_cursor: str) -> set[str]:
"""
Extract parameter names already typed in the current function call.
Scans backwards from the last opening parenthesis to find all
'name=' patterns already present.
Args:
line_to_cursor: The current line up to the cursor position
Returns:
Set of parameter names already used (without the '=')
"""
paren_pos = line_to_cursor.rfind("(")
if paren_pos == -1:
return set()
args_text = line_to_cursor[paren_pos + 1:]
return set(re.findall(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\s*=", args_text))
def detect_scope(self, text: str, current_line: int) -> DetectedScope:
"""
Detect the current scope by scanning previous lines.
@@ -146,12 +208,24 @@ class FormattingCompletionEngine(BaseCompletionEngine):
case Context.FORMAT_TYPE:
return presets.FORMAT_TYPES
case Context.FORMAT_PARAM_NUMBER:
return presets.FORMAT_PARAMS_NUMBER
case Context.FORMAT_PARAM_DATE:
return presets.FORMAT_PARAMS_DATE
case Context.FORMAT_PARAM_BOOLEAN:
return presets.FORMAT_PARAMS_BOOLEAN
case Context.FORMAT_PARAM_TEXT:
return presets.FORMAT_PARAMS_TEXT
case Context.FORMAT_PARAM_ENUM:
return presets.FORMAT_PARAMS_ENUM
case Context.FORMAT_PARAM_CONSTANT:
return presets.FORMAT_PARAMS_CONSTANT
# =================================================================
# After style/format
# =================================================================

View File

@@ -45,8 +45,12 @@ class Context(Enum):
# Format contexts
FORMAT_PRESET = auto() # Inside format("): preset names
FORMAT_TYPE = auto() # After "format.": number, date, etc.
FORMAT_PARAM_NUMBER = auto() # Inside format.number(): prefix=, suffix=, etc.
FORMAT_PARAM_DATE = auto() # Inside format.date(): format=
FORMAT_PARAM_BOOLEAN = auto() # Inside format.boolean(): true_value=, false_value=, etc.
FORMAT_PARAM_TEXT = auto() # Inside format.text(): transform=, etc.
FORMAT_PARAM_ENUM = auto() # Inside format.enum(): source=, default=, etc.
FORMAT_PARAM_CONSTANT = auto() # Inside format.constant(): value=
# After style/format
AFTER_STYLE_OR_FORMAT = auto() # After ")": style(, format(, if
@@ -234,18 +238,21 @@ def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context
if re.search(r'style\s*\(\s*"[^"]*$', line_to_cursor):
return Context.STYLE_PRESET
# After style( without quote - args (preset or params)
if re.search(r"style\s*\(\s*$", line_to_cursor):
# After style( without quote - args (preset or params), including partial word
if re.search(r"style\s*\(\s*[a-zA-Z_]*$", line_to_cursor):
return Context.STYLE_ARGS
# After comma in style() - params
if re.search(r"style\s*\([^)]*,\s*$", line_to_cursor):
# After comma in style() - params, including partial word
if re.search(r"style\s*\([^)]*,\s*[a-zA-Z_]*$", line_to_cursor):
return Context.STYLE_PARAM
# After param= in style - check which param
# After boolean param= in style or format.number
if re.search(r"style\s*\([^)]*(?:bold|italic|underline|strikethrough)\s*=\s*$", line_to_cursor):
return Context.BOOLEAN_VALUE
if re.search(r"format\s*\.\s*\w+\s*\([^)]*absolute\s*=\s*$", line_to_cursor):
return Context.BOOLEAN_VALUE
if re.search(r"style\s*\([^)]*(?:color|background_color)\s*=\s*$", line_to_cursor):
return Context.COLOR_VALUE
@@ -253,30 +260,46 @@ def detect_context(text: str, cursor: Position, scope: DetectedScope) -> Context
# Format contexts
# -------------------------------------------------------------------------
# After "format." - type
if re.search(r"format\s*\.\s*$", line_to_cursor):
# After "format." - type (cursor right after dot or while typing type name)
if re.search(r"format\s*\.\s*[a-zA-Z]*$", line_to_cursor):
return Context.FORMAT_TYPE
# Inside format(" - preset
if re.search(r'format\s*\(\s*"[^"]*$', line_to_cursor):
return Context.FORMAT_PRESET
# Inside format.date( - params
if re.search(r"format\s*\.\s*date\s*\(\s*$", line_to_cursor):
return Context.FORMAT_PARAM_DATE
# After format= in format.date
# After format= in format.date (checked before FORMAT_PARAM_DATE)
if re.search(r"format\s*\.\s*date\s*\([^)]*format\s*=\s*$", line_to_cursor):
return Context.DATE_FORMAT_VALUE
# Inside format.text( - params
if re.search(r"format\s*\.\s*text\s*\(\s*$", line_to_cursor):
return Context.FORMAT_PARAM_TEXT
# After transform= in format.text
# After transform= in format.text (checked before FORMAT_PARAM_TEXT)
if re.search(r"format\s*\.\s*text\s*\([^)]*transform\s*=\s*$", line_to_cursor):
return Context.TRANSFORM_VALUE
# Inside format.number( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*number\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_NUMBER
# Inside format.date( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*date\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_DATE
# Inside format.boolean( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*boolean\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_BOOLEAN
# Inside format.text( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*text\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_TEXT
# Inside format.enum( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*enum\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_ENUM
# Inside format.constant( - right after ( or after a comma, including partial word
if re.search(r"format\s*\.\s*constant\s*\((?:[^)]*,)?\s*[a-zA-Z_]*$", line_to_cursor):
return Context.FORMAT_PARAM_CONSTANT
# -------------------------------------------------------------------------
# After style/format - if or more style/format
# -------------------------------------------------------------------------

View File

@@ -164,27 +164,56 @@ STYLE_PARAMS: list[Suggestion] = [
# =============================================================================
FORMAT_TYPES: list[Suggestion] = [
Suggestion("number", "Number formatting", "type"),
Suggestion("date", "Date formatting", "type"),
Suggestion("boolean", "Boolean formatting", "type"),
Suggestion("text", "Text transformation", "type"),
Suggestion("enum", "Value mapping", "type"),
Suggestion("number(", "Number formatting", "type"),
Suggestion("date(", "Date formatting", "type"),
Suggestion("boolean(", "Boolean formatting", "type"),
Suggestion("text(", "Text transformation", "type"),
Suggestion("enum(", "Value mapping", "type"),
Suggestion("constant(", "Constant value", "type"),
]
# =============================================================================
# Format Parameters by Type
# =============================================================================
FORMAT_PARAMS_NUMBER: list[Suggestion] = [
Suggestion("prefix=", "Text before the value", "parameter"),
Suggestion("suffix=", "Text after the value", "parameter"),
Suggestion("precision=", "Number of decimal places", "parameter"),
Suggestion("thousands_sep=", "Thousands separator", "parameter"),
Suggestion("decimal_sep=", "Decimal separator", "parameter"),
Suggestion("multiplier=", "Multiply value before display", "parameter"),
Suggestion("absolute=", "Display absolute value (no sign)", "parameter"),
]
FORMAT_PARAMS_DATE: list[Suggestion] = [
Suggestion("format=", "strftime pattern", "parameter"),
]
FORMAT_PARAMS_BOOLEAN: list[Suggestion] = [
Suggestion("true_value=", "Display text for True", "parameter"),
Suggestion("false_value=", "Display text for False", "parameter"),
Suggestion("null_value=", "Display text for null", "parameter"),
]
FORMAT_PARAMS_TEXT: list[Suggestion] = [
Suggestion("transform=", "Text transformation", "parameter"),
Suggestion("max_length=", "Maximum length", "parameter"),
Suggestion("ellipsis=", "Truncation suffix", "parameter"),
]
FORMAT_PARAMS_ENUM: list[Suggestion] = [
Suggestion("source=", "Column with labels", "parameter"),
Suggestion("default=", "Fallback display value", "parameter"),
Suggestion("allow_empty=", "Allow empty values", "parameter"),
Suggestion("empty_label=", "Label for empty values", "parameter"),
Suggestion("order_by=", "Sort order", "parameter"),
]
FORMAT_PARAMS_CONSTANT: list[Suggestion] = [
Suggestion("value=", "Constant value to display", "parameter"),
]
# =============================================================================
# Condition Keywords
# =============================================================================

View File

@@ -363,7 +363,7 @@ class DSLTransformer(Transformer):
def _filter_number_kwargs(self, kwargs: dict) -> dict:
"""Filter kwargs for NumberFormatter."""
valid_keys = {"prefix", "suffix", "thousands_sep", "decimal_sep", "precision", "multiplier"}
valid_keys = {"prefix", "suffix", "thousands_sep", "decimal_sep", "precision", "multiplier", "absolute"}
return {k: v for k, v in kwargs.items() if k in valid_keys}
def _filter_date_kwargs(self, kwargs: dict) -> dict:

View File

@@ -39,8 +39,11 @@ class NumberFormatterResolver(BaseFormatterResolver):
if value is None:
return ""
# Convert to float and apply multiplier
num = float(value) * formatter.multiplier
# Convert to float, apply absolute value if requested, then multiplier
num = float(value)
if formatter.absolute:
num = abs(num)
num = num * formatter.multiplier
# Round to precision
if formatter.precision > 0:
@@ -97,6 +100,7 @@ class NumberFormatterResolver(BaseFormatterResolver):
decimal_sep=formatter.decimal_sep if formatter.decimal_sep != "." else preset.get("decimal_sep", "."),
precision=formatter.precision if formatter.precision != 0 else preset.get("precision", 0),
multiplier=formatter.multiplier if formatter.multiplier != 1.0 else preset.get("multiplier", 1.0),
absolute=formatter.absolute if formatter.absolute else preset.get("absolute", False),
)
else:
return NumberFormatter(
@@ -107,6 +111,7 @@ class NumberFormatterResolver(BaseFormatterResolver):
decimal_sep=preset.get("decimal_sep", "."),
precision=preset.get("precision", 0),
multiplier=preset.get("multiplier", 1.0),
absolute=preset.get("absolute", False),
)

View File

@@ -431,14 +431,62 @@ def test_context_format_type_after_dot():
assert context == Context.FORMAT_TYPE
def test_context_format_param_date():
"""Test FORMAT_PARAM_DATE context inside format.date()."""
text = "column amount:\n format.date("
cursor = Position(line=1, ch=16)
def test_context_format_number_absolute_param_suggests_boolean():
"""Test that 'absolute=' in format.number() triggers BOOLEAN_VALUE context."""
text = "column amount:\n format.number(absolute="
cursor = Position(line=1, ch=len(" format.number(absolute="))
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.FORMAT_PARAM_DATE
assert context == Context.BOOLEAN_VALUE
def test_context_format_type_while_typing():
"""Test FORMAT_TYPE context while typing a format type name after 'format.'."""
text = "column amount:\n format.dat"
cursor = Position(line=1, ch=14)
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == Context.FORMAT_TYPE
@pytest.mark.parametrize("line,expected_context", [
(' format.number(', Context.FORMAT_PARAM_NUMBER),
(' format.date(', Context.FORMAT_PARAM_DATE),
(' format.boolean(', Context.FORMAT_PARAM_BOOLEAN),
(' format.text(', Context.FORMAT_PARAM_TEXT),
(' format.enum(', Context.FORMAT_PARAM_ENUM),
(' format.constant(', Context.FORMAT_PARAM_CONSTANT),
])
def test_i_can_detect_format_param_context_after_open_paren(line, expected_context):
"""Test FORMAT_PARAM_* context right after the opening parenthesis."""
text = f"column amount:\n{line}"
cursor = Position(line=1, ch=len(line))
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == expected_context
@pytest.mark.parametrize("line,expected_context", [
(' format.number(prefix="",', Context.FORMAT_PARAM_NUMBER),
(' format.date(format="%Y",', Context.FORMAT_PARAM_DATE),
(' format.boolean(true_value="Yes",', Context.FORMAT_PARAM_BOOLEAN),
(' format.text(transform="upper",', Context.FORMAT_PARAM_TEXT),
(' format.enum(source="label",', Context.FORMAT_PARAM_ENUM),
(' format.constant(value="N/A",', Context.FORMAT_PARAM_CONSTANT),
])
def test_i_can_detect_format_param_context_after_comma(line, expected_context):
"""Test FORMAT_PARAM_* context after a comma (second parameter position)."""
text = f"column amount:\n{line}"
cursor = Position(line=1, ch=len(line))
scope = DetectedScope(scope_type="column", column_name="amount")
context = detect_context(text, cursor, scope)
assert context == expected_context
def test_context_format_param_text():
@@ -682,11 +730,12 @@ def test_suggestions_format_type(provider):
suggestions = engine.get_suggestions(Context.FORMAT_TYPE, scope, "")
labels = [s.label for s in suggestions]
assert "number" in labels
assert "date" in labels
assert "boolean" in labels
assert "text" in labels
assert "enum" in labels
assert "number(" in labels
assert "date(" in labels
assert "boolean(" in labels
assert "text(" in labels
assert "enum(" in labels
assert "constant(" in labels
def test_suggestions_operators(provider):
@@ -761,6 +810,62 @@ def test_suggestions_rule_start(provider):
assert "format." in labels
def test_suggestions_format_param_number(provider):
"""Test suggestions for FORMAT_PARAM_NUMBER context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = engine.get_suggestions(Context.FORMAT_PARAM_NUMBER, scope, "")
labels = [s.label for s in suggestions]
assert "prefix=" in labels
assert "suffix=" in labels
assert "precision=" in labels
assert "thousands_sep=" in labels
assert "decimal_sep=" in labels
assert "multiplier=" in labels
assert "absolute=" in labels
def test_suggestions_format_param_boolean(provider):
"""Test suggestions for FORMAT_PARAM_BOOLEAN context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = engine.get_suggestions(Context.FORMAT_PARAM_BOOLEAN, scope, "")
labels = [s.label for s in suggestions]
assert "true_value=" in labels
assert "false_value=" in labels
assert "null_value=" in labels
def test_suggestions_format_param_enum(provider):
"""Test suggestions for FORMAT_PARAM_ENUM context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = engine.get_suggestions(Context.FORMAT_PARAM_ENUM, scope, "")
labels = [s.label for s in suggestions]
assert "source=" in labels
assert "default=" in labels
assert "allow_empty=" in labels
assert "empty_label=" in labels
assert "order_by=" in labels
def test_suggestions_format_param_constant(provider):
"""Test suggestions for FORMAT_PARAM_CONSTANT context."""
engine = FormattingCompletionEngine(provider, "app.orders")
scope = DetectedScope(scope_type="column", column_name="amount")
suggestions = engine.get_suggestions(Context.FORMAT_PARAM_CONSTANT, scope, "")
labels = [s.label for s in suggestions]
assert "value=" in labels
def test_suggestions_none_context(provider):
"""Test that NONE context returns empty suggestions."""
engine = FormattingCompletionEngine(provider, "app.orders")
@@ -853,6 +958,64 @@ def test_i_can_get_completions_in_comment_returns_empty(provider):
assert result.is_empty
@pytest.mark.parametrize("text,cursor_line,used_param,remaining_param", [
(
'column amount:\n format.number(prefix="",',
1, "prefix=", "suffix=",
),
(
'column amount:\n format.number(prefix="", suffix=" CHF",',
1, "suffix=", "precision=",
),
(
'column amount:\n style(bold=True,',
1, "bold=", "italic=",
),
(
'column amount:\n style(background_color="red",',
1, "background_color=", "color=",
),
])
def test_i_cannot_get_already_used_param_in_suggestions(provider, text, cursor_line, used_param, remaining_param):
"""Test that already-used parameters are not suggested again."""
cursor = Position(line=cursor_line, ch=len(text.split("\n")[cursor_line]))
result = get_completions(text, cursor, provider, "app.orders")
labels = [s.label for s in result.suggestions]
assert used_param not in labels, f"'{used_param}' should not appear after being used"
assert remaining_param in labels, f"'{remaining_param}' should still appear"
def test_i_can_get_completions_while_typing_param_name(provider):
"""Test that suggestions appear while typing a partial parameter name.
Typing 'pre' after a comma should suggest 'precision=' (filtered by prefix)
and exclude 'prefix=' (already used).
"""
text = 'column amount:\n format.number(prefix="", pre'
cursor = Position(line=1, ch=len(text.split("\n")[1]))
result = get_completions(text, cursor, provider, "app.orders")
labels = [s.label for s in result.suggestions]
assert "precision=" in labels
assert "prefix=" not in labels
def test_i_can_get_completions_condition_unaffected_by_param_filtering(provider):
"""Test that condition suggestions are not affected by parameter filtering."""
text = 'column amount:\n style(bold=True) if '
cursor = Position(line=1, ch=len(text.split("\n")[1]))
result = get_completions(text, cursor, provider, "app.orders")
labels = [s.label for s in result.suggestions]
assert "value" in labels
assert "col." in labels
assert "not" in labels
def test_i_can_create_formatting_completion_engine(provider):
"""Test that FormattingCompletionEngine can be instantiated."""
engine = FormattingCompletionEngine(provider, "app.orders")
@@ -882,3 +1045,64 @@ def test_i_can_use_engine_detect_context(provider):
context = engine.detect_context(text, cursor, scope)
assert context == Context.STYLE_ARGS
@pytest.mark.parametrize("format_type,expected_params", [
("number", ["prefix=", "suffix=", "precision="]),
("date", ["format="]),
("boolean", ["true_value=", "false_value=", "null_value="]),
("text", ["transform=", "max_length=", "ellipsis="]),
("enum", ["source=", "default=", "allow_empty="]),
("constant", ["value="]),
])
def test_i_can_get_completions_for_format_params(provider, format_type, expected_params):
"""Test that correct parameters are suggested after 'format.<type>('."""
text = f"column amount:\n format.{format_type}("
cursor = Position(line=1, ch=len(f" format.{format_type}("))
result = get_completions(text, cursor, provider, "app.orders")
assert not result.is_empty
labels = [s.label for s in result.suggestions]
for param in expected_params:
assert param in labels, f"Expected '{param}' in suggestions for format.{format_type}("
def test_i_can_get_completions_for_format_type_after_dot(provider):
"""Test that format types are suggested after 'format.'
The dot must be treated as a word delimiter so that the prefix
is empty after the dot, allowing all FORMAT_TYPE suggestions through.
Without this, the prefix 'format.' would filter out 'number(', 'date(', etc.
"""
text = "column amount:\n format."
cursor = Position(line=1, ch=11)
result = get_completions(text, cursor, provider, "app.orders")
assert not result.is_empty
labels = [s.label for s in result.suggestions]
assert "number(" in labels
assert "date(" in labels
assert "boolean(" in labels
assert "text(" in labels
assert "enum(" in labels
assert "constant(" in labels
def test_i_can_get_completions_for_format_type_filtered_by_prefix(provider):
"""Test that format types are filtered by partial input after 'format.'
Typing 'format.dat' should only suggest 'date(', not the other types.
The dot delimiter ensures the prefix is 'dat', not 'format.dat'.
"""
text = "column amount:\n format.dat"
cursor = Position(line=1, ch=14)
result = get_completions(text, cursor, provider, "app.orders")
labels = [s.label for s in result.suggestions]
assert "date(" in labels
assert "number(" not in labels
assert "boolean(" not in labels
assert "text(" not in labels

View File

@@ -47,6 +47,30 @@ class TestNumberFormatter:
)
assert resolver.resolve(formatter, value) == expected
@pytest.mark.parametrize("value,expected", [
(-42, "42"),
(-1.5, "2"), # rounds abs(-1.5) = 1.5 → 2
(10, "10"),
(-0.0, "0"),
])
def test_i_can_format_number_with_absolute(self, value, expected):
"""Test that absolute=True strips the sign before formatting."""
resolver = FormatterResolver()
formatter = NumberFormatter(absolute=True)
assert resolver.resolve(formatter, value) == expected
def test_i_can_combine_absolute_with_prefix_suffix(self):
"""Test that absolute=True works together with prefix and suffix."""
resolver = FormatterResolver()
formatter = NumberFormatter(absolute=True, prefix="$", suffix=" USD", precision=2)
assert resolver.resolve(formatter, -1234.56) == "$1234.56 USD"
def test_i_can_format_negative_number_without_absolute(self):
"""Test that absolute=False (default) preserves the sign."""
resolver = FormatterResolver()
formatter = NumberFormatter(absolute=False, precision=2)
assert resolver.resolve(formatter, -42.5) == "-42.50"
def test_number_none_value(self):
resolver = FormatterResolver()
formatter = NumberFormatter(precision=2)