Skip to content

Writing Generators

grace generate cog, grace generate model, grace generate migration, and grace generate project (used internally by grace new) are all built on the same extension point: grace.generator.Generator. Any module you drop into grace/generators/, or into your own package registered the same way, that exposes a module-level generator() function is automatically discovered and added under grace generate <NAME>.

Anatomy of a Generator

from grace.generator import Generator


class MyGenerator(Generator):
    NAME = "my_generator"

    def generate(self, *args, **kwargs):
        # Implement the generate method here
        ...


def generator() -> Generator:
    return MyGenerator()
  • NAME becomes the subcommand name: grace generate my_generator.
  • generate() does the actual work and is called after validate() passes.
  • validate() (optional) returns False to reject the arguments before generate() runs; a failed validation raises ValidationError.
  • OPTIONS lets you customize the underlying click.Command — most commonly to declare positional arguments.
  • Inside generate()/validate(), self.app gives you the current Application instance.

Example: A Real Generator

Here's the actual CogGenerator (grace/generators/cog_generator.py), which backs grace generate cog:

from logging import info
from re import match

from click.core import Argument
from jinja2_strcase.jinja2_strcase import to_snake

from grace.generator import Generator


class CogGenerator(Generator):
    NAME: str = "cog"
    OPTIONS: dict = {
        "params": [
            Argument(["name"], type=str),
            Argument(["description"], type=str, required=False, default=""),
        ],
    }

    def generate(self, name: str, description: str = ""):
        info(f"Creating cog '{name}'")

        self.generate_file(
            self.NAME,
            variables={
                "cog_name": name,
                "cog_module_name": to_snake(name),
                "cog_description": description,
            },
            output_dir="bot/extensions",
        )

    def validate(self, name: str, **_kwargs) -> bool:
        """A valid cog name must be PascalCase (letters and numbers only)."""
        return bool(match(r"^[A-Z][a-zA-Z0-9]*$", name))


def generator() -> Generator:
    return CogGenerator()

Composing Generators

A generator can call another generator directly. grace new does exactly this: ProjectGenerator scaffolds the project template, then — unless --no-database was passed — invokes DatabaseGenerator itself to add config/database.cfg, alembic.ini, and db/:

from grace.generators.database_generator import generator as db_generator


class ProjectGenerator(Generator):
    NAME = "project"

    def generate(self, name: str, database: bool = True):
        project_dir = self.generate_template(
            self.NAME,
            variables={"project_name": name, "database": "yes" if database else "no"},
        )

        if database:
            db_generator().generate(output_dir=project_dir)

generate_template returns the path it just generated into, which is how DatabaseGenerator knows where to write its own files. This is also how grace generate database retrofits a database onto an existing --no-database project — it's the same DatabaseGenerator, just invoked directly from the CLI with the current directory as output_dir.

Rendering Output

Generator gives you two ways to produce files, both rooted at grace/generators/templates/:

  • generate_file(template_dir, variables, output_dir) — renders a single Jinja2 template file (used by cog and model). The template directory's file is expected to be named with Jinja2 syntax too (e.g. {{ cog_module_name }}_cog.py), so both the filename and the contents get rendered from variables. Two extra filters are available: camel_case_to_space and pluralize (via jinja2-strcase and inflect).
  • generate_template(template_dir, variables) — runs an entire directory through Cookiecutter (used by project), for scaffolding multi-file/multi-directory output.

Registration

Generators are discovered by register_generators, which is called automatically whenever grace starts inside a project (or before grace new, for the bare CLI). It imports every module under grace.generators and registers whatever generator() returns onto the generate command group.

Note

Generator.templates_path always resolves to grace/generators/templates/ inside the installed grace package — both generate_file and generate_template render from there. This means a new generator, and its templates, currently need to live inside the grace package itself (i.e. contributed to the framework), rather than dropped into an individual bot project. See the Custom Generator example for what that looks like in practice.

Next Steps

Browse the Reference section for the full Generator API, or look at the Custom Generator example for a complete, runnable generator.