[Перевод] Расширение mypy с помощью плагинов

Добрый день, друзья. А мы продолжаем наращивать интенсивность запуска новых курсов и уже сейчас рады сообщить о том, что в конце апреля стартуют занятия по курсу «Web-разработчик на Python». В связи с этим традиционно делимся переводом полезного материала. Начнём.

Известно, что Python — язык с динамической типизацией. Очень просто писать DSL-подобные фреймворки, которые трудно разобрать инструментами статичной проверки типа. Несмотря на это, с помощью последних функциональных новшеств mypy, таких как protocols и literal types, а также с базовой поддержкой метаклассов и поддержкой дескриптора, мы можем чаще получать точные типы, однако по прежнему трудно избежать ложных срабатываний и других негативных факторов. Чтобы решить эту проблему и избежать необходимости кастомизировать систему типов для каждого фреймворка, mypy поддерживает систему плагинов. Плагины — это модули в Python, которые обеспечивают обратные вызовы (plugin hooks), которые mypy вызовет при проверке типов классов и функций, взаимодействующих с библиотекой или фреймворком. Таким образом можно точнее выделить тип возвращаемой функции, который в противном случае выразить крайне трудно, либо автоматически сгенерировать некоторые методы класса, чтобы отразить эффекты декоратора. Чтобы узнать больше об архитектуре системы плагинов и увидеть полный список возможностей, ознакомьтесь с документацией.

ncxlnqctevkfqvkvf2pbfua6oo8.png

Связанные плагины для стандартной библиотеки

Mypy поставляется с плагинами по умолчанию для реализации базовых функций и классов, а также с модулями ctypes, contextlib и dataclasses. Он также включает в себя плагины для attrs (так исторически сложилось, что это первый сторонний плагин написанный для mypy). Эти плагины позволяют mypy точнее определять типы и правильно проверять код на тип с помощью этих функций библиотеки. Чтобы показать это на примере, взгляните на отрывок кода:

from dataclasses import dataclass
    from typing import Generic, TypeVar
    
    @dataclass
    class TaggedVector(Generic[T]):
        data: List[T]
        tag: str
    
    position = TaggedVector([0, 0, 0], 'origin')


Выше, get_class_decorator_hook() вызывается при определении класса. Это добавляет автосоздаваемые методы, включая __init__(), в тело функции. Mypy использует такой конструктор, чтобы правильно вычислить TaggedVector[int] в качестве типа для position. Как видно из примера, плагины работают даже с обобщенными (generic) классами.

Вот еще один фрагмент кода:

from contextlib import contextmanager
    
    @contextmanager
    def timer(title: str) -> Iterator[float]:
        ...
    with timer(9000) as tm:
        ...


Здесь get_function_hook() обеспечивает точный возвращаемый тип для декоратора contextmanager, таким образом вызовы декорированной функции могут быть проверены на соответствие определенному типу. Теперь mypy может распознать ошибку: аргумент для timer() должен быть строкой.

Комбинирование плагинов и заглушек

Помимо использований динамических функций Python, фреймворки часто сталкиваются с проблемой наличия больших API. Mypy нужны файлы заглушки для библиотек для проверки кода, который использует эти библиотеки (только если библиотека не содержит встроенных аннотаций, что встречается не так часто). Распространение заглушек для больших фреймворков с помощью typeshed не является общей практикой:

  • Typeshed имеет относительно медленный цикл выпуска (поставляется вместе с mypy).
  • Неполные заглушки могут привести к ложным вызовам, чего будет крайне сложно избежать.
  • Не просто совмещать заглушки из различных версий typeshed.


Пакеты заглушек (Stub packages), представленные в PEP 561, позволяют делать следующее:

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


Более того, pip позволяет комбинировать различные заглушки для библиотек и соответствующих плагинов mypy в один дистрибутив. Заглушки для фреймворка или соответствующего плагина mypy могут быть легко разработаны и скомпонованы вместе в один дистрибутив, что крайне полезно, поскольку плагины заполняют отсутствующие или неточные определения в заглушках.

Последний пример такого пакета это SQLAlchemy stubs and plugin, с первым публичным релизом версии 0.1, который был некоторое время назад опубликован на PyPI. Несмотря на то, что этот проект находится в ранней Alpha версии, мы уже спокойно можем использовать его в DropBox для улучшения проверки типов. Плагин понимает базовые декларации ORM:

from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy import Column, Integer, String
    
    Base = declarative_base()
    
    class User(Base):
        __tablename__ = 'users'
        id = Column(Integer, primary_key=True)
        name = Column(String)


Во фрагменте кода выше, плагин использует get_dynamic_class_hook(), чтобы сообщить mypy, что Base является допустимым базовым классом, даже если он таковым не выглядит. Затем get_base_class_hook() вызывается для определения User, и добавляет несколько автоматически сгенерированных атрибутов. Дальше мы создаем экземпляр модели:

user = User(id=42, name=42)

Вызвана get_function_hook(), поэтому mypy может указать на ошибку: получено значение типа integer вместо имени пользователя.

Заглушки определяют Column в качестве generic деcкриптора, так, чтобы атрибуты модели получили правильные типы:

id_col = User.id  # Inferred type is "Column[int]"
name = user.name  # Inferred type is "Optional[str]"


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

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

  • Используйте __getattr__(), чтобы избежать ложных срабатываний на ранних стадиях, когда заглушки не завершены (это пресекает ошибки mypy, в случае отсутствия атрибутов модуля). Вы также можете использовать это в файлах __init__.py, если отсутствуют какие-либо подмодули.
  • Дескрипторы часто помогают с более точным определением типов для кастомизированного доступа к атрибутам (как в примере с Column, который мы рассматривали выше). Использовать дескрипторы нормально даже если фактическая реализация среды выполнения использует более сложный механизм, включающий метакласс, например.
  • Не раздумывая объявляйте классы фреймворка как обобщенные. Несмотря на то, что они не являются таковыми во время выполнения, этот прием позволяет точнее определять тип некоторых элементов фреймворка, тогда как ошибки выполнения можно легко обойти. (Мы надеемся, что фреймворки постепенно добавят встроенную поддержку обобщенных (generic) типов, явно наследуя соответствующие классы от typing.Generic.)


Недавно выпущенные плагины mypy

Уже сейчас есть несколько доступных плагинов для популярных фреймворков Python. Отдельно от упомянутого выше плагина для SQLAlchemy, другие примечательные примеры пакетов с заглушками и встроенным плагином mypy включают заглушки для интерфейсов Django и Zope. Сейчас над этими проектами ведется активная работа.

Установка и подключение пакетов заглушек и плагинов

Используйте pip, чтобы установить пакет с плагином для mypy и/или заглушки в виртуальную среду, где уже стоит mypy:

 $ pip install sqlalchemy-stubs


Mypy автоматически обнаружит установленные заглушки. Чтобы подключить установленные плагины, включите их непосредственно в mypy.ini (или в пользовательский файл конфигурации):

[mypy]
plugins = sqlmypy, mypy_django_plugin.main


Разработка плагинов mypy и написание заглушек

Если вы хотите разработать пакет заглушек и плагинов для фреймворка, который вы используете, мы можете использовать репозиторий sqlalchemy-stubs в качестве шаблона. Он включает в себя файл setup.py, тестирование инфраструктуры с использованием тестов, управляемых данными, и пример класса плагина с набором хуков для плагина (plugin hooks). Мы рекомендуем использовать stubgen для автоматической генерации заглушек, которые поставляются с mypy, чтобы начать их использовать. Stubgen несколько улучшился в mypy 0.670.

Изучите документацию, если вы хотите узнать больше о системе плагинов mypy. Также вы можете поискать в интернете исходные коды плагинов, о которых говорилось в статье. Если у вас есть вопросы, вы можете задать их здесь.

15 апреля пройдет бесплатный открытый вебинар по курсу, который проведет один из организаторов сообщества Moscow Python — Владимир Филонов, записывайтесь, будет интересно. А сейчас мы ждём ваши комментарии по переведенному материалу.

© Habrahabr.ru