Adding new templates

Adding new templates is one of the major improvements and community contributions to cookietemple, which is why we are dedicating a whole section to it. Please note that creating new templates is a time consuming task. So be prepared to invest a few hours to bring a new template to life. The integration into cookietemple however, is straightforward if you follow the guide below. Due to the tight coupling of our templates with all cookietemple commands such as create, list, info, lint and bump-version, new templates require the modification of several files.

cookietemple uses cookiecutter to create all templates. You need to familiarize yourself beforehand with cookiecutter to able to write templates, but don’t worry, it’s pretty easy and you usually get by with very few cookiecutter variables. You can start with your very first cookiecutter template and then simply see how the other existing cookietemple templates are made and copy what you need.

The following sections will line out the requirements for new templates and guide you through the process of adding new templates step by step. Nevertheless, we strongly encourage you to discuss your proposed template first with us in public via a Github issue.

Template requirements

To keep the standard of our templates high we enforce several standards, to which all templates must adhere. Exceptions, where applicable, but they would have to be discussed beforehand. Hence, the term should.

  1. New templates must be novel. We do not want a second cli-python template, but you are of course always invited to improve it. A new commandline library does not warrant an additional template, but rather modifications of the existing template with cookiecutter if statements. However, distinct modifications of already existing templates may be eligible. An example would be to add a GUI template for a language, which does not yet have a GUI template. Templates for domains, which we do not yet cover or additional languages to already existing domains are of course more than welcome.

  2. All templates should be cutting edge and not be based on technical debt or obscure requirements. Our target audience are enthusiastic open source contributors and not decades old companies stuck with Python 2.7.

  3. All templates should build as automatically as possible and download all dependencies without manual intervention.

  4. All templates must have a testing and possibly mocking framework included.

  5. All templates must provide a readthedocs setup, a README.rst, usage.rst and installation.rst file, a LICENSE, Github issue and pull request templates and a .gitignore file. Moreover, a .dependabot configuration should be present if applicable. Note that most of these are already included in our common_files and do not need to be rewritten. More on that below.

  6. All templates must provide a Makefile, which wraps heavily used commands to unify common operations such as installing, testing or distributing a project.

  7. All templates should have a Dockerfile, which provides an entrypoint for the project.

  8. All templates must implement all required functionality to allow the application of all commands mentioned above to them, which includes a cookietemple.cfg file, the template being in the available_templates.yml and more.

  9. All templates must have Github workflows, which at least build the documentation and the project.

  10. Every template must also have a workflow inside cookietemple, which creates a project from the template with dummy values.

  11. Your template must support Linux and MacOS. Windows support is optional, but strongly encouraged.

Again, we strongly suggest that new templates are discussed with the core team first.

Step by step guide to adding new templates

Let’s assume that we are planning to add a new commandline Brainfuck template to cookietemple. We discussed our design at length with the core team and they approved our plan. For the sake of this tutorial we assume that the path / always points to /cookietemple. Hence, at this level we see cookietemple_cli.py and a folder per CLI command.

  1. Let’s add our brainfuck template information to /create/templates/available_templates.yml below the cli section.

1cli:
2    brainfuck:
3        name: Brainfuck Commandline Tool
4        handle: cli-brainfuck
5        version: 0.0.1
6        available libraries: none
7        short description: Brainfuck Commandline tool with ANSI coloring
8        long description: Amazing brainfuck tool, which can even show pretty unicorns in the console.
9            Due to ANSI coloring support they can even be pink! Please someone send help.
  1. Next, we add our brainfuck template to /create/templates
    Note that it should adhere to the standards mentioned above and include all required files. Don’t forget to add a cookietemple.cfg file to facilitate bump-version. See Configuration for details. It is mandatory to name the top level folder {{ cookiecutter.project_slug }}, which ensures that the project after creation will have a proper name. Furthermore, the cookiecutter.json file should have at least the following variables:
1{
2"full_name": "Homer Simpson",
3"email": "homer.simpson@posteo.net",
4"project_name": "sample-cli",
5"project_slug": "sample-cli",
6"version": "1.0.0",
7"project_short_description": "Command-line utility to...",
8"github_username": "homer_github"
9}

The file tree of the template should resemble

 1├── cookiecutter.json
 2└── {{ cookiecutter.project_slug }}
 3    ├── docs
 4    │   ├── installation.rst
 5    │   └── usage.rst
 6    ├── .github
 7    │   └── workflows
 8    │       └── build_brainfuck.yml
 9    ├── hello.bf
10    ├── cookietemple.cfg
11    └── README.rst
  1. Now it is time to subclass the TemplateCreator to implement all required functions to create our template!
    Let’s edit /create/domains/cli_creator.py. Note that for new domains you would simply create a new file called DomainCreator.
    In this case we suggest to simply copy the code of an existing Creator and adapt it to the new domain. Your new domain may make use of other creation functions instead of create_template_without_subdomain, if they for example contain subdomains. You can examine create/TemplatorCreator.py to see what’s available. You may also remove functions such as the creation of common files.
    If we have any brainfuck specific cookiecutter variables that we need to populate, we may add them to the TemplateStructCli.
    Our brainfuck templates does not have them, so we just leave it as is.
    For the next step we simply go through the CliCreator class and add our brainfuck template where required. Moreover, we implement a cli_brainfuck_options function, which we use to prompt for template specific cookiecutter variables.
    Assuming cli_creator.py already contains a cli-java template
 1@dataclass
 2class TemplateStructCli(CookietempleTemplateStruct):
 3    """
 4    Intended Use: This class holds all attributes specific for CLI projects
 5    """
 6
 7    """______JAVA______"""
 8    main_class_prefix: str = ''
 9
10    """____BRAINFUCK___"""
11
12
13class CliCreator(TemplateCreator):
14
15    def __init__(self):
16        self.cli_struct = TemplateStructCli(domain='cli')
17        super().__init__(self.cli_struct)
18        self.WD = os.path.dirname(__file__)
19        self.WD_Path = Path(self.WD)
20        self.TEMPLATES_CLI_PATH = f'{self.WD_Path.parent}/templates/cli'
21
22        '"" TEMPLATE VERSIONS ""'
23        self.CLI_JAVA_TEMPLATE_VERSION = super().load_version('cli-java')
24        self.CLI_BRAINFUCK_TEMPLATE_VERSION = super().load_version('cli-brainfuck')
25
26    def create_template(self, path: Path, dot_cookietemple: dict or None):
27        """
28        Handles the CLI domain. Prompts the user for the language, general and domain specific options.
29        """
30
31        self.cli_struct.language = cookietemple_questionary_or_dot_cookietemple(function='select',
32                                                                                question='Choose the project\'s primary language',
33                                                                                choices=['python', 'java', 'brainfuck'],
34                                                                                default='python',
35                                                                                dot_cookietemple=dot_cookietemple,
36                                                                                to_get_property='language')
37
38        # prompt the user to fetch general template configurations
39        super().prompt_general_template_configuration(dot_cookietemple)
40
41        # switch case statement to prompt the user to fetch template specific configurations
42        switcher = {
43            'java': self.cli_java_options,
44            'brainfuck': self.cli_brainfuck_options
45        }
46        switcher.get(self.cli_struct.language)(dot_cookietemple)
47
48        self.cli_struct.is_github_repo, \
49            self.cli_struct.is_repo_private, \
50            self.cli_struct.is_github_orga, \
51            self.cli_struct.github_orga \
52            = prompt_github_repo(dot_cookietemple)
53
54        if self.cli_struct.is_github_orga:
55            self.cli_struct.github_username = self.cli_struct.github_orga
56
57        # create the chosen and configured template
58        super().create_template_without_subdomain(f'{self.TEMPLATES_CLI_PATH}')
59
60        # switch case statement to fetch the template version
61        switcher_version = {
62            'java': self.CLI_JAVA_TEMPLATE_VERSION,
63            'brainfuck': self.CLI_BRAINFUCK_TEMPLATE_VERSION
64        }
65        self.cli_struct.template_version, self.cli_struct.template_handle = switcher_version.get(
66            self.cli_struct.language.lower()), f'cli-{self.cli_struct.language.lower()}'
67
68        super().process_common_operations(path=Path(path).resolve(), domain='cli', language=self.cli_struct.language, dot_cookietemple=dot_cookietemple)
69
70    def cli_python_options(self, dot_cookietemple: dict or None):
71        """ Prompts for cli-python specific options and saves them into the CookietempleTemplateStruct """
72        self.cli_struct.command_line_interface = cookietemple_questionary_or_dot_cookietemple(function='select',
73                                                                                            question='Choose a command line library',
74                                                                                            choices=['Click', 'Argparse', 'No command-line interface'],
75                                                                                            default='Click',
76                                                                                            dot_cookietemple=dot_cookietemple,
77                                                                                            to_get_property='command_line_interface')
78        [...]
79
80    def cli_java_options(self, dot_cookietemple: dict or None) -> None:
81        """ Prompts for cli-java specific options and saves them into the CookietempleTemplateStruct """
82        [...]
83
84    def cli_brainfuck_options(self):
85        """ Prompts for cli-brainfuck specific options and saves them into the CookietempleTemplateStruct """
86        pass
  1. If a new template were added we would also have to import our new Creator in create/create.py and add the new domain to the domain prompt and the switcher.
    However, in this case we can simply skip this step, since cli is already included.
 1def choose_domain(domain: str):
 2    """
 3    Prompts the user for the template domain.
 4    Creates the .cookietemple file.
 5    Prompts the user whether or not to create a Github repository
 6    :param domain: Template domain
 7    """
 8    if not domain:
 9        domain = click.prompt('Choose between the following domains',
10                            type=click.Choice(['cli', 'gui', 'web', 'pub']))
11
12    switcher = {
13        'cli': CliCreator,
14        'web': WebCreator,
15        'gui': GuiCreator,
16        'pub': PubCreator
17    }
18
19    creator_obj = switcher.get(domain.lower())()
20    creator_obj.create_template()
  1. Linting is up next! We need to ensure that our brainfuck template always adheres to the highest standards! Let’s edit lint/domains/cli.py.
    We need to add a new class, which inherits from TemplateLinter and add our linting functions to it.
 1class CliBrainfuckLint(TemplateLinter, metaclass=GetLintingFunctionsMeta):
 2    def __init__(self, path):
 3        super().__init__(path)
 4
 5    def lint(self):
 6        super().lint_project(self, self.methods)
 7
 8    def check_sync_section(self) -> bool:
 9        """
10        Check the sync_files_blacklisted section containing every required file!
11        """
12        config_linter = ConfigLinter(f'{self.path}/cookietemple.cfg', self)
13        result = config_linter.check_section(section_items=config_linter.parser.items('sync_files_blacklisted'), section_name='sync_files_blacklisted',
14                                             main_linter=self, blacklisted_sync_files=[[('changelog', 'CHANGELOG.rst')], -1],
15                                             error_code='cli-brainfuck-2', is_sublinter_calling=True)
16        if result:
17            self.passed.append(('cli-brainfuck-2', 'All required sync blacklisted files are configured!'))
18        else:
19            self.failed.append(('cli-brainfuck-2', 'Blacklisted sync files section misses some required files!'))
20        return result
21
22    def brainfuck_files_exist(self) -> None:
23        """
24        Checks a given pipeline directory for required files.
25        Iterates through the templates's directory content and checkmarks files for presence.
26        Files that **must** be present::
27            'hello.bf',
28        Files that *should* be present::
29            '.github/workflows/build_brainfuck.yml',
30        Files that *must not* be present::
31            none
32        Files that *should not* be present::
33            none
34        """
35
36        # NB: Should all be files, not directories
37        # List of lists. Passes if any of the files in the sublist are found.
38        files_fail = [
39            ['hello.bf'],
40        ]
41        files_warn = [
42            [os.path.join('.github', 'workflows', 'build_brainfuck.yml')],
43        ]
44
45        # List of strings. Fails / warns if any of the strings exist.
46        files_fail_ifexists = [
47
48        ]
49        files_warn_ifexists = [
50
51        ]
52
53        files_exist_linting(self, files_fail, files_fail_ifexists, files_warn, files_warn_ifexists)

We need to ensure that our new linting function is found when linting is applied. Therefore, we turn our eyes to lint/lint.py, import our CliBrainfuckLinter and add it to the switcher.

 1from cookietemple.lint.domains.cli import CliBrainfuckLint
 2
 3switcher = {
 4    'cli-python': CliPythonLint,
 5    'cli-java': CliJavaLint,
 6    'cli-brainfuck': CliBrainfuckLint,
 7    'web-website-python': WebWebsitePythonLint,
 8    'gui-java': GuiJavaLint,
 9    'pub-thesis-latex': PubLatexLint
10}

Our shiny new CliBrainfuckLinter is now ready for action!

  1. The only thing left to do now is to add a new Github Actions workflow for our template. Let’s go one level up in the folder tree and create .github/workflows/create_cli_brainfuck.yml.
    We want to ensure that if we change something in our template, that it still builds!
 1name: Create cli-brainfuck Template
 2
 3on: [push]
 4
 5jobs:
 6  build:
 7
 8      runs-on: ubuntu-latest
 9      strategy:
10        matrix:
11          python: [3.8, 3.9]
12
13      steps:
14      - uses: actions/checkout@v2
15        name: Check out source-code repository
16
17      - name: Setup Python
18        uses: actions/setup-python@v2.2.2
19        with:
20          python-version: ${{ matrix.python }}
21
22      - name: Install Poetry
23          run: |
24              pip install poetry
25
26      - name: Build cookietemple
27          run: |
28              make install
29
30      - name: Create cli-brainfuck Template
31        run: |
32          echo -e "cli\nbrainfuck\nHomer\nhomer.simpson@hotmail.com\nExplodingSpringfield\ndescription\nhomergithub\nn" | poetry run cookietemple create
33
34      - name: Build Package
35        uses: fabasoad/setup-brainfuck-action@master
36        with:
37          version: 0.1.dev1
38      - name: Hello World
39        run: |
40          brainfucky --file ExplodingSpringfield/hello.bf

We were pleasently surprised to see that someone already made a Github Action for brainfuck.

  1. Finally, we add some documentation to /docs/available_templates.rst and explain the purpose, design and frameworks/libraries.

    That’s it! We should now be able to try out your new template using cookietemple create The template should be creatable, it should automatically lint after the creation and Github support should be enabled as well! If we run cookietemple list Our new template should show up as well! I’m sure that you noticed that there’s not actually a brainfuck template in cookietemple (yet!).

    To quote our mighty Math professors: ‘We’ll leave this as an exercise to the reader.’