pybaker.py

A program to change the metadata of a PyPlate project and create a dist

This module sets the project metadata in each of the files, according to the data present in the conf files. It then sets up the dist folder with all necessary files to create a complete distribution of the project.

Run pybaker -h for more options.

PyBaker

Bases: PyPlate

The main class, responsible for the operation of the program

Public methods

main: The main method of the program

This class implements all the needed functionality of PyBaker, to create a distribution from a PyPlate project.

Source code in src/pybaker.py
class PyBaker(PyPlate):
    """
    The main class, responsible for the operation of the program

    Public methods:
        main: The main method of the program

    This class implements all the needed functionality of PyBaker, to create a
    distribution from a PyPlate project.
    """

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

    # ide option strings
    S_ARG_IDE_OPTION = "-i"
    S_ARG_IDE_ACTION = "store_true"
    S_ARG_IDE_DEST = "IDE_DEST"
    # I18N help string for ide cmd line option
    S_ARG_IDE_HELP = _("ask for project folder when running in IDE")

    # lang option strings
    S_ARG_LANG_OPTION = "-l"
    S_ARG_LANG_DEST = "LANG_DEST"
    # I18N: lang option help
    S_ARG_LANG_HELP = _("add a language (*.po) file")
    # I18N: lang file source
    S_ARG_LANG_METAVAR = _("FILE")

    # ide option strings
    S_ARG_VER_OPTION = "-v"
    S_ARG_VER_DEST = "VER_DEST"
    # I18N help string for version cmd line option
    S_ARG_VER_HELP = _(
        "set the new version of the project\n(if used, -l is ignored)"
    )
    # I18N: config file dest
    S_ARG_VER_METAVAR = _("VERSION")

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

    # I18N cmd line instructions string
    S_EPILOG = "\n" + _(
        "Run this program from the directory of the project you want to build."
    )

    # error strings

    # I18N: language already exists in project.json and i18n folder
    S_ERR_LANG_EXIST = _("Language file {} already exists")
    S_ERR_NO_LANG = _("Could not get language code from {}")

    # messages

    # bake msg
    # NB: param is name of project folder
    # I18N: prep for baking
    S_MSG_PREP = _("Preparing to bake {}")
    # NB: param is name of project folder
    # I18N: start baking
    S_MSG_BAKE = _("Baking {}")
    # NB: param is name of project folder
    # I18N: done baking
    S_MSG_DONE = _("Done baking {}")
    # NB: format param is file name
    # I18N: add language at cmd line
    S_MSG_LANG_ADD = _("Adding language file {}...")

    # questions

    # I18N: ask to overwrite
    # NB: format param is file name
    S_ASK_OVER = _("The file {} already exists. Do you want to overwrite it?")

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

    # --------------------------------------------------------------------------
    # Public methods
    # --------------------------------------------------------------------------

    # --------------------------------------------------------------------------
    # The main method of the program
    # --------------------------------------------------------------------------
    def main(self):
        """
        The main method of the program

        This method is the main entry point for the program, initializing the
        program, and performing its steps.
        """

        # call boilerplate code
        self._setup()

        # ----------------------------------------------------------------------
        # main stuff

        # get project info
        self._get_project_info()

        # do any fixing up of dicts (like meta keywords, etc)
        self._do_before_fix()

        # do replacements in final project location
        self._do_fix()

        # do extra stuff to final dir after fix
        self._do_after_fix()

        # do any fixing up of dicts (like meta keywords, etc)
        self._do_before_dist()

        # copy project files into dist folder
        self._do_dist()

        # do any fixing up of dicts (like meta keywords, etc)
        self._do_after_dist()

        # ----------------------------------------------------------------------
        # teardown

        # call boilerplate code
        self._teardown()

        print()

        # just to verify we are in the right project
        print(self.S_MSG_DONE.format(self._dir_prj.name))
        print()

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

        self._parser.prog = "pybaker"

        # add ide option
        self._parser.add_argument(
            self.S_ARG_IDE_OPTION,
            dest=self.S_ARG_IDE_DEST,
            help=self.S_ARG_IDE_HELP,
            action=self.S_ARG_IDE_ACTION,
        )

        # add lang option
        self._parser.add_argument(
            self.S_ARG_LANG_OPTION,
            dest=self.S_ARG_LANG_DEST,
            help=self.S_ARG_LANG_HELP,
            metavar=self.S_ARG_LANG_METAVAR,
        )

        # add version option
        self._parser.add_argument(
            self.S_ARG_VER_OPTION,
            dest=self.S_ARG_VER_DEST,
            help=self.S_ARG_VER_HELP,
            metavar=self.S_ARG_VER_METAVAR,
        )

        # do setup
        super()._setup()

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

        Raises:
            OSError if anything goes wrong

        Check that the PyPlate data is present and correct, so we don't crash
        looking for non-existent files. Also handles command line settings.
        """

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

        # handle -i
        self._handle_i()

        # just to verify we are in the right project
        print(self.S_MSG_PREP.format(self._dir_prj.name))
        print()

        # ----------------------------------------------------------------------
        # sanity checks

        # check if dir_prj has pyplate folder for a valid prj
        path_pyplate = self._dir_prj / P.C.S_PRJ_PP_DIR
        if not path_pyplate.exists():
            print(P.C.S_ERR_NOT_PRJ)
            P.sys.exit(-1)

        # check if data files exist
        path_prv = self._dir_prj / P.C.S_PRJ_PRV_CFG
        path_pub = self._dir_prj / P.C.S_PRJ_PUB_CFG
        if not path_prv.exists() or not path_pub.exists():
            print(P.C.S_ERR_PP_MISSING)
            P.sys.exit(-1)

        # ----------------------------------------------------------------------
        # make dicts from files

        # check if files are valid json
        try:
            # get settings dicts in private.json
            self._dict_prv = F.load_paths_into_dict(path_prv)

            # get settings dicts in project.json
            # NB: may contain dunders
            self._dict_pub = F.load_paths_into_dict(path_pub)

        # if there was a problem
        except OSError as e:  # from load_dicts
            # exit gracefully
            print(self.S_ERR_ERR, e)
            P.sys.exit(-1)

        # ----------------------------------------------------------------------
        # fix dicts
        self._fix_dicts()

        # ----------------------------------------------------------------------
        # handle -d
        # NB: do after _fix_dicts
        if self._cmd_debug:
            self._dict_dbg = dict(P.C.D_DBG_PB)

        # ----------------------------------------------------------------------
        # handle -v
        self._handle_v()

        # ----------------------------------------------------------------------
        # handle -l
        self._handle_l()

        # ----------------------------------------------------------------------
        # print some info
        print()
        print(self.S_MSG_BAKE.format(self._dir_prj.name))

        # blank line before printing progress
        print()

    # --------------------------------------------------------------------------
    # Do any work before making dist
    # --------------------------------------------------------------------------
    def _do_before_dist(self):
        """
        Do any work before making dist

        Do any work on the dist folder before it is created. This method is
        called after _do_after_fix, and before _do_dist.
        """

        P.C.do_before_dist(
            self._dir_prj, self._dict_prv, self._dict_pub, self._dict_dbg
        )

    # --------------------------------------------------------------------------
    # Copy fixed files to final location
    # --------------------------------------------------------------------------
    def _do_dist(self):
        """
        Copy fixed files to final location

        Gets dirs/files from project and copies them to the dist/assets dir.
        """

        # print info
        print(P.C.S_ACTION_DIST, end="", flush=True)

        # ----------------------------------------------------------------------
        # do common dist stuff

        # find old dist? nuke it from orbit! it's the only way to be sure!
        a_dist = self._dir_prj / P.C.S_DIR_DIST
        if a_dist.is_dir():
            shutil.rmtree(a_dist)

        # make child dir in case we nuked
        name_fmt = self._dict_prv_prj["__PP_FMT_DIST__"]
        p_dist = a_dist / name_fmt
        p_dist.mkdir(parents=True)

        # for each key, val (type, dict)
        for key, val in self._dict_pub_dist.items():

            # get src/dst rel to prj dir/dist dir
            src = self._dir_prj / key
            dst = p_dist / str(val)
            if not dst.exists():
                dst.mkdir(parents=True)
            dst = dst / src.name

            # do the copy
            if src.exists() and src.is_dir():
                shutil.copytree(src, dst, dirs_exist_ok=True)
            elif src.exists() and src.is_file():
                shutil.copy2(src, dst)

        # ----------------------------------------------------------------------
        # done copying project files
        F.printc(P.C.S_ACTION_DONE, fg=F.C_FG_GREEN, bold=True)

    # --------------------------------------------------------------------------
    # Do any work after making dist
    # --------------------------------------------------------------------------
    def _do_after_dist(self):
        """
        Do any work after making dist

        Do any work on the dist folder after it is created. This method is
        called after _do_dist. Currently, this method purges any "ABOUT" file
        used as placeholders for github syncing. It also tars the source folder
        if it is a package, making for one (or two) less steps in the user's
        install process.
        """

        P.C.do_after_dist(
            self._dir_prj, self._dict_prv, self._dict_pub, self._dict_dbg
        )

    # --------------------------------------------------------------------------
    # Handle the -i option
    # --------------------------------------------------------------------------
    def _handle_i(self):
        """
        Docstring for _handle_i

        :param self: Description
        """

        ide = self._dict_args.get(self.S_ARG_IDE_DEST, False)
        if not ide:
            return

        # ask for prj name rel to cwd
        in_str = P.C.S_ASK_IDE.format(self._dir_prj)
        while True:
            prj_name = input(in_str)
            if prj_name == "":
                continue

            # if running in ide, cwd is pyplate prj dir, so move up + down
            tmp_dir = P.Path(self._dir_prj / prj_name).resolve()

            # check if project exists
            if not tmp_dir.exists():
                e_str = P.C.S_ERR_NOT_EXIST.format(tmp_dir)
                print(e_str)
                continue

            # set project dir and exit loop
            self._dir_prj = tmp_dir
            # print()
            break

    # --------------------------------------------------------------------------
    # Handle the -v option
    # --------------------------------------------------------------------------
    def _handle_v(self):
        """
        Docstring for _handle_v

        :param self: Description
        """

        # first check if passed on cmd line
        ver = self._dict_args.get(self.S_ARG_VER_DEST, None)
        if ver:

            # check version before we start fixing
            pattern = P.C.S_SEM_VER_VALID
            version = ver
            ver_ok = P.re.search(pattern, version) is not None

            # ask if user wants to keep invalid version or quit
            if not ver_ok:
                res = F.dialog(
                    P.C.S_ERR_SEM_VER,
                    [F.S_ASK_YES, F.S_ASK_NO],
                    default=F.S_ASK_NO,
                    loop=True,
                )
                if res != F.S_ASK_YES:
                    P.sys.exit(-1)

        # not passed, ask question
        else:

            # format and ask question
            old_ver = self._dict_pub_meta[P.C.S_KEY_META_VERSION]
            ask_ver = P.C.S_ASK_VER.format(old_ver)

            # loop until condition
            while True:

                # ask for new version
                new_ver = input(ask_ver)

                # user pressed Enter, return original
                if new_ver == "":

                    # set the same version, and we are done
                    ver = old_ver
                    break

                # check version before we start fixing
                pattern = P.C.S_SEM_VER_VALID
                version = new_ver
                ver_ok = P.re.search(pattern, version) is not None

                # ask if user wants to keep invalid version or quit
                if ver_ok:

                    # set the new version, and we are done
                    ver = new_ver
                    break

                # print version error
                print(P.C.S_ERR_SEM_VER)

        # change in project.json
        self._dict_pub_meta[P.C.S_KEY_META_VERSION] = ver

        # set version in install dict
        prj_type = self._dict_prv_prj["__PP_TYPE_PRJ__"]
        if prj_type in P.C.L_APP_INSTALL:
            self._dict_pub_inst[P.C.S_KEY_INST_VER] = ver

    # TODO: save version to install.json
    # --------------------------------------------------------------------------
    # Handle the -l option
    # --------------------------------------------------------------------------
    def _handle_l(self):
        """
        Docstring for _handle_l

        :param self: Description
        """

        # if no lang, no go
        lang_file = self._dict_args.get(self.S_ARG_LANG_DEST, None)
        if not lang_file:
            return

        print(self.S_MSG_LANG_ADD.format(lang_file), flush=True, end="")

        # default lang code
        lang_code = ""

        # get code from file
        p_lang = self._dir_prj / lang_file

        # in case of typo -)
        if not p_lang.exists():
            F.printc(P.C.S_ACTION_FAIL, fg=F.C_FG_RED, bold=True)
            return

        # find the line
        with open(p_lang, "r", encoding=P.C.S_ENCODING) as a_file:
            string = a_file.read()

        # find the lang
        res = re.search(P.C.S_PO_LANG_SCH, string)
        if res:
            lang_code = res.group(2)

        # make sure it worked before doing api
        if lang_code == "":
            F.printc(P.C.S_ACTION_FAIL, fg=F.C_FG_RED, bold=True)
            return

        # get lang dict from props
        dict_lang = self._dict_pub_i18n[P.C.S_KEY_PUB_I18N_WLANGS]

        # only add once (might be old)
        if not lang_code in dict_lang:
            dict_lang.append(lang_code)

        # check file exists
        dst = self._dir_prj / P.C.S_DIR_I18N / P.C.S_DIR_PO / lang_code
        dst_file = dst / lang_file
        if dst_file.exists():
            print()

            # ask to overwrite
            msg = self.S_ASK_OVER.format(lang_file)
            ask = F.dialog(msg, [F.S_ASK_YES, F.S_ASK_NO], default=F.S_ASK_NO)
            if ask != F.S_ASK_YES:
                F.printc(P.C.S_ACTION_FAIL, fg=F.C_FG_RED, bold=True)
                return

        # copy file to dest
        dst.mkdir(parents=True, exist_ok=True)
        shutil.copy(p_lang, dst_file)

        F.printc(P.C.S_ACTION_DONE, fg=F.C_FG_GREEN, bold=True)

main()

The main method of the program

This method is the main entry point for the program, initializing the program, and performing its steps.

Source code in src/pybaker.py
def main(self):
    """
    The main method of the program

    This method is the main entry point for the program, initializing the
    program, and performing its steps.
    """

    # call boilerplate code
    self._setup()

    # ----------------------------------------------------------------------
    # main stuff

    # get project info
    self._get_project_info()

    # do any fixing up of dicts (like meta keywords, etc)
    self._do_before_fix()

    # do replacements in final project location
    self._do_fix()

    # do extra stuff to final dir after fix
    self._do_after_fix()

    # do any fixing up of dicts (like meta keywords, etc)
    self._do_before_dist()

    # copy project files into dist folder
    self._do_dist()

    # do any fixing up of dicts (like meta keywords, etc)
    self._do_after_dist()

    # ----------------------------------------------------------------------
    # teardown

    # call boilerplate code
    self._teardown()

    print()

    # just to verify we are in the right project
    print(self.S_MSG_DONE.format(self._dir_prj.name))
    print()