Первоначально опубликовано в моем блоге : https://sobolevn.me/2019/08/testing-mypy-types
Вы когда-нибудь пытались:
- Создайте сложные универсальные типы в вашем собственном проекте?
- Напишите Распространенные заглушки Для вашей библиотеки?
- Создать Пользовательский
Marpy
плагин ? - Написать пользовательский тип проверки?
Если вы попытаетесь сделать любой из них, вы скоро узнаете, что вам нужно проверить ваши типы. Чего ждать? Позвольте мне подробно объяснить этот парадокс.
Первые тесты для типов в 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})'
Что мы здесь делаем?
- Мы получаем исходный код из
Главная:
ключ - Затем добавьте
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”