home · contact · privacy
Optimize performance by caching some values. master
authorChristian Heller <c.heller@plomlompom.de>
Mon, 2 Feb 2026 19:44:11 +0000 (20:44 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Mon, 2 Feb 2026 19:44:11 +0000 (20:44 +0100)
src/ledgplom/ledger.py

index 2a1ee5915e1efeac1ec23d3409e333c72b74750b..27624985e063369ec4f95ed6b949b5acb8ae33b2 100644 (file)
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
 from datetime import date as dt_date
 from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
 from pathlib import Path
 from datetime import date as dt_date
 from decimal import Decimal, InvalidOperation as DecimalInvalidOperation
 from pathlib import Path
-from typing import Any, Iterator, Optional, Self
+from typing import Any, Callable, Iterator, Optional, Self
 
 
 SPACE = ' '
 
 
 SPACE = ' '
@@ -284,27 +284,46 @@ class _TransferLine(_BookingLine):
 
 
 class _LinesBlock:
 
 
 class _LinesBlock:
-    _lines: list[_DatLine]
+    _lines: tuple[_DatLine, ...] = tuple()
+    _cached_id = -1
+    _cached: dict[str, Any]
 
     def __init__(self) -> None:
 
     def __init__(self) -> None:
-        self._lines = []
+        self._cached = {}
+
+    @staticmethod
+    def _cache(key_cached: str) -> Callable:
+        def decorator(f: Callable) -> Callable:
+            def wrapper(self):
+                # pylint: disable=protected-access
+                if key_cached not in self._cached:
+                    self._cached[key_cached] = f(self)
+                return self._cached[key_cached]
+            return wrapper
+        return decorator
 
     @property
     def lines(self) -> tuple[_DatLine, ...]:
         'Return collected lines.'
 
     @property
     def lines(self) -> tuple[_DatLine, ...]:
         'Return collected lines.'
-        return tuple(self._lines)
+        return self._lines
+
+    @lines.setter
+    def lines(self, new_lines: tuple[_DatLine, ...]) -> None:
+        self._lines = new_lines
+        self._cached.clear()
 
     def add(self, lines: tuple[_DatLine, ...], at_end=True) -> None:
         'Grow block downwards by this one DatLine.'
         if at_end:
 
     def add(self, lines: tuple[_DatLine, ...], at_end=True) -> None:
         'Grow block downwards by this one DatLine.'
         if at_end:
-            self._lines += lines
+            self.lines += lines
         else:
         else:
-            self._lines[0:0] = list(lines)
+            self.lines = lines + self.lines
 
 
 class _Booking(_LinesBlock):
 
     @property
 
 
 class _Booking(_LinesBlock):
 
     @property
+    @_LinesBlock._cache('_sink_account')
     def _sink_account(self) -> str:
         for tf_line in [tl for tl in self.transfer_lines
                         if tl.amount is None and not tl.errors]:
     def _sink_account(self) -> str:
         for tf_line in [tl for tl in self.transfer_lines
                         if tl.amount is None and not tl.errors]:
@@ -312,8 +331,8 @@ class _Booking(_LinesBlock):
         return ''
 
     @property
         return ''
 
     @property
-    def lines(self) -> tuple[_BookingLine, ...]:
-        'Return collected lines.'
+    def booking_lines(self) -> tuple[_BookingLine, ...]:
+        'Sequence of .intro_line and .transfer_lines.'
         return (self.intro_line, ) + self.transfer_lines
 
     @property
         return (self.intro_line, ) + self.transfer_lines
 
     @property
@@ -336,6 +355,7 @@ class _Booking(_LinesBlock):
         return {acc: diff for acc, diff in self._diffs_targeted.items() if acc}
 
     @property
         return {acc: diff for acc, diff in self._diffs_targeted.items() if acc}
 
     @property
+    @_LinesBlock._cache('sink_error')
     def sink_error(self) -> str:
         'Message on error, if any, regarding sink calculation/placement.'
         for _ in [tl for tl in self.transfer_lines
     def sink_error(self) -> str:
         'Message on error, if any, regarding sink calculation/placement.'
         for _ in [tl for tl in self.transfer_lines
@@ -362,12 +382,13 @@ class _Booking(_LinesBlock):
     @property
     def intro_line(self) -> _IntroLine:
         'Return collected _IntroLine.'
     @property
     def intro_line(self) -> _IntroLine:
         'Return collected _IntroLine.'
-        return _IntroLine(self._lines[0].raw)
+        return _IntroLine(self.lines[0].raw)
 
     @property
 
     @property
+    @_LinesBlock._cache('transfer_lines')
     def transfer_lines(self) -> tuple[_TransferLine, ...]:
         'Any lines past the first with .code.'
     def transfer_lines(self) -> tuple[_TransferLine, ...]:
         'Any lines past the first with .code.'
-        return tuple(_TransferLine(line.raw) for line in self._lines[1:]
+        return tuple(_TransferLine(line.raw) for line in self.lines[1:]
                      if line.code)
 
     @property
                      if line.code)
 
     @property
@@ -405,6 +426,7 @@ class _DatBlock(_LinesBlock):
         return bool([line for line in self.lines if line.len_indent])
 
     @property
         return bool([line for line in self.lines if line.len_indent])
 
     @property
+    @_LinesBlock._cache('gap_lines')
     def gap_lines(self) -> tuple[_DatLine, ...]:
         'Sequence of all included DatLines without code and indent.'
         return tuple(line for line in self.lines
     def gap_lines(self) -> tuple[_DatLine, ...]:
         'Sequence of all included DatLines without code and indent.'
         return tuple(line for line in self.lines
@@ -426,10 +448,11 @@ class _DatBlock(_LinesBlock):
 
     def remove_redundant_empty_lines(self):
         'From self remove .redundant_empty_lines.'
 
     def remove_redundant_empty_lines(self):
         'From self remove .redundant_empty_lines.'
-        for line in self.redundant_empty_lines:
-            self._lines.remove(line)
+        self.lines = tuple(line for line in self.lines
+                           if line not in self.redundant_empty_lines)
 
     @property
 
     @property
+    @_LinesBlock._cache('booking')
     def booking(self) -> Optional[_Booking]:
         'Booking made from lines indented or with code.'
         booking_lines = tuple(_DatLine(line.raw) for line in self.lines
     def booking(self) -> Optional[_Booking]:
         'Booking made from lines indented or with code.'
         booking_lines = tuple(_DatLine(line.raw) for line in self.lines
@@ -617,7 +640,7 @@ class Ledger:
             if block.booking:
                 if block.booking.sink_error:
                     return False
             if block.booking:
                 if block.booking.sink_error:
                     return False
-                if [line for line in block.booking.lines if line.errors]:
+                if [bl for bl in block.booking.booking_lines if bl.errors]:
                     return False
             if block.date_error:
                 return False
                     return False
             if block.date_error:
                 return False
@@ -693,7 +716,7 @@ class Ledger:
         if lines:
             return {'raw_gap_lines': [dl.raw for dl in block.gap_lines],
                     'booking_lines': ([line.as_dict
         if lines:
             return {'raw_gap_lines': [dl.raw for dl in block.gap_lines],
                     'booking_lines': ([line.as_dict
-                                       for line in block.booking.lines]
+                                       for line in block.booking.booking_lines]
                                       if block.booking else tuple())}
         accounts = self._calc_accounts()
         roots: list[dict[str, Any]] = []
                                       if block.booking else tuple())}
         accounts = self._calc_accounts()
         roots: list[dict[str, Any]] = []