uninstall.py

The uninstall script for this project

THis module uninstalls the project, removing its files and folders to the appropriate locations on the user's computer.

This file is real ugly b/c we can't access the venv, so we do it manually.

CNFormatter

Bases: RawTextHelpFormatter, RawDescriptionHelpFormatter

A dummy class to combine multiple argparse formatters

Parameters:

Name Type Description Default
RawTextHelpFormatter

Maintains whitespace for all sorts of help text,

required
RawDescriptionHelpFormatter

Indicates that description and epilog are

required

A dummy class to combine multiple argparse formatters.

Source code in install/uninstall.py
class CNFormatter(
    argparse.RawTextHelpFormatter, argparse.RawDescriptionHelpFormatter
):
    """
    A dummy class to combine multiple argparse formatters

    Args:
        RawTextHelpFormatter: Maintains whitespace for all sorts of help text,
        including argument descriptions.
        RawDescriptionHelpFormatter: Indicates that description and epilog are
        already correctly formatted and should not be line-wrapped.

    A dummy class to combine multiple argparse formatters.
    """

CNUninstall

The class to use for uninstalling

This class performs the uninstall operation.

Source code in install/uninstall.py
class CNUninstall:
    """
    The class to use for uninstalling

    This class performs the uninstall operation.
    """

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

    # keys
    S_KEY_INST_NAME = "INST_NAME"
    S_KEY_INST_VER = "INST_VER"
    S_KEY_INST_DESK = "INST_DESK"
    S_KEY_INST_CONT = "INST_CONT"

    # short description
    S_PP_SHORT_DESC = "A program for creating and building CLI/GUI/Packages in Python from a template"

    # version string
    S_PP_VERSION = "0.0.3"

    # debug option strings
    S_ARG_DRY_OPTION = "-d"
    S_ARG_DRY_ACTION = "store_true"
    S_ARG_DRY_DEST = "DRY_DEST"
    # I18N help string for debug cmd line option
    S_ARG_DRY_HELP = _("enable dry run 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")

    # about string (to be set by subclass)
    S_ABOUT = (
        "\n"
        "PyPlate\n"
        f"{S_PP_SHORT_DESC}\n"
        f"{S_PP_VERSION}\n"
        "https://github.com/cyclopticnerve/PyPlate\n"
    )

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

    # cmd line instructions string (to be set by subclass)
    S_EPILOG = ""

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

    # messages

    # NB: format param is prog_name
    # I18N: uninstall the program
    S_MSG_UNINST_START = _("Uninstalling {}")
    # NB: format param is prog_name
    # I18N: done uninstalling
    S_MSG_UNINST_END = _("{} uninstalled")
    # I18N: done with step
    S_MSG_DONE = _("Done")
    # I18N: step failed
    S_MSG_FAIL = _("Fail")
    # I18N: show the copy step
    S_MSG_DEL_START = _("Deleting files... ")
    # I18N: uninstall aborted
    S_MSG_ABORT = _("Uninstallation aborted")

    # 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?")

    # errors

    # NB: format param is file path
    # I18N: config file not found
    S_ERR_NOT_FOUND = _("File {} not found")
    # NB: format param is file path
    # I18N: config file is not valid json
    S_ERR_NOT_JSON = _("File {} is not a JSON file")

    # dry run messages

    # NB: format param is file or dir path
    S_DRY_REMOVE = "\nremove\n{}"

    # --------------------------------------------------------------------------
    # Class methods
    # --------------------------------------------------------------------------

    # --------------------------------------------------------------------------
    # Initialize the class
    # --------------------------------------------------------------------------
    def __init__(self):
        """
        Initialize the class

        Creates a new instance of the object and initializes its properties.
        """

        # set arg properties
        self._dict_args = {}
        self._dry_run = False

        # project stuff
        self._dir_usr_inst = Path()
        self._path_cfg_uninst = Path()

        # config stuff
        self._dict_cfg = {}

        # cmd line stuff
        # NB: placeholder to avoid comparing to None
        self._parser = argparse.ArgumentParser()

    # ------------------------------------------------------------------------------
    # Uninstall the program
    # ------------------------------------------------------------------------------
    def main(self, dir_usr_inst, path_cfg_uninst):
        """
        Uninstall the program

        Args:
            dir_usr_inst: The program's install folder in which files are
            placed
            path_cfg_uninst: Path to the currently installed program's
            uninstall dict info

        Runs the uninstall operation.
        """

        # set props from params
        if dir_usr_inst:
            dir_usr_inst = Path(dir_usr_inst)
            if not dir_usr_inst.is_absolute():
                # make abs rel to self
                dir_usr_inst = P_DIR_PRJ / dir_usr_inst
        self._dir_usr_inst = dir_usr_inst

        if path_cfg_uninst:
            path_cfg_uninst = Path(path_cfg_uninst)
            if not path_cfg_uninst.is_absolute():
                # make abs rel to self
                path_cfg_uninst = P_DIR_PRJ / path_cfg_uninst
        self._path_cfg_uninst = path_cfg_uninst

        # do setup
        self._setup()

        # parse cmd line and get args
        self._do_cmd_line()

        # get prj info from cfg
        self._get_project_info()

        # create an instance of the class
        self._uninstall_content()

        # wind down
        self._teardown()

    # --------------------------------------------------------------------------
    # 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 setting properties.
        """

        # print default about text
        print(self.S_ABOUT)

        # create a parser object in case we need it
        self._parser = argparse.ArgumentParser(
            add_help=False,
            epilog=self.S_EPILOG,
            formatter_class=CNFormatter,
        )

        # add help text to about block
        print(self.S_ABOUT_HELP)

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

        # add dry run option
        self._parser.add_argument(
            self.S_ARG_DRY_OPTION,
            dest=self.S_ARG_DRY_DEST,
            help=self.S_ARG_DRY_HELP,
            action=self.S_ARG_DRY_ACTION,
        )

    # --------------------------------------------------------------------------
    # Parse the arguments from the command line
    # --------------------------------------------------------------------------
    def _do_cmd_line(self):
        """
        Parse the arguments from the command line

        Parse the arguments from the command line, after the parser has been
        set up.
        """

        # get namespace object
        args = self._parser.parse_args()

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

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

        # no -h, print epilog
        print(self.S_EPILOG)

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

        # get the args
        self._dry_run = self._dict_args.get(self.S_ARG_DRY_DEST, False)

    # --------------------------------------------------------------------------
    # Get project info
    # --------------------------------------------------------------------------
    def _get_project_info(self):
        """
        Get project info

        Get the install info from the config file.
        """

        # get project info
        self._dict_cfg = self._get_dict_from_file(self._path_cfg_uninst)

        # get prg name/version
        prog_name = self._dict_cfg[self.S_KEY_INST_NAME]

        # ask to uninstall
        str_ask = self._dialog(
            self.S_ASK_UNINST.format(prog_name),
            [self.S_ASK_YES, self.S_ASK_NO],
            self.S_ASK_NO,
        )

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

        # print start msg
        print(self.S_MSG_UNINST_START.format(prog_name))

    # uninstall

    # --------------------------------------------------------------------------
    # Uninstall the program
    # --------------------------------------------------------------------------
    def _uninstall_content(self):
        """
        Uninstall the program

        Runs the uninstall operation.
        """

        # uninstall

        # show some info
        print(self.S_MSG_DEL_START, flush=True, end="")

        # content list from dict
        content = self._dict_cfg.get(self.S_KEY_INST_CONT, [])

        # for each key, value
        for item in content:

            # get full path of destination
            src = Path.home() / item

            # debug may omit certain assets
            if not src.exists():
                continue

            # (maybe) do delete
            if self._dry_run:
                print(self.S_DRY_REMOVE.format(item))
            else:

                # if the source is a dir
                if src.is_dir():
                    # remove dir
                    shutil.rmtree(src)

                # if the source is a file
                else:
                    # copy file
                    src.unlink()

        # show some info
        print(self.S_MSG_DONE)

    # --------------------------------------------------------------------------
    # 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.
        """

        # just show we are done
        prog_name = self._dict_cfg[self.S_KEY_INST_NAME]
        print(self.S_MSG_UNINST_END.format(prog_name))

    # --------------------------------------------------------------------------
    # These are the minor steps, called from major steps for support
    # --------------------------------------------------------------------------

    # --------------------------------------------------------------------------
    # Get a dict from a file
    # --------------------------------------------------------------------------
    def _get_dict_from_file(self, a_file):
        """
        Get a dict from a file

        Args:
            a_file: The file to load the dict from

        Raises:
            OSError if the file cannot be found or is not a valid JSON file

        Returns:
            The dict found in the file

        Get a dict from a file, checking if the file exists and is a valid JSON
        file

        """
        # default result
        a_dict = {}

        # get dict from file
        try:
            with open(a_file, "r", encoding="UTF-8") as a_file:
                a_dict = json.load(a_file)

        # file not found
        except FileNotFoundError as e:
            raise OSError(self.S_ERR_NOT_FOUND.format(a_file)) from e

        # not valid json in file
        except json.JSONDecodeError as e:
            raise OSError(self.S_ERR_NOT_JSON.format(a_file)) from e

        # return result
        return a_dict

    # --------------------------------------------------------------------------
    # Create a dialog-like question and return the result
    # --------------------------------------------------------------------------
    def _dialog(
        self, message, buttons, default="", btn_sep="/", msg_fmt="{} [{}]: "
    ):
        """
        Create a dialog-like question and return the result

        Args:
            message: The message to display
            buttons: List of single char answers to the question
            default: The button item to return when the user presses Enter at \
                the question (default: "")
            btn_sep: Char to use to separate button items
            msg_fmt: Format string to present message/buttons to the user

        Returns:
            String that matches button (or empty string if entered option is not in button list)

        This method returns the string entered on the command line in response
        to a question. If the entered option does not match any of the buttons,
        a blank string is returned. If you set a default and the option entered
        is just the Return key, the default string will be returned. If no
        default is present, the entered string must match one of the buttons
        array values. All returned values are lowercased.
        """

        # make all params lowercase
        buttons = [item.lower() for item in buttons]
        default = default.lower()

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

        # if we passes a default
        if default != "":

            # find the default
            if not default in buttons:

                # not found, add at end of buttons
                buttons.append(default)

            # upper case it
            buttons[buttons.index(default)] = default.upper()

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

        # add buttons to message
        btns_all = btn_sep.join(buttons)
        str_fmt = msg_fmt.format(message, btns_all)

        # lower everything again for compare
        buttons = [item.lower() for item in buttons]

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

        while True:

            # ask the question, get the result
            inp = input(str_fmt)
            inp = inp.lower()

            # # no input (empty)
            if inp == "" and default != "":
                return default

            # input a button
            if inp in buttons:
                return inp

__init__()

Initialize the class

Creates a new instance of the object and initializes its properties.

Source code in install/uninstall.py
def __init__(self):
    """
    Initialize the class

    Creates a new instance of the object and initializes its properties.
    """

    # set arg properties
    self._dict_args = {}
    self._dry_run = False

    # project stuff
    self._dir_usr_inst = Path()
    self._path_cfg_uninst = Path()

    # config stuff
    self._dict_cfg = {}

    # cmd line stuff
    # NB: placeholder to avoid comparing to None
    self._parser = argparse.ArgumentParser()

main(dir_usr_inst, path_cfg_uninst)

Uninstall the program

Parameters:

Name Type Description Default
dir_usr_inst

The program's install folder in which files are

required
path_cfg_uninst

Path to the currently installed program's

required

Runs the uninstall operation.

Source code in install/uninstall.py
def main(self, dir_usr_inst, path_cfg_uninst):
    """
    Uninstall the program

    Args:
        dir_usr_inst: The program's install folder in which files are
        placed
        path_cfg_uninst: Path to the currently installed program's
        uninstall dict info

    Runs the uninstall operation.
    """

    # set props from params
    if dir_usr_inst:
        dir_usr_inst = Path(dir_usr_inst)
        if not dir_usr_inst.is_absolute():
            # make abs rel to self
            dir_usr_inst = P_DIR_PRJ / dir_usr_inst
    self._dir_usr_inst = dir_usr_inst

    if path_cfg_uninst:
        path_cfg_uninst = Path(path_cfg_uninst)
        if not path_cfg_uninst.is_absolute():
            # make abs rel to self
            path_cfg_uninst = P_DIR_PRJ / path_cfg_uninst
    self._path_cfg_uninst = path_cfg_uninst

    # do setup
    self._setup()

    # parse cmd line and get args
    self._do_cmd_line()

    # get prj info from cfg
    self._get_project_info()

    # create an instance of the class
    self._uninstall_content()

    # wind down
    self._teardown()