diff --git a/conservancy_beancount/reports/accrual.py b/conservancy_beancount/reports/accrual.py index fe4ac6d4c514849bb0066cf3e7326e367fc1f3d1..83ac99f3f12e36e8034a58db2f9230e718db6036 100644 --- a/conservancy_beancount/reports/accrual.py +++ b/conservancy_beancount/reports/accrual.py @@ -62,6 +62,7 @@ import argparse import collections import datetime import enum +import logging import re import sys @@ -98,6 +99,8 @@ from .. import rtutil PostGroups = Mapping[Optional[MetaValue], core.RelatedPostings] RTObject = Mapping[str, str] +_logger = logging.getLogger('conservancy_beancount.reports.accrual') + class Account(NamedTuple): name: str balance_paid: Callable[[core.Balance], bool] @@ -129,9 +132,9 @@ class AccrualAccount(enum.Enum): class BaseReport: - def __init__(self, out_file: TextIO, err_file: TextIO) -> None: + def __init__(self, out_file: TextIO) -> None: self.out_file = out_file - self.err_file = err_file + self.logger = _logger.getChild(type(self).__name__) def _since_last_nonzero(self, posts: core.RelatedPostings) -> core.RelatedPostings: retval = core.RelatedPostings() @@ -170,11 +173,10 @@ class BalanceReport(BaseReport): class OutgoingReport(BaseReport): - def __init__(self, rt_client: rt.Rt, out_file: TextIO, err_file: TextIO) -> None: + def __init__(self, rt_client: rt.Rt, out_file: TextIO) -> None: + super().__init__(out_file) self.rt_client = rt_client self.rt_wrapper = rtutil.RT(rt_client) - self.out_file = out_file - self.err_file = err_file def _primary_rt_id(self, posts: core.RelatedPostings) -> rtutil.TicketAttachmentIds: rt_ids = posts.all_meta_links('rt-id') @@ -202,10 +204,10 @@ class OutgoingReport(BaseReport): ticket = None errmsg = error.args[0] if ticket is None: - print("error: can't generate outgoings report for {}" - " because no RT ticket available: {}".format( - invoice, errmsg, - ), file=self.err_file) + self.logger.error( + "can't generate outgoings report for %s because no RT ticket available: %s", + invoice, errmsg, + ) return try: @@ -379,11 +381,23 @@ metadata to match. A single ticket number is a shortcut for args.search_terms = [SearchTerm.parse(s) for s in args.search] return args +def setup_logger(logger: logging.Logger, loglevel: int, stream: TextIO=sys.stderr) -> None: + formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s') + handler = logging.StreamHandler(stream) + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(loglevel) + def main(arglist: Optional[Sequence[str]]=None, stdout: TextIO=sys.stdout, stderr: TextIO=sys.stderr, config: Optional[configmod.Config]=None, + logger: Optional[logging.Logger]=None, ) -> int: + if logger is None: + global _logger + _logger = logger = logging.getLogger('accrual-report') + setup_logger(_logger, logging.INFO, stderr) returncode = 0 args = parse_arguments(arglist) if config is None: @@ -412,20 +426,17 @@ def main(arglist: Optional[Sequence[str]]=None, if args.report_type is None: args.report_type = ReportType.default_for(groups) if not groups: - print("warning: no matching entries found to report", file=stderr) + logger.warning("no matching entries found to report") returncode |= ReturnFlag.NOTHING_TO_REPORT report: Optional[BaseReport] = None if args.report_type is ReportType.OUTGOING: rt_client = config.rt_client() if rt_client is None: - print( - "error: unable to generate outgoing report: RT client is required", - file=stderr, - ) + logger.error("unable to generate outgoing report: RT client is required") else: - report = OutgoingReport(rt_client, stdout, stderr) + report = OutgoingReport(rt_client, stdout) else: - report = args.report_type.value(stdout, stderr) + report = args.report_type.value(stdout) if report is None: returncode |= ReturnFlag.REPORT_ERRORS else: diff --git a/tests/test_reports_accrual.py b/tests/test_reports_accrual.py index 1af316d940ba6dca473628ab4a9ba979b9e091a6..2406058e4d8f652eee9e45d69a75b8631e49a76b 100644 --- a/tests/test_reports_accrual.py +++ b/tests/test_reports_accrual.py @@ -259,10 +259,9 @@ def run_outgoing(invoice, postings, rt_client=None): if not isinstance(postings, core.RelatedPostings): postings = relate_accruals_by_meta(postings, invoice) output = io.StringIO() - errors = io.StringIO() - report = accrual.OutgoingReport(rt_client, output, errors) + report = accrual.OutgoingReport(rt_client, output) report.run({invoice: postings}) - return output, errors + return output @pytest.mark.parametrize('invoice,expected', [ ('rt:505/5050', "Zero balance outstanding since 2020-05-05"), @@ -270,22 +269,21 @@ def run_outgoing(invoice, postings, rt_client=None): ('rt:510/6100', "-280.00 USD outstanding since 2020-06-10"), ('rt://ticket/515/attachments/5150', "1,500.00 USD outstanding since 2020-05-15",), ]) -def test_balance_report(accrual_postings, invoice, expected): +def test_balance_report(accrual_postings, invoice, expected, caplog): related = relate_accruals_by_meta(accrual_postings, invoice) output = io.StringIO() - errors = io.StringIO() - report = accrual.BalanceReport(output, errors) + report = accrual.BalanceReport(output) report.run({invoice: related}) - assert not errors.getvalue() + assert not caplog.records check_output(output, [invoice, expected]) -def test_outgoing_report(accrual_postings): +def test_outgoing_report(accrual_postings, caplog): invoice = 'rt:510/6100' - output, errors = run_outgoing(invoice, accrual_postings) + output = run_outgoing(invoice, accrual_postings) rt_url = RTClient.DEFAULT_URL[:-9] rt_id_url = rf'\b{re.escape(f"{rt_url}Ticket/Display.html?id=510")}\b' contract_url = rf'\b{re.escape(f"{rt_url}Ticket/Attachment/4000/4000/contract.pdf")}\b' - print(output.getvalue()) + assert not caplog.records check_output(output, [ r'^PAYMENT FOR APPROVAL:$', r'^REQUESTOR: Mx\. 510 $', @@ -304,11 +302,11 @@ def test_outgoing_report(accrual_postings): r'^\s+Expenses:FilingFees\s+60\.00 USD$', ]) -def test_outgoing_report_custom_field_fallbacks(accrual_postings): +def test_outgoing_report_custom_field_fallbacks(accrual_postings, caplog): invoice = 'rt:510/6100' rt_client = RTClient(want_cfs=False) - output, errors = run_outgoing(invoice, accrual_postings, rt_client) - assert not errors.getvalue() + output = run_outgoing(invoice, accrual_postings, rt_client) + assert not caplog.records check_output(output, [ r'^PAYMENT FOR APPROVAL:$', r'^REQUESTOR: $', @@ -316,10 +314,10 @@ def test_outgoing_report_custom_field_fallbacks(accrual_postings): r'^PAYMENT METHOD:\s*$', ]) -def test_outgoing_report_fx_amounts(accrual_postings): +def test_outgoing_report_fx_amounts(accrual_postings, caplog): invoice = 'rt:520/5200' - output, errors = run_outgoing(invoice, accrual_postings) - assert not errors.getvalue() + output = run_outgoing(invoice, accrual_postings) + assert not caplog.records check_output(output, [ r'^PAYMENT FOR APPROVAL:$', r'^REQUESTOR: Mx\. 520 $', @@ -417,7 +415,7 @@ def test_main_no_books(): ]) def test_main_no_matches(arglist): check_main_fails(arglist, None, 8, [ - r'^warning: no matching entries found to report$', + r': WARNING: no matching entries found to report$', ]) def test_main_no_rt(): @@ -425,5 +423,5 @@ def test_main_no_rt(): books_path=testutil.test_path('books/accruals.beancount'), ) check_main_fails(['-t', 'out'], config, 4, [ - r'^error: unable to generate outgoing report: RT client is required\b', + r': ERROR: unable to generate outgoing report: RT client is required\b', ])