Exemple #1
0
def main():
    """The main entry point to alot.  It parses the command line and prepares
    for the user interface main loop to run."""
    options, command = parser()

    # logging
    root_logger = logging.getLogger()
    for log_handler in root_logger.handlers:
        root_logger.removeHandler(log_handler)
    root_logger = None
    numeric_loglevel = getattr(logging, options.debug_level.upper(), None)
    logformat = '%(levelname)s:%(module)s:%(message)s'
    logging.basicConfig(level=numeric_loglevel,
                        filename=options.logfile,
                        filemode='w',
                        format=logformat)

    # locate alot config files
    if options.config is None:
        alotconfig = os.path.join(
            os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')),
            'alot', 'config')
        if not os.path.exists(alotconfig):
            alotconfig = None
    else:
        alotconfig = options.config

    try:
        settings.read_config(alotconfig)
        settings.read_notmuch_config(options.notmuch_config)
    except (ConfigError, OSError, IOError) as e:
        sys.exit(e)

    # store options given by config swiches to the settingsManager:
    if options.colour_mode:
        settings.set('colourmode', options.colour_mode)

    # get ourselves a database manager
    indexpath = settings.get_notmuch_setting('database', 'path')
    indexpath = options.mailindex_path or indexpath
    dbman = DBManager(path=indexpath, ro=options.read_only)

    # determine what to do
    if command is None:
        try:
            cmdstring = settings.get('initial_command')
        except CommandParseError as err:
            sys.exit(err)
    elif command.subcommand in _SUBCOMMANDS:
        cmdstring = ' '.join(options.command)

    # set up and start interface
    UI(dbman, cmdstring)

    # run the exit hook
    exit_hook = settings.get_hook('exit')
    if exit_hook is not None:
        exit_hook()
Exemple #2
0
def main():
    """The main entry point to alot.  It parses the command line and prepares
    for the user interface main loop to run."""
    options, command = parser()

    # logging
    root_logger = logging.getLogger()
    for log_handler in root_logger.handlers:
        root_logger.removeHandler(log_handler)
    root_logger = None
    numeric_loglevel = getattr(logging, options.debug_level.upper(), None)
    logformat = '%(levelname)s:%(module)s:%(message)s'
    logging.basicConfig(level=numeric_loglevel, filename=options.logfile,
                        filemode='w', format=logformat)

    # locate alot config files
    if options.config is None:
        alotconfig = os.path.join(
            os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')),
            'alot', 'config')
        if os.path.exists(alotconfig):
            settings.alot_rc_path = alotconfig
    else:
        settings.alot_rc_path = options.config

    settings.notmuch_rc_path = options.notmuch_config

    try:
        settings.read_config()
        settings.read_notmuch_config()
    except (ConfigError, OSError, IOError) as e:
        sys.exit(e)

    # store options given by config swiches to the settingsManager:
    if options.colour_mode:
        settings.set('colourmode', options.colour_mode)

    # get ourselves a database manager
    indexpath = settings.get_notmuch_setting('database', 'path')
    indexpath = options.mailindex_path or indexpath
    dbman = DBManager(path=indexpath, ro=options.read_only)

    # determine what to do
    if command is None:
        try:
            cmdstring = settings.get('initial_command')
        except CommandParseError as err:
            sys.exit(err)
    elif command.subcommand in _SUBCOMMANDS:
        cmdstring = ' '.join(options.command)

    # set up and start interface
    UI(dbman, cmdstring)

    # run the exit hook
    exit_hook = settings.get_hook('exit')
    if exit_hook is not None:
        exit_hook()
Exemple #3
0
        if os.path.exists(configfilename):
            alotconfig = configfilename
            break  # use only the first

    try:
        settings.read_config(alotconfig)
        settings.read_notmuch_config(notmuchconfig)
    except (ConfigError, OSError, IOError), e:
        sys.exit(e)

    # store options given by config swiches to the settingsManager:
    if args['colour-mode']:
        settings.set('colourmode', args['colour-mode'])

    # get ourselves a database manager
    indexpath = settings.get_notmuch_setting('database', 'path')
    indexpath = args['mailindex-path'] or indexpath
    dbman = DBManager(path=indexpath, ro=args['read-only'])

    # determine what to do
    try:
        if args.subCommand == 'search':
            query = ' '.join(args.subOptions.args)
            cmdstring = 'search %s %s' % (args.subOptions.as_argparse_opts(),
                                          query)
        elif args.subCommand == 'compose':
            cmdstring = 'compose %s' % args.subOptions.as_argparse_opts()
            if args.subOptions.rest is not None:
                cmdstring += ' ' + args.subOptions.rest
        else:
            cmdstring = settings.get('initial_command')
Exemple #4
0
def main():
    # interpret cml arguments
    args = Options()
    try:
        args.parseOptions()  # When given no argument, parses sys.argv[1:]
    except usage.UsageError as errortext:
        print "%s" % errortext
        print "Try --help for usage details."
        sys.exit(1)

    # logging
    root_logger = logging.getLogger()
    for log_handler in root_logger.handlers:
        root_logger.removeHandler(log_handler)
    root_logger = None
    numeric_loglevel = getattr(logging, args["debug-level"].upper(), None)
    logfilename = os.path.expanduser(args["logfile"])
    logformat = "%(levelname)s:%(module)s:%(message)s"
    logging.basicConfig(level=numeric_loglevel, filename=logfilename, filemode="w", format=logformat)

    # locate alot config files
    configfiles = [os.path.join(os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), "alot", "config")]
    if args["config"]:
        expanded_path = os.path.expanduser(args["config"])
        if not os.path.exists(expanded_path):
            msg = 'Config file "%s" does not exist. Goodbye for now.'
            sys.exit(msg % expanded_path)
        configfiles.insert(0, expanded_path)

    # locate notmuch config
    notmuchpath = os.environ.get("NOTMUCH_CONFIG", "~/.notmuch-config")
    if args["notmuch-config"]:
        notmuchpath = args["notmuch-config"]
    notmuchconfig = os.path.expanduser(notmuchpath)

    alotconfig = None
    # read the first alot config file we find
    for configfilename in configfiles:
        if os.path.exists(configfilename):
            alotconfig = configfilename
            break  # use only the first

    try:
        settings.read_config(alotconfig)
        settings.read_notmuch_config(notmuchconfig)
    except (ConfigError, OSError, IOError) as e:
        sys.exit(e)

    # store options given by config swiches to the settingsManager:
    if args["colour-mode"]:
        settings.set("colourmode", args["colour-mode"])

    # get ourselves a database manager
    indexpath = settings.get_notmuch_setting("database", "path")
    indexpath = args["mailindex-path"] or indexpath
    dbman = DBManager(path=indexpath, ro=args["read-only"])

    # determine what to do
    try:
        if args.subCommand == "search":
            query = " ".join(args.subOptions.args)
            cmdstring = "search %s %s" % (args.subOptions.as_argparse_opts(), query)
        elif args.subCommand == "compose":
            cmdstring = "compose %s" % args.subOptions.as_argparse_opts()
            if args.subOptions.rest is not None:
                cmdstring += " " + args.subOptions.rest
        else:
            cmdstring = settings.get("initial_command")
    except CommandParseError as e:
        sys.exit(e)

    # set up and start interface
    UI(dbman, cmdstring)

    # run the exit hook
    exit_hook = settings.get_hook("exit")
    if exit_hook is not None:
        exit_hook()
Exemple #5
0
def main():
    # set up the parser to parse the command line options.
    parser = argparse.ArgumentParser()
    parser.add_argument('-v', '--version', action='version',
                        version=alot.__version__)
    parser.add_argument('-r', '--read-only', action='store_true',
                        help='open db in read only mode')
    parser.add_argument('-c', '--config', help='config file',
                        type=lambda x: argparse.FileType('r')(x).name)
    parser.add_argument('-n', '--notmuch-config', default=os.environ.get(
                            'NOTMUCH_CONFIG',
                            os.path.expanduser('~/.notmuch-config')),
                        type=lambda x: argparse.FileType('r')(x).name,
                        help='notmuch config')
    parser.add_argument('-C', '--colour-mode',
                        choices=(1, 16, 256), type=int, default=256,
                        help='terminal colour mode [default: %(default)s].')
    parser.add_argument('-p', '--mailindex-path', #type=directory,
                        help='path to notmuch index')
    parser.add_argument('-d', '--debug-level', default='info',
                        choices=('debug', 'info', 'warning', 'error'),
                        help='debug log [default: %(default)s]')
    parser.add_argument('-l', '--logfile', default='/dev/null',
                        type=lambda x: argparse.FileType('w')(x).name,
                        help='logfile [default: %(default)s]')
    # We will handle the subcommands in a seperate run of argparse as argparse
    # does not support optional subcommands until now.
    subcommands = ('search', 'compose', 'bufferlist', 'taglist', 'pyshell')
    parser.add_argument('command', nargs=argparse.REMAINDER,
                        help='possible subcommands are {}'.format(
                            ', '.join(subcommands)))
    options = parser.parse_args()
    if options.command:
        # We have a command after the initial options so we also parse that.
        # But we just use the parser that is already defined for the internal
        # command that will back this subcommand.
        parser = argparse.ArgumentParser()
        subparsers = parser.add_subparsers(dest='subcommand')
        for subcommand in subcommands:
            subparsers.add_parser(subcommand,
                                  parents=[COMMANDS['global'][subcommand][1]])
        command = parser.parse_args(options.command)
    else:
        command = None

    # logging
    root_logger = logging.getLogger()
    for log_handler in root_logger.handlers:
        root_logger.removeHandler(log_handler)
    root_logger = None
    numeric_loglevel = getattr(logging, options.debug_level.upper(), None)
    logformat = '%(levelname)s:%(module)s:%(message)s'
    logging.basicConfig(level=numeric_loglevel, filename=options.logfile,
                        filemode='w', format=logformat)

    # locate alot config files
    if options.config is None:
        alotconfig = os.path.join(
            os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')),
            'alot', 'config')
        if not os.path.exists(alotconfig):
            alotconfig = None
    else:
        alotconfig = options.config

    try:
        settings.read_config(alotconfig)
        settings.read_notmuch_config(options.notmuch_config)
    except (ConfigError, OSError, IOError) as e:
        sys.exit(e)

    # store options given by config swiches to the settingsManager:
    if options.colour_mode:
        settings.set('colourmode', options.colour_mode)

    # get ourselves a database manager
    indexpath = settings.get_notmuch_setting('database', 'path')
    indexpath = options.mailindex_path or indexpath
    dbman = DBManager(path=indexpath, ro=options.read_only)

    # determine what to do
    if command is None:
        try:
            cmdstring = settings.get('initial_command')
        except CommandParseError as err:
            sys.exit(err)
    elif command.subcommand in subcommands:
        cmdstring = ' '.join(options.command)

    # set up and start interface
    UI(dbman, cmdstring)

    # run the exit hook
    exit_hook = settings.get_hook('exit')
    if exit_hook is not None:
        exit_hook()
Exemple #6
0
    def flush(self):
        """
        write out all queued write-commands in order, each one in a separate
        :meth:`atomic <notmuch.Database.begin_atomic>` transaction.

        If this fails the current action is rolled back, stays in the write
        queue and an exception is raised.
        You are responsible to retry flushing at a later time if you want to
        ensure that the cached changes are applied to the database.

        :exception: :exc:`~errors.DatabaseROError` if db is opened read-only
        :exception: :exc:`~errors.DatabaseLockedError` if db is locked
        """
        if self.ro:
            raise DatabaseROError()
        if self.writequeue:
            # read notmuch's config regarding imap flag synchronization
            sync = settings.get_notmuch_setting('maildir', 'synchronize_flags')

            # go through writequeue entries
            while self.writequeue:
                current_item = self.writequeue.popleft()
                logging.debug('write-out item: %s' % str(current_item))

                # watch out for notmuch errors to re-insert current_item
                # to the queue on errors
                try:
                    # the first two coordinants are cnmdname and post-callback
                    cmd, afterwards = current_item[:2]
                    logging.debug('cmd created')

                    # aquire a writeable db handler
                    try:
                        mode = Database.MODE.READ_WRITE
                        db = Database(path=self.path, mode=mode)
                    except NotmuchError:
                        raise DatabaseLockedError()
                    logging.debug('got write lock')

                    # make this a transaction
                    db.begin_atomic()
                    logging.debug('got atomic')

                    if cmd == 'add':
                        logging.debug('add')
                        path, tags = current_item[2:]
                        msg, status = db.add_message(path,
                                                     sync_maildir_flags=sync)
                        logging.debug('added msg')
                        msg.freeze()
                        logging.debug('freeze')
                        for tag in tags:
                            msg.add_tag(tag.encode(DB_ENC),
                                        sync_maildir_flags=sync)
                        logging.debug('added tags ')
                        msg.thaw()
                        logging.debug('thaw')

                    elif cmd == 'remove':
                        path = current_item[2]
                        db.remove_message(path)

                    else:  # tag/set/untag
                        querystring, tags = current_item[2:]
                        query = db.create_query(querystring)
                        for msg in query.search_messages():
                            msg.freeze()
                            if cmd == 'tag':
                                for tag in tags:
                                    msg.add_tag(tag.encode(DB_ENC),
                                                sync_maildir_flags=sync)
                            if cmd == 'set':
                                msg.remove_all_tags()
                                for tag in tags:
                                    msg.add_tag(tag.encode(DB_ENC),
                                                sync_maildir_flags=sync)
                            elif cmd == 'untag':
                                for tag in tags:
                                    msg.remove_tag(tag.encode(DB_ENC),
                                                   sync_maildir_flags=sync)
                            msg.thaw()

                    logging.debug('ended atomic')
                    # end transaction and reinsert queue item on error
                    if db.end_atomic() != notmuch.STATUS.SUCCESS:
                        raise DatabaseError('end_atomic failed')
                    logging.debug('ended atomic')

                    # close db
                    db.close()
                    logging.debug('closed db')

                    # call post-callback
                    if callable(afterwards):
                        logging.debug(str(afterwards))
                        afterwards()
                        logging.debug('called callback')

                # re-insert item to the queue upon Xapian/NotmuchErrors
                except (XapianError, NotmuchError) as e:
                    logging.exception(e)
                    self.writequeue.appendleft(current_item)
                    raise DatabaseError(unicode(e))
                except DatabaseLockedError as e:
                    logging.debug('index temporarily locked')
                    self.writequeue.appendleft(current_item)
                    raise e
                logging.debug('flush finished')
Exemple #7
0
def main():
    # interpret cml arguments
    args = Options()
    try:
        args.parseOptions()  # When given no argument, parses sys.argv[1:]
    except usage.UsageError as errortext:
        print('%s' % errortext)
        print('Try --help for usage details.')
        sys.exit(1)

    # logging
    root_logger = logging.getLogger()
    for log_handler in root_logger.handlers:
        root_logger.removeHandler(log_handler)
    root_logger = None
    numeric_loglevel = getattr(logging, args['debug-level'].upper(), None)
    logfilename = os.path.expanduser(args['logfile'])
    logformat = '%(levelname)s:%(module)s:%(message)s'
    logging.basicConfig(level=numeric_loglevel, filename=logfilename,
                        filemode='w', format=logformat)

    # locate alot config files
    configfiles = [
        os.path.join(os.environ.get('XDG_CONFIG_HOME',
                                    os.path.expanduser('~/.config')),
                     'alot', 'config'),
    ]
    if args['config']:
        expanded_path = os.path.expanduser(args['config'])
        if not os.path.exists(expanded_path):
            msg = 'Config file "%s" does not exist. Goodbye for now.'
            sys.exit(msg % expanded_path)
        configfiles.insert(0, expanded_path)

    # locate notmuch config
    notmuchpath = os.environ.get('NOTMUCH_CONFIG', '~/.notmuch-config')
    if args['notmuch-config']:
        notmuchpath = args['notmuch-config']
    notmuchconfig = os.path.expanduser(notmuchpath)

    alotconfig = None
    # read the first alot config file we find
    for configfilename in configfiles:
        if os.path.exists(configfilename):
            alotconfig = configfilename
            break  # use only the first

    try:
        settings.read_config(alotconfig)
        settings.read_notmuch_config(notmuchconfig)
    except (ConfigError, OSError, IOError) as e:
        sys.exit(e)

    # store options given by config swiches to the settingsManager:
    if args['colour-mode']:
        settings.set('colourmode', args['colour-mode'])

    # get ourselves a database manager
    indexpath = settings.get_notmuch_setting('database', 'path')
    indexpath = args['mailindex-path'] or indexpath
    dbman = DBManager(path=indexpath, ro=args['read-only'])

    # determine what to do
    try:
        if args.subCommand == 'search':
            query = ' '.join(args.subOptions.args)
            cmdstring = 'search %s %s' % (args.subOptions.as_argparse_opts(),
                                          query)
        elif args.subCommand == 'compose':
            cmdstring = 'compose %s' % args.subOptions.as_argparse_opts()
            if args.subOptions.rest is not None:
                cmdstring += ' ' + args.subOptions.rest
        else:
            cmdstring = settings.get('initial_command')
    except CommandParseError as e:
        sys.exit(e)

    # set up and start interface
    UI(dbman, cmdstring)

    # run the exit hook
    exit_hook = settings.get_hook('exit')
    if exit_hook is not None:
        exit_hook()
Exemple #8
0
    def flush(self):
        """
        write out all queued write-commands in order, each one in a separate
        :meth:`atomic <notmuch.Database.begin_atomic>` transaction.

        If this fails the current action is rolled back, stays in the write
        queue and an exception is raised.
        You are responsible to retry flushing at a later time if you want to
        ensure that the cached changes are applied to the database.

        :exception: :exc:`~errors.DatabaseROError` if db is opened read-only
        :exception: :exc:`~errors.DatabaseLockedError` if db is locked
        """
        if self.ro:
            raise DatabaseROError()
        if self.writequeue:
            # read notmuch's config regarding imap flag synchronization
            sync = settings.get_notmuch_setting('maildir', 'synchronize_flags')

            # go through writequeue entries
            while self.writequeue:
                current_item = self.writequeue.popleft()
                logging.debug('write-out item: %s' % str(current_item))

                # watch out for notmuch errors to re-insert current_item
                # to the queue on errors
                try:
                    # the first two coordinants are cnmdname and post-callback
                    cmd, afterwards = current_item[:2]
                    logging.debug('cmd created')

                    # aquire a writeable db handler
                    try:
                        mode = Database.MODE.READ_WRITE
                        db = Database(path=self.path, mode=mode)
                    except NotmuchError:
                        raise DatabaseLockedError()
                    logging.debug('got write lock')

                    # make this a transaction
                    db.begin_atomic()
                    logging.debug('got atomic')

                    if cmd == 'add':
                        logging.debug('add')
                        path, tags = current_item[2:]
                        msg, status = db.add_message(path,
                                                     sync_maildir_flags=sync)
                        logging.debug('added msg')
                        msg.freeze()
                        logging.debug('freeze')
                        for tag in tags:
                            msg.add_tag(tag.encode(DB_ENC),
                                        sync_maildir_flags=sync)
                        logging.debug('added tags ')
                        msg.thaw()
                        logging.debug('thaw')

                    elif cmd == 'remove':
                        path = current_item[2]
                        db.remove_message(path)

                    else:  # tag/set/untag
                        querystring, tags = current_item[2:]
                        query = db.create_query(querystring)
                        for msg in query.search_messages():
                            msg.freeze()
                            if cmd == 'tag':
                                for tag in tags:
                                    msg.add_tag(tag.encode(DB_ENC),
                                                sync_maildir_flags=sync)
                            if cmd == 'set':
                                msg.remove_all_tags()
                                for tag in tags:
                                    msg.add_tag(tag.encode(DB_ENC),
                                                sync_maildir_flags=sync)
                            elif cmd == 'untag':
                                for tag in tags:
                                    msg.remove_tag(tag.encode(DB_ENC),
                                                   sync_maildir_flags=sync)
                            msg.thaw()

                    logging.debug('ended atomic')
                    # end transaction and reinsert queue item on error
                    if db.end_atomic() != notmuch.STATUS.SUCCESS:
                        raise DatabaseError('end_atomic failed')
                    logging.debug('ended atomic')

                    # close db
                    db.close()
                    logging.debug('closed db')

                    # call post-callback
                    if callable(afterwards):
                        logging.debug(str(afterwards))
                        afterwards()
                    logging.debug('called callback')

                # re-insert item to the queue upon Xapian/NotmuchErrors
                except (XapianError, NotmuchError) as e:
                    logging.exception(e)
                    self.writequeue.appendleft(current_item)
                    raise DatabaseError(unicode(e))
                except DatabaseLockedError as e:
                    logging.debug('index temporarily locked')
                    self.writequeue.appendleft(current_item)
                    raise e
                logging.debug('flush finished')
Exemple #9
0
        if os.path.exists(configfilename):
            alotconfig = configfilename
            break  # use only the first

    try:
        settings.read_config(alotconfig)
        settings.read_notmuch_config(notmuchconfig)
    except (ConfigError, OSError, IOError), e:
        sys.exit(e)

    # store options given by config swiches to the settingsManager:
    if args['colour-mode']:
        settings.set('colourmode', args['colour-mode'])

    # get ourselves a database manager
    indexpath = settings.get_notmuch_setting('database', 'path')
    indexpath = args['mailindex-path'] or indexpath
    dbman = DBManager(path=indexpath, ro=args['read-only'])

    # determine what to do
    try:
        if args.subCommand == 'search':
            query = ' '.join(args.subOptions.args)
            cmdstring = 'search %s %s' % (args.subOptions.as_argparse_opts(),
                                          query)
            cmd = commands.commandfactory(cmdstring, 'global')
        elif args.subCommand == 'compose':
            cmdstring = 'compose %s' % args.subOptions.as_argparse_opts()
            cmd = commands.commandfactory(cmdstring, 'global')
        else:
            default_commandline = settings.get('initial_command')
Exemple #10
0
def main():
    # interpret cml arguments
    args = Options()
    try:
        args.parseOptions()  # When given no argument, parses sys.argv[1:]
    except usage.UsageError as errortext:
        print('%s' % errortext)
        print('Try --help for usage details.')
        sys.exit(1)

    # logging
    root_logger = logging.getLogger()
    for log_handler in root_logger.handlers:
        root_logger.removeHandler(log_handler)
    root_logger = None
    numeric_loglevel = getattr(logging, args['debug-level'].upper(), None)
    logfilename = os.path.expanduser(args['logfile'])
    logformat = '%(levelname)s:%(module)s:%(message)s'
    logging.basicConfig(level=numeric_loglevel,
                        filename=logfilename,
                        filemode='w',
                        format=logformat)

    # locate alot config files
    configfiles = [
        os.path.join(
            os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')),
            'alot', 'config'),
    ]
    if args['config']:
        expanded_path = os.path.expanduser(args['config'])
        if not os.path.exists(expanded_path):
            msg = 'Config file "%s" does not exist. Goodbye for now.'
            sys.exit(msg % expanded_path)
        configfiles.insert(0, expanded_path)

    # locate notmuch config
    notmuchpath = os.environ.get('NOTMUCH_CONFIG', '~/.notmuch-config')
    if args['notmuch-config']:
        notmuchpath = args['notmuch-config']
    notmuchconfig = os.path.expanduser(notmuchpath)

    alotconfig = None
    # read the first alot config file we find
    for configfilename in configfiles:
        if os.path.exists(configfilename):
            alotconfig = configfilename
            break  # use only the first

    try:
        settings.read_config(alotconfig)
        settings.read_notmuch_config(notmuchconfig)
    except (ConfigError, OSError, IOError) as e:
        sys.exit(e)

    # store options given by config swiches to the settingsManager:
    if args['colour-mode']:
        settings.set('colourmode', args['colour-mode'])

    # get ourselves a database manager
    indexpath = settings.get_notmuch_setting('database', 'path')
    indexpath = args['mailindex-path'] or indexpath
    dbman = DBManager(path=indexpath, ro=args['read-only'])

    # determine what to do
    try:
        if args.subCommand == 'search':
            query = ' '.join(args.subOptions.args)
            cmdstring = 'search %s %s' % (args.subOptions.as_argparse_opts(),
                                          query)
        elif args.subCommand == 'compose':
            cmdstring = 'compose %s' % args.subOptions.as_argparse_opts()
            if args.subOptions.rest is not None:
                cmdstring += ' ' + args.subOptions.rest
        else:
            cmdstring = settings.get('initial_command')
    except CommandParseError as e:
        sys.exit(e)

    # set up and start interface
    UI(dbman, cmdstring)

    # run the exit hook
    exit_hook = settings.get_hook('exit')
    if exit_hook is not None:
        exit_hook()