Loading Django management commands from custom locations
Published: 2024-11-04
Django management commands are extremely useful, however they do have a slight limitation - they are only loaded from Django apps. If you look at the source there is no way to really hook into this functionality.
This actually isn't much of a problem usually, of course until it is. Recently at Xelix, due to a big refactor I will at some write blog about (or do a conference talk), this became a problem for us. We already had a bunch of django apps which were really only registered for management commands, which was all good - Django doesn't have a problem with apps only having management commands, as long as the name of the app is unique. That is exactly where our problem came from, we suddenly had dozens of apps with name clashes (shhhhh, don't ask why, later).
There were two options really. One is a new app where all commands live (which would then have dozens management commands, quite confusing). If we think moving to one app is bad, the second one is worse, having a new app for each name clash, which is better for not having everything in place, but then it doubles number of apps. Both would also require moving all commands, their tests, etc. And so many conflicts probably!
Example
Just to briefly highlight what sort of structure could be problematic. Imagine the folder structure is something like this:
src
|- apps
| |- app1
| | | - models.py
| |- app2
| | - models.py
| commands
| - app1
| | - management
| | - commands
| | - command1.py
| - app2
| - management
| - commands
| - command2.py
You cannot put src.apps.app1
and src.commands.app1
into installed apps, as it would clash (the app1
is the important part). But if you do want to have a structure like this, or are forced to by some other circumstances, what to do?
django-custom-commands
Well, I didn't find anything out there that solved this well1, so of course, I wrote my own package for this. This makes it fairly easy to make this functionality possible.
First step is of course installation, I still prefer Poetry, but any other pip installation method will work.
poetry add django-custom-commands
Second step is configuration, you will need to update your django settings to add CUSTOM_COMMAND_LOCATIONS
. This a list of "app-like" modules which may contain management commands. So if using the example above, it would be
INSTALLED_APPS = [
"src.apps.app1",
"src.apps.app2",
]
CUSTOM_COMMAND_LOCATIONS = [
"src.commands.app1",
"src.commands.app2",
]
Finally, update imports. The package overrides the management command handling, meaning you need to import some functions from the package instead of django. Specifically import call_command
, get_commands
and execute_from_command_line
from django_custom_commands.management
instead of django.core.management
. My current favourite method of making sure this is followed in the entire project is ruff
:
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"django.core.management.call_command" = { msg = "Use django_custom_commands.management.call_command" }
"django.core.management.execute_from_command_line" = { msg = "Use django_custom_commands.management.execute_from_command_line" }
"django.core.management.get_commands" = { msg = "Use django_custom_commands.management.get_commands" }
That should be it! Now, when you run ./manage.py command1
, it will run the command, even though it's not a Django app.
Conclusion
While you may question why this exists, but if you need it to exist, it may help you! Available on PyPI and source is on GitHub.
There is django-management-commands which would be the ideal solution, it actually does lot more than this package I wrote, but it lacks some critical functionality while breaking other, so I could not use it.↩