import datetime
import decimal
from typing import Iterable, Iterator, Mapping, Optional, Union, get_args
from typing_extensions import Self
from . import internal, meta_item_internal
from .generated import custom
from .generated.custom import CustomLabel, CustomRawValue
from .block_comment import BlockComment
from .escaped_string import EscapedString
from .date import Date
from .account import Account
from .amount import Amount
from .bool import Bool
from .inline_comment import InlineComment
from .number_expr import NumberExpr
from .number_unary_expr import NumberUnaryExpr
from .meta_item import MetaItem
from .meta_value import MetaRawValue, MetaValue
_ValueTypeSimplified = str | datetime.date | bool | decimal.Decimal
_ValueTypePreserved = Account | Amount
CustomValue = Union[_ValueTypeSimplified, _ValueTypePreserved]
def _simplify_value(raw_value: CustomRawValue) -> CustomValue:
if isinstance(raw_value, EscapedString | Date | Bool | NumberExpr):
return raw_value.value
return raw_value
def _unsimplify_value(value: CustomValue | CustomRawValue) -> CustomRawValue:
match value:
case str():
return EscapedString.from_value(value)
case datetime.date():
return Date.from_value(value)
case bool():
return Bool.from_value(value)
case decimal.Decimal():
return NumberExpr.from_value(value)
case _:
return value
def _disambiguate_values(values: Iterable[CustomRawValue]) -> Iterator[CustomRawValue]:
prev = None
for value in values:
if isinstance(prev, NumberExpr):
if isinstance(value, Amount):
number = value.raw_number
elif isinstance(value, NumberExpr):
number = value
else:
number = None
if number is not None and isinstance(
number.raw_number_add_expr.raw_operands[0].raw_operands[0], NumberUnaryExpr):
number.wrap_with_parenthesis()
yield value
prev = value
def _update_raw(raw_value: CustomRawValue, value: CustomValue) -> bool:
match raw_value, value:
case EscapedString() as r, str() as v:
r.value = v
case Date() as r, datetime.date() as v:
r.value = v
case Bool() as r, bool() as v:
r.value = v
case NumberExpr() as r, decimal.Decimal() as v:
r.value = v
case _:
return False
return True
# TODO: disambiguate values inserted through repeated wrapper
[docs]@internal.tree_model
class Custom(custom.Custom):
@internal.cached_custom_property
def values(self) -> internal.RepeatedValueWrapper[CustomRawValue, CustomValue]:
return internal.RepeatedValueWrapper[CustomRawValue, CustomValue](
raw_wrapper=self.raw_values,
raw_type=get_args(CustomRawValue),
from_raw_type=_simplify_value,
to_raw_type=_unsimplify_value,
update_raw=_update_raw,
)
[docs] @classmethod
def from_children(
cls,
date: Date,
type: EscapedString,
values: Iterable[CustomRawValue],
*,
leading_comment: Optional[BlockComment] = None,
inline_comment: Optional[InlineComment] = None,
meta: Iterable[MetaItem | BlockComment] = (),
trailing_comment: Optional[BlockComment] = None,
indent_by: str = ' ',
) -> Self:
return super().from_children(
date,
type,
_disambiguate_values(values),
leading_comment=leading_comment,
inline_comment=inline_comment,
meta=meta,
trailing_comment=trailing_comment,
indent_by=indent_by)
[docs] @classmethod
def from_value(
cls,
date: datetime.date,
type: str,
values: Iterable[CustomValue | CustomRawValue],
*,
leading_comment: Optional[str] = None,
inline_comment: Optional[str] = None,
meta: Optional[Mapping[str, MetaValue | MetaRawValue]] = None,
trailing_comment: Optional[str] = None,
indent_by: str = ' ',
) -> Self:
return cls.from_children(
leading_comment=BlockComment.from_value(leading_comment) if leading_comment is not None else None,
date=Date.from_value(date),
type=EscapedString.from_value(type),
values=map(_unsimplify_value, values),
inline_comment=InlineComment.from_value(inline_comment) if inline_comment is not None else None,
meta=meta_item_internal.from_mapping(meta, indent=indent_by) if meta is not None else (),
trailing_comment=BlockComment.from_value(trailing_comment) if trailing_comment is not None else None,
indent_by=indent_by,
)