typecode / sykle

Rake like tool for coordinating docker-compose projects
0 stars 1 forks source link

django bootstrapping #42

Closed ashilen closed 5 years ago

ashilen commented 5 years ago

What is this?

This is a system for composing new django projects from templates. The idea is that we have a base template that's roughly like django's own django-admin startproject template here: https://github.com/django/django/tree/master/django/conf/project_template, sans lots of comments that we're used to removing, and plus all sorts of little things like differentiation of the different kinds of apps included in INSTALLED_APPS, separate environment specific settings files, assignments from os.env[<setting_variable>], etc.. Then we also have optional modifications of the base template with, e.g., the kind of cms framework that should be installed. So to create a django project with wagtail already installed and stubbed out (note that the actual stubbing of wagtail here is actually much sparer than in wagtail's own startproject command, but I expect that we'll continue to flesh these templates out with the familiar boilerplate) we would write:

syk dj new-project project-with-wagtail --cms-framework=wagtail

To also include s3 as a storage backend, elasticsearch_dsl as the search backend, and grahpql as the api framework, we'd:

syk dj new-project project-with-all-sorts-of-needs \
  --cms-framework=wagtail \
  --storage-backend=s3 \
  --search-backend=elasticsearch_dsl \
  --api-framework=graphql

To include several stubbed out app directories, all included in the INSTALLED_APPS setting:

syk dj new-project --apps=fee,fi,fo,fum

How it works

Alongside the base template directory is a parallel directory for each argument, e.g. --cms-framework, --storage-backend, etc., and within each of those is a directory for each valid option for that argument. Then within, for instance, the cms_framework/wagtail directory is a tree of templates that corresponds to the base template tree. A template in that tree that's parallel to a base template of the same name automatically extends it in the traditional templating fashion, i.e. inherits its contents and has access to its blocks. If we've applied more than one extension, for instance with syk dj new-project --error-logging=sentry --task-queue=celery, and each of those options has a settings/production.py-tpl file, then the inheritance hierarchy becomes:

base/<project_name>/settings/production.py-tpl
  error_logging/sentry/<project_name>/settings/production.py-tpl
    task_queue/celery/<project_name>/settings/production.py-tpl

The inheritance order is determined by the order of the arguments passed on the command line and at the moment can be arbitrary. In the implementation we just prepend each template with the appropriate "extends" statement at runtime. This introduces the bulk of the complexity in the PR, but is necessary if we want our options to be optional -- there's no way to know how the templates will extend each other until the desired extensions are known.

Notes

Files can be included in extension trees that have no parallel in the base tree, but currently the case where two extension trees have included identical files not present in the base tree is unanticipated.

Two special directory names exist, project_name and app_name. project_name is replaced with the first positional argument to the new-project command. The tree beneath the app_name directory, which itself lives under the project_name directory, is duplicated for each value passed to --apps. Any files defined under the app_name directory of an extension, for instance the search_backend/elasticsearch_dsl/project_name/app_name/documents.py-tpl template, are included in each copy and can be extended as usual by other extensions.

requirements.txt files in extension directories are concatenated to each other.

I've used the jinja2 template library rather than django's own, because django's adds newlines for every block.super statement and for for loops, which is fine for html but ugly for this. See https://code.djangoproject.com/ticket/2594.

I originally developed this approach with django's template system, and in the course of switching to jinja I've noticed that some of its features could be used to solve the problem differently. At least one other approach might exploit the jinja extends statement's acceptance of an actual template object passed in via the context.

I mentioned above that argument/inheritance order is arbitrary, but it's conceivable that extensions might want to be aware of each other and introduce code conditionally, based on each other. Implementing that should follow naturally from what's already here.