Development
Beyond crafting content in-game, there’s a lot more development tooling that can be used to work on the core functions of django-moo.
Prerequisites
Docker
VSCode
with Dev Containers plugin
Git
Getting Started
In the django-moo README there are instructions on setting up Docker and using Docker Compose to run the application stack. You’ll still want to follow them all:
Checkout the project and use Docker Compose to run the necessary components:
git clone https://gitlab.com/bubblehouse/django-moo cd django-moo docker compose upRun
migrate,collectstatic, and bootstrap the initial database with some sample objects and users:docker compose run webapp manage.py migrate docker compose run webapp manage.py collectstatic docker compose run webapp manage.py moo_init docker compose run webapp manage.py createsuperuser --username phil docker compose run webapp manage.py moo_enableuser --wizard phil Wizard
Once this part is complete, though, you’ll want to open up the project folder in VSCode. You should be prompted to reopen the project as a Dev Container, if not, invoke “Dev Containers: Reopen in Container” from the command bar.
Environment Setup Details
The project uses uv for dependency management and pytest for testing.
Install Dependencies
uv sync
This installs all dependencies including development tools like PyLint, pytest, and coverage analysis.
Django Management Commands
Essential management commands for development. All examples use
docker compose run webapp manage.py; if you’re working inside a Dev
Container or have activated the project venv, the bare manage.py <command> form works equivalently.
# Migrate database to latest schema
docker compose run webapp manage.py migrate
# Initialize the game world with bootstrap data
docker compose run webapp manage.py moo_init
# Access Django interactive shell for debugging
docker compose run webapp manage.py shell
Creating Player Accounts
There are three ways to create a player account:
moo_createuser — creates a Django user and a linked MOO avatar in one command. Use this for agents, test accounts, or any regular player created from the CLI:
docker compose run webapp manage.py moo_createuser yourname YourAvatar --password secret
Omit --password to be prompted. Add --wizard to grant in-game wizard privileges.
Wizard account — the initial wizard account is a two-step flow because the admin interface needs a Django superuser:
docker compose run webapp manage.py createsuperuser --username yourname
docker compose run webapp manage.py moo_enableuser --wizard yourname YourWizardName
Web registration — regular players can sign up through the web interface at /accounts/signup/. The form creates a Django user and a linked MOO avatar in one step. Email verification is required by default (ACCOUNT_EMAIL_VERIFICATION = "mandatory").
The web registration form accepts: username, email, password, character name, gender (neuter/male/female/plural), and an optional description. If a user is already logged in when they reach the signup page, they are logged out automatically before the new registration is processed.
Using VSCode with django-moo
The first thing to do with your development environment is to make sure you can run the unit tests:

In addition to testing core functionality, there’s also integration tests for the default verbs at creation time.
Running the Server
The Dev Containers use the Compose file normally, except for the Celery broker, which isn’t run by default. Instead
the terminals you create will all be on the celery container instance, and you can run the Celery server in debug
mode using the launch job on the “Run and Debug” tab.

Testing
Running Tests
# Run all tests with coverage reporting
uv run pytest -n auto --cov
# Run a specific test file
uv run pytest -n auto moo/core/tests/test_code.py
# Run a specific test function
uv run pytest -n auto moo/core/tests/test_code.py::test_trivial_printing
# Run with verbose output and stop on first failure
uv run pytest -vv -x
# Run with pdb debugger on failure
uv run pytest --pdb
Test Organization
Path |
What it holds |
|---|---|
|
Minimal bootstrap data used by core unit tests via the |
|
Production-shape bootstrap package (orchestrator + numbered scripts) used to populate playable databases |
|
Verb sources for the |
|
Unit tests for models, permissions, the verb execution engine |
|
Integration tests for default verbs against a fully bootstrapped world |
Shared pytest fixtures live in moo/conftest.py (notably t_init and t_wizard).
Writing Tests
All tests should:
Use
@pytest.mark.django_dbdecorator to access the databaseTest both success and failure cases
Follow PEP 8 naming conventions (test functions start with
test_)
Core tests
Core tests exercise models and the verb execution engine without needing a full bootstrap. They typically use @pytest.mark.django_db alone:
import pytest
from moo.sdk import create, lookup
from moo.core.exceptions import PermissionDenied
@pytest.mark.django_db
def test_object_creation_and_properties():
obj = create("Test Object")
assert obj.name == "Test Object"
obj.set_property("description", "A test object")
assert obj.get_property("description") == "A test object"
@pytest.mark.django_db
def test_permission_denial():
obj = create("Protected Object")
obj.owner = lookup("Wizard")
obj.save()
with pytest.raises(PermissionDenied):
obj.can_caller("write")
Default tests
Tests in moo/bootstrap/default/tests/ exercise verbs against a fully bootstrapped world.
They use two shared fixtures from moo/conftest.py:
t_init: Bootstraps the full game world by runningdefault.py. Yields the system object (#1). Must be requested with@pytest.mark.parametrize("t_init", ["default"], indirect=True).t_wizard: Returns the Wizard player object, which starts in The Laboratory.
Output sent to the player is captured via a _writer callback passed to code.ContextManager. Commands are executed with parse.interpret. After a verb modifies database state, call refresh_from_db() before asserting:
import pytest
from moo.core import code, parse
from moo.sdk import create, lookup
from moo.core.models import Object
@pytest.mark.django_db(transaction=True, reset_sequences=True)
@pytest.mark.parametrize("t_init", ["default"], indirect=True)
def test_drop_from_inventory(t_init: Object, t_wizard: Object):
printed = []
def _writer(msg): # This is what `print()` ends up calling
printed.append(msg)
# set up a context with Wizard as the player
with code.ContextManager(t_wizard, _writer) as ctx:
system = lookup(1)
lab = t_wizard.location
player = lookup("Player")
widget = create("widget", parents=[system.thing], location=t_wizard)
# other calls to write() (besides print()) can be caught here
with pytest.warns(RuntimeWarning, match=r"ConnectionError") as warnings:
parse.interpret(ctx, "drop widget")
assert [str(x.message) for x in warnings.list] == [
f"ConnectionError(#{player.pk} (Player)): #{t_wizard.pk} (Wizard) drops widget."
]
widget.refresh_from_db()
assert widget.location == lab
assert printed == ['You drop widget.']
Verbs can also be called directly as Python methods inside a code.ContextManager block. This is useful for testing helper verbs (message formatters, lock checks, etc.) without going through the command parser:
with code.ContextManager(t_wizard, _writer):
system = lookup(1)
widget = create("widget", parents=[system.thing], location=t_wizard)
# Call the verb directly — equivalent to the MOO expression widget:drop_succeeded_msg()
assert widget.drop_succeeded_msg() == f"You drop {widget.title()}."
# Test moveto without the drop command
lab = t_wizard.location
widget.moveto(lab)
widget.refresh_from_db()
assert widget.location == lab
Testing lack of results is important too; for example, to test that a lock prevents movement, set a key property on the destination before calling moveto. The lock expression ["!", id] blocks the object whose id matches:
destination.set_property("key", ["!", widget.id])
widget.moveto(destination)
widget.refresh_from_db()
assert widget.location != destination # move was blocked
For commands that call write() on other objects (as opposed to those that just use print()), wrap the parse.interpret call with pytest.warns:
with pytest.warns(RuntimeWarning, match=r"ConnectionError") as warnings:
parse.interpret(ctx, "go north")
assert [str(x.message) for x in warnings.list] == [
f"ConnectionError(#{t_wizard.pk} (Wizard)): You leave ...",
]
Code Quality Tools
# Run PyLint to check code quality
DJANGO_SETTINGS_MODULE=moo.settings.test uv run pylint moo
# View coverage report
uv run coverage report
# Format code with Ruff
uv run ruff format moo
Common Development Tasks
Debugging Objects in Django Shell
docker compose run webapp manage.py shell
>>> from moo.sdk import lookup
>>> sys = lookup(1)
>>> sys.root_class.name
'Root Class'
>>> sys.root_class.properties.all()
<QuerySet [...]>
>>> sys.root_class.verbs.all()
<QuerySet [...]>
Picking up bootstrap changes
For most iteration, you don’t need to reset anything. After editing a verb file or adding objects to a numbered bootstrap script, run:
docker compose run webapp manage.py moo_init --bootstrap default --sync
--sync re-runs the bootstrap against the existing database.
get_or_create_object skips already-existing objects and
load_verbs(replace=True) overwrites verb source in place.
Truly resetting the database
When you actually need to start over (a corrupt schema, an in-progress
migration that can’t roll forward, an experimental dataset you want
to throw away), recreate the postgres container rather than running
migrate zero:
docker compose down -v
docker compose up -d
docker compose run webapp manage.py migrate
docker compose run webapp manage.py moo_init
-v drops the postgres volume so the next up starts from an empty
database. You’ll need to re-run createsuperuser / moo_enableuser
afterwards.
Performance Profiling
Use pytest-profiling to identify slow operations:
# Run tests with profiling
uv run pytest --profile
# View the generated profile
python -m pstats .prof
Documentation
Documentation is generated from source code docstrings using Sphinx:
# Build documentation locally
cd docs
uv run make html
# View the built documentation
open build/html/index.html
If autosummary fails to import a moved or removed verb file, wipe the generated stub directory and rebuild:
rm -rf docs/source/generated
cd docs && uv run make clean && uv run make html
Always include docstrings on:
All classes and methods
All module-level functions
Complex verb code
Git Workflow
Create a feature branch:
git checkout -b feat/my-featureMake changes and test locally
Follow Conventional Commits for commit messages:
feat(core): add new featurefix(shell): handle disconnectdocs(context): update docstring
Push and create a merge request targeting
mainAddress review feedback and ensure CI passes
CI/CD Pipeline
The project uses GitLab CI/CD. On each commit:
Lint stage - PyLint checks code quality (minimum score 8.0)
Test stage - pytest runs with coverage tracking
Release stage (on
mainonly) - Semantic versioning and Docker image buildDeploy stage (on
mainonly) - ReadTheDocs documentation build
Check .gitlab-ci.yml for the full pipeline configuration.