api/backend/python/xl-client/dial_xl/field.py (225 lines of code) (raw):
from typing import Iterable
from dial_xl.calculate import FieldData
from dial_xl.compile import FieldType
from dial_xl.decorator import Decorator, _FieldDecorator
from dial_xl.doc_string import _DocString, _FieldDocLine
from dial_xl.events import (
Event,
ObservableNode,
ObservableObserver,
notify_observer,
)
from dial_xl.reader import _Reader
from dial_xl.utils import _escape_field_name, _unescape_field_name
class _FieldModifier(ObservableNode):
__name: str
__after: str = " "
def __init__(self, name: str):
self.__name = name
@property
def name(self) -> str:
return self.__name
@name.setter
@notify_observer
def name(self, value: str):
"""Set the name of the modifier and invalidates compilation/computation results and sheet parsing errors"""
self.__name = value
def to_dsl(self) -> str:
"""Converts the modifier to DSL format."""
return f"{self.__name}{self.__after}"
@classmethod
def _deserialize(cls, reader: _Reader) -> "_FieldModifier":
result = cls("")
result.__name = reader.next(lambda d: d["span"]["to"])
result.__after = reader.before_next()
return result
class Field(ObservableObserver):
__before: str = " "
__doc_string: _DocString
__decorators: list[_FieldDecorator]
__modifiers: list[_FieldModifier]
__name: str
__separator: str = " = "
__formula: str | None
__after: str = "\n"
__decorator_indices: dict[str, int]
__field_type: FieldType | str | None = None
__field_data: FieldData | str | None = None
def __init__(self, name: str, formula: str | None):
self.__name = _escape_field_name(name)
self.__formula = formula
self.__doc_string = _DocString([], _FieldDocLine)
self.__decorators = []
self.__modifiers = []
self.__decorator_indices = {}
@property
def key(self) -> bool:
return any(modifier.name == "key" for modifier in self.__modifiers)
@key.setter
def key(self, value: bool):
"""Set the key modifier and invalidates compilation/computation results and sheet parsing errors"""
if value != self.key:
if value:
modifier = _FieldModifier("key")
modifier._attach(self)
self.__modifiers.insert(0, modifier)
else:
self._remove_modifier("key")
@property
def dim(self) -> bool:
return any(modifier.name == "dim" for modifier in self.__modifiers)
@dim.setter
def dim(self, value: bool):
"""Set the dim modifier and invalidates compilation/computation results and sheet parsing errors"""
if value != self.dim:
if value:
modifier = _FieldModifier("dim")
modifier._attach(self)
self.__modifiers.append(modifier)
else:
self._remove_modifier("dim")
def _remove_modifier(self, name: str):
index = next(
(
index
for index, modifier in enumerate(self.__modifiers)
if modifier.name == name
),
-1,
)
if index == -1:
raise ValueError(f"Modifier '{name}' not found")
modifier = self.__modifiers[index]
modifier._detach()
del self.__modifiers[index]
@property
def name(self) -> str:
return _unescape_field_name(self.__name)
@name.setter
@notify_observer
def name(self, value: str):
"""Set the name of the field and invalidates compilation/computation results and sheet parsing errors"""
self.__name = _escape_field_name(value)
@property
def formula(self):
return self.__formula
@formula.setter
@notify_observer
def formula(self, value: str | None):
"""Set the formula of the field and invalidates compilation/computation results and sheet parsing errors"""
self.__formula = value
@property
def field_type(self) -> FieldType | str | None:
return self.__field_type
@property
def field_data(self) -> FieldData | str | None:
return self.__field_data
@property
def doc_string(self):
return self.__doc_string.text
@doc_string.setter
def doc_string(self, value: str):
self.__doc_string.text = value
def get_decorator(self, name: str) -> Decorator:
index = self._find_decorator(name)
if index == -1:
raise ValueError(f"Decorator '{name}' not found")
return self.__decorators[index].decorator
@notify_observer
def add_decorator(self, decorator: Decorator):
"""Add a decorator to the field and invalidates compilation/computation results and sheet parsing errors"""
if decorator.name in self.__decorator_indices:
raise ValueError(f"Decorator '{decorator.name}' already exists")
field_decorator = _FieldDecorator(decorator)
field_decorator._attach(self)
self.__decorators.append(field_decorator)
self.__decorator_indices[decorator.name] = len(self.__decorators) - 1
@notify_observer
def remove_decorator(self, name: str) -> Decorator:
"""Remove a decorator from the field and invalidates compilation/computation results and sheet parsing errors"""
index = self._find_decorator(name)
if index == -1:
raise ValueError(f"Decorator '{name}' not found")
decorator = self.__decorators[index]
decorator._detach()
del self.__decorators[index]
self.__decorator_indices = {
decorator.decorator.name: index
for index, decorator in enumerate(self.__decorators)
}
return decorator.decorator
def _find_decorator(self, name: str) -> int:
return self.__decorator_indices.get(name, -1)
@property
def decorator_names(self) -> Iterable[str]:
"""Enumerates decorator names"""
return (decorator.decorator.name for decorator in self.__decorators)
@property
def decorators(self) -> Iterable[Decorator]:
"""Enumerates decorators"""
return (decorator.decorator for decorator in self.__decorators)
def _notify_before(self, event: Event):
if self._observer:
self._observer._notify_before(event)
sender = event.sender
if isinstance(sender, Decorator) and event.method_name == "name":
self._on_decorator_rename(sender.name, event.kwargs["value"])
def _on_decorator_rename(self, old_name: str, new_name: str):
index = self._find_decorator(old_name)
if index == -1:
raise ValueError(f"Decorator '{old_name}' not found")
if new_name in self.__decorator_indices:
raise ValueError(f"Decorator '{new_name}' already exists")
self.__decorator_indices[new_name] = self.__decorator_indices.pop(
old_name
)
def to_dsl(self) -> str:
"""Converts the field to DSL format."""
return (
f"{self.__before}"
f"{self.__doc_string.to_dsl()}"
f"{''.join(decorator.to_dsl() for decorator in self.__decorators)}"
f"{''.join(modifier.to_dsl() for modifier in self.__modifiers)}"
f"{self.__name}"
f"{'' if self.__formula is None else self.__separator + self.__formula}"
f"{self.__after}"
)
def _set_field_type(self, field_type: FieldType):
self.__field_type = field_type
def _set_field_data(self, field_data: FieldData):
self.__field_data = field_data
@classmethod
def _deserialize(cls, reader: _Reader) -> "Field":
result = cls("", None)
result.__before = reader.next(lambda d: d["span"]["from"])
if reader.entity.get("docs"):
docs: list[_FieldDocLine] = []
for index, doc_entity in enumerate(reader.entity.get("docs", [])):
doc_reader = reader.with_entity(doc_entity)
doc = _FieldDocLine._deserialize(doc_reader)
docs.append(doc)
reader.position = doc_reader.position
result.__doc_string = _DocString(docs, _FieldDocLine)
for index, decorator_entity in enumerate(
reader.entity.get("decorators", [])
):
decorator_reader = reader.with_entity(decorator_entity)
decorator = _FieldDecorator._deserialize(decorator_reader)
result.__decorators.append(decorator)
decorator._attach(result)
reader.position = decorator_reader.position
key = reader.entity.get("key")
dim = reader.entity.get("dim")
modifiers = []
if key:
modifiers.append(key)
if dim:
modifiers.append(dim)
modifiers.sort(key=lambda d: d["span"]["from"])
for modifier_entity in modifiers:
modifier_reader = reader.with_entity(modifier_entity)
modifier = _FieldModifier._deserialize(modifier_reader)
modifier._attach(result)
result.__modifiers.append(modifier)
reader.position = modifier_reader.position
result.__prefix = reader.next(lambda d: d["name"]["span"]["from"])
result.__name = reader.next(lambda d: d["name"]["span"]["to"])
formula = reader.entity.get("formula")
if formula:
result.__separator = reader.next(formula["span"]["from"])
result.__formula = reader.next(formula["span"]["to"])
result.__after = reader.till_linebreak()
result.__decorator_indices = {
decorator.decorator.name: index
for index, decorator in enumerate(result.__decorators)
}
return result