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 installeduv lock --locked
, if using uvplain 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 errno
2import logging
3import re
4import socket
5import sys
6from pdb import Pdb
7
8log = logging.getLogger(__name__)
9
10
11def cry(message, stderr=sys.__stderr__):
12 log.critical(message)
13 print(message, file=stderr)
14 stderr.flush()
15
16
17class LF2CRLF_FileWrapper:
18 def __init__(self, connection):
19 self.connection = connection
20 self.stream = fh = connection.makefile("rw")
21 self.read = fh.read
22 self.readline = fh.readline
23 self.readlines = fh.readlines
24 self.close = fh.close
25 self.flush = fh.flush
26 self.fileno = fh.fileno
27 if hasattr(fh, "encoding"):
28 self._send = lambda data: connection.sendall(data.encode(fh.encoding))
29 else:
30 self._send = connection.sendall
31
32 @property
33 def encoding(self):
34 return self.stream.encoding
35
36 def __iter__(self):
37 return self.stream.__iter__()
38
39 def write(self, data, nl_rex=re.compile("\r?\n")):
40 data = nl_rex.sub("\r\n", data)
41 self._send(data)
42
43 def writelines(self, lines, nl_rex=re.compile("\r?\n")):
44 for line in lines:
45 self.write(line, nl_rex)
46
47
48class DevPdb(Pdb):
49 """
50 This will run pdb as a ephemeral telnet service. Once you connect no one
51 else can connect. On construction this object will block execution till a
52 client has connected.
53
54 Based on https://github.com/tamentis/rpdb I think ...
55
56 To use this::
57
58 DevPdb(host='0.0.0.0', port=4444).set_trace()
59
60 Then run: telnet 127.0.0.1 4444
61 """
62
63 active_instance = None
64
65 def __init__(self, host, port, patch_stdstreams=False, quiet=False):
66 self._quiet = quiet
67 listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
68 listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
69 listen_socket.bind((host, port))
70 if not self._quiet:
71 cry(
72 "DevPdb session open at {}:{}, waiting for connection ...".format(
73 *listen_socket.getsockname()
74 )
75 )
76 listen_socket.listen(1)
77 connection, address = listen_socket.accept()
78 if not self._quiet:
79 cry(f"DevPdb accepted connection from {repr(address)}.")
80 self.handle = LF2CRLF_FileWrapper(connection)
81 Pdb.__init__(self, completekey="tab", stdin=self.handle, stdout=self.handle)
82 self.backup = []
83 if patch_stdstreams:
84 for name in (
85 "stderr",
86 "stdout",
87 "__stderr__",
88 "__stdout__",
89 "stdin",
90 "__stdin__",
91 ):
92 self.backup.append((name, getattr(sys, name)))
93 setattr(sys, name, self.handle)
94 DevPdb.active_instance = self
95
96 def __restore(self):
97 if self.backup and not self._quiet:
98 cry(f"Restoring streams: {self.backup} ...")
99 for name, fh in self.backup:
100 setattr(sys, name, fh)
101 self.handle.close()
102 DevPdb.active_instance = None
103
104 def do_quit(self, arg):
105 self.__restore()
106 return Pdb.do_quit(self, arg)
107
108 do_q = do_exit = do_quit
109
110 def set_trace(self, frame=None):
111 if frame is None:
112 frame = sys._getframe().f_back
113 try:
114 Pdb.set_trace(self, frame)
115 except OSError as exc:
116 if exc.errno != errno.ECONNRESET:
117 raise
118
119
120def set_trace(
121 frame=None, host="127.0.0.1", port=4444, patch_stdstreams=False, quiet=False
122):
123 """
124 Opens a remote PDB over a host:port.
125 """
126 devpdb = DevPdb(
127 host=host, port=port, patch_stdstreams=patch_stdstreams, quiet=quiet
128 )
129 devpdb.set_trace(frame=frame)