Changeset - 8597a526d787
[Not reviewed]
0 10 0
Brett Smith - 4 years ago 2020-07-30 19:53:31
brettcsmith@brettcsmith.org
cliutil: Use semi-standardized BSD exit codes.
10 files changed with 82 insertions and 54 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/cliutil.py
Show inline comments
...
 
@@ -34,2 +34,4 @@ from pathlib import Path
 

	
 
import rt.exceptions as rt_error
 

	
 
from . import data
...
 
@@ -63,6 +65,3 @@ VERSION = pkg_resources.require(PKGNAME)[0].version
 
class ExceptHook:
 
    def __init__(self,
 
                 logger: Optional[logging.Logger]=None,
 
                 default_exitcode: int=3,
 
    ) -> None:
 
    def __init__(self, logger: Optional[logging.Logger]=None) -> None:
 
        if logger is None:
...
 
@@ -70,3 +69,2 @@ class ExceptHook:
 
        self.logger = logger
 
        self.default_exitcode = default_exitcode
 

	
...
 
@@ -77,3 +75,4 @@ class ExceptHook:
 
    ) -> NoReturn:
 
        exitcode = self.default_exitcode
 
        error_type = type(exc_value).__name__
 
        msg = ": ".join(str(arg) for arg in exc_value.args)
 
        if isinstance(exc_value, KeyboardInterrupt):
...
 
@@ -82,9 +81,30 @@ class ExceptHook:
 
            signal.pause()
 
        elif isinstance(exc_value, (
 
                rt_error.AuthorizationError,
 
                rt_error.NotAllowed,
 
        )):
 
            exitcode = os.EX_NOPERM
 
            error_type = "RT access denied"
 
        elif isinstance(exc_value, rt_error.ConnectionError):
 
            exitcode = os.EX_TEMPFAIL
 
            error_type = "RT connection error"
 
        elif isinstance(exc_value, rt_error.RtError):
 
            exitcode = os.EX_UNAVAILABLE
 
            error_type = f"RT {error_type}"
 
        elif isinstance(exc_value, OSError):
 
            exitcode += 1
 
            msg = "I/O error: {e.filename}: {e.strerror}".format(e=exc_value)
 
            if exc_value.filename is None:
 
                exitcode = os.EX_OSERR
 
                error_type = "OS error"
 
                msg = exc_value.strerror
 
            else:
 
                # There are more specific exit codes for input problems vs.
 
                # output problems, but without knowing how the file was
 
                # intended to be used, we can't use them.
 
                exitcode = os.EX_IOERR
 
                error_type = "I/O error"
 
                msg = f"{exc_value.filename}: {exc_value.strerror}"
 
        else:
 
            parts = [type(exc_value).__name__, *exc_value.args]
 
            msg = "internal " + ": ".join(parts)
 
        self.logger.critical(msg)
 
            exitcode = os.EX_SOFTWARE
 
            error_type = f"internal {error_type}"
 
        self.logger.critical("%s%s%s", error_type, ": " if msg else "", msg)
 
        self.logger.debug(
...
 
@@ -95,2 +115,13 @@ class ExceptHook:
 

	
 
class ExitCode(enum.IntEnum):
 
    # BSD exit codes commonly used
 
    NoConfiguration = os.EX_CONFIG
 
    NoConfig = NoConfiguration
 
    NoDataFiltered = os.EX_DATAERR
 
    NoDataLoaded = os.EX_NOINPUT
 

	
 
    # Our own exit codes, working down from that range
 
    BeancountErrors = 63
 

	
 

	
 
class InfoAction(argparse.Action):
...
 
@@ -134,22 +165,2 @@ class LogLevel(enum.IntEnum):
 

	
 
class ReturnFlag(enum.IntFlag):
 
    """Common return codes for tools
 

	
 
    Tools should combine these flags to report different errors, and then use
 
    ReturnFlag.returncode(flags) to report their final exit status code.
 

	
 
    Values 1, 2, 4, and 8 should be reserved for this class to be shared across
 
    all tools. Flags 16, 32, and 64 are available for tools to report their own
 
    specific errors.
 
    """
 
    LOAD_ERRORS = 1
 
    NOTHING_TO_REPORT = 2
 
    _RESERVED4 = 4
 
    _RESERVED8 = 8
 

	
 
    @classmethod
 
    def returncode(cls, flags: int) -> int:
 
        return 0 if flags == 0 else 16 + flags
 

	
 

	
 
class SearchTerm(NamedTuple):
conservancy_beancount/reports/accrual.py
Show inline comments
...
 
@@ -711,2 +711,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        entries, load_errors, _ = books.Loader.load_none(config.config_file_path())
 
        returncode = cliutil.ExitCode.NoConfiguration
 
    else:
...
 
@@ -714,2 +715,6 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        entries, load_errors, _ = books_loader.load_all(load_since)
 
        if load_errors:
 
            returncode = cliutil.ExitCode.BeancountErrors
 
        elif not entries:
 
            returncode = cliutil.ExitCode.NoDataLoaded
 
    filters.remove_opening_balance_txn(entries)
...
 
@@ -717,3 +722,2 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        bc_printer.print_error(error, file=stderr)
 
        returncode |= cliutil.ReturnFlag.LOAD_ERRORS
 

	
...
 
@@ -724,3 +728,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        logger.warning("no matching entries found to report")
 
        returncode |= cliutil.ReturnFlag.NOTHING_TO_REPORT
 
        returncode = returncode or cliutil.ExitCode.NoDataFiltered
 
    # groups is a mapping of metadata value strings to AccrualPostings.
...
 
@@ -778,6 +782,6 @@ def main(arglist: Optional[Sequence[str]]=None,
 
    if report is None:
 
        returncode |= 16
 
        returncode = cliutil.ExitCode.NoConfiguration
 
    else:
 
        report.run(groups)
 
    return cliutil.ReturnFlag.returncode(returncode)
 
    return returncode
 

	
conservancy_beancount/reports/fund.py
Show inline comments
...
 
@@ -364,7 +364,11 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        entries, load_errors, _ = books.Loader.load_none(config.config_file_path())
 
        returncode = cliutil.ExitCode.NoConfiguration
 
    else:
 
        entries, load_errors, _ = books_loader.load_fy_range(args.start_date, args.stop_date)
 
        if load_errors:
 
            returncode = cliutil.ExitCode.BeancountErrors
 
        elif not entries:
 
            returncode = cliutil.ExitCode.NoDataLoaded
 
    for error in load_errors:
 
        bc_printer.print_error(error, file=stderr)
 
        returncode |= cliutil.ReturnFlag.LOAD_ERRORS
 

	
...
 
@@ -389,3 +393,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        logger.warning("no matching postings found to report")
 
        returncode |= cliutil.ReturnFlag.NOTHING_TO_REPORT
 
        returncode = returncode or cliutil.ExitCode.NoDataFiltered
 
    elif args.report_type is ReportType.TEXT:
...
 
@@ -406,3 +410,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        ods_report.save_file(ods_file)
 
    return cliutil.ReturnFlag.returncode(returncode)
 
    return returncode
 

	
conservancy_beancount/reports/ledger.py
Show inline comments
...
 
@@ -772,7 +772,11 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        entries, load_errors, options = books.Loader.load_none(config.config_file_path())
 
        returncode = cliutil.ExitCode.NoConfiguration
 
    else:
 
        entries, load_errors, options = books_loader.load_fy_range(args.start_date, args.stop_date)
 
        if load_errors:
 
            returncode = cliutil.ExitCode.BeancountErrors
 
        elif not entries:
 
            returncode = cliutil.ExitCode.NoDataLoaded
 
    for error in load_errors:
 
        bc_printer.print_error(error, file=stderr)
 
        returncode |= cliutil.ReturnFlag.LOAD_ERRORS
 

	
...
 
@@ -815,3 +819,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        logger.warning("no matching postings found to report")
 
        returncode |= cliutil.ReturnFlag.NOTHING_TO_REPORT
 
        returncode = returncode or cliutil.ExitCode.NoDataFiltered
 

	
...
 
@@ -827,3 +831,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 
    report.save_file(ods_file)
 
    return cliutil.ReturnFlag.returncode(returncode)
 
    return returncode
 

	
conservancy_beancount/tools/opening_balances.py
Show inline comments
...
 
@@ -193,7 +193,11 @@ def main(arglist: Optional[Sequence[str]]=None,
 
        entries, load_errors, _ = books.Loader.load_none(config.config_file_path())
 
        returncode = cliutil.ExitCode.NoConfiguration
 
    else:
 
        entries, load_errors, _ = books_loader.load_fy_range(0, args.as_of_date)
 
        if load_errors:
 
            returncode = cliutil.ExitCode.BeancountErrors
 
        elif not entries:
 
            returncode = cliutil.ExitCode.NoDataLoaded
 
    for error in load_errors:
 
        bc_printer.print_error(error, file=stderr)
 
        returncode |= cliutil.ReturnFlag.LOAD_ERRORS
 

	
...
 
@@ -249,3 +253,3 @@ def main(arglist: Optional[Sequence[str]]=None,
 
    bc_printer.print_entry(opening, dcontext, file=stdout)
 
    return cliutil.ReturnFlag.returncode(returncode)
 
    return returncode
 

	
setup.py
Show inline comments
...
 
@@ -7,3 +7,3 @@ setup(
 
    description="Plugin, library, and reports for reading Conservancy's books",
 
    version='1.6.3',
 
    version='1.6.4',
 
    author='Software Freedom Conservancy',
tests/test_cliutil.py
Show inline comments
...
 
@@ -155,3 +155,3 @@ def test_excepthook_oserror(errnum, caplog):
 
        cliutil.ExceptHook()(type(error), error, None)
 
    assert exc_check.value.args[0] == 4
 
    assert exc_check.value.args[0] == os.EX_IOERR
 
    assert caplog.records
...
 
@@ -170,3 +170,3 @@ def test_excepthook_bug(exc_type, caplog):
 
        cliutil.ExceptHook()(exc_type, error, None)
 
    assert exc_check.value.args[0] == 3
 
    assert exc_check.value.args[0] == os.EX_SOFTWARE
 
    assert caplog.records
tests/test_reports_accrual.py
Show inline comments
...
 
@@ -686,6 +686,7 @@ def run_main(arglist, config=None, out_type=io.StringIO):
 

	
 
def check_main_fails(arglist, config, error_flags):
 
def check_main_fails(arglist, config, expect_retcode):
 
    if not isinstance(expect_retcode, int):
 
        expect_retcode = cliutil.ExitCode[expect_retcode]
 
    retcode, output, errors = run_main(arglist, config)
 
    assert retcode > 16
 
    assert (retcode - 16) & error_flags
 
    assert retcode == expect_retcode
 
    assert not output.getvalue()
...
 
@@ -777,3 +778,3 @@ def test_main_aging_report(arglist):
 
def test_main_no_books():
 
    errors = check_main_fails([], testutil.TestConfig(), 1 | 2)
 
    errors = check_main_fails([], testutil.TestConfig(), 'NoConfiguration')
 
    testutil.check_lines_match(iter(errors), [
...
 
@@ -788,3 +789,3 @@ def test_main_no_books():
 
def test_main_no_matches(arglist, caplog):
 
    check_main_fails(arglist, None, 2)
 
    check_main_fails(arglist, None, 'NoDataFiltered')
 
    testutil.check_logs_match(caplog, [
...
 
@@ -797,3 +798,3 @@ def test_main_no_rt(caplog):
 
    )
 
    check_main_fails(['-t', 'out'], config, 16)
 
    check_main_fails(['-t', 'out'], config, 'NoConfiguration')
 
    testutil.check_logs_match(caplog, [
tests/test_reports_fund.py
Show inline comments
...
 
@@ -282,3 +282,3 @@ def test_main_no_postings(caplog):
 
    retcode, output, errors = run_main(io.StringIO, ['NonexistentProject'])
 
    assert retcode == 18
 
    assert retcode == 65
 
    assert any(log.levelname == 'WARNING' for log in caplog.records)
tests/test_reports_ledger.py
Show inline comments
...
 
@@ -559,3 +559,3 @@ def test_main_no_postings(caplog):
 
    retcode, output, errors = run_main(['NonexistentProject'])
 
    assert retcode == 18
 
    assert retcode == 65
 
    assert any(log.levelname == 'WARNING' for log in caplog.records)
0 comments (0 inline, 0 general)