Доброго времени суток всем.
На сегодняшний день Python является одним из наиболее используемых языков в сфере создания не только непосредственно программных продуктов, но также обеспечения их инфраструктуры. Вследствие этого многим девопсам, по их воле или против оной, пришлось учить новый язык для последующего использования в качестве дополнения к старым добрым Bash-скриптам. Однако Bash и Python исповедуют различные подходы к написанию кода и имеют определенные особенности, в виду чего портирование Bash-скриптов на «змеиный язык» иногда оказывается ёмкой и далеко не тривиальной задачей.
Чтобы упростить жизнь девопсам, создано и продолжает создаваться много полезных библиотек и утилит на Python. Данная статья описывает сразу две новых библиотеки, созданные автором сего поста — smart-env и python-shell — и призванные избавить девопса от необходимости уделять много внимания тонкостям работы с Python, оставляя простор для более интересных задач. Сфера деятельности библиотек — переменные окружения и запуск внешних утилит.
Кого заинтересовало, прошу под кат.
Новые «велосипеды»?
Казалось бы, зачем создавать новые пакеты для достаточно обыденных операций? Что мешает использовать напрямую os.environ и subprocess.<метод или класс на ваш вкус>?
Доказательства в пользу каждой из библиотек приведу отдельно.
Библиотека smart-env
Перед тем, как писать собственное детище, полезно полезть в Интернет и поискать готовые решения. Конечно, есть риск не найти то, что нужно, но это, скорее, «страховой случай». Как правило, подход срабатывает и экономит кучу времени и сил.
По результатам поиска было выявлено следующее:
- есть пакеты, действительно оборачивающие вызовы к os.environ, однако при этом требующие кучу отвлекающих действий (создание экземпляра класса, спец-параметры в вызовах и пр.);
- есть неплохие пакеты, которые, однако, жестко завязаны на определенную экосистему (в основном, на веб-фреймворки вроде Django) и потому без напильника совсем не универсальные;
- есть редкие попытки сделать что-то новое. Например, добавить типизацию и явно парсить значения переменных путем вызова методов вида
get_<typename>(var_name)
Или вот еще одно решение, которое, однако, не поддерживает ныне опальный Python 2 (на котором, несмотря на официальный R.I.P., все еще остаются горы написанного кода и целые экосистемы);
- есть школьно-студенческие поделки, вообще непонятно зачем оказавшиеся в апстримном PyPI и только создающие проблемы с именованием новых пакетов (в частности, название «smart-env» — вынужденная мера).
И этот список можно продолжать долго. Однако и приведенных выше пунктов хватило, чтобы загореться идеей сделать нечто удобное и универсальное.
Требования, которые ставились перед написанием smart-env:
- Максимально простая схема использования
- Легко конфигурируемая поддержка типизации данных
- Совместимость с Python 2.7
- Хорошее покрытие кода тестами
В конечном итоге, все это удалось реализовать. Вот пример использования:
from smart_env import ENV
print(ENV.HOME) # Equals print(os.environ['HOME'])
# assuming you set env variable MYVAR to "True"
ENV.enable_automatic_type_cast()
my_var = ENV.MY_VAR # Equals boolean True
ENV.NEW_VAR = 100 # Sets a new environment variable
Как видно из примера, для работы с новым классом достаточно его импортировать (создавать экземпляр не нужно — минус лишнее действие). Доступ к любой переменной окружения достигается путем обращения к ней как к переменной класса ENV, что, фактически, делает этот класс интуитивно понятной обёрткой нативного системного окружения, параллельно превращая его в возможный вариант объекта конфигурации практически любой системы (похожий подход, например, достигается в Django, только там конфигурационным объектом выступает непосредственно модуль/пакет settings).
Включение/выключение режима поддержки автоматической типизации достигается использованием двух методов — enable_automatic_type_cast() и disable_automatic_type_cast(). Это может быть удобно, если в переменной окружения лежит сериализованный JSON-подобный объект или даже просто булевая константа (явное прописывание переменной DEBUG в Django путем сравнения переменной окружения с «допустимыми» строками — один из часто встречающихся случаев). Но теперь нет нужды явно конвертировать строки — большая часть необходимых действий уже заложена в недрах библиотеки и только ждет сигнала к действию. :) В целом же типизация работает прозрачно и поддерживает почти все имеющиеся встроенные типы данных (не тестировались frozenset, complex и bytes).
Требование поддержки Python 2 было реализовано практически без жертв (отказ от typing и некоторых «сахарных конфеток» последних версий Python 3), в частности, благодаря вездесущему six (для решения проблем использования метаклассов).
Но есть и немного ограничений:
- Поддержка Python 3 подразумевает версию 3.5 и выше (их наличие в вашем проекте — результат либо лени, либо отсутствия необходимости в улучшениях, т.к. сложно придумать объективную причину, почему вы до сих пор сидите на 3.4);
- В Python 2.7 библиотека не поддерживает десериализацию литералов множеств. Описание тут. Но, если кто-нибудь захочет реализовать — welcome:);
Библиотека также исповедует механизм исключений на случай ошибок парсинга. Если строку не удалось распознать ни одним из имеющихся анализаторов, значение остается строковым (скорее, из соображений удобства и обратной совместимости с привычной логикой работы переменных в Bash).
Библиотека python-shell
Теперь расскажу о второй библиотеке (описание недостатков имеющихся аналогов опущу — оно похоже на описанное для smart-env. Аналоги — тут и тут).
В целом, идея реализации и требования к ней аналогичны описанным для smart-env, что видно из примера:
from python_shell import Shell
Shell.ls('-l', '$HOME') # Equals "ls -l $HOME"
command = Shell.whoami() # Equals "whoami"
print(command.output) # prints your current user name
print(command.command) # prints "whoami"
print(command.return_code) # prints "0"
print(command.arguments) # prints ""
Shell.mkdir('-p', '/tmp/new_folder') # makes a new folder
Идея такова:
- Единый класс, олицетворяющий Bash в мире Python;
- Каждая Bash-команда вызывается как функция класса Shell;
- Параметры вызова каждой функции далее пробрасываются в вызов соответствующей команды Bash;
- Каждая команда выполняется «здесь и сейчас» в момент ее вызова, т.е. работает синхронный подход;
- есть возможность получить доступ к выхлопу команды в stdout, а также код ее возврата;
- Если команда отсутствует в системе — бросается исключение.
Как и в случае со smart-env, обеспечена поддержка Python 2 (правда, жертвенной крови потребовалось немного больше) и отсутствует поддержка Python 3.0-3.4.
Планы по развитию библиотек
Использовать библиотеки можно уже сейчас: обе выложены на официальный PyPI. Исходники доступны на Github (см. ниже).
Обе библиотеки будут развиваться с учетом фидбека, собранного от заинтересовавшихся. И, если в smart-env, может, и сложно придумать разнообразие новых фич, то в python-shell точно есть еще что добавить:
- поддержка неблокирующих вызовов;
- возможность интерактивного общения с командой (работа с stdin);
- добавление новых свойств (например, property для получения выхлопа из stderr);
- реализация каталога доступных команд (для использования с функцией dir());
- и т.д.
Ссылки
- Библиотека smart-env: Github и PyPI
- Библиотека python-shell: Github и PyPI
- Телеграм-канал обновлений библиотек
UPD 23.02.2020:
* Репозитории перенесены, соответствующие ссылки обновлены
* Версия python-shell==1.0.1 готовится к выходу 29.02.2020. Среди изменений — поддержка автокомплита команд и команды dir(Shell), запуск команд с Python-невалидным идентификатором, исправление багов.
Автор: Alex Sokolov