spaceoddity_base.py

The base file for a cli or gui program

This file contains all the boring boilerplate code for making a robust CLI/GUI application. It is not intended to be run directly, but rather subclassed. The subclass should contain, at minimum, the main method and the top-level run code (examples are given in the cli/src and gui/src subdirectories of the template directory).

SpaceoddityBase

The main class, responsible for the operation of the program

Public methods

main: The main method of the program

This class does the most of the work of a typical CLI program. It parses command line options, loads/saves config files, and performs the operations required for the program.

Source code in src/spaceoddity_base.py
class SpaceoddityBase:
    """
    The main class, responsible for the operation of the program

    Public methods:
        main: The main method of the program

    This class does the most of the work of a typical CLI program. It parses
    command line options, loads/saves config files, and performs the operations
    required for the program.
    """

    # --------------------------------------------------------------------------
    # Class constants
    # --------------------------------------------------------------------------

    # --------------------------------------------------------------------------
    # bools

    # set load/save oder
    # NB: if true, only load from first file found, in this order:
    # 1. cmdline (-c)
    # 2. conf dir (P_CFG_DEF)
    # 3. internal (self._dict_cfg)
    # if false, load all and combine in this order:
    # 1. internal
    # 2. conf dir
    # 3. cmdline
    B_LOAD_FIRST = False
    # NB: if true, only save to first file found, in this order:
    # 1. cmdline (-c)
    # 2. conf dir (P_CFG_DEF)
    # if false, save to all
    B_SAVE_FIRST= False

    # --------------------------------------------------------------------------
    # ints

    # rotating log stuff
    I_LOG_SIZE = 2097152  # max log file size in bytes (2 Mb)
    I_LOG_COUNT = 5  # max number of log files

    # --------------------------------------------------------------------------
    # strings

    # NB: used for logger
    S_APP_NAME = "spaceoddity"

    # short description
    # pylint: disable=line-too-long
    # NB: need to keep on one line for replacement
    S_PP_SHORT_DESC = "Set the NASA Astronomy Picture of the Day as your wallpaper"
    # pylint: enable=line-too-long

    # version string
    S_PP_VERSION = "Version 0.1.0"

    # config option strings
    S_ARG_CFG_OPTION = "-c"
    S_ARG_CFG_DEST = "CFG_DEST"
    # I18N: config file option help
    S_ARG_CFG_HELP = _("load configuration from file")
    # I18N: config file dest (indicate it should be a file name/path)
    S_ARG_CFG_METAVAR = _("FILE")

    # debug option strings
    S_ARG_DBG_OPTION = "-d"
    S_ARG_DBG_ACTION = "store_true"
    S_ARG_DBG_DEST = "DBG_DEST"
    # I18N: debug mode help
    S_ARG_DBG_HELP = _("enable debugging mode")

    # config option strings
    S_ARG_HLP_OPTION = "-h"
    S_ARG_HLP_ACTION = "store_true"
    S_ARG_HLP_DEST = "HLP_DEST"
    # I18N: help option help
    S_ARG_HLP_HELP = _("show this help message and exit")

    # config option strings
    S_ARG_UNINST_OPTION = "--uninstall"
    S_ARG_UNINST_ACTION = "store_true"
    S_ARG_UNINST_DEST = "UNINST_DEST"
    # I18N: uninstall option help
    S_ARG_UNINST_HELP = _("uninstall this program")

    # about string
    S_ABOUT = (
        "SpaceOddity\n"
        f"{S_PP_SHORT_DESC}\n"
        f"{S_PP_VERSION}\n"
        "https://github.com/cyclopticnerve/SpaceOddity"
    )

    # I18N if using argparse, add help at end of about
    S_ABOUT_HELP = _("Use -h for help")

    # default format for log files
    S_LOG_FMT = "%(asctime)s [%(levelname)-7s] %(message)s"
    S_LOG_DATE_FMT = "%Y-%m-%d %I:%M:%S"

    # --------------------------------------------------------------------------
    # questions

    # I18N: answer yes
    S_ASK_YES = _("y")
    # I18N: answer no
    S_ASK_NO = _("N")
    # NB: format param is prog name
    # I18N: ask to uninstall
    S_ASK_UNINST = _("This will uninstall {}.\nDo you want to continue?")

    # --------------------------------------------------------------------------
    # messages

    # I18N: process aborted
    S_MSG_ABORT = _("Aborted")

    # --------------------------------------------------------------------------
    # error messages

    # I18N: an error occurred
    S_ERR_ERR = _("Error:")
    # I18N: uninstall not found
    S_ERR_NO_UNINST = _("Uninstall files not found")
    # NB: format param is file path
    # I18N: could not find -c file
    S_ERR_NO_CFG = _("Config file {} not found")

    # --------------------------------------------------------------------------
    # Instance methods
    # --------------------------------------------------------------------------

    # --------------------------------------------------------------------------
    # Initialize the new object
    # --------------------------------------------------------------------------
    def __init__(self):
        """
        Initialize the new object

        Initializes a new instance of the class, setting the default values
        of its properties, and any other code that needs to run to create a
        new object.
        """

        # set defaults

        # args and arg props
        self._dict_args = {}
        self._arg_cfg = None
        self._arg_debug = False

        # cfg stuff
        self._dict_cfg = {}
        self._path_cfg_def = P_CFG_DEF

        # ----------------------------------------------------------------------

        # make some folders
        if not P_DIR_CONF.exists():
            Path.mkdir(P_DIR_CONF)
        if not P_DIR_LOG.exists():
            Path.mkdir(P_DIR_LOG)

        # make a rotating handler
        handler = RotatingFileHandler(
            str(P_LOG_DEF),
            maxBytes=self.I_LOG_SIZE,
            backupCount=self.I_LOG_COUNT,
        )

        # add a formatter to rot handler
        formatter = logging.Formatter(
            self.S_LOG_FMT, datefmt=self.S_LOG_DATE_FMT
        )

        # set formatter to handler
        handler.setFormatter(formatter)

        # create logger and add rot handler
        self._logger = logging.getLogger(self.S_APP_NAME)
        self._logger.setLevel(logging.INFO)
        self._logger.addHandler(handler)

        # ----------------------------------------------------------------------

        # cmd line stuff
        self._parser = argparse.ArgumentParser(
            formatter_class=CNFormatter, add_help=False, prog=self.S_APP_NAME
        )

    # --------------------------------------------------------------------------
    # Private methods
    # --------------------------------------------------------------------------

    # NB: these are the main steps, called in order from main()

    # --------------------------------------------------------------------------
    # Boilerplate to use at the start of main
    # --------------------------------------------------------------------------
    def _setup(self):
        """
        Boilerplate to use at the start of main

        Perform some mundane stuff like running the arg parser and loading
        config files.
        """

        # ----------------------------------------------------------------------
        # use cmd line

        # always add help option
        self._parser.add_argument(
            self.S_ARG_HLP_OPTION,
            action=self.S_ARG_HLP_ACTION,
            dest=self.S_ARG_HLP_DEST,
            help=self.S_ARG_HLP_HELP,
        )

        # add uninstall option
        self._parser.add_argument(
            self.S_ARG_UNINST_OPTION,
            action=self.S_ARG_UNINST_ACTION,
            dest=self.S_ARG_UNINST_DEST,
            help=self.S_ARG_UNINST_HELP,
        )

        # run the parser
        args = self._parser.parse_args()

        # convert namespace to dict
        self._dict_args = vars(args)

        # ----------------------------------------------------------------------
        # check for one-shot args

        # if -h passed, this will print and exit
        if self._dict_args.get(self.S_ARG_HLP_DEST, False):

            # print default about text
            print()
            print(self.S_ABOUT)
            print()

            # print usage and arg info and exit
            self._parser.print_help()
            print()
            sys.exit(0)

        # ----------------------------------------------------------------------
        # check for -d (debug)

        # set self and lib debug
        self._arg_debug = self._dict_args.get(
            self.S_ARG_DBG_DEST, self._arg_debug
        )
        F.B_DEBUG = self._arg_debug

        # ----------------------------------------------------------------------
        # check for --uninstall

        # punt to uninstall func
        if self._dict_args.get(self.S_ARG_UNINST_DEST, False):

            # uninstall and exit
            self._do_uninstall()
            # NB: exit is handled by _do_uninstall

        # ----------------------------------------------------------------------
        # set props from args

        # set cfg path
        self._arg_cfg = self._dict_args.get(self.S_ARG_CFG_DEST, self._arg_cfg)

        # sanity checks
        if self._path_cfg_def:
            self._path_cfg_def = Path(self._path_cfg_def)
            if not self._path_cfg_def.is_absolute():
                # make abs rel to self
                self._path_cfg_def = P_DIR_PRJ / self._path_cfg_def

        # accept path or str
        if self._arg_cfg:
            self._arg_cfg = Path(self._arg_cfg)
            if not self._arg_cfg.is_absolute():
                # make abs rel to self
                self._arg_cfg = P_DIR_PRJ / self._arg_cfg

        # ----------------------------------------------------------------------
        # use cfg
        self._load_config()

    # --------------------------------------------------------------------------
    # Boilerplate to use at the end of main
    # --------------------------------------------------------------------------
    def _teardown(self):
        """
        Boilerplate to use at the end of main

        Perform some mundane stuff like saving config files.
        """

        # ----------------------------------------------------------------------
        # use cfg

        # call to save config
        self._save_config()

    # --------------------------------------------------------------------------
    # Load config data from a file
    # --------------------------------------------------------------------------
    def _load_config(self):
        """
        Load config data from a file

        This method loads data from a config file. It is written to load a dict
        from a json file, but it can be used for other formats as well. It uses
        the values of _dict_cfg (hard-coded), P_CFG_DEF (the default file
        location), and _arg_cfg (file passed on command line) to
        successively load the config data.
        """

        # paths of config files
        l_paths = [self._arg_cfg, self._path_cfg_def]

        for a_path in l_paths:

            # set whole dict to file
            try:
                self._dict_cfg = F.load_paths_into_dict(a_path, self._dict_cfg)

                # stop after first found
                if self.B_LOAD_FIRST:
                    break
            except OSError as e:  # from load_dicts
                F.printd(self.S_ERR_ERR, str(e))

    # --------------------------------------------------------------------------
    # Save config data to a file
    # --------------------------------------------------------------------------
    def _save_config(self):
        """
        Save config data to a file

        This method saves the config data to all the files it can create. It is
        written to save a dict to a json file, but it can be used for other
        formats as well. It uses the values of _dict_cfg, _path_cfg_def, and
        _arg_cfg to save the config data.
        """

        # paths of config files
        l_paths = [self._arg_cfg, self._path_cfg_def]

        # order of saving (highest to lowest)
        for a_path in l_paths:

            # set whole file to dict
            try:
                F.save_dict_into_paths(self._dict_cfg, a_path)

                # stop after first found
                if self.B_SAVE_FIRST:
                    break
            except OSError as e:  # from save_dict
                F.printd(self.S_ERR_ERR, str(e))

    # --------------------------------------------------------------------------
    # Handle the --uninstall cmd line op
    # --------------------------------------------------------------------------
    def _do_uninstall(self):
        """
        Handle the --uninstall cmd line op
        """

        # print some text
        print(self.S_ABOUT)
        print()

        # ask to uninstall
        str_ask = F.dialog(
            self.S_ASK_UNINST.format("SpaceOddity"),
            [F.S_ASK_YES, F.S_ASK_NO],
            F.S_ASK_NO,
        )

        # user hit enter or typed "n/N"
        if str_ask != F.S_ASK_YES:
            print()
            print(self.S_MSG_ABORT)
            print()
            sys.exit(0)

        # ----------------------------------------------------------------------

        # if path exists
        path_uninst = P_UNINST

        # try for install loc's uninstall
        if not path_uninst.exists():
            path_uninst = P_UNINST_DIST

            # still not found? error
            if not path_uninst.exists():
                print(self.S_ERR_NO_UNINST)
                print()
                sys.exit(-1)

        # format cmd line
        cmd = str(path_uninst) + " -f -q"
        if self._arg_debug:
            cmd += " -d"

        # ----------------------------------------------------------------------

        try:
            F.run(cmd, shell=True)
            print()
            sys.exit(0)
        except F.CNRunError as e:
            print(e.output)
            print()
            sys.exit(e.returncode)

__init__()

Initialize the new object

Initializes a new instance of the class, setting the default values of its properties, and any other code that needs to run to create a new object.

Source code in src/spaceoddity_base.py
def __init__(self):
    """
    Initialize the new object

    Initializes a new instance of the class, setting the default values
    of its properties, and any other code that needs to run to create a
    new object.
    """

    # set defaults

    # args and arg props
    self._dict_args = {}
    self._arg_cfg = None
    self._arg_debug = False

    # cfg stuff
    self._dict_cfg = {}
    self._path_cfg_def = P_CFG_DEF

    # ----------------------------------------------------------------------

    # make some folders
    if not P_DIR_CONF.exists():
        Path.mkdir(P_DIR_CONF)
    if not P_DIR_LOG.exists():
        Path.mkdir(P_DIR_LOG)

    # make a rotating handler
    handler = RotatingFileHandler(
        str(P_LOG_DEF),
        maxBytes=self.I_LOG_SIZE,
        backupCount=self.I_LOG_COUNT,
    )

    # add a formatter to rot handler
    formatter = logging.Formatter(
        self.S_LOG_FMT, datefmt=self.S_LOG_DATE_FMT
    )

    # set formatter to handler
    handler.setFormatter(formatter)

    # create logger and add rot handler
    self._logger = logging.getLogger(self.S_APP_NAME)
    self._logger.setLevel(logging.INFO)
    self._logger.addHandler(handler)

    # ----------------------------------------------------------------------

    # cmd line stuff
    self._parser = argparse.ArgumentParser(
        formatter_class=CNFormatter, add_help=False, prog=self.S_APP_NAME
    )