Значение расширения .pyi в Python и его содержимого
Правильно ли я понимаю, что такие модули предоставляют аналог интерфейсов в Java/С#? Или все таки они что-то другое значат?
Отслеживать
426 1 1 золотой знак 4 4 серебряных знака 19 19 бронзовых знаков
задан 10 авг 2019 в 7:58
575 4 4 серебряных знака 9 9 бронзовых знаков
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
.pyi файлы — это стабы (stubs), их назначение и формат описаны в PEP 484. Эти файлы вообще никак не используются интерпретатором, их назначение — предоставлять информацию о типизации кода. К примеру, у тебя есть сторонний модуль без типизации, который не твой, редактировать его ты не можешь, а прописать типы хотелось бы:
# fizz.py def greet(who): return f'Hello '
Выход находится с помощью стаба: создаешь файл fizz.pyi , который содержит типизированную сигнатуру greet без имплементации:
# fizz.pyi def greet(who: str) -> str: .
Теперь утилиты статической типизации типа mypy или автодополнение кода в Pycharm знают, где найти типизацию для функции greet .
Правильно ли я понимаю, что такие модули предстовляют аналог интерфейса в (Java,С#) языках?
Нет, в Python интерфейсов в понимании Java/C# нет. Если в Java/C# интерфейсы являются частью программы и напрямую влияют на ее компиляцию (пример: сигнатура класса, имплементирующего интерфейс, расходится с сигнатурой интерфейса
interface Fizz < public void buzz(); >class FizzImpl implements Fizz < public int buzz() < return 42; >>
и код банально не скомпилируется). То стабы в Python вообще никак не влияют на работу программы, поскольку интерпретатор их не знает и не видит — они могут отсутствовать, содержать неверный код или вообще абракадабру; проверка типов в редакторе кода сломается, но сама программа будет работать дальше как ни в чем не бывало.
Http-stubs — поиск идеального инструмента
Всем хорошего дня, я backend-разработчик компании Uma.Tech. Сегодня я хочу рассказать, как однажды нашему отделу разработки поступила задача от отдела тестирования: локально развернуть сервис создания заглушек для http-запросов. Если интересно, как проходил поиск, сравнение разных opensource и не только инструментов, до чего мы в итоге докатились и причём тут попугай на картинке — прошу под кат.
Оговоримся сразу: мы не рекламируем ни один из приведенных ниже инструментов, а просто делимся своим опытом.
TLDR: Если нет желания читать много букв, в конце общая сводная таблица по всем инструментам.
Начало
Сначала расскажу немного о проекте, для понимания с какими проблемами сталкиваются наши инженеры из отдела тестирования.
В нашем подразделении Uma.Tech мы занимается всем, что связано с видеоконтентом. На нашей платформе работают проекты холдинга «Газпром-Медиа». Если вы используете приложение PREMIER, смотрите на сайте телеканал 2×2 или, предположим, предпочитаете онлайн-трансляции дерби «Спартак-ЦСКА» с сайта или в приложении «Матч ТВ» – значит, вы тоже пользователь нашей видеоплатформы.
Платформа в нашем понимании – это набор сервисов. Каждый сервис имеет свою архитектуру и охватывает определенный сектор работы с контентом, например, видеоотдача, работа с покупками и метаинформацией видео, конвертация, и т.д.
Подробнее, если интересно, расскажем в отдельных статьях.
Основной язык разработки для нашей команды — Python разных версий. Все сервисы общаются между собой по разным каналам передачи информации, как синхронным, так и асинхронным. Для взаимодействия в числе прочих используются webhook’и и обычные REST API.
Отдел тестирования в зависимости от задачи проверяет как платформу в целом, так и отдельные сервисы. Большое количество наших сервисов взаимодействует с внешними системами и проектами, которые поддерживаются другими командами компании. Они используют интерфейсы платформы для получения данных. Разработка часто ведётся параллельно: мы только разрабатываем API, а наши коллеги могут уже её интегрировать себе по документации. Поэтому тестировщикам не редко приходится проверять выполненную разработчиками задачу на одном сервисе, который обращается к другому, а другой еще не готов. Или требуется подменить его ответ на что-то своё в рамках тест-кейса. Именно для покрытия таких целей нам понадобился отдельный инструмент.
Исторически тестировщики пользовались бесплатным публичным сервисом webhook.site. Однако, он имеет некоторые ограничения, кроме того, он находится вне контура компании, поэтому его использование небезопасно, так как существует вероятность утечки данных во время тестирования. Поэтому нам нужен инструмент, развернутый внутри контура, причем достаточно функциональный – по возможностям приближенный к webhook.site – и гибкий, с возможностью создания кастомных путей.
В поисках идеала
Оговорюсь сразу, я тут перечислю не все инструменты, из которых мы выбирали, наверняка, какие-то просто не попались на глаза. Буду благодарен, если в комментариях вы поделитесь другими подобными сервисами. Я обязательно их проверю и добавлю интересные в статью.
Наличие необходимых функций было далеко не последним требованием. При выборе приходилось учитывать много факторов:
- Поддерживается ли инструмент на данный момент?
- Насколько просто его развернуть на серверах компании?
- Удобно ли дорабатывать под свои нужды?
- Есть ли у продукта лицензия, разрешающая свободное коммерческое применение и
существуют ли какие-то ограничения на использование и модификацию?
Напомню, наша команда пишет преимущественно на Python. Это основной язык, в котором у нас больше всего экспертизы. Поэтому удобный для разработки язык тоже является большим преимуществом для искомого нами инструмента.
webhook.site
Начнем с того, что использовали исторически — webhook.site
Это хороший и продуманный сервис, имеет достаточно функций для тестирования: здесь http-заглушки, фейковые email адреса и т.д.
У сервиса есть платная версия, расширяющая и без того небедный набор возможностей, которые легко можно найти в документации. Важный фактор «в плюс»: у webhook.site открыт исходный код на GitHub и распространяется он по лицензии MIT.
Основная страница webhook.site выглядит так:

Из минусов, которые оказались значимыми для нас: непрофильные языки для нашей команды – PHP для backend и javascript для frontend.
Судя по информации с GitHub, весь проект практически состоит из frontend, то есть кода javascript там подавляющее количество, и поэтому вносить доработки в этот сервис и поддерживать его внутри компании пришлось бы на не очень хорошо знакомом стеке, что проблемно и трудозатратно.
Кроме того, оказалось, что не все необходимые нам функции были в версии, доступной на GitHub.
Не идеально, продолжаем искать альтернативные инструменты для нашей задачи.
postbin
Следующим вариантом, который мы рассмотрели под текущую задачу стал достаточно простой сервис postbin, находящийся в публичном доступе.
Его страничка выглядит примерно так:

Распространение по лицензии ISC, ныне не очень популярной, но почти эквивалентной MIT.
У postbin доступна публично развёрнутая версия, а вот с функционалом совсем скудно — есть только http-заглушки.
Стек — чистый javascript, для frontend и backend. В общем смотрим дальше.
httplive
Ещё один инструмент, попавший в обзор это httplive.
В отличие от двух других, тут нет публичной развернутой версии, поэтому пришлось, отбросив лень, разворачивать и тестировать самостоятельно. К сожалению, далеко продвинуться не удалось. Команда для установки через go get из документации упала с ошибкой. Так что дальнейшая оценка инструмента производилась только на основе информации, которая есть в репозитории.
Скриншот интерфейса httplive мы брали из документации:

В репозитории есть видео, где можно посмотреть на основные функции.
Лицензия здесь MIT, что тоже является плюсом.
Порадовала возможность создавать свои кастомные эндпоинты и простой интерфейс. Из приятного: backend на golang, этот язык мы используем в разработках для платформы.
Однако, функциональных возможностей тут маловато. httplive не предусматривает настройку кастомных заголовков для ответов, нет возможности задать время ответа.
Отсутствие тестов – большой минус с точки зрения поддержки. Исправить эту проблему, конечно, можно, но начинать дорабатывать opensource-проект с полного покрытия его тестами не очень хотелось.
Отметили для себя этот вариант как «интересный», но поиск нужно было продолжать.
irontest
Следующий продукт — irontest.
Целый комбайн написанный на java с множеством функций: от тестирования ftp до IBM Integration Bus.
Скриншот интерфейса предоставлять не буду, потому что страниц и настроек там множество, все это есть подробно в документации.
Распространение по лицензии Apache 2.
Инструмент интересный, но всё же слишком нагруженный для нашей задачи. Кроме того, java – язык совсем не из нашего стека.
Вероятно, когда понадобится тестировать что-то кроме http, мы повторно вернёмся к irontest.
duckrails
В наших поисках дошли и до инструмента, написанного на Ruby — duckrails.
Распространяется по лицензии MIT.
Скриншотов также не будет, так как их много в readme-проекта.
По функциональности есть нужное нам: инструмент позволяет создавать http-заглушки со всем необходим. Есть и важная для нас «киллер фича» – создание своих кастомных скриптов, но либо на Ruby, либо JavaScript.
Отметим приятный и не перегруженный интерфейс, наличие свободной лицензии и, что важно, наличие тестов.
Однако, проект уже долгое время не поддерживается. На момент написания статьи последний релиз был летом 2019 года.
Наш выбор
На этом было решено заканчивать поиски и попробовать дополнить этот богатый мир еще одним инструментом. Мы перешли к разработке велосипеда собственного производства.
В качестве стека были выбраны максимально простые и знакомые технологии: Python и web-фреймворк Django. С фронтом возиться не хотелось, так как наша команда это backend-разработка, поэтому был найден визуально очень хороший плагин на административную панель — simpleui.
Благодаря выбору многофункционального швейцарского ножа Django, велосипед был собран всего за несколько часов с минимальным количеством кода. А как известно, меньше кода — меньше багов.
И теперь, ко всеобщему вниманию, ещё один инструмент в сфере интеграционного тестирования — Parrot!
Parrot
Мы решили, что наш сервис должен быть похож на попугая: быть милым, симпатичным, ручным, домашним и четко повторять ответы, которым его обучили.
Проект распространяется по лицензии Apache 2.
Свободен для коммерческого использования, имеет репозиторий на GitHub со множеством автопроверок кода: автотестами, статическими анализаторами, линтингом и дружелюбен для контрибьютинга.
Написан на языке Python третьей версии.
Запакован в docker для простого развертывания на сервере.
Основной функционал — это создание http-заглушек, вечное хранение лога (в дальнейшем сделаем настройки автоочистки), возможность использования любых путей для заглушек.
Вот так, на данный момент, выглядит наш маленький попугай:

Интерфейс хоть и изменён визуально, но не сильно отличается от стандартного интерфейса Django.
В разделе Authentication and Authorization можно управлять пользователями, группами и правами для них.
Основной раздел — HTTP Stubs, в нём можно создавать заглушки и просматривать логи запросов. Из интересного: для URL можно использовать regex-выражение, остальное плюс-минус стандартно, как в прочих инструментах.
После развертывания инструмента через docker, либо любым другим стандартным для Django способом, необходимо выполнить джанговские менеджмент-команды: применить миграции и создать суперпользователя. После этого можно будет зайти в стандартную админку Django под этим пользователем.
Сейчас функционал уступает некоторым рассмотренным инструментам, но он развивается и дополняется. Будем рады открытым запросам на необходимые пользователям функции.
В ближайших обновлениях: шаблонизация ответа, выполнение кастомных скриптов на запрос и полноценное проксирование к внешнему сервису, с обработкой полученного ответа, такой своего рода Man In The Middle.
Общее сравнение
| webhook.site | postbin | httplive | irontest | duckrails | parrot | |
|---|---|---|---|---|---|---|
| Язык бекенда | php | javascript | golang | java | ruby | python |
| Лицензия | MIT | ISC | MIT | Apache2 | MIT | Apache2 |
| Коммит меньше месяца назад | — | — | — | + | — | + |
| Тестирование email-сообщений | + | — | — | — | — | — |
| Настройка заголовков ответа | + | — | — | + | + | + |
| Настройка кода ответа | + | — | — | + | + | + |
| Настройка тела ответа | + | — | + | + | + | + |
| Установка задержки для ответа | + | — | — | — | — | + |
| Шаблонизация ответа | + | — | — | — | — | — |
| Выполнение кастомных скриптов | — | — | — | — | + | — |
| Настройка пути для http-заглушки | — | — | + | + | + | + |
| Regex-выражение для пути | — | — | — | — | — | + |
| Режим Man In The Middle | — | — | — | — | — | — |
Что в итоге
Приложением стали активно пользоваться наши тестировщики, а после его презентации на внутреннем демо, Parrot заинтересовал и другие команды из компании Uma.Tech.
Увидев, что инструмент удобен нам и интересен другим, мы решили поделиться им со всем сообществом. Будем очень рады обратной связи и вашим pull requests.
Нам эта история показала, что иногда лучше изобрести свой велосипед, чем ехать на чужом.
- webhook
- тестирование веб-приложений
- велосипедостроение
Stub файлы и работа с нетипизированными библиотеками
Важно понимать, что type hinting работает не только для аргументов функций и возвращаемых значений. Мы можем просто создать переменную и указать ей тип:
book: str = "Тополек мой в красной косынке"
В таком сценарии это избыточно — IDE и статический анализатор кода и так видят, что в переменной book хранится значение типа str . Однако в таком сценарии:
book: str = find_book_in_library("Тополек мой в красной косынке")
функция поиска книги find_book_in_library может быть не нашей функцией, а функцией какой-то внешней библиотеки, которая не использует подсказки типов. То есть для функции может быть не проставлен тип возвращаемого значения. Чтобы IDE и статический анализатор знали, что тип данных, который будет храниться в book , это именно str , можно таким образом подсказать инструментам о верном типе. Иногда это бывает очень полезно, когда библиотека не использует подсказки типов, а возвращаемый тип данных какой-то сложный и мы хотим, чтобы IDE и mypy нам помогали анализировать наш код и типы.
В то же время в Python существует механизм так называемых стаб-файлов, которые позволяют типизировать в том числе внешние библиотеки. Например, для django есть пакет в pip, который называется django-stubs . О стаб-файлах есть видео на канале Диджитализируй!.
Если вы используете нетипизированную библиотеку — можно поискать готовые стаб-файлы для неё, чтобы воспользоваться преимуществами типизированного Python.
Stub files#
A stub file is a file containing a skeleton of the public interface of that Python module, including classes, variables, functions – and most importantly, their types. Mypy uses stub files stored in the typeshed repository to determine the types of standard library and third-party library functions, classes, and other definitions. You can also create your own stubs that will be used to type check your code.
Creating a stub#
- Write a stub file for the library (or an arbitrary module) and store it as a .pyi file in the same directory as the library module.
- Alternatively, put your stubs ( .pyi files) in a directory reserved for stubs (e.g., myproject/stubs ). In this case you have to set the environment variable MYPYPATH to refer to the directory. For example:
$ export MYPYPATH=~/work/myproject/stubs
Use the normal Python file name conventions for modules, e.g. csv.pyi for module csv . Use a subdirectory with __init__.pyi for packages. Note that PEP 561 stub-only packages must be installed, and may not be pointed at through the MYPYPATH (see PEP 561 support ).
If a directory contains both a .py and a .pyi file for the same module, the .pyi file takes precedence. This way you can easily add annotations for a module even if you don’t want to modify the source code. This can be useful, for example, if you use 3rd party open source libraries in your program (and there are no stubs in typeshed yet).
Now you can access the module in mypy programs and type check code that uses the library. If you write a stub for a library module, consider making it available for other programmers that use mypy by contributing it back to the typeshed repo.
Mypy also ships with two tools for making it easier to create and maintain stubs: Automatic stub generation (stubgen) and Automatic stub testing (stubtest) .
The following sections explain the kinds of type annotations you can use in your programs and stub files.
You may be tempted to point MYPYPATH to the standard library or to the site-packages directory where your 3rd party packages are installed. This is almost always a bad idea – you will likely get tons of error messages about code you didn’t write and that mypy can’t analyze all that well yet, and in the worst case scenario mypy may crash due to some construct in a 3rd party package that it didn’t expect.
Stub file syntax#
Stub files are written in normal Python syntax, but generally leaving out runtime logic like variable initializers, function bodies, and default arguments.
If it is not possible to completely leave out some piece of runtime logic, the recommended convention is to replace or elide them with ellipsis expressions ( . ). Each ellipsis below is literally written in the stub file as three dots:
# Variables with annotations do not need to be assigned a value. # So by convention, we omit them in the stub file. x: int # Function bodies cannot be completely removed. By convention, # we replace them with `. ` instead of the `pass` statement. def func_1(code: str) -> int: . # We can do the same with default arguments. def func_2(a: int, b: int = . ) -> int: .
The ellipsis . is also used with a different meaning in callable types and tuple types .
Using stub file syntax at runtime#
You may also occasionally need to elide actual logic in regular Python code – for example, when writing methods in overload variants or custom protocols .
The recommended style is to use ellipses to do so, just like in stub files. It is also considered stylistically acceptable to throw a NotImplementedError in cases where the user of the code may accidentally call functions with no actual logic.
You can also elide default arguments as long as the function body also contains no runtime logic: the function body only contains a single ellipsis, the pass statement, or a raise NotImplementedError() . It is also acceptable for the function body to contain a docstring. For example:
from typing_extensions import Protocol class Resource(Protocol): def ok_1(self, foo: list[str] = . ) -> None: . def ok_2(self, foo: list[str] = . ) -> None: raise NotImplementedError() def ok_3(self, foo: list[str] = . ) -> None: """Some docstring""" pass # Error: Incompatible default for argument "foo" (default has # type "ellipsis", argument has type "list[str]") def not_ok(self, foo: list[str] = . ) -> None: print(foo)