def __init__(self, init=False, gpg_ids=None): # initialize avendesora (these should already be done if called from # main, but it is safe to call them again) read_config() GnuPG.initialize() # check the integrity of avendesora validate_componenets() # create the avendesora data directory if init: self.initialize(gpg_ids, init) terminate() # read the accounts files self.all_accounts = set() for filename in get_setting('accounts_files', []): try: path = to_path(get_setting('settings_dir'), filename) account_file = PythonFile(path) contents = account_file.run() master_password = contents.get('master_password') # traverse through all accounts, determine which are new, bind # required information to new accounts, and update account list. for account in Account.all_accounts(): if account not in self.all_accounts: account.add_fileinfo(master_password, account_file) # save a copy of account so it is not garbage collected self.all_accounts.add(account) except Error as err: err.terminate() terminate_if_errors()
def main(): try: # read config file read_config() # read command line cmdline = docopt( __doc__.format(commands=Command.summarize()), version='avendesora {} ({})'.format(__version__, __released__), options_first=True, ) # start logging logfile = BufferedFile(get_setting('log_file'), True) Inform(logfile=logfile, hanging_indent=False, stream_policy='header', notify_if_no_tty=True) shlib.set_prefs(use_inform=True, log_cmd=True) # run the requested command Command.execute(cmdline['<command>'], cmdline['<args>']) done() except KeyboardInterrupt: output('\nTerminated by user.') terminate() except (PasswordError, Error) as e: e.terminate() except OSError as e: fatal(os_error(e)) done()
def main(): with Inform() as inform: # read command line cmdline = docopt( __doc__.format(commands=Command.summarize()), options_first=True ) config = cmdline['--config'] command = cmdline['<command>'] args = cmdline['<args>'] options = cull([ 'verbose' if cmdline['--verbose'] else '', 'narrate' if cmdline['--narrate'] else '', 'trial-run' if cmdline['--trial-run'] else '', ]) if cmdline['--narrate']: inform.narrate = True try: cmd, name = Command.find(command) with Settings(config, cmd.REQUIRES_EXCLUSIVITY) as settings: cmd.execute(name, args, settings, options) except KeyboardInterrupt: display('Terminated by user.') except Error as err: err.terminate() except OSError as err: fatal(os_error(err)) terminate()
def main(): with Inform(error_status=2, flush=True, version=version) as inform: # read command line cmdline = docopt(expanded_synopsis, options_first=True, version=version) config = cmdline["--config"] command = cmdline["<command>"] args = cmdline["<args>"] if cmdline["--mute"]: inform.mute = True if cmdline["--quiet"]: inform.quiet = True emborg_opts = cull( [ "verbose" if cmdline["--verbose"] else "", "narrate" if cmdline["--narrate"] else "", "dry-run" if cmdline["--dry-run"] else "", "no-log" if cmdline["--no-log"] else "", ] ) if cmdline["--narrate"]: inform.narrate = True try: # find the command cmd, cmd_name = Command.find(command) # execute the command initialization exit_status = cmd.execute_early(cmd_name, args, None, emborg_opts) if exit_status is not None: terminate(exit_status) worst_exit_status = 0 try: while True: with Settings(config, cmd, emborg_opts) as settings: try: exit_status = cmd.execute( cmd_name, args, settings, emborg_opts ) except Error as e: settings.fail(e, cmd=' '.join(sys.argv)) e.terminate() if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except NoMoreConfigs: pass # execute the command termination exit_status = cmd.execute_late(cmd_name, args, None, emborg_opts) if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except KeyboardInterrupt: display("Terminated by user.") except Error as e: e.terminate() except OSError as e: fatal(os_error(e)) terminate(worst_exit_status)
def generate(self, field_name, field_key, account): try: if self.secret: return except AttributeError: pass account_name = account.get_name() account_seed = account.get_seed() if self.master is None: master = account.get_field("master", default=None) master_source = account.get_field("_master_source", default=None) else: master = self.master master_source = "secret" if not master: master = get_setting("user_key") master_source = "user_key" if not master: try: try: master = getpass.getpass("master password for %s: " % account_name) master_source = "user" except EOFError: output() if not master: warn("master password is empty.") except (EOFError, KeyboardInterrupt): terminate() log("Generating secret, source of master seed:", master_source) field_key = self.get_key(field_key) if self.version: version = self.version else: version = account.get_field("version", default="") if account.request_seed(): try: try: interactive_seed = getpass.getpass("seed for %s: " % account_name) except EOFError: output() if not interactive_seed: warn("seed is empty.") except (EOFError, KeyboardInterrupt): terminate() else: interactive_seed = "" seeds = [master, account_seed, field_name, field_key, version, interactive_seed] key = " ".join([str(seed) for seed in seeds]) # Convert the key into 512 bit number digest = hashlib.sha512((key).encode("utf-8")).digest() bits_per_byte = 8 radix = 1 << bits_per_byte bits = 0 for byte in digest: bits = radix * bits + byte self.pool = bits
def main(): with Inform(error_status=2, flush=True, version=version) as inform: # read command line cmdline = docopt(expanded_synopsis, options_first=True, version=version) config = cmdline['--config'] command = cmdline['<command>'] args = cmdline['<args>'] if cmdline['--mute']: inform.mute = True if cmdline['--quiet']: inform.quiet = True options = cull([ 'verbose' if cmdline['--verbose'] else '', 'narrate' if cmdline['--narrate'] else '', 'trial-run' if cmdline['--trial-run'] else '', 'no-log' if cmdline['--no-log'] else '', ]) if cmdline['--narrate']: inform.narrate = True try: # find the command cmd, cmd_name = Command.find(command) # execute the command initialization exit_status = cmd.execute_early(cmd_name, args, None, options) if exit_status is not None: terminate(exit_status) worst_exit_status = 0 try: while True: with Settings(config, cmd, options) as settings: try: exit_status = cmd.execute(cmd_name, args, settings, options) except Error as e: settings.fail(e) e.terminate() if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except NoMoreConfigs: pass # execute the command termination exit_status = cmd.execute_late(cmd_name, args, None, options) if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except KeyboardInterrupt: display('Terminated by user.') except Error as e: e.terminate() except OSError as e: fatal(os_error(e)) terminate(worst_exit_status)
def main(): """ Construct, encrypt, and publish backup keys. As the primary entry point for the end user, this function is also responsible for integrating information from command-line arguments, configuration files, and `setuptools` plugins. """ set_shlib_prefs(use_inform=True, log_cmd=True) args = docopt.docopt(__doc__) if args['--verbose']: set_output_prefs(verbose=True, narrate=True) elif args['--quiet']: set_output_prefs(quiet=True) try: config_path, config = load_config() try: if args['plugins']: list_plugins(config) sys.exit() # Get the passcode before building the archive, so if something # goes wrong with the passcode, we don't need to worry about # cleaning up the unencrypted archive. passcode = query_passcode(config) batch = args['--yes'] or args['--quiet'] archive = build_archive(config, not batch) encrypt_archive(config, archive, passcode) publish_archive(config, archive) except ConfigError as e: e.reraise(culprit=config_path) finally: if 'archive' in locals(): delete_archive(config, archive) except KeyboardInterrupt: print() except Error as e: if args['--verbose']: raise else: e.report() except OSError as e: fatal(os_error(e)) terminate()
def main(): # read config file read_config() # read command line cmdline = docopt( __doc__.format(commands=Command.summarize()), options_first=True ) # start logging logfile = BufferedFile(get_setting('log_file'), True) with Inform(logfile=logfile, hanging_indent=False): try: Command.execute(cmdline['<command>'], cmdline['<args>']) except KeyboardInterrupt: output('Terminated by user.') except Error as err: err.terminate() except OSError as err: fatal(os_error(err)) terminate()
def main(): with Inform(error_status=2, flush=True, version=version) as inform: # read command line cmdline = docopt(expanded_synopsis, options_first=True, version=version) config = cmdline['--config'] command = cmdline['<command>'] args = cmdline['<args>'] if cmdline['--mute']: inform.mute = True options = cull([ 'verbose' if cmdline['--verbose'] else '', 'narrate' if cmdline['--narrate'] else '', 'trial-run' if cmdline['--trial-run'] else '', 'no-log' if cmdline['--no-log'] else '', ]) if cmdline['--narrate']: inform.narrate = True try: cmd, cmd_name = Command.find(command) with Settings(config, cmd.REQUIRES_EXCLUSIVITY, options) as settings: try: exit_status = cmd.execute(cmd_name, args, settings, options) except Error as e: settings.fail(e) e.terminate(True) except KeyboardInterrupt: display('Terminated by user.') exit_status = 0 except Error as e: e.terminate() except OSError as e: fatal(os_error(e)) terminate(exit_status)
def main(): version = f'{__version__} ({__released__})' cmdline = docopt(__doc__, version=version) quiet = cmdline['--quiet'] problem = False with Inform(flush=True, quiet=quiet, version=version) as inform: # read the settings file settings_file = PythonFile(CONFIG_DIR, OVERDUE_FILE) settings_filename = settings_file.path settings = settings_file.run() # gather needed settings default_maintainer = settings.get('default_maintainer') default_max_age = settings.get('default_max_age', 28) dumper = settings.get('dumper', f'{getusername()}@{gethostname()}') repositories = settings.get('repositories') root = settings.get('root') # process repositories table backups = [] if is_str(repositories): for line in repositories.split('\n'): line = line.split('#')[0].strip() # discard comments if not line: continue backups.append([c.strip() for c in line.split('|')]) else: for each in repositories: backups.append([ each.get('host'), each.get('path'), each.get('maintainer'), each.get('max_age') ]) def send_mail(recipient, subject, message): if cmdline['--mail']: display(f'Reporting to {recipient}.\n') mail_cmd = ['mailx', '-r', dumper, '-s', subject, recipient] Run(mail_cmd, stdin=message, modes='soeW0') # check age of repositories now = arrow.now() display(f'current time = {now}') for host, path, maintainer, max_age in backups: maintainer = default_maintainer if not maintainer else maintainer max_age = int(max_age) if max_age else default_max_age try: path = to_path(root, path) if not path.is_dir(): raise Error('does not exist or is not a directory.', culprit=path) paths = list(path.glob('index.*')) if not paths: raise Error('no sentinel file found.', culprit=path) if len(paths) > 1: raise Error('too many sentinel files.', *paths, sep='\n ') path = paths[0] mtime = arrow.get(path.stat().st_mtime) delta = now - mtime age = 24 * delta.days + delta.seconds / 3600 report = age > max_age display( dedent(f""" HOST: {host} sentinel file: {path!s} last modified: {mtime} since last change: {age:0.1f} hours maximum age: {max_age} hours overdue: {report} """)) if report: problem = True subject = f"backup of {host} is overdue" msg = overdue_message.format(host=host, path=path, age=age) send_mail(maintainer, subject, msg) except OSError as e: problem = True msg = os_error(e) error(msg) if maintaner: send_mail(maintainer, f'{get_prog_name()} error', error_message.format(msg)) except Error as e: problem = True e.report() if maintaner: send_mail(maintainer, f'{get_prog_name()} error', error_message.format(str(e))) terminate(problem)
def main(): version = f"{__version__} ({__released__})" cmdline = docopt(__doc__, version=version) quiet = cmdline["--quiet"] problem = False use_color = Color.isTTY() and not cmdline["--no-color"] passes = Color("green", enable=use_color) fails = Color("red", enable=use_color) if cmdline["--verbose"]: overdue_message = verbose_overdue_message else: overdue_message = terse_overdue_message # prepare to create logfile log = to_path(DATA_DIR, OVERDUE_LOG_FILE) if OVERDUE_LOG_FILE else False if log: data_dir = to_path(DATA_DIR) if not data_dir.exists(): try: # data dir does not exist, create it data_dir.mkdir(mode=0o700, parents=True, exist_ok=True) except OSError as e: warn(os_error(e)) log = False with Inform(flush=True, quiet=quiet, logfile=log, version=version): # read the settings file try: settings_file = PythonFile(CONFIG_DIR, OVERDUE_FILE) settings = settings_file.run() except Error as e: e.terminate() # gather needed settings default_maintainer = settings.get("default_maintainer") default_max_age = settings.get("default_max_age", 28) dumper = settings.get("dumper", f"{username}@{hostname}") repositories = settings.get("repositories") root = settings.get("root") # process repositories table backups = [] if is_str(repositories): for line in repositories.split("\n"): line = line.split("#")[0].strip() # discard comments if not line: continue backups.append([c.strip() for c in line.split("|")]) else: for each in repositories: backups.append([ each.get("host"), each.get("path"), each.get("maintainer"), each.get("max_age"), ]) def send_mail(recipient, subject, message): if cmdline["--mail"]: if cmdline['--verbose']: display(f"Reporting to {recipient}.\n") mail_cmd = ["mailx", "-r", dumper, "-s", subject, recipient] Run(mail_cmd, stdin=message, modes="soeW0") # check age of repositories for host, path, maintainer, max_age in backups: maintainer = default_maintainer if not maintainer else maintainer max_age = float(max_age) if max_age else default_max_age try: path = to_path(root, path) if path.is_dir(): paths = list(path.glob("index.*")) if not paths: raise Error("no sentinel file found.", culprit=path) if len(paths) > 1: raise Error("too many sentinel files.", *paths, sep="\n ") path = paths[0] mtime = arrow.get(path.stat().st_mtime) delta = now - mtime age = 24 * delta.days + delta.seconds / 3600 report = age > max_age overdue = ' -- overdue' if report else '' color = fails if report else passes if report or not cmdline["--no-passes"]: display(color(fmt(overdue_message))) if report: problem = True subject = f"backup of {host} is overdue" msg = fmt(mail_overdue_message) send_mail(maintainer, subject, msg) except OSError as e: problem = True msg = os_error(e) error(msg) if maintainer: send_mail( maintainer, f"{get_prog_name()} error", error_message.format(msg), ) except Error as e: problem = True e.report() if maintainer: send_mail( maintainer, f"{get_prog_name()} error", error_message.format(str(e)), ) terminate(problem)
def main(): with Inform( error_status=2, flush=True, logfile=LoggingCache(), prog_name='emborg', version=version, ) as inform: # read command line cmdline = docopt(expanded_synopsis, options_first=True, version=version) config = cmdline["--config"] command = cmdline["<command>"] args = cmdline["<args>"] if cmdline["--mute"]: inform.mute = True if cmdline["--quiet"]: inform.quiet = True if cmdline["--relocated"]: os.environ['BORG_RELOCATED_REPO_ACCESS_IS_OK'] = 'YES' emborg_opts = cull([ "verbose" if cmdline["--verbose"] else "", "narrate" if cmdline["--narrate"] else "", "dry-run" if cmdline["--dry-run"] else "", "no-log" if cmdline["--no-log"] else "", ]) if cmdline["--narrate"]: inform.narrate = True Hooks.provision_hooks() worst_exit_status = 0 try: # find the command cmd, cmd_name = Command.find(command) # execute the command initialization exit_status = cmd.execute_early(cmd_name, args, None, emborg_opts) if exit_status is not None: terminate(exit_status) queue = ConfigQueue(cmd) while queue: with Settings(config, emborg_opts, queue) as settings: try: exit_status = cmd.execute(cmd_name, args, settings, emborg_opts) except Error as e: exit_status = 2 settings.fail(e, cmd=' '.join(sys.argv)) e.terminate() if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status # execute the command termination exit_status = cmd.execute_late(cmd_name, args, None, emborg_opts) if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status except KeyboardInterrupt: display("Terminated by user.") except Error as e: e.report() exit_status = 2 except OSError as e: exit_status = 2 error(os_error(e)) if exit_status and exit_status > worst_exit_status: worst_exit_status = exit_status terminate(worst_exit_status)
def main(): version = f'{__version__} ({__released__})' cmdline = docopt(__doc__, version=version) quiet = cmdline['--quiet'] problem = False use_color = Color.isTTY() and not cmdline['--no-color'] passes = Color('green', enable=use_color) fails = Color('red', enable=use_color) # prepare to create logfile log = to_path(DATA_DIR, OVERDUE_LOG_FILE) if OVERDUE_LOG_FILE else False if log: data_dir = to_path(DATA_DIR) if not data_dir.exists(): try: # data dir does not exist, create it data_dir.mkdir(mode=0o700, parents=True, exist_ok=True) except OSError as e: warn(os_error(e)) log = False with Inform(flush=True, quiet=quiet, logfile=log, version=version): # read the settings file try: settings_file = PythonFile(CONFIG_DIR, OVERDUE_FILE) settings = settings_file.run() except Error as e: e.terminate() # gather needed settings default_maintainer = settings.get('default_maintainer') default_max_age = settings.get('default_max_age', 28) dumper = settings.get('dumper', f'{username}@{hostname}') repositories = settings.get('repositories') root = settings.get('root') # process repositories table backups = [] if is_str(repositories): for line in repositories.split('\n'): line = line.split('#')[0].strip() # discard comments if not line: continue backups.append([c.strip() for c in line.split('|')]) else: for each in repositories: backups.append([ each.get('host'), each.get('path'), each.get('maintainer'), each.get('max_age') ]) def send_mail(recipient, subject, message): if cmdline['--mail']: display(f'Reporting to {recipient}.\n') mail_cmd = ['mailx', '-r', dumper, '-s', subject, recipient] Run(mail_cmd, stdin=message, modes='soeW0') # check age of repositories for host, path, maintainer, max_age in backups: maintainer = default_maintainer if not maintainer else maintainer max_age = float(max_age) if max_age else default_max_age try: path = to_path(root, path) if path.is_dir(): paths = list(path.glob('index.*')) if not paths: raise Error('no sentinel file found.', culprit=path) if len(paths) > 1: raise Error('too many sentinel files.', *paths, sep='\n ') path = paths[0] mtime = arrow.get(path.stat().st_mtime) delta = now - mtime age = 24 * delta.days + delta.seconds / 3600 report = age > max_age color = fails if report else passes if report or not cmdline['--no-passes']: display( color( dedent(f""" HOST: {host} sentinel file: {path!s} last modified: {mtime} since last change: {age:0.1f} hours maximum age: {max_age} hours overdue: {report} """).lstrip())) if report: problem = True subject = f"backup of {host} is overdue" msg = overdue_message.format(host=host, path=path, age=age) send_mail(maintainer, subject, msg) except OSError as e: problem = True msg = os_error(e) error(msg) if maintainer: send_mail(maintainer, f'{get_prog_name()} error', error_message.format(msg)) except Error as e: problem = True e.report() if maintainer: send_mail(maintainer, f'{get_prog_name()} error', error_message.format(str(e))) terminate(problem)
def main(): try: # Read command line {{{1 cmdline = docopt(__doc__) keys = cmdline["--keys"].split(",") if cmdline["--keys"] else [] update = cmdline["--update"].split(",") if cmdline["--update"] else [] skip = cmdline["--skip"].split(",") if cmdline["--skip"] else [] Inform( narrate=cmdline["--narrate"] or cmdline["--verbose"], verbose=cmdline["--verbose"], logfile=".sshdeploy.log", prog_name=False, flush=True, version=__version__, ) if keys and not cmdline["--trial-run"]: fatal( "Using the --keys option results in incomplete authorized_keys files.", "It may only be used for testing purposes.", "As such, --trial-run must also be specified when using --keys.", sep="\n", ) # Generated detailed help {{{1 if cmdline["manual"]: from pkg_resources import resource_string try: Run(cmd=["less"], modes="soeW0", stdin=resource_string("src", "manual.rst").decode("utf8")) except OSError as err: error(os_error(err)) terminate() # Read config file {{{1 try: config_file = cmdline.get("--config-file") config_file = config_file if config_file else "sshdeploy.conf" contents = to_path(config_file).read_text() except OSError as err: fatal(os_error(err)) code = compile(contents, config_file, "exec") config = {} try: exec(code, config) except Exception as err: fatal(err) # Move into keydir {{{1 keydir = cmdline["--keydir"] keydir = to_path(keydir if keydir else "keys-" + date) if cmdline["generate"]: comment("creating key directory:", keydir) rm(keydir) mkdir(keydir) cd(keydir) elif cmdline["distribute"]: cd(keydir) # determine default values for key options defaults = {} for name, default in [ ("keygen-options", DefaultKeygenOpts), ("abraxas-account", DefaultAbraxasAccount), ("remote-include-filename", DefaultRemoteIncludeFilename), ]: defaults[name] = config.get(name, default) # Generate keys {{{1 if cmdline["generate"]: for keyname in sorted(config["keys"].keys()): data = config["keys"][keyname] if keys and keyname not in keys: # user did not request this key continue # get default values for missing key options for option in defaults: data[option] = data.get(option, defaults[option]) # generate the key key = Key(keyname, data, update, skip, cmdline["--trial-run"]) key.generate() # Publish keys {{{1 elif cmdline["distribute"]: for keyname in sorted(config["keys"].keys()): data = config["keys"][keyname] if keys and keyname not in keys: continue # user did not request this key # get default values for missing key options for option in defaults: data[option] = data.get(option, defaults[option]) # publish the key pair to clients key = Key(keyname, data, update, skip, cmdline["--trial-run"]) key.publish_private_key() key.gather_public_keys() # publish authorized_keys files to servers {{{1 if cmdline["distribute"]: for each in sorted(AuthKeys.known): authkey = AuthKeys.known[each] authkey.publish() authkey.verify() # Process hosts {{{1 elif cmdline["test"] or cmdline["clean"] or cmdline["hosts"]: hosts = set() for keyname, data in config["keys"].items(): if keys and keyname not in keys: continue # user did not request this key # add servers to list of hosts for server, options in data["servers"].items(): if update and server not in update or server in skip: continue if "bypass" not in options: hosts.add(server) # add clients to list of hosts for client in data["clients"].keys(): if update and client not in update or client in skip: continue hosts.add(client) # process the hosts if cmdline["test"]: # test host for host in sorted(hosts): test_access(host) elif cmdline["clean"]: # clean host for host in sorted(hosts): clean(host) else: # list hosts for host in sorted(hosts): display(host) except OSError as err: error(os_error(err)) except KeyboardInterrupt: display("Killed by user") done()
def initialize(self, account, field_name, field_key=None): if self.secret: return account_name = account.get_name() account_seed = account.get_seed() if self.master is None: master_seed = account.get_scalar('master_seed', default=None) master_source = account.get_scalar('_master_source_', default=None) else: master_seed = self.master master_source = 'secret' if not master_seed: master_seed = get_setting('user_key') master_source = 'user_key' if not master_seed: try: try: master_seed = getpass.getpass('master seed: ') master_source = 'user' except EOFError: output() if not master_seed: warn("master seed is empty.") except (EOFError, KeyboardInterrupt): terminate() if self.version: version = self.version else: version = account.get_scalar('version', default='') log( 'Generating secret ', '.'.join([str(n) for n in cull([account_name, field_name, field_key, version], remove=(None, ''))]), ', source of master seed: ', master_source, sep='' ) field_key = self.get_key_seed(field_key) request_seed = account.request_seed() interactive_seed = '' if request_seed is True: try: interactive_seed = getpass.getpass('seed: ') except (EOFError, KeyboardInterrupt): terminate() elif callable(request_seed): interactive_seed = request_seed() elif is_str(request_seed): interactive_seed = request_seed elif request_seed: warn("invalid seed.") if request_seed and not interactive_seed: warn("seed is empty.") seeds = [ master_seed, account_seed, field_name, field_key, version, interactive_seed ] self.set_seeds(seeds) assert(self.pool)
def test_pardon(): with messenger() as (msg, stdout, stderr, logfile): try: terminate() assert False except SystemExit as e: assert e.args == (0,) try: raise Error('hey now!', culprit='nutz', extra='foo', codicil='putz') assert False except Error as err: assert err.get_message() == 'hey now!' assert err.get_culprit() == ('nutz',) assert err.get_codicil() == ('putz',) assert join_culprit(err.get_culprit()) == 'nutz' assert err.extra == 'foo' assert str(err) == 'nutz: hey now!' assert errors_accrued() == 0 # errors don't accrue until reported try: raise Error( 'hey now!', culprit=('nutz', 'crunch'), extra='foo', codicil=('putz', 'toodle'), ) assert False except Error as err: assert err.get_message() == 'hey now!' assert err.get_culprit() == ('nutz', 'crunch') assert err.get_codicil() == ('putz', 'toodle') assert join_culprit(err.get_culprit()) == 'nutz, crunch' assert err.extra == 'foo' assert str(err) == 'nutz, crunch: hey now!' assert err.get_message() == 'hey now!' assert err.get_message('{extra}, {}') == 'foo, hey now!' assert err.render() == 'nutz, crunch: hey now!' assert err.render('{extra}, {}') == 'nutz, crunch: foo, hey now!' assert errors_accrued() == 0 # errors don't accrue until reported try: err.terminate() assert False except SystemExit as e: assert e.args == (1,) try: done() assert False except SystemExit as e: assert e.args == (0,) try: rv = done(exit=False) assert rv == 0 except SystemExit as e: assert False try: terminate() assert False except SystemExit as e: assert e.args == (1,) try: rv = terminate(exit=False) assert rv == 1 except SystemExit as e: assert False try: rv = terminate(True, exit=False) assert rv == 1 except SystemExit as e: assert False try: rv = terminate('fuxit', exit=False) assert rv == 1 except SystemExit as e: assert False try: rv = terminate(6, exit=False) assert rv == 6 except SystemExit as e: assert False try: terminate_if_errors() assert False except SystemExit as e: assert e.args == (1,) assert True try: rv = terminate_if_errors(exit=False) assert rv == 1 except SystemExit as e: assert False try: raise Error('hey now', culprit=('nutz', 347)) assert False except Error as err: assert err.get_message() == 'hey now' assert err.get_culprit() == ('nutz', 347) assert join_culprit(err.get_culprit()) == 'nutz, 347' assert join_culprit(err.get_culprit(66)) == '66, nutz, 347' assert join_culprit(err.get_culprit(('a', 'b'))) == 'a, b, nutz, 347' assert str(err) == 'nutz, 347: hey now'