Django: как писать собственные команды Manage.py

Платформа Django поставляется с довольно обширной библиотекой предварительно упакованных команд администратора, выполняемых через интерфейс manage.py, которые могут сделать управление вашим сайтом и приложениями более простым и автоматизированным. Знаете ли вы, что эта же платформа также предоставляет механизм для создания ваших собственных совершенно новых команд администратора?

Давайте рассмотрим, как это сделать, и рассмотрим пошаговый процесс.

Настраивать

Прежде чем мы углубимся в код, нам нужно установить определенную структуру каталогов в нашем приложении Django для наших пользовательских команд:

my_app/
    __init__.py
    models.py
    management/
        __init__.py
        commands/
            __init__.py
            my_custom_command.py
    urls.py
    views.py

Первое изменение — это новый каталог management в любом приложении, в котором будут использоваться специальные команды. В этом новом каталоге нам также необходимо создать подкаталог с именем commands, в котором будет один файл Python для каждой создаваемой нами пользовательской команды.

Важно, чтобы имя файла соответствовало команде, которая его вызывает. В приведенном выше примере my_custom_command.py будет запущен с помощью команды: python manage.py my_custom_command. В случае нескольких пользовательских команд каждая команда получает свой собственный файл в каталоге commands.

Далее давайте заглянем внутрь файла my_custom_command.py.

Определение нашей пользовательской команды администратора

Файл my_custom_command.py содержит настроенную логику, которая запускается, когда системный администратор вызывает команду: python manage.py my_custom_command

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = 'Customized admin command. Hi readers!'

    def handle(self, *args, **options):
        # Logic goes here!
        self.stdout.write('This is a custom command. With custom output. Hello World!')

Файл my_custom_command.py содержит только один класс, всегда имеющий имя Command и наследуемый от класса BaseCommand. Имя файла Python определяет имя команды.

В этом классе есть два важных компонента: атрибут help и метод handle.

Атрибут help — это строка, которая позволяет разработчику написать текст справки для новой пользовательской команды. Этот текст отображается в качестве описания рядом с пользовательской командой, когда администратор выводит список всех доступных команд, запуская python manage.py без указания конкретной команды.

Метод handle — это то, что содержит весь наш настроенный код и куда мы поместим всю нашу логику. Это может импортировать другие модели и выполнять над ними действия или совершать любые другие магические действия.

Обратите внимание, что в этой ситуации мы используем метод self.stdout.write вместо метода print для вывода текста на стандартный выход. И наоборот, self.stderr.write может использоваться для обозначения стандартной ошибки при печати сообщений об ошибках и т.п.

Вот что мы видим, когда запускаем наш собственный метод:

$ python manage.py

Type 'manage.py help <subcommand>' for help on a specific subcommand.

Available subcommands:

[auth]
    changepassword
    createsuperuser

[contenttypes]
    remove_stale_contenttypes

[django]
    check
...
    startapp
    startproject
    test
    testserver

[my_app]
    my_custom_command

$ python manage.py my_custom_command
This is a custom command. With custom output. Hello World!

Для придания изюминки вы можете дополнительно раскрасить вывод, используя следующий метод стиля:

self.stdout.write(self.style.SUCCESS('This is success text! Yay!'))

Доступны и другие параметры, например ERROR, NOTICE, WARNING. Обратитесь к документации Django для получения полного списка пользовательских параметров цвета при выводе текста.

Наконец, исключение CommandError может быть вызвано, чтобы указать, что что-то пошло не так, и прекратить выполнение нашей специальной команды. Платформа Django обработает это исключение корректно для пользователя, выведет текст исключения в стандартную ошибку и закроет приложение с указанным returncode

Вот пример, который объединяет все это:

from django.core.management.base import BaseCommand, CommandError
from .models import Bar

class Command(BaseCommand):
    help = 'Foos all enabled Bars in the system.'

    def handle(self, *args, **options):
        try:
            Bar.objects.filter(enabled=True).update(foo=True)
        except Exception as e:
            raise CommandError(f'Something bad hapenned: {e}')
        self.stdout.write(self.style.SUCCESS('Successfully Foo-ed all enabled Bars!'))

Передача аргументов командной строки

Может наступить момент, когда мы захотим передать аргументы командной строки нашей новой пользовательской команде администратора. К счастью, Django предоставляет нам очень знакомый механизм для этого, используя популярный модуль Python argparse.

Для начала нам нужно переопределить метод add_arguments, например:

def add_arguments(self, parser):
    parser.add_argument('id', type=int, help='ID of the record being updated')

В нашем методе handle мы можем получить доступ к аргументам через параметр options, переданный в метод. Например:

def handle(self, *args, **options):
    obj_id = options.get('id')
    try:
        bar = Bar.objects.get(id=obj_id)
    except Bar.DoesNotExist as e:
        raise CommandError(f'Object with ID {obj_id} not found!')
    bar.foo = True
    bar.save()
    self.stdout.write(self.style.SUCCESS('Bar has been Foo-ed!'))

Когда мы запускаем нашу новую команду, вот что мы видим:

$ python manage.py my_custom_command 1
Bar has been Foo-ed!
$ python manage.py my_custom_command 2
CommandError: Object with ID 2 not found!

Пример модульного теста

Как хороший разработчик, мы затем хотим провести модульное тестирование нашего нового специального метода. В конце концов, любое количество автоматизированных процессов может полагаться на нашу специальную команду, чтобы продолжать работать по назначению.

Django предоставляет нам очень простой способ вызвать нашу новую пользовательскую команду администратора из среды модульного тестирования (или где-либо еще, если на то пошло) с помощью метода call_command. Этот метод принимает строку, представляющую команду администратора для запуска.

Пример модульного теста, позволяющий убедиться, что все наши Bars были Fooed, будет выглядеть следующим образом:

from django.core.management import call_command
from django.test import TestCase
from .models import Bar

class CustomCommandTest(TestCase):
    def test_all_bars_fooed(self):
        Bar.objects.create(foo=False, enabled=False)
        Bar.objects.create(foo=False, enabled=True)
        call_command('my_custom_command')
        self.assertEqual(1, Bar.objects.filter(foo=True).count())
        self.assertEqual(1, Bar.objects.filter(foo=False).count())

В качестве лучшей практики я рекомендую абстрагировать как можно больше логики за пределами класса Command в отдельные модули, которые можно тестировать индивидуально и легко повторно использовать в другом месте.

Вот и все! Django упрощает настройку нашего сайта для наших системных администраторов или других автоматических перехватчиков для выполнения дополнительного кода в рамках платформы Django и среды приложения.

Внешние ресурсы