В этой статье я описал примеры из моего личного опыта, как мы используем библиотеки inspect, ast и фреймворк входящий в Django check для улучшения нашего процесса разработки
У каждой команды свой стиль разработки. Некоторые команды осуществляют локализацию и создают перевод всего тестового контента. Некоторые команды более чувствительны к проблемам с базами данных и требуют более тщательной обработки индексов и ограничений. Существующие инструменты не всегда могут решить эти конкретные проблемы из коробки, поэтому мы придумали способ реализации нашего собственного стиля разработки с использованием фреймворка check (входящего в Django), модулей inspect и ast из стандартной библиотеки Python.
Check (Проверка) в Django являются частью инфраструктуры системных проверко Django (Django System Check framework). Из официальной документации:
Система проверки — это набор статических проверок для валидации проектов Django. Он позволяет обнаруживать общие проблемы и предоставляет советы по их устранению. Фреймворк является расширяемым, поэтому вы можете легко добавлять свои собственные проверки.
Одна проверка, с которой вы, возможно, сталкивались, это проверка системы административной панели:
SystemCheckError: System check identified some issues: ERRORS: <class 'app.admin.BarAdmin> (admin.E108) The value of 'list_display[3]' refers to 'foo', which is not a callable, an attribute of 'Bar', or an attribute or method on 'app.Bar'.
Разработчики панели администратора добавили системную проверку, чтобы предупредить разработчиков о полях в административной модели, которых нет в реальной модели. В нашем примере говориться что поле ‘foo‘ не существует в модели Bar.
Проверки выполняются всякий раз при запуске некоторых команд управления, таких как makemigrations и migrate. Также возможно явно запустить проверку, используя manage.py:
$ ./manage.py check
Хорошая идея, чтобы включить запуск проверок в ваш процесс CI. Если вы хотите что бы не прохождение проверок регистрировалось в CI как предупреждение, вы можете сделать это, установив флаг:
$ ./manage.py check --fail-level=WARNING
Простой пример того, как Django использует проверки, можно найти в исходном коде модели Field проверки.
Большинство наших приложений не предназначены для носителей английского языка, поэтому мы широко используем переводы. Этому мы уделяем большое внимание при проверке кода, чтобы убедиться, что все переведено правильно.
Одна из основных проблем, возникающих при проверке кода, заключается в том, что разработчики часто забывают установить verbose_name в полях модели.
Проверка того, что у поля есть verbose_name, является довольно простой задачей, и мы хотели автоматизировать процесс, чтобы быть уверенными, что оно всегда установлено.
Для начала мы определим простую модель профиля клиента:
class CustomerProfile(models.Model): id = models.PositiveSmallIntegerField( primary_key=True, verbose_name=_('id'), ) name = models.CharField( max_length=100, ) created_by = models.ForeignKey( User, on_delete=models.PROTECT, )
В нашем примере поле «name» не имеет verbose_name. Давайте посмотрим, сможем ли мы определить это, используя только атрибут _meta модели:
>>> name_field = CustomerProfile._meta.get_field('name') >>> name_field.verbose_name name
Похоже, Django сделало что-то в бекграунде, чтобы установить verbose_name. Если посмотреть в содержимое исходников класса Field, то там есть функция set_attributes_from_name, которая заполняет verbose_name путем преобразования имени поля — отсюда и получилось verbose_name равное «name».
Поскольку Django устанавливает verbose_name самостоятельно, строка «name» не будет воспринята makemessages и не будет автоматически добавлена в po-файл. Это, вероятно, приведет к тому, что строка «name» останется незамеченной. Это не то что нам нужно!
Кроме того, поскольку Django заполняет поле автоматически, мы не можем использовать _meta, чтобы проверить, было ли изначально задано verbose_name. Для этого нам нужно проверить фактический исходный код.
В Python есть модуль inspect, который мы можем использовать для проверки кода:
Модуль inspect предоставляет несколько полезных функций, которые помогают получить информацию о объектах, таких как модули, классы, методы, функции, обратные вызовы, объекты фреймов и объекты кода. Например, он может помочь вам изучить содержимое класса, получить исходный код метода, извлечь и отформатировать список аргументов для функции или получить всю информацию, необходимую для отображения подробного traceback.
Давайте посмотрим, что мы можем получить из inspect:
>>> import inspect >>> inspect.getsource(CustomerProfile) "class CustomerProfile(models.Model):\n id = models.PositiveSmallIntegerField(\n primary_key=True,\n verbose_name=_('Name'),\n )\n name = models.CharField(\n max_length=100,\n )\n created_by = models.ForeignKey(\n User,\n on_delete=models.PROTECT,\n )\n\n def __str__(self):\n return self.name\n"
Это довольно захватывающе. Мы дали inspect наш класс и получили исходный код для этого класса в виде текста.
Получив исходный код, мы могли бы использовать какой-то необычный RegExp для разбора кода, но, опять же, в Python все уже есть для нашей задачи.
Разбор кода в Python выполняется модулем ast:
Модуль ast помогает приложениям Python обрабатывать деревья грамматики абстрактного синтаксиса Python.
Супер! С деревом работать намного проще, чем с текстом.
Используем ast для анализа исходного кода нашей модели:
>>> import inspect >>> import ast >>> model_source = inspect.getsource(CustomerProfile) >>> model_node = ast.parse(model_source) >>> ast.dump(model_node, False) Module([ ClassDef('CustomerProfile', [Attribute(Name('models', Load()), 'Model', Load())], [], [ Assign( [Name('id', Store())], Call( Attribute(Name('models', Load()), 'PositiveSmallIntegerField', Load()), [], [ keyword('primary_key', NameConstant(True)), keyword('verbose_name', Call(Name('_', Load()), [Str('Name')], [])) ] ) ), Assign( [Name('name', Store())], Call( Attribute(Name('models', Load()), 'CharField', Load()), [], [keyword('max_length', Num(100))] ) ), Assign( [Name('created_by', Store())], Call( Attribute(Name('models', Load()), 'ForeignKey', Load()), [Name('User', Load())], [keyword('on_delete', Attribute(Name('models', Load()), 'PROTECT', Load()))] ) ), FunctionDef( '__str__', arguments([arg('self', None)],None,[],[],None,[]), [Return(Attribute(Name('self', Load()), 'name', Load()))], [], None ) ], [] ) ])
Если мы внимательно посмотрим на этот дамп, мы можем определить, что все наши поля модели являются узлами Assign.
Давайте рассмотрим поле «name»:
Assign( [Name('name', Store())], Call( Attribute(Name('models', Load()), 'CharField', Load()), [], [keyword('max_length', Num(100))] ) )
Поле модели — это назначение узла Call (CharField) узлу Name («name»). Узел Call имеет список аргументов. В нашем случае у нас есть только один аргумент «max_length» с числовым значением 100.
Наше поле id выглядит так:
Assign( [Name('id', Store())], Call( Attribute(Name('models', Load()), 'PositiveSmallIntegerField', Load()), [], [ keyword('primary_key', NameConstant(True)), keyword('verbose_name', Call( Name('_', Load()), [Str('Name')], [] ) ) ]) )
Поле id также является узлом Assign с узлом Name и узлом Call. Поле id имеет два ключевых слова — primary_key и verbose_name, которое мы и ищем.
Для оценки полей нам сначала нужно их идентифицировать. Мы уже видели, что поля модели являются узлами Assign, но мы не можем полагаться на то, что они являются единственными узлами Assign в классе.
Единственное, на что мы можем положиться, это то, что на верхнем уровне класса имена атрибутов уникальны. Это означает, что если мы знаем, что есть поле с именем «name», мы можем предположить, что атрибут «name» класса является полем.
Давайте объединим усилия с атрибутом _meta, чтобы найти узлы полей модели:
from django.db.models import FieldDoesNotExist for node in model_node.body[0].body: if not isinstance(node, ast.Assign): continue if len(node.targets) != 1: continue if not isinstance(node.targets[0], ast.Name): continue field_name = node.targets[0].id try: field = model._meta.get_field(field_name) except FieldDoesNotExist: continue # node is field!
Здесь делается следующее:
Теперь у нас есть узел поля, и мы можем проверить, определен ли атрибут verbose_name.
Давайте итерируем keywords и поищем verbose_name:
for kw in node.value.keywords: if kw.arg == 'verbose_name': verbose_name = kw break else: verbose_name = None
На данный момент, если verbose_name имеет значение None, мы знаем, что атрибут не был установлен, и мы готовы создать наше первое предупреждение!
Для создание проверки нам нужно зарегистрировать нашу новую функцию проверки с помощью фреймворка check:
from django.core import check checks @checks.register(checks.Tags.models) def run_custom_checks(app_configs, **kwargs): # implement check logic
Внутри функции мы реализуем логику проверки и вернем список проверок.
Мы хотим предупредить разработчика, что в поле отсутствует атрибут verbose_name, поэтому, как только мы найдем поле, которое не имеет verbose_name, мы создаем CheckMessage типа Warning:
from django.core.checks import Warning @checks.register(checks.Tags.models) def run_custom_checks(app_configs, **kwargs): # inspect and parse models... return [( Warning( 'Field has no verbose name', hint='Set verbose name on field {}.'.format(field.name), obj=field, id='H001', ) )]
Я назначил код H00X своим предупреждениям (угадайте почему …). Для каждого предупреждения мы также можем добавить подсказку, чтобы проинформировать разработчика о том, как решить проблему, вызванную предупреждением.
Напомним, что мы сделали до сих пор:
Каркас функции, которая проверяет одну модель:
# common/checks.py def check_model(model): """Check a single model. Yields (django.checks.CheckMessage) """ model_source = inspect.getsource(model) model_node = ast.parse(model_source) for node in model_node.body[0].body: # Check if node is a model field. # Check if field has verbose name defined yield Warning( 'Field has no verbose name', hint='Set verbose name on field {}.'.format(field.name), obj=field, id='H001', )
Следующим шагом является реализация единой функции для итерации по всем моделям, запуска наших проверок и регистрации ее в инфраструктуре проверки Django:
# common/checks.py @checks.register(checks.Tags.models) def check_models(app_configs, **kwargs): errors = [] for app in django.apps.apps.get_app_configs(): # Skip third party apps. if app.path.find('site-packages') > -1: continue for model in app.get_models(): for check_message in check_model(model): errors.append(check_message) return errors
Мы используем небольшой трюк, чтобы пропустить модели из сторонних приложений. Мы предполагаем, что при установке сторонних приложений с помощью pip install они устанавливаются в каталог под названием «site-packages».
Единственное, что осталось сделать, это импортировать этот файл куда-нибудь в код и все готово.
app/__init__.py from common.checks import
Давайте посмотрим нашу новую проверку в действии:
$ ./manage.py check SystemCheckError: System check identified some issues: WARNINGS: app.CustomerProfile.name: (H001) Field has no verbose name HINT: Set verbose name on the field "name". System check identified 1 issues (0 silenced).
Именно то, что мы хотели!
Чтобы понять, что вы можете делать с проверками Django, я приведу список проверок, которые мы используем в нашей базе кода:
Это пример, который мы только что видели.
Убедитесь, что verbose_name всегда имеет вид verbose_name = _ (‘text’). Если значение не использует gettext, оно не будет переведено.
Мы решили использовать только нижний регистр букв в verbose_name. Используя нижний регистр, мы смогли повторно использовать фразы в большинстве переводов. Единственным исключением из этого правила являются такие сокращения, как API и ETL. Общее правило, которым мы в конечном итоге придерживались, заключается в том, чтобы все слова были либо в нижнем, либо в верхнем регистре. Например, «etl run» допустим, «ETL run» также допустим, «Etl Run» недопустимо.
Текст справки отображается пользователю в административных формах и подробных вьюхах, поэтому он должен так же использовать gettext и переводиться.
Перевод названия модели определен в мета-классе модели, поэтому каждая модель должна иметь класс Meta.
verbose names моделей определены в классе Meta и отображаются пользователю в админке, поэтому их следует переводить.
Множественные названия моделей используются в админке и отображаются пользователю, поэтому они должны быть переведены.
Это должно быть самой полезной проверкой, которую мы определили. Эта проверка заставляет разработчика явно устанавливать db_index для каждого поля ForeignKey. В прошлом я писал о том, как индекс базы данных создается неявно для каждого поля внешнего ключа ( how a database index is created implicitly for every foreign key field). Убедившись в том, что разработчик знает об этом, нужно заставить его решить, нужен ли индекс или нет, в итоге у вас останутся только те индексы, которые вам действительно нужны!
Оригинальная статья: Haki Benita Automating the Boring Stuff in Django Using the Check Framework
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…
View Comments
Познавательно. Спасибо что поделились опытом. Возможно тоже буду внедрять такие проверки в своих проектах.