Рубрики
Без рубрики

Тестирование магических заглушек, плагинов и типов

Первоначально опубликовано в моем блоге: https://sobolevn.me/2019/08/testing-mypy-types Ты когда нибудь пробовал… Помечено с Python, WebDev, Mypy, начинающими.

Первоначально опубликовано в моем блоге : https://sobolevn.me/2019/08/testing-mypy-types

Вы когда-нибудь пытались:

Если вы попытаетесь сделать любой из них, вы скоро узнаете, что вам нужно проверить ваши типы. Чего ждать? Позвольте мне подробно объяснить этот парадокс.

Первые тесты для типов в Python

Начнем с урока истории. Первый раз, когда я заинтересован майка Я нашел свою технику тестирования уникальным и интересным. Вот как это выглядит подобно:

[case testNestedListAssignmentToTuple]
from typing import List
a, b, c = None, None, None # type: (A, B, C)

a, b = [a, b]
a, b = [a]  # E: Need more than 1 value to unpack (2 expected)
a, b = [a, b, c]  # E: Too many values to unpack (2 expected, 3 provided)

Это выглядит знакомым:

  • [дело] Определяет новый тест, как Def Test_ делает
  • Содержание внутри сырье Python Линии исходных кода, которые обрабатываются с майка
  • # Е: Комментарии – Утверждать заявления, которые говорят, что Marpy Вывод ожидается на каждой строке

Итак, мы можем написать такие тесты на наши библиотеки, верно? Это был вопрос, когда я начал писать Возвращает Библиотека (которая является типизированным монадом в Python). Итак, мне нужно было проверить, что происходит внутри, и какие типы раскрываются майка . Затем я пытался повторно использовать эти тестовые случаи из Marpy Отказ

Долгая история короткая, это невозможно. Этот маленький помощник встроен внутри Marpy Исходный код и не может быть повторно используется. Итак, я начал искать другие решения.

Современный подход

Я наткнулся на pytest-mypy-plugins упаковка. Первоначально он был создан, чтобы убедиться, что это типы для Джанго работает нормально в TypedDjango проект. Проверить мой предыдущий пост об этом.

Чтобы установить pytest-mypy-plugins В вашем проекте Run:

pip install pytest-mypy-plugins

Это работает похоже на Marpy Собственные испытательные случаи, но с немного другим дизайном. Давайте создадим Ямл файл и поместите его как ./typesafety/test_compose.yml.yml. :

# ./typesafety/test_compose.yml
- case: compose_two_functions
  main: |
    from myapp import first, second

    reveal_type(second(first(1)))  # N: Revealed type is 'builtins.str*'
  files:
    - path: myapp.py
      content: |
        def first(num: int) -> float:
            return float(num)

        def second(num: float) -> str:
            return str(num)

Что мы имеем здесь?

  • дело Определение, это в основном имя теста
  • Главная Раздел, который содержит Python Исходный код, который требуется для теста
  • # N: Комментарий, который указывает на ноту от майка
  • Файлы Раздел, где вы можете создавать временные файлы HELPER, которые будут использоваться в этом тесте

Хороший! Как мы можем запустить его? С pytest-mypy-plugins это pteest Плагин, нам нужно только запустить pteest как обычно и указать наши Marpy Файл конфигурации (по умолчанию на mypy.ini ):

pytest --mypy-ini-file=setup.cfg

Вы можете иметь два Marpy Конфигурации: один для вашего проекта, один для тестов. Просто говорю. Давайте посмотрим на наш setup.cfg Содержание:

[mypy]
check_untyped_defs = True
ignore_errors = False
ignore_missing_imports = True
strict_optional = True

Это результат привода:

» pytest --mypy-ini-file=setup.cfg
================================ test session starts =================================
platform darwin -- Python 3.7.4, pytest-5.1.1, py-1.8.0, pluggy-0.12.0
rootdir: /code/, inifile: setup.cfg
plugins: mypy-plugins-1.0.3
collected 1 item

typesafety/test_compose.yml .                                                  [100%]

================================= 1 passed in 2.00s ==================================

Оно работает! Давайте усложним наш пример немного.

Проверка на ошибки

Мы также можем использовать pytest-mypy-plugins Чтобы обеспечить и проверять ограничения на нашему сложному типу спецификации. Давайте представим, что у вас есть определение типа со сложными дженеранами, и вы хотите убедиться, что он работает правильно.

Это на самом деле очень полезно, потому что вы можете проверить случаи успеха с RAW Marpy проверяет, пока вы не можете сказать Marpy ожидать ошибку для конкретного выражения или вызова.

Начнем с нашего комплексного типа определения:

# returns/functions.py
from typing import Callable, TypeVar

# Aliases:
_FirstType = TypeVar('_FirstType')
_SecondType = TypeVar('_SecondType')
_ThirdType = TypeVar('_ThirdType')

def compose(
    first: Callable[[_FirstType], _SecondType],
    second: Callable[[_SecondType], _ThirdType],
) -> Callable[[_FirstType], _ThirdType]:
    """Allows typed function composition."""
    return lambda argument: second(first(argument))

Этот код принимает две функции и проверяет, что их типы совпадают, поэтому они могут быть составлены. Давайте проверим это:

# ./typesafety/test_compose.yml
- case: compose_two_wrong_functions
  main: |
    from returns.functions import compose

    def first(num: int) -> float:
        return float(num)

    def second(num: str) -> str:
        return str(num)

    reveal_type(compose(first, second))
  out: |
    main:9: error: Cannot infer type argument 2 of "compose"
    main:9: note: Revealed type is 'def (Any) -> Any'

В этом примере я изменил, как мы сделаем утверждение типа: OUT легче для многострочного выхода, чем встроенные комментарии.

Теперь у нас есть два проходящих теста:

» pytest --mypy-ini-file=setup.cfg
================================ test session starts =================================
platform darwin -- Python 3.7.4, pytest-5.1.1, py-1.8.0, pluggy-0.12.0
rootdir: /code, inifile: setup.cfg
plugins: mypy-plugins-1.0.3
collected 2 items

typesafety/test_compose.yml ..                                                 [100%]

================================= 2 passed in 2.65s ==================================

Давайте проверим еще один сложный случай.

Дополнительные настройки Mypy

Мы можем изменить Marpy Конфигурация на базах для каждого теста. Добавим некоторые новые значения в существующую конфигурацию:

- case: compose_optional_functions
  mypy_config:  # appends options for this test
    no_implicit_optional = True
  main: |
    from returns.functions import compose

    def first(num: int = None) -> float:
        return float(num)

    def second(num: float) -> str:
        return str(num)

    reveal_type(compose(first, second))
  out: |
    main:3: error: Incompatible default for argument "num" (default has type "None", argument has type "int")
    main:9: note: Revealed type is 'def (builtins.int*) -> builtins.str*'

Мы добавили no_implicit_optional Опция конфигурации, которая требует добавления явного Дополнительно [] Введите аргументы, где мы устанавливаем Нет как значение по умолчанию. И наш тест получил его из mypy_config Раздел, который добавляет варианты на базу Marpy Настройки от - Мипи-ini-файл параметр.

Пользовательские DSL

pytest-mypy-plugins Также позволяет создавать пользовательские ямл -Базирован DSL s, чтобы сделать ваш процесс тестирования проще и тестировать случаи короче.

Представь, что мы хотим иметь Review_type как ключ верхнего уровня. Он просто будет выяснить тип линии исходного кода, которая передается ему. Вот так:

-   case: reveal_type_extension_is_loaded
    main: |
      def my_function(arg: int) -> float:
          return float(arg)
    reveal_type: my_function
    out: |
      main:4: note: Revealed type is 'def (arg: builtins.int) -> builtins.float'

Давайте посмотрим на то, что нужно для достижения этого:

# reveal_type_hook.py
from pytest_mypy.item import YamlTestItem

def hook(item: YamlTestItem) -> None:
    parsed_test_data = item.parsed_test_data
    main_source = parsed_test_data['main']
    obj_to_reveal = parsed_test_data.get('reveal_type')
    if obj_to_reveal:
        for file in item.files:
            if file.path.endswith('main.py'):
                file.content = f'{main_source}\nreveal_type({obj_to_reveal})'

Что мы здесь делаем?

  1. Мы получаем исходный код из Главная: ключ
  2. Затем добавьте product_type () Звоните из Review_type: ключ

В результате у нас есть пользовательский DSL Это соответствует нашей первоначальной идее.

Бег:

» pytest --mypy-ini-file=setup.cfg --mypy-extension-hook=reveal_type_hook.hook
================================ test session starts =================================
platform darwin -- Python 3.7.4, pytest-5.1.1, py-1.8.0, pluggy-0.12.0
rootdir: /code, inifile: setup.cfg
plugins: mypy-plugins-1.0.3
collected 1 item

typesafety/test_hook.yml .                                                     [100%]

================================= 1 passed in 0.87s ==================================

Передаем новый флаг: - Гиписко-расширение-крючок Какие указывает на себя DSL реализация. И это работает отлично! Вот как можно повторно использовать большие количества кода в ямл -Бозные испытания.

Заключение

pytest-mypy-plugins Абсолют должен для людей, которые много работают с типами или Marpy плагины в Python Отказ Это упрощает процесс рефакторинга и распространения типов.

Вы можете взглянуть на реальный пример использования этих тестов в:

Поделитесь тем, что ваша употребление случаев! Мы все еще в довольно ранней стадии этого проекта, и мы хотели бы узнать, что думают наши пользователи.

Оригинал: “https://dev.to/wemake-services/testing-mypy-stubs-plugins-and-types-1b71”