# 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](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-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 up > > Run `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 ```bash 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 ` form works equivalently. ```bash # 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: ```bash 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: ```bash 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: ![django-moo unit tests](https://gitlab.com/bubblehouse/django-moo/-/raw/main/docs/images/vscode-testing.png) 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. ![django-moo debugging](https://gitlab.com/bubblehouse/django-moo/-/raw/main/docs/images/vscode-debug.png) ## Testing ### Running Tests ```bash # 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 | |------|---------------| | `moo/bootstrap/test.py` | Minimal bootstrap data used by core unit tests via the `t_init` fixture | | `moo/bootstrap/default/` | Production-shape bootstrap package (orchestrator + numbered scripts) used to populate playable databases | | `moo/bootstrap/default/verbs/` | Verb sources for the `default` dataset, organised by root-class name | | `moo/core/tests/` | Unit tests for models, permissions, the verb execution engine | | `moo/bootstrap/default/tests/` | 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_db` decorator to access the database * Test 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: ```python 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 running `default.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: ```python 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: ```python 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: ```python 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`: ```python 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 ```bash # 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 ```bash docker compose run webapp manage.py shell ``` ```python >>> from moo.sdk import lookup >>> sys = lookup(1) >>> sys.root_class.name 'Root Class' >>> sys.root_class.properties.all() >>> sys.root_class.verbs.all() ``` ### 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: ```bash 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`: ```bash 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: ```bash # 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: ```bash # 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: ```bash 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 1. Create a feature branch: `git checkout -b feat/my-feature` 2. Make changes and test locally 3. Follow Conventional Commits for commit messages: * `feat(core): add new feature` * `fix(shell): handle disconnect` * `docs(context): update docstring` 4. Push and create a merge request targeting `main` 5. Address review feedback and ensure CI passes ## CI/CD Pipeline The project uses GitLab CI/CD. On each commit: 1. **Lint stage** - PyLint checks code quality (minimum score 8.0) 2. **Test stage** - pytest runs with coverage tracking 3. **Release stage** (on `main` only) - Semantic versioning and Docker image build 4. **Deploy stage** (on `main` only) - ReadTheDocs documentation build Check `.gitlab-ci.yml` for the full pipeline configuration.