pymaker.py

A program to create a PyPlate project from a few variables

This module gets the project type, the project's destination dir, copies the required dirs/files for the project type from the template to the specified destination, and performs some initial fixes/replacements of text and path names in the resulting files.

Run pymaker -h for more options.

PyMaker

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 PyMaker, to create a PyPlate project from a template.

Source code in src/pymaker.py
class PyMaker(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 PyMaker, to create a
    PyPlate project from a template.
    """

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

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

    # I18N cmd line instructions string
    S_EPILOG = _("Run this program from the directory where you want to create \
a project.")

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

        # do super main
        super().main()

        # do before template
        self._do_before_template()

        # copy template
        self._do_template()

        # do before template
        self._do_after_template()

        # 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()

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

        # do parent setup
        super()._setup()

        # parse command line
        self._do_cmd_line()
        if self._debug:
            self._dict_debug = C.D_DBG_PM

        # do not run pymaker in pyplate dir
        if self._dir_prj.is_relative_to(self.P_DIR_PP):
            print(C.S_ERR_PRJ_DIR_IS_PP)
            sys.exit()

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

        Asks the user for project info, such as type and name, to be saved to
        self._dict_prv_prj.
        """

        # ----------------------------------------------------------------------
        # first question is type
        # NB: this makes the string to display in terminal

        # sanity check
        prj_type = ""

        # build the input question
        types = []
        for item in C.L_TYPES:
            s = C.S_ASK_TYPE_FMT.format(item[0], item[1])
            types.append(s)
        str_types = C.S_ASK_TYPE_JOIN.join(types)

        # format the question
        in_type = C.S_ASK_TYPE.format(str_types)

        # loop forever until we get a valid type
        while True:

            # ask for type of project (single letter)
            prj_type = input(in_type)

            # check for valid type
            if self._check_type(prj_type):
                prj_type = prj_type[0].lower()

                # at this point, type is valid so exit loop
                break

        # ----------------------------------------------------------------------
        # next question is name

        # sanity check
        cwd = Path.cwd()

        # if in debug mode
        if self._debug:

            # get long name
            for item in C.L_TYPES:
                if item[0] == prj_type:
                    # get debug name of project
                    name_prj = f"{item[1]} DEBUG"
                    break

            # dir name, no spaces
            name_prj_big = name_prj.replace(" ", "_")

            # set up for existence check
            tmp_dir = cwd / name_prj_big

            # check if project already exists
            if tmp_dir.exists():

                # if it does exist, "nuke it from orbit! it's the only way to
                # be sure!"
                # NB: yes i know ive used this joke more than once... FUCK YOU
                # ITS FUNNY
                shutil.rmtree(tmp_dir)

        # not debug
        else:

            # loop forever until we get a valid name that does not exist
            while True:

                # ask for name of project
                name_prj = input(C.S_ASK_NAME)
                name_prj = name_prj.strip(" ")

                # check for valid name
                if self._check_name(name_prj):

                    # dir name, no spaces
                    name_prj_big = name_prj.replace(" ", "_")

                    # set up for existence check
                    tmp_dir = cwd / name_prj_big

                    # check if project already exists
                    if tmp_dir.exists():

                        # tell the user that the old name exists
                        print(C.S_ERR_EXIST.format(name_prj_big))
                    else:
                        break

        # save global property
        self._dir_prj = tmp_dir

        # save other names
        name_prj_small = name_prj_big.lower()
        name_prj_pascal = F.pascal_case(name_prj_small)

        # ----------------------------------------------------------------------
        # here we figure out the binary/package/window name for a project

        # NB: for a cli, the binary name is the project name lowercased
        # for a gui we should ask for the main window class name
        # for a package we should ask for the module name

        name_sec = ""
        name_sec_big = ""
        name_sec_small = ""
        name_sec_pascal = ""

        # do we need a second name?
        if prj_type in C.D_NAME_SEC:

            # dup prj names if debug
            if self._debug:
                name_sec = name_prj
                name_sec_big = name_prj_big
                name_sec_small = name_prj_small
                name_sec_pascal = name_prj_pascal

            # if not debug, if need second name, ask for it
            else:

                # format question for second name
                s_sec_ask = C.D_NAME_SEC[prj_type]
                s_sec_ask_fmt = s_sec_ask.format(name_prj_small)

                # loop forever until we get a valid name or empty string
                while True:

                    # ask for second name
                    name_sec = input(s_sec_ask_fmt)
                    name_sec = name_sec.strip(" ")

                    # empty, keep default
                    if name_sec == "":
                        name_sec = name_prj_small

                    # check for valid name
                    if self._check_name(name_sec):
                        name_sec_big = name_sec.replace(" ", "_")
                        break

                # save other names
                name_sec_small = name_sec_big.lower()
                name_sec_pascal = F.pascal_case(name_sec_small)

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

        # create global and calculated settings dicts in private.json
        self._dict_prv = {
            C.S_KEY_PRV_ALL: C.D_PRV_ALL,
            C.S_KEY_PRV_PRJ: C.D_PRV_PRJ,
        }

        # create individual dicts in pyplate.py
        self._dict_pub = {
            C.S_KEY_PUB_BL: C.D_PUB_BL,
            C.S_KEY_PUB_DBG: C.D_PUB_DBG,
            C.S_KEY_PUB_DOCS: C.D_PUB_DOCS[prj_type],
            # NB: placeholder until we get prj type
            C.S_KEY_PUB_DIST: {},
            C.S_KEY_PUB_I18N: C.D_PUB_I18N,
            C.S_KEY_PUB_META: C.D_PUB_META,
        }

        # reload dicts after modify
        # NB: VERY IMPORTANT!!!
        self._reload_dicts()

        # ----------------------------------------------------------------------
        # calculate dunder values now that we have project info

        # save project stuff
        self._dict_prv_prj["__PP_TYPE_PRJ__"] = prj_type
        self._dict_prv_prj["__PP_NAME_PRJ__"] = name_prj
        self._dict_prv_prj["__PP_NAME_PRJ_BIG__"] = name_prj_big
        self._dict_prv_prj["__PP_NAME_PRJ_SMALL__"] = name_prj_small
        self._dict_prv_prj["__PP_NAME_PRJ_PASCAL__"] = name_prj_pascal
        self._dict_prv_prj["__PP_NAME_SEC_BIG__"] = name_sec_big
        self._dict_prv_prj["__PP_NAME_SEC_SMALL__"] = name_sec_small
        self._dict_prv_prj["__PP_NAME_SEC_PASCAL__"] = name_sec_pascal
        self._dict_prv_prj["__PP_NAME_VENV__"] = C.S_VENV_FMT_NAME.format(
            name_prj_small
        )
        self._dict_prv_prj["__PP_FILE_APP__"] = C.S_APP_FILE_FMT.format(
            name_prj_small
        )
        self._dict_prv_prj["__PP_CLASS_APP__"] = name_prj_pascal
        self._dict_prv_prj["__PP_FILE_WIN__"] = C.S_WIN_FILE_FMT.format(
            name_sec_small
        )
        self._dict_prv_prj["__PP_CLASS_WIN__"] = name_sec_pascal

        # add dist stuff
        self._dict_pub[C.S_KEY_PUB_DIST] = C.D_PUB_DIST[prj_type].copy()

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

        # remove home dir from PyPlate path
        h = str(Path.home())
        p = str(self.P_DIR_PP)
        p = p.lstrip(h).strip("/")
        p = p.lstrip(h).strip("\\")
        # NB: change global val
        self._dict_prv_prj["__PP_DEV_PP__"] = p

        # reload dicts after modify
        self._reload_dicts()

        # blank line before printing progress
        print()

    # --------------------------------------------------------------------------
    # Do any work before template copy
    # --------------------------------------------------------------------------
    def _do_before_template(self):
        """
        Do any work before template copy

        Do any work before copying the template. This method is called just
        before _do_template, before any files have been copied.\n
        It is mostly used to make final adjustments to the 'dict_prv' and
        'dict_pub' dicts before any copying occurs.
        """

        C.do_before_template(
            self._dir_prj, self._dict_prv, self._dict_pub, self._dict_debug
        )

    # --------------------------------------------------------------------------
    # Copy template files to final location
    # --------------------------------------------------------------------------
    def _do_template(self):
        """
        Copy template files to final location

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

        # show info
        print(C.S_ACTION_COPY, end="", flush=True)

        # ----------------------------------------------------------------------
        # do template/all

        # copy template/all
        src = self.P_DIR_PP / C.S_DIR_TEMPLATE / C.S_DIR_ALL
        dst = self._dir_prj
        shutil.copytree(src, dst, dirs_exist_ok=True)

        # ----------------------------------------------------------------------
        # copy template/type

        # get some paths
        prj_type_short = self._dict_prv_prj["__PP_TYPE_PRJ__"]
        prj_type_long = ""

        # get long type of project
        for item in C.L_TYPES:
            if item[0] == prj_type_short:
                prj_type_long = item[2]
                break

        # get the src dir in the template dir
        src = self.P_DIR_PP / C.S_DIR_TEMPLATE / prj_type_long
        dst = self._dir_prj
        shutil.copytree(src, dst, dirs_exist_ok=True)

        # ----------------------------------------------------------------------
        # do stuff outside template all/type

        # copy linked files
        for key, val in C.D_COPY.items():

            # get src/dst
            src = self.P_DIR_PP / key
            dst = self._dir_prj / val

            # copy dir/file
            if src.is_dir():
                shutil.copytree(src, dst)
            elif src.is_file():
                shutil.copy2(src, dst)

        # ----------------------------------------------------------------------
        # merge reqs

        # merge reqs files from all and prj
        self._merge_reqs(prj_type_long)

        # ----------------------------------------------------------------------
        # done
        print(C.S_ACTION_DONE)

    # --------------------------------------------------------------------------
    # Do any work after template copy
    # --------------------------------------------------------------------------
    def _do_after_template(self):
        """
        Do any work after template copy

        Do any work after copying the template. This method is called after
        _do_template, and before _do_before_fix.
        """

        C.do_after_template(
            self._dir_prj, self._dict_prv, self._dict_pub, self._dict_debug
        )

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/pymaker.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.
    """

    # do super main
    super().main()

    # do before template
    self._do_before_template()

    # copy template
    self._do_template()

    # do before template
    self._do_after_template()

    # 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()