reports: Start BaseSpreadsheet class.

This commit is contained in:
Brett Smith 2020-06-03 18:54:49 -04:00
parent c88c5ef3b0
commit d920c5842a
2 changed files with 152 additions and 0 deletions

View file

@ -14,6 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import abc
import collections
import operator
@ -31,6 +32,7 @@ from typing import (
Callable,
DefaultDict,
Dict,
Generic,
Iterable,
Iterator,
List,
@ -51,6 +53,8 @@ from ..beancount_types import (
DecimalCompat = data.DecimalCompat
BalanceType = TypeVar('BalanceType', bound='Balance')
RelatedType = TypeVar('RelatedType', bound='RelatedPostings')
RT = TypeVar('RT', bound=Sequence)
ST = TypeVar('ST')
class Balance(Mapping[str, data.Amount]):
"""A collection of amounts mapped by currency
@ -275,3 +279,72 @@ class RelatedPostings(Sequence[data.Posting]):
default: Optional[MetaValue]=None,
) -> Set[Optional[MetaValue]]:
return {post.meta.get(key, default) for post in self}
class BaseSpreadsheet(Generic[RT, ST], metaclass=abc.ABCMeta):
"""Abstract base class to help write spreadsheets
This class provides the very core logic to write an arbitrary set of data
rows to arbitrary output. It calls hooks when it starts writing the
spreadsheet, starts a new "section" of rows, ends a section, and ends the
spreadsheet.
RT is the type of the input data rows. ST is the type of the section
identifier that you create from each row. If you don't want to use the
section logic at all, set ST to None and define section_key to return None.
"""
@abc.abstractmethod
def section_key(self, row: RT) -> ST:
"""Return the section a row belongs to
Given a data row, this method should return some identifier for the
"section" the row belongs to. The write method uses this to
determine when to call start_section and end_section.
If your spreadsheet doesn't need sections, define this to return None.
"""
...
@abc.abstractmethod
def write_row(self, row: RT) -> None:
"""Write a data row to the output spreadsheet
This method is called once for each data row in the input.
"""
...
# The next four methods are all called by the write method when the name
# says. You may override them to output headers or sums, record
# state, etc. The default implementations are all noops.
def start_spreadsheet(self) -> None:
pass
def start_section(self, key: ST) -> None:
pass
def end_section(self, key: ST) -> None:
pass
def end_spreadsheet(self) -> None:
pass
def write(self, rows: Iterable[RT]) -> None:
prev_section: Optional[ST] = None
self.start_spreadsheet()
for row in rows:
section = self.section_key(row)
if section != prev_section:
if prev_section is not None:
self.end_section(prev_section)
self.start_section(section)
prev_section = section
self.write_row(row)
try:
should_end = section is not None
except NameError:
should_end = False
if should_end:
self.end_section(section)
self.end_spreadsheet()

View file

@ -0,0 +1,79 @@
"""test_reports_spreadsheet - Unit tests for spreadsheet classes"""
# Copyright © 2020 Brett Smith
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest
from . import testutil
from conservancy_beancount.reports import core
class BaseTester(core.BaseSpreadsheet[tuple, str]):
def __init__(self):
self.start_call = None
self.end_call = None
self.started_sections = []
self.ended_sections = []
self.written_rows = []
def section_key(self, row):
return row[0]
def start_spreadsheet(self):
self.start_call = self.started_sections.copy()
def start_section(self, key):
self.started_sections.append(key)
def end_section(self, key):
self.ended_sections.append(key)
def end_spreadsheet(self):
self.end_call = self.ended_sections.copy()
def write_row(self, key):
self.written_rows.append(key)
@pytest.fixture
def spreadsheet():
return BaseTester()
def test_spreadsheet(spreadsheet):
rows = [(ch, ii) for ii, ch in enumerate('aabbcc', 1)]
spreadsheet.write(iter(rows))
assert spreadsheet.written_rows == rows
assert spreadsheet.ended_sections == spreadsheet.started_sections
assert spreadsheet.started_sections == list('abc')
assert spreadsheet.start_call == []
assert spreadsheet.end_call == spreadsheet.ended_sections
def test_empty_spreadsheet(spreadsheet):
empty_list = []
spreadsheet.write(iter(empty_list))
assert spreadsheet.start_call == empty_list
assert spreadsheet.end_call == empty_list
assert spreadsheet.started_sections == empty_list
assert spreadsheet.ended_sections == empty_list
assert spreadsheet.written_rows == empty_list
def test_one_section_spreadsheet(spreadsheet):
rows = [('A', n) for n in range(1, 4)]
spreadsheet.write(iter(rows))
assert spreadsheet.written_rows == rows
assert spreadsheet.ended_sections == spreadsheet.started_sections
assert spreadsheet.started_sections == list('A')
assert spreadsheet.start_call == []
assert spreadsheet.end_call == spreadsheet.ended_sections