Django management commands
Throughout the previous chapters
-- including this one -- you've relied on management commands
invoked through the manage.py
script included in all
Django projects. For example, to start the development server of a
Django project you've used the runserver
command (e.g.
python manage.py runserver
) and to consolidate a
project's static resources you've used the
collectstatic
command (e.g. python manage.py
collectstatic
).
Django management commands are
included as part of Django apps and are designed to fulfill
repetitive or complex tasks through a one keyword command line
instruction. Every Django management command is backed by a script
that contains the step-by-step Python logic to fulfill its duties.
So when you type python manage.py runserver
, behind
the scenes Django triggers a much more complex Python routine.
If you type python
manage.py
(i.e. without a command) on a Django project,
you'll see a list of Django management commands classified by app
(e.g. auth
, django
,
staticfiles
). From this list you can gain insight into
the various management commands available on all your Django
apps.
I'll describe the purpose of Django management commands associated with core or third party Django apps as they come up in the book, just as I've done up to this point (e.g. static file management commands in static file topics, model management commands in model topics).
What I'll do next is describe how to create custom management commands in your Django apps, so you can simplify the execution of routine or complex tasks through a single instruction.
Custom management command structure
Custom management commands are
structured as Python classes that inherit their behavior from the
Django django.core.management.base.BaseCommand
class.
This last class provides the necessary structure to execute any
Python logic (e.g. file system, database or Django specific) and at
the same time process arguments typically used with Django
management commands. Listing 5-33 illustrates one of the most
Django management commands possible.
Listing 5-33. Django management command class with no arguments
from django.core.management.base import BaseCommand, CommandError from django.conf import settings class Command(BaseCommand): help = 'Send test emails' def handle(self, *args, **options): for admin_name,email in settings.ADMINS: try: self.stdout.write(self.style.WARNING("About to send email to %s" % (email))) # Logic to send email here # Any other Python logic can also go here self.stdout.write(self.style.SUCCESS('Successfully sent email to "%s"' % email)) raise Exception except Exception: raise CommandError('Failed to send test email')
Notice in listing 5-33, the
management command class must be named Command
and
inherit its behavior from the Django BaseCommand
class. Next, there's a help
attribute to describe the
purpose of the management command. If you type python
manage.py help <task_file_name>
or python
manage.py <task_file_name> --help
Django outputs the
value of the help
attribute.
The handle
method
contains the core command logic and is automatically run when
invoking the command. Notice the handle method declares three input
argument: self
to reference the class instance;
*args
to reference arguments of the method itself; and
**options
to reference arguments passed as part of the
management command. The task logic in listing 5-33 only uses the
self
reference. The other task management example --
in listing 5-34 -- illustrates how to use arguments.
The task logic in listing 5-33 is
limited to looping over the ADMINS
value in
settings.py
and outputting the task results. However,
there's no limit to the logic you can execute inside the handle
method, so long as it's valid Python.
Although standard Python try/except blocks work as expected inside Django management tasks, there are two syntax particularities you need to be aware of when creating Django management tasks: outputting messages and error handling.
To send output messages while
executing task logic -- success or informative -- you can see
listing 5-33 uses the self.stdout.write
reference,
which represents the standard output channel where management tasks
run. In addition, you can see self.stdout.write
uses
both the self.style.WARNING
and
self.style.SUCCESS
to declare the actual messages to
output. The wrapping of messages inside self.style.*
is optional, but outputs colored formatted messages (e.g. SUCCESS
in green font, WARNING in yellow font) in accordance with Django
syntax coloring roles[11].
To send error messages while
executing task logic, you can use the
self.stderr.write
reference, which represents the
standard error channel where management tasks run. And to terminate
the execution of a management task due to an error, you can
raise
the
django.core.management.base.CommandError
exception --
as it's done in listing 5-33 -- which accepts an error message,
that gets sent to the self.stderr.write
channel.
In most circumstances, it's rare
to have a fixed Django management command like the one in listing 5-33 that uses no arguments to alter its logical workflow. For
example, the Django runserver
command accepts argument
like addrport
and --nothreading
to
influence how a web server is launched.
Django management commands can use two types of arguments: positional arguments -- where the order in which they're declared gives them their meaning -- or named arguments -- which are preceded by names with two dashes -- (a.k.a.flags) to give them their meaning.
Although the
**options
argument of the handle()
method
-- as shown in listing 5-33 -- provides access to a management
command's arguments to alter the logical workflow. In order to use
arguments in a custom Django management command, you must also
declare the add_arguments()
method.
The add_arguments()
method must define a management task's arguments, including their
type -- positional or named -- default value, choice values and
help message, among other things. In essence, the
add_arguments()
method works as a pre-processor to
command arguments, which are then made available in the
**options
argument of the handle()
method.
The parser
reference
of the add_arguments(self,parser)
signature, is an
argument parser based on the standard Python argparse
package[12] designed to easily process command
line arguments for Python scripts.
To add command arguments inside
the add_arguments()
method you do so via the
parser.add_argument()
method, as illustrated in
listing 5-34.
Listing 5-34. Django management task class with arguments
from django.core.management.base import BaseCommand, CommandError from django.conf import settings class Command(BaseCommand): help = 'Clean up stores' def add_arguments(self, parser): # Positional arguments are standalone name parser.add_argument('store_id') # Named (optional) arguments start with -- parser.add_argument( '--delete', default=False, help='Delete store instead of cleaning it up', ) def handle(self, *args, **options): # Access arguments inside **options dictionary
#options={'store_id': '1', 'settings': None, 'pythonpath': None, # 'verbosity': 1, 'traceback': False, 'no_color': False, 'delete': False}
The management command in listing
5-34 declares both a positional and a named argument. Notice both
arguments are added with the parser.add_argument()
method. The difference being, named arguments use leading dashes --
and If omitted an argument is assumed to be positional.
Positional arguments by
definition are required. So in the case of listing 5-34, the
store_id
argument is expected (e.g. python
manage.py cleanupstores 1
, where 1
is the
store_id
), otherwise Django throws a 'too few
arguments' error.
Named arguments are always
optional. And because named arguments are optional, you can see in
listing 5-34 the --delete
argument declares a
default=False
value, ensuring the argument always
receives a default value to run the logic inside the
handle()
method.
The --delete
argument in listing 5-34 also uses the help
attribute
to define a descriptive text about the purpose of the argument. In
addition to default
and help
, the
parser.add_argument()
method supports a wide variety
of attributes, based on the Python argparse package -- see the
previous footnote to consult some of the arguments support by this
method.
Finally, you can see in listing
5-34 the handle()
method gets access to the command
arguments via the **options
dictionary, where the
values can then be used toward the structuring of the command
logic. Note the additional arguments available in
**options
-- settings
,
pythonpath
,etc -- are inherited by default due to the
BaseCommand
class.
Custom management command installation
All Django management tasks are
placed inside individual Python files (i.e. one command per file)
and stored inside an app directory structure under the
/management/commands/
folder. Listing 5-35 shows the
folder structure for a couple of apps with custom management
tasks.
Listing 5-35. Django management task folder structure and location
+-<BASE_DIR_project_name> | +-manage.py | | +---+-<PROJECT_DIR_project_name> | +-__init__.py +-settings.py +-urls.py +-wsgi.py | +-about(app)-+ | +-__init__.py | +-models.py | +-tests.py | +-views.py | +-management-+ | +-__init__.py | +-commands-+ | +-__init__.py | | | | | +-sendtestemails.py | +-stores(app)-+ +-__init__.py +-models.py +-tests.py +-views.py +-management-+ +-__init__.py +-commands-+ +-__init__.py | | +-cleanupstores.py +-updatemenus.py
As you can see in listing 5-35,
the about
app has a single management command inside
the /management/commands/
folder and the
stores
app has two management commands nested inside
its own /management/commands/
folder.
Caution To ensure the visibility of an app's management commands, don't forget to add the empty __init__.py files to the /management/ and /commands/ folders as shown in listing 5-35 and declare the apps as part of INSTALLED_APPS in a project's settings.py file.
Management command automation
Django management commands are typically run from the command line, requiring human intervention. However, there can be times when it's helpful or necessary to automate the execution of management commands from other locations (e.g. a Django view method or shell).
For example, if a user uploads an
image in a Django application and you want the image to become
publicly accessible, you'll need to run the
collectstatic
command so the image makes its way to
the public & consolidation location (STATIC_ROOT
)
. Similarly, you may want to run a cleanuprofile
command every time a user logs in.
To automate the execution of
management commands Django offers the
django.core.management.call_command()
method. Listing
5-36 illustrates the various ways in which you can use the
call_command()
method.
Listing 5-36. Django management automation with call_command()
from django.core import management # Option 1, no arguments management.call_command('sendtestemails') # Option 2, no pause to wait for input management.call_command('collectstatic', interactive=False) # Option 3, command input with Command() from django.core.management.commands import loaddata management.call_command(loaddata.Command(), 'stores', verbosity=0) # Option 4, positional and named command arguments management.call_command('cleanupdatastores', 1, delete=True)
The first option in listing 5-35
executes a management without any arguments. The second option in
listing 5-35 uses the interactive=False
argument to
indicate the command must not pause for user input (e.g.
collectstatic
always asks if you're sure if you want
to overwrite pre-existing files, the interactive=False
argument avoids this pause and need for input).
The third option in listing 5-35
invokes the management command by first importing it and then
invoking its Command()
class directly vs. using the
command string value. And finally, the fourth option -- just like
the third -- in listing 5-35, uses a positional argument --
declared as a standalone value (e.g. 'stores'
,
1
) and a named argument -- declared as a
key=value
(e.g. verbosity=0
,
delete=True
).