Changeset - a23d075add0e
[Not reviewed]
0 4 0
Brett Smith - 4 years ago 2020-06-09 13:04:27
brettcsmith@brettcsmith.org
books: Add Loader.load_none() method.
4 files changed with 38 insertions and 7 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/books.py
Show inline comments
...
 
@@ -21,24 +21,26 @@ from pathlib import Path
 
from beancount import loader as bc_loader
 

	
 
from typing import (
 
    Any,
 
    Iterable,
 
    Iterator,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Union,
 
)
 
from .beancount_types import (
 
    Error,
 
    Errors,
 
    LoadResult,
 
)
 

	
 
PathLike = Union[str, Path]
 
Year = Union[int, datetime.date]
 

	
 
class FiscalYear(NamedTuple):
 
    month: int = 3
 
    day: int = 1
 

	
 
    def for_date(self, date: Optional[datetime.date]=None) -> int:
 
        if date is None:
...
 
@@ -162,12 +164,31 @@ class Loader:
 
                      from_fy: Year,
 
                      to_fy: Optional[Year]=None,
 
    ) -> LoadResult:
 
        """Load books for a range of fiscal years
 

	
 
        This method generates a range of fiscal years by calling
 
        FiscalYear.range() with its arguments. It loads all the books within
 
        that range.
 
        """
 
        fy_range = self.fiscal_year.range(from_fy, to_fy)
 
        fy_paths = self._iter_fy_books(fy_range)
 
        return self._load_paths(fy_paths)
 

	
 
    @classmethod
 
    def load_none(cls, config_path: Optional[PathLike]=None, lineno: int=0) -> LoadResult:
 
        """Load no books and generate an error about it
 

	
 
        This is a convenience method for reporting tools that already handle
 
        general Beancount errors. If a configuration problem prevents them from
 
        loading the books, they can call this method in place of a regular
 
        loading method, and then continue on their normal code path.
 

	
 
        The path and line number given in the arguments will be named as the
 
        source of the error.
 
        """
 
        source = {
 
            'filename': str(config_path or 'conservancy_beancount.ini'),
 
            'lineno': lineno,
 
        }
 
        errors: Errors = [Error(source, "no books to load in configuration", None)]
 
        return [], errors, {}
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -104,24 +104,25 @@ from ..beancount_types import (
 
    MetaKey,
 
    MetaValue,
 
    Transaction,
 
)
 

	
 
import odf.style  # type:ignore[import]
 
import odf.table  # type:ignore[import]
 
import rt
 

	
 
from beancount.parser import printer as bc_printer
 

	
 
from . import core
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
 
from .. import data
 
from .. import filters
 
from .. import rtutil
 

	
 
PROGNAME = 'accrual-report'
 

	
 
CompoundAmount = TypeVar('CompoundAmount', data.Amount, core.Balance)
 
PostGroups = Mapping[Optional[MetaValue], 'AccrualPostings']
 
RTObject = Mapping[str, str]
 
T = TypeVar('T')
...
 
@@ -684,30 +685,25 @@ def main(arglist: Optional[Sequence[str]]=None,
 
    if cliutil.is_main_script(PROGNAME):
 
        global logger
 
        logger = logging.getLogger(PROGNAME)
 
        sys.excepthook = cliutil.ExceptHook(logger)
 
    args = parse_arguments(arglist)
 
    cliutil.setup_logger(logger, args.loglevel, stderr)
 
    if config is None:
 
        config = configmod.Config()
 
        config.load_file()
 

	
 
    books_loader = config.books_loader()
 
    if books_loader is None:
 
        entries: Entries = []
 
        source = {
 
            'filename': str(config.config_file_path()),
 
            'lineno': 1,
 
        }
 
        load_errors: Errors = [Error(source, "no books to load in configuration", None)]
 
        entries, load_errors, _ = books.Loader.load_none(config.config_file_path())
 
    elif args.report_type is ReportType.AGING:
 
        entries, load_errors, _ = books_loader.load_all()
 
    else:
 
        entries, load_errors, _ = books_loader.load_all(args.since)
 
    filters.remove_opening_balance_txn(entries)
 

	
 
    returncode = 0
 
    postings = filter_search(data.Posting.from_entries(entries), args.search_terms)
 
    groups: PostGroups = dict(AccrualPostings.group_by_meta(postings, 'invoice'))
 
    for error in load_errors:
 
        bc_printer.print_error(error, file=stderr)
 
        returncode |= ReturnFlag.LOAD_ERRORS
tests/test_books_loader.py
Show inline comments
...
 
@@ -98,12 +98,26 @@ def test_load_all(conservancy_loader, from_year):
 
    date(2020, 5, 31),
 
])
 
def test_load_all_from_date(conservancy_loader, from_date):
 
    from_year = from_date.year
 
    if from_date.month < FY_START_MONTH:
 
        from_year -= 1
 
    entries, errors, options_map = conservancy_loader.load_all(from_date)
 
    assert not errors
 
    check_openings(entries)
 
    actual_years = txn_years(entries)
 
    assert actual_years.issuperset(range(from_year, 2021))
 
    assert min(actual_years) == from_year
 

	
 
def test_load_none_full_args():
 
    entries, errors, options_map = books.Loader.load_none('test.cfg', 42)
 
    assert not entries
 
    assert errors
 
    assert all(err.source['filename'] == 'test.cfg' for err in errors)
 
    assert all(err.source['lineno'] == 42 for err in errors)
 

	
 
def test_load_none_no_args():
 
    entries, errors, options_map = books.Loader.load_none()
 
    assert not entries
 
    assert errors
 
    assert all(isinstance(err.source['filename'], str) for err in errors)
 
    assert all(isinstance(err.source['lineno'], int) for err in errors)
tests/test_reports_accrual.py
Show inline comments
...
 
@@ -728,25 +728,25 @@ def test_main_aging_report(tmp_path, arglist):
 
        pay_rows = AGING_AP
 
    output_path = tmp_path / 'AgingReport.ods'
 
    arglist.insert(0, f'--output-file={output_path}')
 
    retcode, output, errors = run_main(arglist)
 
    assert not errors.getvalue()
 
    assert retcode == 0
 
    assert not output.getvalue()
 
    with output_path.open('rb') as ods_file:
 
        check_aging_ods(ods_file, None, recv_rows, pay_rows)
 

	
 
def test_main_no_books():
 
    check_main_fails([], testutil.TestConfig(), 1 | 8, [
 
        r':1: +no books to load in configuration\b',
 
        r':[01]: +no books to load in configuration\b',
 
    ])
 

	
 
@pytest.mark.parametrize('arglist', [
 
    ['499'],
 
    ['505/99999'],
 
    ['entity=NonExistent'],
 
])
 
def test_main_no_matches(arglist):
 
    check_main_fails(arglist, None, 8, [
 
        r': WARNING: no matching entries found to report$',
 
    ])
 

	
0 comments (0 inline, 0 general)