Fixed FormattingRules not being applied

This commit is contained in:
2026-03-15 16:50:21 +01:00
parent feb9da50b2
commit 0c9c8bc7fa
7 changed files with 671 additions and 416 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -220,7 +220,10 @@ class DataGrid(MultipleInstance):
name, namespace = (conf.name, conf.namespace) if conf else ("No name", "__default__") name, namespace = (conf.name, conf.namespace) if conf else ("No name", "__default__")
self._settings = DatagridSettings(self, save_state=save_state, name=name, namespace=namespace) self._settings = DatagridSettings(self, save_state=save_state, name=name, namespace=namespace)
self._state = DatagridState(self, save_state=self._settings.save_state) self._state = DatagridState(self, save_state=self._settings.save_state)
self._formatting_engine = FormattingEngine() self._formatting_provider = DatagridMetadataProvider(self._parent)
self._formatting_engine = FormattingEngine(
rule_presets_provider=lambda: self._formatting_provider.rule_presets
)
self._columns = None self._columns = None
self.commands = Commands(self) self.commands = Commands(self)
@@ -268,8 +271,7 @@ class DataGrid(MultipleInstance):
# self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed()) # self._columns_manager.bind_command("SaveColumnDetails", self.commands.on_column_changed())
if self._settings.enable_formatting: if self._settings.enable_formatting:
provider = DatagridMetadataProvider(self._parent) completion_engine = FormattingCompletionEngine(self._formatting_provider, self.get_table_name())
completion_engine = FormattingCompletionEngine(provider, self.get_table_name())
editor_conf = DslEditorConf(engine_id=completion_engine.get_id()) editor_conf = DslEditorConf(engine_id=completion_engine.get_id())
dsl = FormattingDSL() dsl = FormattingDSL()
self._formatting_editor = DataGridFormattingEditor(self, self._formatting_editor = DataGridFormattingEditor(self,

View File

@@ -140,7 +140,7 @@ class HierarchicalCanvasGraph(MultipleInstance):
self._query.bind_command("CancelQuery", self.commands.apply_filter()) self._query.bind_command("CancelQuery", self.commands.apply_filter())
# Add Menu # Add Menu
self._menu = Menu(self, conf=MenuConf(["ResetView"])) self._menu = Menu(self, conf=MenuConf(["ResetView"]), _id="-menu")
logger.debug(f"HierarchicalCanvasGraph created with id={self._id}, " logger.debug(f"HierarchicalCanvasGraph created with id={self._id}, "
f"nodes={len(conf.nodes)}, edges={len(conf.edges)}") f"nodes={len(conf.nodes)}, edges={len(conf.edges)}")

View File

@@ -15,12 +15,12 @@ from myfasthtml.core.formatting.dataclasses import RulePreset
from myfasthtml.core.formatting.presets import ( from myfasthtml.core.formatting.presets import (
DEFAULT_FORMATTER_PRESETS, DEFAULT_STYLE_PRESETS, DEFAULT_RULE_PRESETS, DEFAULT_FORMATTER_PRESETS, DEFAULT_STYLE_PRESETS, DEFAULT_RULE_PRESETS,
) )
from myfasthtml.core.instances import SingleInstance, InstancesManager from myfasthtml.core.instances import UniqueInstance, InstancesManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider): class DatagridMetadataProvider(UniqueInstance, BaseMetadataProvider):
"""Concrete session-scoped metadata provider for DataGrid DSL engines. """Concrete session-scoped metadata provider for DataGrid DSL engines.
Implements BaseMetadataProvider by delegating live data queries to Implements BaseMetadataProvider by delegating live data queries to
@@ -36,8 +36,7 @@ class DatagridMetadataProvider(SingleInstance, BaseMetadataProvider):
all_tables_formats: Global format rules applied to all tables. all_tables_formats: Global format rules applied to all tables.
""" """
def __init__(self, parent=None, session: Optional[dict] = None, def __init__(self, parent=None, session: Optional[dict] = None, _id: Optional[str] = None):
_id: Optional[str] = None):
super().__init__(parent, session, _id) super().__init__(parent, session, _id)
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy() self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy() self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()

View File

@@ -31,7 +31,8 @@ class FormattingEngine:
style_presets: dict = None, style_presets: dict = None,
formatter_presets: dict = None, formatter_presets: dict = None,
rule_presets: dict = None, rule_presets: dict = None,
lookup_resolver: Callable[[str, str, str], dict] = None lookup_resolver: Callable[[str, str, str], dict] = None,
rule_presets_provider: Callable[[], dict] = None,
): ):
""" """
Initialize the FormattingEngine. Initialize the FormattingEngine.
@@ -41,11 +42,20 @@ class FormattingEngine:
formatter_presets: Custom formatter presets. If None, uses defaults. formatter_presets: Custom formatter presets. If None, uses defaults.
rule_presets: Named rule presets (list of FormatRule dicts). If None, uses defaults. rule_presets: Named rule presets (list of FormatRule dicts). If None, uses defaults.
lookup_resolver: Function for resolving enum datagrid sources. lookup_resolver: Function for resolving enum datagrid sources.
rule_presets_provider: Callable returning the current rule_presets dict.
When provided, takes precedence over rule_presets on every apply_format call.
Use this to keep the engine in sync with a shared provider.
""" """
self._condition_evaluator = ConditionEvaluator() self._condition_evaluator = ConditionEvaluator()
self._style_resolver = StyleResolver(style_presets) self._style_resolver = StyleResolver(style_presets)
self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver) self._formatter_resolver = FormatterResolver(formatter_presets, lookup_resolver)
self._rule_presets = rule_presets if rule_presets is not None else DEFAULT_RULE_PRESETS self._rule_presets = rule_presets if rule_presets is not None else DEFAULT_RULE_PRESETS
self._rule_presets_provider = rule_presets_provider
def _get_rule_presets(self) -> dict:
if self._rule_presets_provider is not None:
return self._rule_presets_provider()
return self._rule_presets
def apply_format( def apply_format(
self, self,
@@ -99,8 +109,8 @@ class FormattingEngine:
""" """
Replace any FormatRule that references a rule preset with the preset's rules. Replace any FormatRule that references a rule preset with the preset's rules.
A rule is a rule preset reference when its formatter has a preset name A rule is a rule preset reference when its formatter or style has a preset name
that exists in rule_presets (and not in formatter_presets). that exists in rule_presets.
Args: Args:
rules: Original list of FormatRule rules: Original list of FormatRule
@@ -108,22 +118,26 @@ class FormattingEngine:
Returns: Returns:
Expanded list with preset references replaced by their FormatRules Expanded list with preset references replaced by their FormatRules
""" """
rule_presets = self._get_rule_presets()
expanded = [] expanded = []
for rule in rules: for rule in rules:
preset_name = self._get_rule_preset_name(rule) preset_name = self._get_rule_preset_name(rule, rule_presets)
if preset_name: if preset_name:
expanded.extend(self._rule_presets[preset_name].rules) expanded.extend(rule_presets[preset_name].rules)
else: else:
expanded.append(rule) expanded.append(rule)
return expanded return expanded
def _get_rule_preset_name(self, rule: FormatRule) -> str | None: def _get_rule_preset_name(self, rule: FormatRule, rule_presets: dict) -> str | None:
"""Return the preset name if the rule's formatter references a rule preset, else None.""" """Return the preset name if the rule references a rule preset via format() or style(), else None."""
if rule.formatter is None: if rule.formatter is not None:
return None preset = getattr(rule.formatter, "preset", None)
preset = getattr(rule.formatter, "preset", None) if preset and preset in rule_presets:
if preset and preset in self._rule_presets: return preset
return preset if rule.style is not None:
preset = getattr(rule.style, "preset", None)
if preset and preset in rule_presets:
return preset
return None return None
def _get_matching_rules( def _get_matching_rules(

View File

@@ -5,7 +5,7 @@ from typing import Optional, Literal
from myfasthtml.controls.helpers import Ids from myfasthtml.controls.helpers import Ids
from myfasthtml.core.commands import BoundCommand, Command from myfasthtml.core.commands import BoundCommand, Command
from myfasthtml.core.constants import NO_DEFAULT_VALUE from myfasthtml.core.constants import NO_DEFAULT_VALUE
from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal, debug_session
VERBOSE_VERBOSE = False VERBOSE_VERBOSE = False
@@ -36,7 +36,13 @@ class BaseInstance:
session = args[1] if len(args) > 1 and isinstance(args[1], dict) else kwargs.get("session", None) session = args[1] if len(args) > 1 and isinstance(args[1], dict) else kwargs.get("session", None)
_id = args[2] if len(args) > 2 and isinstance(args[2], str) else kwargs.get("_id", None) _id = args[2] if len(args) > 2 and isinstance(args[2], str) else kwargs.get("_id", None)
if VERBOSE_VERBOSE: if VERBOSE_VERBOSE:
logger.debug(f" parent={parent}, session={session}, _id={_id}") logger.debug(f" parent={parent}, session={debug_session(session)}, _id={_id}")
# for UniqueInstance, the parent is always the ultimate root parent
if issubclass(cls, UniqueInstance):
parent = BaseInstance.get_ultimate_root_parent(parent)
if VERBOSE_VERBOSE:
logger.debug(f" UniqueInstance detected. parent is set to ultimate root {parent=}")
# Compute _id # Compute _id
_id = cls.compute_id(_id, parent) _id = cls.compute_id(_id, parent)
@@ -163,7 +169,7 @@ class BaseInstance:
def compute_id(cls, _id: Optional[str], parent: Optional['BaseInstance']): def compute_id(cls, _id: Optional[str], parent: Optional['BaseInstance']):
if _id is None: if _id is None:
prefix = cls.compute_prefix() prefix = cls.compute_prefix()
if issubclass(cls, SingleInstance): if issubclass(cls, (SingleInstance, UniqueInstance)):
_id = prefix _id = prefix
else: else:
_id = f"{prefix}-{str(uuid.uuid4())}" _id = f"{prefix}-{str(uuid.uuid4())}"
@@ -174,6 +180,17 @@ class BaseInstance:
return _id return _id
@staticmethod
def get_ultimate_root_parent(instance):
if instance is None:
return None
parent = instance
while True:
if parent.get_parent() is None:
return parent
parent = parent.get_parent()
class SingleInstance(BaseInstance): class SingleInstance(BaseInstance):
""" """
@@ -200,7 +217,7 @@ class UniqueInstance(BaseInstance):
_id: Optional[str] = None, _id: Optional[str] = None,
auto_register: bool = True, auto_register: bool = True,
on_init=None): on_init=None):
super().__init__(parent, session, _id, auto_register) super().__init__(BaseInstance.get_ultimate_root_parent(parent), session, _id, auto_register)
if on_init is not None: if on_init is not None:
on_init() on_init()

View File

@@ -350,3 +350,32 @@ class TestPresets:
assert "background-color: purple" in css.css assert "background-color: purple" in css.css
assert "color: yellow" in css.css assert "color: yellow" in css.css
def test_i_can_expand_rule_preset_via_style(self):
"""A rule preset referenced via style() must be expanded like via format().
Why: style("traffic_light") should expand the traffic_light rule preset
(which has conditional style rules) instead of looking up "traffic_light"
as a style preset name (where it doesn't exist).
"""
engine = FormattingEngine()
rules = [FormatRule(style=Style(preset="traffic_light"))]
css, _ = engine.apply_format(rules, cell_value=-5)
assert css is not None
assert css.cls == "mf-formatting-error"
def test_i_can_expand_rule_preset_via_style_with_no_match(self):
"""A rule preset via style() with a non-matching condition returns no style.
Why: traffic_light has style("error") only if value < 0.
A positive value should produce no style output.
"""
engine = FormattingEngine()
rules = [FormatRule(style=Style(preset="traffic_light"))]
css, _ = engine.apply_format(rules, cell_value=10)
assert css is not None
assert css.cls == "mf-formatting-success"