Skip to content

Commit

Permalink
feat: olive updates, more scaffolding, and basic 'make test'
Browse files Browse the repository at this point in the history
* Confirm that cookiecutter generates a plugin that is compatible
  with the Tutor v15 (and Open edX Olive).
  Closes overhangio#12.

* Replace COMMANDS_INIT (which is deprecated in v15/Olive) with
  equivalent usage of CLI_DO_INIT_TASKS (which is new in v15/Olive).
  Closes overhangio#15.

* Add boilerplate & explanation for CLI_DO_COMMANDS (new in v15/Olive),
  include a dummy example `tutor local do say-hi` job.

* Add boilerplate and explanation for CLI_COMMANDS, including
  a dummy example `tutor myplugin print-repo` command.

* Un-comment as much of the image management block as possible
  in order to help avoid instances of syntactically invalid commented-out
  code such as: overhangio#16

* Add Makefile to both cookiecutter root as well as generated plugin,
  providing make rules `test`, `test-format`, `test-lint`, and
  `test-types`. They are not yet run in CI.
  Part of overhangio#7.
  • Loading branch information
kdmccormick committed Dec 9, 2022
1 parent 7b6b9f3 commit d6ea727
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 24 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
/tutor-contrib-myplugin
28 changes: 28 additions & 0 deletions Makefile
@@ -0,0 +1,28 @@
.PHONY: generate-plugin-for-tests help test test-plugin test-plugin-install \
test-plugin-quality
.DEFAULT_GOAL := help

# Warning: These checks are not yet run on every PR.
# We will add them to CI as part of https://github.com/overhangio/cookiecutter-tutor-plugin/issues/7).
test: generate-plugin-for-tests test-plugin ## Runs all checks for this repo.

generate-plugin-for-tests: ## Generate a plugin using the cookiecutter defaults.
rm -rf tutor-contrib-myplugin
cookiecutter --no-input .

test-plugin: test-plugin-quality test-plugin-install ## Test the default plugin.

test-plugin-quality: ## Run static checks on the default plugin.
cd tutor-contrib-myplugin && make test

test-plugin-install: ## Smoke-test that the default plugin works with Tutor.
pip install -e tutor-contrib-myplugin
tutor plugins enable myplugin
tutor config save
tutor myplugin example-command # This should just print a line and exit 0.

ESCAPE = 
help: ## Print this help
@grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \
| sed 's/######* \(.*\)/@ $(ESCAPE)[1;31m\1$(ESCAPE)[0m/g' | tr '@' '\n' \
| awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}'
28 changes: 28 additions & 0 deletions {{ cookiecutter.package_name }}/Makefile
@@ -0,0 +1,28 @@
.DEFAULT_GOAL := help
.PHONY: docs
SRC_DIRS = ./{{ cookiecutter.module_name }}
BLACK_OPTS = --exclude templates ${SRC_DIRS}

# Warning: These checks are not necessarily run on every PR.
test: test-lint test-types test-format # Run some static checks.

test-format: ## Run code formatting tests
black --check --diff $(BLACK_OPTS)

test-lint: ## Run code linting tests
pylint --errors-only --enable=unused-import,unused-argument --ignore=templates --ignore=docs/_ext ${SRC_DIRS}

test-types: ## Run type checks.
mypy --exclude=templates --ignore-missing-imports --implicit-reexport --strict ${SRC_DIRS}

format: ## Format code automatically
black $(BLACK_OPTS)

isort: ## Sort imports. This target is not mandatory because the output may be incompatible with black formatting. Provided for convenience purposes.
isort --skip=templates ${SRC_DIRS}

ESCAPE = 
help: ## Print this help
@grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \
| sed 's/######* \(.*\)/@ $(ESCAPE)[1;31m\1$(ESCAPE)[0m/g' | tr '@' '\n' \
| awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}'
160 changes: 136 additions & 24 deletions {{ cookiecutter.package_name }}/{{ cookiecutter.module_name }}/plugin.py
@@ -1,12 +1,16 @@
from glob import glob
from __future__ import annotations

import os
import pkg_resources
import os.path
import shlex
from glob import glob

import click
import pkg_resources
from tutor import hooks

from .__about__ import __version__


########################################
# CONFIGURATION
########################################
Expand Down Expand Up @@ -45,35 +49,78 @@
# INITIALIZATION TASKS
########################################

# To run the script from templates/{{ cookiecutter.plugin_name }}/tasks/myservice/init, add:
# hooks.Filters.COMMANDS_INIT.add_item((
# "myservice",
# ("{{ cookiecutter.plugin_name }}", "tasks", "myservice", "init"),
# ))
# To add a custom initialization task, create a bash script template under:
# {{ cookiecutter.module_name }}/templates/{{ cookiecutter.plugin_name }}/jobs/init/
# and then add it to the MY_INIT_TASKS list. Each task is in the format:
# ("<service>", ("<path>", "<to>", "<script>", "<template>"))
MY_INIT_TASKS: list[tuple[str, tuple[str, ...]]] = [
("lms", ("{{ cookiecutter.plugin_name }}", "jobs", "init", "lms.sh")),
("cms", ("{{ cookiecutter.plugin_name }}", "jobs", "init", "cms.sh")),
("mysql", ("{{ cookiecutter.plugin_name }}", "jobs", "init", "mysql.sh")),
]


# For each task added to MY_INIT_TASKS, we load the task template
# and add it to the CLI_DO_INIT_TASKS filter, which tells Tutor to
# run it as part of the `init` job.
for service, template_path in MY_INIT_TASKS:
full_path = pkg_resources.resource_filename(
"{{ cookiecutter.module_name }}", os.path.join("templates", *template_path)
)
with open(full_path, encoding="utf-8") as init_task_file:
init_task = shlex.join(["bash", "-c", init_task_file.read()])
hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task))


########################################
# DOCKER IMAGE MANAGEMENT
########################################

# To build an image with `tutor images build myimage`, add a Dockerfile to templates/{{ cookiecutter.plugin_name }}/build/myimage and write:
# hooks.Filters.IMAGES_BUILD.add_item((
# "myimage",
# ("plugins", "{{ cookiecutter.plugin_name }}", "build", "myimage"),
# "docker.io/myimage:{{ '{{' }} {{ cookiecutter.plugin_name|upper|replace('-', '_') }}_VERSION {{ '}}' }}",
# (),
# ))

# To pull/push an image with `tutor images pull myimage` and `tutor images push myimage`, write:
# hooks.Filters.IMAGES_PULL.add_item((
# "myimage",
# "docker.io/myimage:{{ '{{' }} {{ cookiecutter.plugin_name|upper|replace('-', '_') }}_VERSION {{ '}}' }}",
# ))
# Images to be built by `tutor images build`.
# Each item is a quadruple in the form:
# ("<tutor_image_name>", ("path", "to", "build", "dir"), "<docker_image_tag>", "<build_args>")
hooks.Filters.IMAGES_BUILD.add_items(
[
# To build `myimage` with `tutor images build myimage`,
# you would add a Dockerfile to templates/{{ cookiecutter.plugin_name }}/build/myimage,
# and then write:
# (
# "myimage",
# ("plugins", "{{ cookiecutter.plugin_name }}", "build", "myimage"),
# "docker.io/myimage:{{ '{{' }} {{ cookiecutter.plugin_name|upper|replace('-', '_') }}_VERSION {{ '}}' }}",
# (),
# ),
]
)


# Images to be pulled as part of `tutor images pull`.
# Each item is a pair in the form:
# ("<tutor_image_name>", "<docker_image_tag>")
hooks.Filters.IMAGES_PULL.add_items(
[
# To pull `myimage` with `tutor images pull myimage`, you would write:
# (
# "myimage",
# "docker.io/myimage:{{ '{{' }} {{ cookiecutter.plugin_name|upper|replace('-', '_') }}_VERSION {{ '}}' }}",
# ),
]
)


# hooks.Filters.IMAGES_PUSH.add_item((
# "myimage",
# "docker.io/myimage:{{ '{{' }} {{ cookiecutter.plugin_name|upper|replace('-', '_') }}_VERSION {{ '}}' }}",
# ))
# Images to be pushed as part of `tutor images push`.
# Each item is a pair in the form:
# ("<tutor_image_name>", "<docker_image_tag>")
hooks.Filters.IMAGES_PUSH.add_items(
[
# To push `myimage` with `tutor images push myimage`, you would write:
# (
# "myimage",
# "docker.io/myimage:{{ '{{' }} {{ cookiecutter.plugin_name|upper|replace('-', '_') }}_VERSION {{ '}}' }}",
# ),
]
)


########################################
Expand Down Expand Up @@ -118,3 +165,68 @@
):
with open(path, encoding="utf-8") as patch_file:
hooks.Filters.ENV_PATCHES.add_item((os.path.basename(path), patch_file.read()))


########################################
# CUSTOM JOBS (a.k.a. "do-commands")
########################################

# A job is a set of tasks, each of which run inside a certain container.
# Jobs are invoked using the `do` command, for example: `tutor local do importdemocourse`.
# A few jobs are built in to Tutor, such as `init` and `createuser`.
# You can also add your own custom jobs:

# To add a custom job, define a Click command that returns a list of tasks,
# where each task is a pair in the form ("<service>", "<shell_command>").
# For example:
@click.command()
@click.option("-n", "--name", default="plugin developer")
def say_hi(name: str) -> list[tuple[str, str]]:
"""
An example job that just prints 'hello' from within both LMS and CMS.
"""
return [
("lms", f"echo 'Hello from LMS, {name}!'"),
("cms", f"echo 'Hello from CMS, {name}!'"),
]


# Then, add the command function to CLI_DO_COMMANDS:
hooks.Filters.CLI_DO_COMMANDS.add_item(say_hi)
#
# Now, you can run your job like this:
# $ tutor local do say-hi --name="{{ cookiecutter.author }}"


#######################################
# CUSTOM CLI COMMANDS
#######################################

# Your plugin can also add custom commands directly to the Tutor CLI.
# These commands are run directly on the user's host computer
# (unlike jobs, which are run in containers).

# To define a command group for your plugin, define a Click group and then
# add it to CLI_COMMANDS:


@click.group()
def {{ cookiecutter.plugin_name }}() -> None:
pass


hooks.Filters.CLI_COMMANDS.add_item({{ cookiecutter.plugin_name }})

# Then, add subcommands directly to the Click group, for example:


@{{ cookiecutter.plugin_name }}.command()
def example_command() -> None:
"""
This is helptext for an example command.
"""
print("You've run an example command.")


# And run:
# $ tutor {{ cookiecutter.plugin_name }} example-command
@@ -0,0 +1 @@
# Add your CMS initialization commands here.
@@ -0,0 +1 @@
# Add your LMS initialization commands here.
@@ -0,0 +1 @@
# Add your MySQL initialization commands here.
Empty file.

0 comments on commit d6ea727

Please sign in to comment.