yt templating

I recently started working on a CookieCutter template for yt called ytcookiecutter! The aim is to provide a convenient method for spinning up a new yt-related package, particularly new frontend plugins. The usage follows any CookieCutter template.

pip install cookiecutter
cookiecutter https://github.com/data-exp-lab/ytcookiecutter

will install CookieCutter and bring up the CookieCutter recipe builder. The template is built off audreyfeldroy’s excellent cookiecutter-pypackage with some extra yt-specific options. At present, the yt options include options for including some expanded yt-dependencies in a requirements.txt file and the initial code needed for a frontend:

include_yt_requirements [y]:
Select frontend_type:
1 - None
2 - AMR Skeleton
3 - Stream: Uniform Grid
4 - Stream: AMR Grids
5 - Stream: Particles
6 - Stream: Octree
7 - Stream: Hexahedral Mesh
8 - Stream: Unstructured Mesh
Choose from 1, 2, 3, 4, 5, 6, 7, 8 [1]: 2

Once selections are made, a new directory with everything you need to start building your new yt project is available! This includes source code but also github actions and other configurations (like pre-commit).

how it works

One of the neat things about ytcookiecutter is that it actually does some meta-templating to generate the CookieCutter template! If you check out the base ytcookiecutter repository, you’ll see some things common to other CookieCutter templates. The directory {{cookiecutter.project_slug}} contains the actual template code. When you run

cookiecutter https://github.com/data-exp-lab/ytcookiecutter

the options you choose will fill in or select certain files in the template code.

But writing the template code itself can be time consuming. One of the frontend options I added is for a wrapper around an in-memory stream dataset. Now it’s true that if you’re familiar with yt, you can simply use a function like yt.load_uniform_grid to load in-memory data as a yt dataset. But several times I’ve found myself writing wrappers to yt.load_uniform_grid to load from disk and then load into yt. I do this a lot working with gridded seismic data that is small enough to work with in memory and not worth the extra development time of a full yt frontend. So I wanted to add a frontend option for a stream wrapper.

But if you actually check out the stream template code, you’ll see it’s fairly long and repetitive ( link). Each of the Stream types has different arguments and so each needed their own version in the template. Annoying to write by hand…

BUT I don’t have to write it by hand!

While ytcookiecutter is a template, it is also a package itself to maintain the template. Among other things, it uses jinja (which CookieCutter is actually built on) for some meta-templating. The base stream meta-template looks like:

{%- if cookiecutter.frontend_type|lower == "!{frontend_type_str}!" -%}
from yt.loaders import !{load_func}!

def load(filename: str):

    # write code to load data from filename into memory!
    <% for arg in argnames -%>
    !{arg}! = ????????
    <% endfor %>
    # set or delete optional kwargs
    <% for key, value in kwarg_dict.items() -%>
    !{key}! = !{value}!
    <% endfor %>
    # call the stream data loader
    ds = !{load_func}!(
        <% for arg in argnames -%>
        !{arg}!,
        <% endfor -%>
        <% for key in kwarg_dict.keys() -%>
        !{key}! = !{key}!,
        <% endfor %>
    )

    # return the in-memory ds
    return ds
<% if include_docstring -%>
# description of !{load_func}! for convenience:

"""
!{docstring}!
"""
<% endif %>
{%- endif -%}

The main thing to notice is that this contains two sets of jinja start/end strings to identify the templated lines. The standard sets, {% %}, {{ }} are those used by CookieCutter while <% %> and !{ }! are custom start/end strings used by ytcookiecutter. To generate the stream template, I used inspect.getfullarspec to pull out the arguments and default values for each stream loading function and then used the above meta-template to generate the cookiecutter template, resulting in a template file containing all the arguments for the stream loading functions. And when a stream frontend is selected when building the CookieCutter recipe, only that one will remain in the generated project. Neat-o!

The Skeleton frontend template gets generated a little more simply. Using the PyGithub package, I simply fetch yt’s frontend/_skeleton code and replace the Skeleton occurrences with cookiecutter template parameters so that when a user selects a Skeleton frontend, the new frontend name can easily be swapped in.

Both of these approaches have the benefit of easily updating templates if yt changes upstream. Simply go and re-run the generation and push the new templates!