plain.dev
A single command that runs everything you need for local development.
The plain.dev
package can be installed from PyPI, and does not need to be added to INSTALLED_PACKAGES
.
plain dev
The plain dev
command does several things:
- Sets
PLAIN_CSRF_TRUSTED_ORIGINS
to localhost by default - Runs
plain preflight
to check for any issues - Executes any pending model migrations
- Starts
gunicorn
with--reload
- Runs
plain tailwind compile --watch
, ifplain.tailwind
is installed - Any custom process defined in
pyproject.toml
attool.plain.dev.run
- Necessary services (ex. Postgres) defined in
pyproject.toml
attool.plain.dev.services
Services
Use services to define databases or other processes that your app needs to be functional. The services will be started automatically in plain dev
, but also in plain pre-commit
(so preflight and tests have a database).
Ultimately, how you run your development database is up to you. But a recommended starting point is to use Docker:
# pyproject.toml
[tool.plain.dev.services]
postgres = {cmd = "docker run --name app-postgres --rm -p 54321:5432 -v $(pwd)/.plain/dev/pgdata:/var/lib/postgresql/data -e POSTGRES_PASSWORD=postgres postgres:15 postgres"}
Custom processes
Unlike services, custom processes are only run during plain dev
. This is a good place to run something like ngrok or a Plain worker, which you might need to use your local site, but don't need running for executing tests, for example.
# pyproject.toml
[tool.plain.dev.run]
ngrok = {command = "ngrok http $PORT"}
plain dev services
Starts your services by themselves.
plain pre-commit
A built-in pre-commit hook that can be installed with plain pre-commit --install
.
Runs:
- Custom commands defined in
pyproject.toml
attool.plain.pre-commit.run
plain code check
, ifplain.code
is installedpoetry check --lock
, if using Poetryplain preflight --database default
plain migrate --check
plain makemigrations --dry-run --check
plain compile
plain test
VS Code debugging
Since plain dev
runs multiple processes at once, the regular pdb debuggers don't quite work.
Instead, we include microsoft/debugpy and an attach
function to make it even easier to use VS Code's debugger.
First, import and run the debug.attach()
function:
class HomeView(TemplateView):
template_name = "home.html"
def get_template_context(self):
context = super().get_template_context()
# Make sure the debugger is attached (will need to be if runserver reloads)
from plain.dev import debug; debug.attach()
# Add a breakpoint (or use the gutter in VS Code to add one)
breakpoint()
return context
When you load the page, you'll see "Waiting for debugger to attach...".
You can then run the VS Code debugger and attach to an existing Python process, at localhost:5678.
1import os
2import subprocess
3import time
4from importlib.util import find_spec
5from pathlib import Path
6
7import click
8import tomllib
9
10from plain.runtime import APP_PATH
11
12from .pid import Pid
13from .poncho.manager import Manager as PonchoManager
14from .utils import has_pyproject_toml
15
16
17class Services:
18 @staticmethod
19 def get_services(root):
20 if not has_pyproject_toml(root):
21 return {}
22
23 with open(Path(root, "pyproject.toml"), "rb") as f:
24 pyproject = tomllib.load(f)
25
26 return (
27 pyproject.get("tool", {})
28 .get("plain", {})
29 .get("dev", {})
30 .get("services", {})
31 )
32
33 def __init__(self):
34 self.poncho = PonchoManager()
35
36 def run(self):
37 services = self.get_services(APP_PATH.parent)
38 for name, data in services.items():
39 env = {
40 **os.environ,
41 "PYTHONUNBUFFERED": "true",
42 **data.get("env", {}),
43 }
44 self.poncho.add_process(name, data["cmd"], env=env)
45
46 self.poncho.loop()
47
48 def __enter__(self):
49 if not self.get_services(APP_PATH.parent):
50 # No-op if no services are defined
51 return
52
53 if Pid().exists():
54 click.secho("Services already running in `plain dev` command", fg="yellow")
55 return
56
57 print("Starting `plain dev services`")
58 self._subprocess = subprocess.Popen(
59 ["plain", "dev", "services"], cwd=APP_PATH.parent
60 )
61
62 if find_spec("plain.models"):
63 time.sleep(0.5) # Give it a chance to hit on the first try
64 subprocess.check_call(["plain", "models", "db-wait"], env=os.environ)
65 else:
66 # A bit of a hack to wait for the services to start
67 time.sleep(3)
68
69 def __exit__(self, *args):
70 if not hasattr(self, "_subprocess"):
71 return
72
73 self._subprocess.terminate()
74
75 # Flush the buffer so the output doesn't spill over
76 self._subprocess.communicate()