As we get closer to the 1.0 release, I've decided to bite off a few more of the larger, backwards-incompatible changes that I've been considering. Some of these may even be controversial 😱 There's no time like the present! - [Removed backwards migrations](#removed-backwards-migrations) - [Development db backups](#development-db-backups) - [New url `Router`, updated url functions](#new-url-router-updated-url-functions) - [`@register` everything](#register-everything) - [Removed swappable models](#removed-swappable-models) - [Tailwind v4](#tailwind-v4) - [Other notable changes](#other-notable-changes) - [Next steps](#next-steps) ## Removed backwards migrations In my experience, backwards migrations can be helpful in development, but in production you would never use them. This deserves an entire blog post (and I'd love to hear the counter arguments), but the migrations are code (committed in your repo!) and coordinating the *rollback* of migrations + code can be ridiculously complex in production. You can't simply `git revert` or `heroku rollback` because of the way everything is intertwined. Instead, you're probably going to deploy more code (and update/add migrations), and possibly restore database backups or manually fix data. ## Development db backups Ok, so backwards migrations are gone. What do we do in development if we need to undo something? **What if we used backups instead?!** The `plain migrate` command will now prompt to make a local backup. ![](https://assets.plainframework.com/site/updates/migrate-backup.16aae8f.png) You can also manually create, list, and restore backups using the `plain backups` command. ![](https://assets.plainframework.com/site/updates/plain-backups.05ba636.png) Turns out, this is incredibly useful if you work on git branches that have new migrations. When you switch branches, just restore the backup for that branch! We'll continue to polish the experience here, but I like the direction it's headed. ## New url `Router`, updated url functions There are a number of things that I consistenly find confusing with urls, url namespaces, the `path()` and `include()` functions, and even the params of the `reverse` function. Lots of small details. This is what it looks like now! ```python from plain.assets.urls import AssetsRouter from plain.urls import Router, include, path from . import views # The primary entrypoint for your app class AppRouter(Router): namespace = "" # A required attribute! urls = [ # Use include() by itself, not path(include()) include("assets/", AssetsRouter), path("login/", views.LoginView, name="login"), path("", views.HomeView), ] # The primary router is pointed to in settings.py URLS_ROUTER = "app.urls.AppRouter" # And reversing urls is now like this (simpler args/kwargs) reverse("users:detail", pk=1) ``` I will admit that the Router looks more complex than simply defining a list named `urlpatterns`. But being more explicit is one of Plain's design goals. Doing it this way means: - `urls.py` is no longer a special module - `urlpatterns` is no longer a special module-level variable - `app_name` is no longer a special module-level variable (which is not a good name anyway IMO) - namespaces are front and center, and *strongly* encouraged (basically everything should have a namespace except the root router) ## `@register` everything In the name of explicitness and clarity being a good thing, I've decided to go all-in on the `@register` decorator pattern. When you create a model, you'll now need to `@register_model` it. ```python from plain import models @models.register_model class User(models.Model): email = models.EmailField() ``` You'll see a similar pattern in worker jobs, package configs, Jinja extensions and filters, custom CLIs, admin views, and preflight checks. Why do this? I learned Python by learning Django, over 12 years ago. There was so much "magic" involved that I didn't really understand what was Django and what was Python. My `Model` classes were gigantic, because I honestly didn't know that you could write classes and objects that weren't models! There's a lot to blame for this, including me and my ways of learning, but I think there is also a lot of value to saying "look, here's the line that makes that happen!" which is something you get by explicitly writing things like `@register_model`. This is a pattern that already existed for system checks and a couple other things, and it's one that I think works well. *Sidenote: this takes us one step closer to delivering a "single file application" experience, at least in principle. The `models.py` (and most filename conventions) don't matter much anymore, so you could start to envision the example `app.py` that defines a model, router, and a couple views which make up a small but useful app.* ## Removed swappable models One of the first things I did with Plain was change how the `User` model works. In Django, the `django.contrib.auth` package comes with a `User` model, but how you go forward from there is the source of lots of debate. My solution is simply to put the `User` model in the user's code! We still have an `AUTH_USER_MODEL` setting so we can refer to it across the framework (the concept of a user is a crucial thing), but otherwise it's a regular model like anything else. ```python # app/users/models.py from plain import models from plain.models.functions import Lower from plain.passwords.models import PasswordField @models.register_model class User(models.Model): email = models.EmailField() password = PasswordField() is_admin = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) class Meta: constraints = [ models.UniqueConstraint( Lower("email"), name="unique_lower_email", ), ] ``` Anyway, Django has a concept of "swappable" models, and as far as I know it's really only used by for this user-modeling situation. "Swapping" a built-in `User` for your own is not how Plain does it, so I removed the swappable models feature entirely. This kind of change is mostly internal, but I mention it because it is one of those key design decisions that has a surprising number of trickle-down effects. You'd be surprised how many places `AUTH_USER_MODEL` was special-cased in the migrations code. ## Tailwind v4 Since Tailwind is a core element of Plain (any Plain package with templates can use Tailwind), it's important to note that we're now using Tailwind v4! I do have one workaround in place related to the [`.venv` gitignore behavior](https://github.com/tailwindlabs/tailwindcss/issues/15452), but I'm hoping that will get resolved soon. The way this works under the hood is that your app has a `tailwind.css`, and we `@source` the Plain packages themselves. ```css /* tailwind.css */ @import 'tailwindcss'; /* Add Plain packages to the sources */ @import "./.plain/tailwind.css"; /* Form styling plugin (optional) */ @plugin "@tailwindcss/forms"; ``` ```css /* .plain/tailwind.css */ @source "../.venv/lib/python3.12/site-packages/plain/admin"; @source "../.venv/lib/python3.12/site-packages/plain/auth"; @source "../.venv/lib/python3.12/site-packages/plain/htmx"; @source "../.venv/lib/python3.12/site-packages/plain/sessions"; @source "../.venv/lib/python3.12/site-packages/plain/models"; @source "../.venv/lib/python3.12/site-packages/plain/elements"; @source "../.venv/lib/python3.12/site-packages/plain/tailwind"; @source "../.venv/lib/python3.12/site-packages/plain/passwords"; ``` The end result is a single stylesheet across the application that covers both your code and the installed packages you're using. ## Other notable changes - Removed a bunch of signals (not my favorite pattern...) - Fully separated the concept of models from the core `plain.packages` registry - Renamed the `plain compile` command to `plain build` - [Vultured](https://github.com/jendrikseipp/vulture) tons of dead code (mostly from `plain.utils`) - Removed support for "unmanaged" models (for now anyway) ## Next steps There are only a few more things on my list that *have to change* before 1.0, but the nature of working through them tends to reveal others. I really enjoy that process but I also know it could go on forever! I do think we're getting close though. Honestly the documentation is the thing I'm dreading the most — I think there's a lot of other cool stuff buried in Plain that you have no way to know about. [x.com/davegaeddert](https://x.com/davegaeddert)