Create a custom Django manage.py command
December 19, 2020 Β βΒ 5Β min read
When you used Django before you undoubtedly used a subcommand of manage.py
, such as runserver
and migrate
. You can also create custom django commands that can be invoked via manage.py
. This can be useful for administrative tasks in your application. Like running a background job for example.
Create a command
Django looks for custom management commands in the folder management/commands
of the apps you registered in the INSTALLED_APPS
setting. The command itself is just a python file in the commands
folder.
If we would have a plants app in our Django application that should have a waterplants
command, to water the plants, we add a waterplants.py
file to the commands
folder.
So that our plants app has the following files. Just an fyi, files starting with a underscore(_
) are not registered as custom management command.
$ tree plants
plants
βββ admin.py
βββ apps.py
βββ __init__.py
βββ management
β βββ commands
β βββ waterplants.py
βββ migrations
β βββ __init__.py
βββ models.py
βββ tests.py
βββ views.py
$ tree plants
lol :)
If you run python manage.py help
you should see your new command as available subcommand.
$ python manage.py help
Type 'manage.py help <subcommand>' for help on a specific subcommand.
Available subcommands:
...
[plants]
waterplants
...
However, running the command at this state doesn't do a whole lot. Actually, it throws an exception:
AttributeError: module 'plants.management.commands.waterplants' has no attribute 'Command'
.
What Django expects a class Command
which is a subclass of BaseCommand
in this waterplants.py
we just created. To get rid of this exception can past in the following piece of code.
# ./plants/management/commands/waterplants.py
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Water the plants'
def handle(self, *args, **options):
pass
The handle
method is the entry point of the command. When the command is invoked by Django the handle
method will be called.
As you see we have set a help
property for the Command class. This string is used in the help message for the management command if we pass the --help
flag. We see that we get some optional arguments for free from the BaseCommand class.
$ python manage.py waterplants --help
usage: manage.py waterplants [-h] [--version] [-v {0,1,2,3}]
[--settings SETTINGS] [--pythonpath PYTHONPATH]
[--traceback] [--no-color] [--force-color]
[--skip-checks]
Water the plants
optional arguments:
-h, --help show this help message and exit
--version show program's version number and exit
-v {0,1,2,3}, --verbosity {0,1,2,3}
Verbosity level; 0=minimal output, 1=normal output,
2=verbose output, 3=very verbose output
--settings SETTINGS The Python path to a settings module, e.g.
"myproject.settings.main". If this isn't provided, the
DJANGO_SETTINGS_MODULE environment variable will be
used.
--pythonpath PYTHONPATH
A directory to add to the Python path, e.g.
"/home/djangoprojects/myproject".
--traceback Raise on CommandError exceptions
--no-color Don't colorize the command output.
--force-color Force colorization of the command output.
--skip-checks Skip system checks.
Accessing models
Since you're not gonna make management commands for the sake of it. You probably want them to interact with your models in some way.
There is not much special about doing this from a command compared to any other place in your Django application. You can just import the model class in your command file and interact with it as you would normally do from a view for example.
from django.core.management.base import BaseCommand
from plants.models import Plant
class Command(BaseCommand):
help = 'Water the plants'
def handle(self, *args, **options):
plants = Plant.objects.all()
for plant is plants:
plant.water()
Writing output
A simple print()
will work. But the BaseCommand comes with two other options as well. The BaseCommand sets the stdout
and stderr
output streams as its properties and we can write to them by calling a write
function.
from django.core.management.base import BaseCommand
from plants.models import Plant
class Command(BaseCommand):
help = 'Water the plants'
def handle(self, *args, **options):
plants = Plant.objects.all()
for plant in plants:
plant.water()
self.stdout.write(f'watered plant "{plant}"')
Now that we added that to our command we actually see some output when we run the custom management command.
$ python manage.py waterplants
watered plant "Monstera"
watered plant "Ficus"
watered plant "Cactus"
If you like you can also add some colors to the output. If we were to change the line that writes to stdout to:
self.stdout.write(self.style.SUCCESS(f'watered plant "{plant}"'))
We would see the output having green text when rerunning the command.
Some useful style options you get for writing output are:
- Success:
self.style.SUCCESS('...')
- Notice:
self.style.NOTICE('...')
- Warning:
self.style.WARNING('...')
- Error:
self.style.ERROR('...')
Command errors
When throwing exceptions in your command class you normally want to use the CommandError
class that is provided by Django. Since Django will intercept it and write the error message to stderr
, besides that it exits the command with a non-zero return code as well.
from django.core.management.base import BaseCommand, CommandError
from plants.models import Plant
class Command(BaseCommand):
help = 'Water the plants'
def handle(self, *args, **options):
plants = Plant.objects.all()
if not plants:
raise CommandError('No plants to water')
for plant in plants:
plant.water()
self.stdout.write(self.style.SUCCESS(f'watered plant "{plant}"'))
Custom return codes
As said above, the CommandError
class exits the script with a non-zero return code. It returns 1
to be precise. Using a different return code might be useful in some cases.
To do so you can easily pass the returncode
keyword argument when throwing the exception.
from django.core.management.base import BaseCommand, CommandError
from plants.models import Plant
class Command(BaseCommand):
help = 'Water the plants'
def handle(self, *args, **options):
plants = Plant.objects.all()
if not plants:
raise CommandError('No plants to water', returncode=2)
for plant in plants:
plant.water()
self.stdout.write(self.style.SUCCESS(f'watered plant "{plant}"'))