<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://pusto.org/post/feed.xml</id>
    <link href="https://pusto.org"/>
    <link href="https://pusto.org/post/feed.xml" ref="self"/>
    <title>None</title>
    <updated>2025-07-29T12:26:34.041134+03:00</updated>
    <author>
        <name>naspeh</name>
    </author>

    <entry xml:base="https://pusto.org/post/feed.xml">
        <title type="text">Встречайте Mailr</title>
        <link href="https://pusto.org/post/mailr-intro/"/>
        <id>https://pusto.org/post/mailr-intro/</id>
        <updated>2014-04-09T08:00:00+03:00</updated>
        <author>
            <name>naspeh</name>
        </author>
        <content type="html"><![CDATA[ <p>Уже лет семь использую gmail, в этот период несколько раз пытался уйти от него, но всегда
возвращался. И что интересно, когда смотрел очередную альтернативу, то первое требование
было – цепочки писем по удобству сопоставимые с gmail. В gmail сделаны идеальные для меня
цепочки писем (ну или почти идеальные).</p>
<p>Пару-тройку месяцев назад мне нужна была идея для проекта и в итоге решил написать webmail
клиент с gmail-подобными цепочками писем. Это то чего мне не хватает, и чем бы пользовался
регулярно на ежедневной основе.</p>
<!-- MORE -->
<p>Mailr на ранней стадии разработки, еще <strong>очень многое</strong> предстоит сделать.</p>
<p>Сейчас есть <a class="reference external" href="http://demo.pusto.org">демо концепта</a>, которое довольно быстро работает. Из того что сделано
большая часть не очень видна, так как она связана с IMAP общением, отложенной
синхронизацией, парсингом писем, а видимая часть – немного рабочего интерфейса. Можно
послать письмо на <strong>demo[at]pusto.org</strong> и оно появится в Inbox.</p>
<p>Рабочее название проекта <strong>Mailr</strong>. Код на <a class="reference external" href="https://github.com/naspeh/mailr">github.</a></p>
<img alt="/mailur/intro/screenshot-s.png" src="https://pusto.org/mailur/intro/screenshot-s.png"/>
<div class="section" id="section-1">
<h1>Мое видение первой версии</h1>
<p>Mailr будет иметь быстрый и удобный веб интерфейс, которым будет удобно пользоваться на
небольшом экране ноутбука, на большом мониторе и на iPad Mini, все эти девайсы у меня есть
и хочется иметь единый настраиваемый интерфейс для них.</p>
<p>Mailr будет иметь gmail-совместимый режим через IMAP, чтоб можно было вернуться в любое
время на gmail. Так как пока версия под мобильные телефоны не планируется, то этот режим
будет тоже полезен, можно будет параллельно коннектиться к gmail привычным мобильным
клиентом, если нужно. Кроме прочего с gmail за спиной проще начинать разработку и
сконцентрироваться на удобном интерфейсе.</p>
<p>Многие функции из gmail нужно реализовать: удобные цепочки писем, метки, быстрый поиск,
фильтры для сортировки входящей почты, хороший механизм схлопывания цитируемых писем,
горячие клавиши, поддержка SSL…</p>
<p>Также будут дополнительные функции.</p>
<p><strong>Объединение цепочек писем.</strong> Google хорошо находит соответствие писем и цепочек, но
иногда его алгоритмы не работают:</p>
<img alt="/mailur/intro/unmatched-thread.png" src="https://pusto.org/mailur/intro/unmatched-thread.png"/>
<p>Возможность вручную объединить цепочки – это выход в таких ситуациях.</p>
<p><strong>Markdown для написания писем.</strong> Мне нравится <a class="reference external" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a> и <a class="reference external" href="http://en.wikipedia.org/wiki/ReStructuredText">reStructuredText</a> и мне бы
хотелось писать письма используя эти текстовые языки разметки, после конвертации они
выдают отличный для чтения HTML. Текущий редактор писем в gmail для меня очень неудобный.</p>
<p><strong>Две панели.</strong> Это моя любимая функция :), две панели видно на скриншоте. Давно уже
использую двухпанельный режим в своем текстовом редакторе VIM и мне уже не комфортно в
однопанельных редакторах. Вторая панель расширяет обычно контекст, когда работаешь в
первой. В будущем будет возможность отключить этот режим.</p>
<p><strong>Настраеваемый интерфейс.</strong> Как говорил выше, мне нужен удобный интерфейс на
разнообразных разрешениях экрана, темы и настройки интерфейса будут решать эту задачу.</p>
<p><strong>Вся почта в одном табе.</strong> Я использую Chrome и мне нравится, что он открывает
“Настройки”, “Скачанные файлы”, “Расширения” в новых вкладках, а не окнах (раньше
использовал Firefox – он многое открывает в новых окнах). У меня весь серфинг интернета
живет в одном окне браузера, а вся почта, в идеале, хочется, чтоб жила в одной вкладке
(включая все аккаунты)</p>
<p><strong>Простой backup.</strong> Это важно для Open Source продукта, чтоб была возможность взять все
данные (аккаунты, фильтры, цепочки) и перенести с одной инсталяции на свою локальную или
на сервер своего проверенного друга-гика.</p>
</div>
<div class="section" id="section-2">
<h1>Следующие версии</h1>
<p>Когда можно будет использовать Mailr c gmail в качестве IMAP сервера, то дальше мне
хочется уйти все таки от gmail и использовать свой email адрес. И, скорее всего, следующим
шагом будет интеграция с <a class="reference external" href="http://www.mailgun.com/">Mailgun</a>. Поднять свой правильный почтовый сервер с
антиспам-фильтром – дело не самое легкое, с Mailgun будет проще, тем более они не хранят
письма у себя.</p>
<p>Дальше много мыслей для продолжения: поддержка других IMAP серверов, множественные
аккаунты в одном табе, PGP шифрование, списки рассылок для друзей…</p>
</div>
<div class="section" id="section-3">
<h1>Напоследок</h1>
<p>Хочется в этом проекте использовать минимум зависимостей и непереусложнить с кодом, ведь
потом все нужно будет поддерживать.</p>
<dl class="docutils">
<dt>Стек технологий:</dt>
<dd><ul class="first last simple">
<li>Python 3, werkzeug, jinja2, sqlalchemy, lxml;</li>
<li>PostgreSQL с его крутыми типами данных и не только;</li>
<li>lessjs, jquery на фронтенде.</li>
</ul>
</dd>
</dl>
<p>Да, только jquery – из-за архитектурного решения. Мне больше нравится писать Python код,
а JavaScript хочется очень минимизировать, поэтому вместо модного REST и рендеринга на
стороне клиента мне захотелось генерировать семантичный HTML на стороне сервера. Это
полезно, например, для iPad Mini, в нем процессор слабее и памяти меньше, чем обычно на
ноутбуках и десктопах. В будущем эта ситуация может измениться.</p>
<p>В этом проекте еще нужно многое придумать, многое реализовать, многое оптимизировать. В
последнее время я занимался им очень интенсивно, но мой отпуск заканчивается и нужно
возвращаться к работе, то есть времени на проект будет намного меньше. Очень хочется его
довести до стадии, чтоб заменить наконец gmail :).</p>
<p>Open Source – это круто и мне всегда хотелось отдать дань этому сообществу. Если
задуманное мной в этом email клиенте удастся реализовать и получится хороший продукт, то
это будет отличный вклад.</p>
</div>
<div class="section" id="p-s">
<h1>P.S.</h1>
<p>Статья была на <strong>хабре</strong>, но им не понравилась ссылка на демо %).</p>
<dl class="docutils">
<dt>Примеры с цепочками писем, которые зацепили:</dt>
<dd><ul class="first last simple">
<li><a class="reference external" href="http://www.mozilla.org/thunderbird/">Thunderbird</a> и <a class="reference external" href="https://addons.mozilla.org/thunderbird/addon/gmail-conversation-view/">Thunderbird Conversations</a>;</li>
<li><a class="reference external" href="http://www.yorba.org/geary">Geary</a> – gmail-подобный десктопный клиент;</li>
<li><a class="reference external" href="https://fastmail.fm">fastmail.fm</a> – платная почта, которая какое-то время была под Opera Software.</li>
</ul>
</dd>
</dl>
<p>И, конечно, смотрел на <a class="reference external" href="https://www.mailpile.is/">mailpile.is</a>, но они пошли странным путем.</p>
</div>
 ]]></content>
    </entry>
    <entry xml:base="https://pusto.org/post/feed.xml">
        <title type="text">Arch Linux. Система бекапов</title>
        <link href="https://pusto.org/post/archlinux-backup/"/>
        <id>https://pusto.org/post/archlinux-backup/</id>
        <updated>2013-12-29T08:00:00+02:00</updated>
        <author>
            <name>naspeh</name>
        </author>
        <content type="html"><![CDATA[ <div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">Слово <strong>бекап</strong> (англ. backup) использую в тексте потому, что оно короче и привычнее
русского аналога “резервная копия”.</p>
</div>
<p>В июле купил SSD диск для ноута и решил, что нужно поставить Arch Linux кошерно с нуля. До
этого у меня проработала инсталяция около года без переустановки, казусы случались, но их
удавалось решить на существующей системе. Ну и хотелось этот новенький чистенький Arch
забекапить и вообще сделать хорошую регулярную систему бекапов для моих рабочих машин.</p>
<!-- MORE -->
<p>До этого уже занимался бекапами. В одной из версий использовал <a class="reference external" href="http://duplicity.nongnu.org/">duplicity</a>. Последняя была
основана на <tt class="docutils literal">rsync</tt>, а дельты упаковывались в <tt class="docutils literal">tar</tt>, на Debian Testing у меня эта
система работала регулярно по крону, а в Arch Linux использовал нерегулярно вручную,
хотелось ее переделать.</p>
<dl class="docutils">
<dt>К системе бекапов у меня есть ряд требований:</dt>
<dd><ul class="first last simple">
<li><tt class="docutils literal">latest:</tt> набор самых важных файлов (<tt class="docutils literal">/home, /etc,</tt> список установленных пакетов и
рабочие директории), доступ к которым должен быть очень легким и естественным, то есть
обычные команды типа: <tt class="docutils literal">cp, ls, cat</tt> с этими фалами должны просто работать;</li>
<li><tt class="docutils literal">delta:</tt> набор изменений, их может быть много, доступ к ним может быть немного
сложнее чем к <tt class="docutils literal">latest</tt> (допускается <tt class="docutils literal">tar</tt>), но желательно чтоб дельт плодилось не
очень много, так как рассматривать их после месяца работы бекапов не очень хотелось (в
последней версии бекапов у меня был этот недостаток);</li>
<li><tt class="docutils literal">full:</tt> почти все файлы системы, чтоб делать полный бекап с работающей системы,
нужно  предусмотрительно использовать <tt class="docutils literal">Btrfs</tt> или <tt class="docutils literal">LVM</tt>, которые могут делать
снепшоты (англ. snapshots) файловой системы.</li>
</ul>
</dd>
</dl>
<p>Перед тем как делать систему бекапов, опять стал смотреть на заточенные для этого
инструменты. Внимательно смотрел на <a class="reference external" href="http://www.miek.nl/projects/rdup/">rdup</a>,  <a class="reference external" href="https://github.com/bup/bup">bup</a> и <a class="reference external" href="http://liw.fi/obnam/">obnam</a>. Последние две используют
свои репозитории, в которых реализуют дедупликацию файлов. У <tt class="docutils literal">bup</tt> формат совместимый с
<tt class="docutils literal">git</tt>. Смотрел на них потому, что можно было отказаться от разделения на <tt class="docutils literal">latest</tt> и
<tt class="docutils literal">delta</tt>, ведь они бы сами все красиво раскладывали. Проблема с нестандартными форматами
репозиториев такая же как и для архивов, то что нельзя просто посмотреть <tt class="docutils literal">ls</tt>. У <tt class="docutils literal">bup</tt>
есть даже своя FUSE файловая система, но в ней не видны атрибуты файлов (можно посмотреть
список файлов, но дату изменения или владельца файла посмотреть нельзя). <tt class="docutils literal">rdup</tt> можно
было бы использовать, но все таки вернулся к старому способу с <tt class="docutils literal">rsync</tt>. В свое время
отказался от <tt class="docutils literal">duplicity</tt> и от архивов в целом опять же из-за простоты доступа к файлам.
Хоть с обычными архивами умеет прозрачно работать <a class="reference external" href="http://www.midnight-commander.org/">mc</a>, но делать инкрементный бекап на
архиве невозможно (ну или слишком сложно), а разархивирование нескольких гигов данных даже
без компрессии - операция не самая быстрая.</p>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">От шифрования и архивов отказался, потому что не храню бекапы в интернете.</p>
</div>
<p>К схеме с <tt class="docutils literal">latest &amp; delta</tt> пришел потому, что <strong>реально</strong> мне важно было сохранить
последнее состояние системы, а дельта нужна была для возможности посмотреть на старые
файлы, которые случайно удалил, или посмотреть на файлы с какой-то важной информацией,
которой нет в последней версии. Мне чаще нужно посмотреть что изменилось в системе, и
дельта тут тоже очень кстати, ведь если сохранять снимок всех данных, то нужно производить
лишние действия, чтоб получить дельту . Еще заморозкой дельты решаю вопрос данных, которые
хорошо бы сохранить и которые хочется почистить в текущей файловой системе (то есть удаляю
файлы, а потом архивирую дельту с определенной меткой). В общем схема с <tt class="docutils literal">latest &amp; delta</tt>
мне очень подходит.</p>
<dl class="docutils">
<dt>Итак, что имеется на входе:</dt>
<dd><ul class="first last simple">
<li>ноут ThinkPad X230 с SSD 128GB и любимым Arch Linux;</li>
<li>неттоп Zotac ZBOX ID83-BE с HDD 500GB (на него еще нужно накатить новый Arch);</li>
<li>внешний винчестер на 1TB с USB 3.0;</li>
<li>домашний WI-FI.</li>
</ul>
</dd>
<dt>Схема следующая:</dt>
<dd><ul class="first last simple">
<li>неттоп по имени <strong>Box</strong>, на нем делается регулярный бекап текущей системы локально в
<tt class="docutils literal"><span class="pre">"box:/backups/box"</span></tt>, плюс к нему подключен постоянно внешний винт по имени <strong>Sea</strong>
через USB 3.0 и на него идет регулярный слив <tt class="docutils literal"><span class="pre">"box:/backups"</span></tt> в <tt class="docutils literal"><span class="pre">"sea:/backups"</span></tt>;</li>
<li>ноут по имени <strong>Pad</strong>, на нем делается регулярный бекап, а если он находится в
домашней сети, то идет слив на неттоп в <tt class="docutils literal"><span class="pre">"box:/backups/pad"</span></tt>.</li>
</ul>
</dd>
</dl>
<p>Схема вроде ясная, сделано так, чтоб к ноуту не нужно было ничего подключать для
сохранения копии бекапов, а так как в домашней сети нахожусь очень часто, то достаточно
чтоб был включен неттоп для получения двух свежих копий.</p>
<p>На внешний винчестер ставлю тоже Arch (называю его LiveHard, по аналогии с LiveCD), чтоб
иметь под рукой загрузочный винт с привычно настроенным окружением. Кроме того у меня есть
еще один внешний винчестер (более старый с USB 2.0), из него делаю тоже LiveHard, чтоб
таскать с собой, ведь первый постоянно висит на неттопе.</p>
<div class="admonition note" id="script">
<p class="first admonition-title">Note</p>
<dl class="last docutils">
<dt>Все перечисленные ниже команды - это суть выполняемых команд, в реальности использую:</dt>
<dd><ul class="first last simple">
<li><a class="reference external" href="https://github.com/naspeh/dotfiles/blob/master/bin/pkglist">pkglist</a> для генерации списка пакетов для бекапа и не только;</li>
<li><a class="reference external" href="https://github.com/naspeh/dotfiles/blob/master/bin/backup">backup</a> все действия над бекапами.</li>
</ul>
</dd>
</dl>
</div>
<p>Для бекапа системы в первую очередь нужно сохранить правильный список пакетов. Обычный
<tt class="docutils literal">"pacman <span class="pre">-Q"</span></tt> не подходит, потому что мне нужны пакеты установленные мной и не включая
две базовые группы пакетов base и base-dev, потому что обе группы обычно ставятся в начале
установки нового Arch. В принципе, с pacman это делается несложно:</p>
<pre class="literal-block">
$ comm -23 &lt;(pacman -Qeq) &lt;(pacman -Qgq base base-devel | sort) &gt; /backup/pkglist.txt
</pre>
<p>Полезность этого списка не только в целях бекапа, но и просто посмотреть пакеты, которые
не используются и можно удалить, чтоб система была максимально чистой.</p>
<p>Дальше нужно сохранить все важные файлы:</p>
<pre class="literal-block">
$ rsync -aAXHvh --delete \
&gt;   --backup --backup-dir=/backup/delta/ \
&gt;   -f '+ /boot/' -f '+ /etc/' -f '+ /home/' -f '- /**/*.pyc' -f '- /*' \
&gt;   / /backup/latest/
</pre>
<p>Появляется <tt class="docutils literal">latest &amp; delta</tt>, при этом <tt class="docutils literal">delta</tt> у меня не сохраняется каждый раз, а
накапливается в одной папке. А tar архив создается отдельной командой, которая запускается
реже, чем регулярный бекап:</p>
<pre class="literal-block">
$ cd /backup &amp;&amp; tar -cvf $(date '+%Y-%m-%d').tar --directory=delta .
</pre>
<p>Или с меткой о каком-то намеренном удалении:</p>
<pre class="literal-block">
$ cd /backup &amp;&amp; tar -cvf $(date '+%Y-%m-%d')--cleanup-home.tar --directory=delta .
</pre>
<p>Потом все перекидываем по SSH на неттоп:</p>
<pre class="literal-block">
$ rsync -aAXHvhx --delete /backup/ box:/backups/pad/
</pre>
<p>В целом это уже рабочая система бекапов. Вначале не думал про полный бекап и новую систему
установил без LVM. Но потом решил, что полный бекап нужен, и выбрал снепшоты LVM, потому
что Btrfs никогда не использовал и репутация у этой файловой системы не самая лучшая. Я
скопировал через <tt class="docutils literal">dd</tt> необходимые разделы и записал их на внешний винт, чтоб сделать
новую разметку диска для LVM. Когда закончил с новой разметкой, то обнаружил, что главный
раздел у меня не записался полностью o_O, не хватило места на винте, а на сообщение не
обратил внимание. Но не все так плохо, у меня ведь был бекап, который как раз для подобных
неожиданных случаев и создан.</p>
<p><strong>Немного про разметку диска.</strong> Раньше любил выносить home на отдельный раздел, но по сути
home у меня - это набор конфигурационных файлов, а downloads, music и рабочие директории
выношу на отдельный большой раздел. В итоге схема следующая:</p>
<pre class="literal-block">
- /dev/sda1 EFI System /boot 100-200 MB
+ /dev/sda2 Linux LVM 128GB
  - /dev/pad/root /root 30GB
  - /dev/pad/arch /arch 50GB
  - остальное место для снепшотов или для увеличения разделов
</pre>
<p>Отдельный раздел для загрузки обязателен для <a class="reference external" href="https://wiki.archlinux.org/index.php/UEFI">UEFI</a> загрузчика, а остальное отдается для
LVM. Так как за ноутбуком обычно работаю, то 50GB на <tt class="docutils literal">/arch</tt> раздел мне достаточно. Для
мультимедия у меня есть неттоп с хорошим большим монитором и хорошими колонками.</p>
<p>Режим востановления из неполного бекапа следующий:</p>
<pre class="literal-block">
# гружусь с LiveHard
$ mount /dev/pad/root /mnt
$ mount -L P-BOOT /mnt/boot

# ставлю базовою систему
$ pacstrap -c /mnt base base-devel
$ cp /etc/pacman.conf /mnt/etc/

# переключаюсь на новый Arch и ставлю все нужные пакеты
$ arch-chroot /mnt
$ pacman -S $(cat /backups/pad/pkglist.txt)
$ yaourt -S $(cat /backups/pad/pkgaur.txt) --noconfirm

# восстанавливаю все важные файлы
$ rsync -aAXHvh /backups/pad/latest/ /

# выхожу из chroot, перегружаюсь
</pre>
<p>Система готова и находится в полном соответствии со старой. В принципе, шагов не много, но
было бы проще и быстрее с полным бекапом.</p>
<p><strong>Полный бекап</strong> делаю через LVM снепшот плюс опять же rsync:</p>
<pre class="literal-block">
$ lvcreate --size 10G --snapshot --name snap /dev/pad/root \
&gt; &amp;&amp; mount /dev/pad/snap /backups/mnt \
&gt; &amp;&amp; rsync -aAXHvhyx \
&gt;   --exclude="{/dev/*,/proc/*,/sys/*,/tmp/*,/run/*,/mnt/*,/media/*,/lost+found}"
&gt; &amp;&amp; umount /backups/mnt
&gt; &amp;&amp; lvremove -f /dev/pad/snap
</pre>
<p><strong>Для регулярного запуска бекапов использую асинхронный cron.</strong> Асинхронный потому, что
хотя и работаю за ноутбуком регулярно, но работаю в разное время. <a class="reference external" href="http://fcron.free.fr">fcron</a> может запускать
команды в зависимости от времени работы ноутбука, например каждые шесть часов работы.
Обычный cron рассчитан на то, что машина все время работает.</p>
<p>Теперь если вспомнить про мой <a class="reference external" href="https://pusto.org/post/archlinux-backup/#script">backup скрипт</a> и добавить, что он был написан с
оглядкой на крон и в нем реализовано логирование, то <a class="reference external" href="http://fcron.free.fr/doc/en/fcrontab.5.html">fcrontab</a> будет выглядеть очень
просто:</p>
<pre class="literal-block">
SHELL=/bin/zsh
PATH="/usr/bin:/root/bin"
BACKUP_LOG=1

@ 6h backup run &amp;&amp; backup call pad_to_box
@ 2d1h backup tar
@ 2d2h backup full
@ 2d4h backup full
</pre>
<p>Таким образом, у меня каждые шесть часов делается неполный бекап, а раз в два дня - полный
бекап и архивирование <tt class="docutils literal">delta</tt>.</p>
<p>Кроме всего, полный бекап используется для разворачивания новых Arch Linux, например на
неттопе и LiveHards. Теперь развернуть привычно настроенный Arch очень просто и быстро.</p>
<dl class="docutils">
<dt>Материалы по теме:</dt>
<dd><ul class="first last simple">
<li><a class="reference external" href="https://wiki.archlinux.org/index.php/Backup_Programs">https://wiki.archlinux.org/index.php/Backup_Programs</a></li>
<li><a class="reference external" href="https://wiki.archlinux.org/index.php/Full_System_Backup_with_rsync">https://wiki.archlinux.org/index.php/Full_System_Backup_with_rsync</a></li>
</ul>
</dd>
</dl>
 ]]></content>
    </entry>
    <entry xml:base="https://pusto.org/post/feed.xml">
        <title type="text">Django tests. Практические советы</title>
        <link href="https://pusto.org/post/django-tests-practical-tips/"/>
        <id>https://pusto.org/post/django-tests-practical-tips/</id>
        <updated>2013-10-28T08:00:00+02:00</updated>
        <author>
            <name>naspeh</name>
        </author>
        <content type="html"><![CDATA[ <!-- - введение
- избегайте static fixtures
- транзакции - наше все
- в несколько процессов (djtest-bootstrap)
- трюк с settings
- cache and redis -->
<p>С апреля уже не работаю в ostrovok.ru, но опыт по внедрению тестов в разработку,
полученный в этой команде, очень хороший. Хочу записать по горячим следам ряд практических
советов и замечаний по поводу внедрения тестов и django тестов в частности.</p>
<!-- MORE -->
<p>Внедрять тесты в уже существующий проект с кучей всесторонних зависимостей от внешних
сервисов и API - задача, требующая довольно много времени. Плюс возвращаться и
дорабатывать их нужно будет не один раз. Хорошо что в ostrovok.ru все понимали, что тесты
нужны, просто не знали с какой стороны к ним подойти и нужен был человек, который
“заражен” тестами и возьмется за внедрение.</p>
<p>Про первые шаги <a class="reference external" href="https://pusto.org/post/django-tests-practical-tips/habr/">писал на хабре (26.06.2012)</a>, потом позже рассказывал на <a class="reference external" href="https://pusto.org/s/2013-ru-pycon/">Pycon Russia
(24.02.2013)</a> про оптимизацию и запуск тестов в несколько процессов. Когда уходил из
ostrovok.ru тестов было ~1000 (6 мин на моем ноутбуке, 4 процесса), через полгода сказали,
что уже ~1500 (6 мин на <a class="reference external" href="http://ru.wikipedia.org/wiki/Непрерывная_интеграция">CI</a> сервере, больше 10 процессов). Тесты удались, хотя некоторые
места хотелось бы сделать лучше.</p>
<p>Поговорим о некоторых проблемах и способах их не допустить.</p>
<div class="section" id="section-1">
<h1>Транзакции для изоляции базы</h1>
<p>В моем мозгу изоляция окружения для конкретного теста - это обязательное условие для
существования хороших тестов. Для изоляции базы самый верный способ - это транзакции, они
быстрые. В  начале теста мы открываем транзакцию, а после прохождения теста делаем ее
откат (rollback), ну и условие напрашивается само - не должно быть прерывания открытой
транзакции. У нас вышло так, что в процессе бронирования было ручное управление
транзакциями (это и есть прерывание), а тестов вокруг бронирования у нас было очень много
(больше всего хотелось покрыть тестами именно этот процесс), поэтому мне пришлось
придумывать свой механизм изоляции базы на основе ее копирования из шаблона. Хотя <a class="reference external" href="https://pusto.org/s/2013-ru-pycon/#id14">этот
механизм</a> был быстрее <a class="reference external" href="https://docs.djangoproject.com/en/dev/topics/testing/overview/#transactiontestcase">стандартного в django для этого случая</a>, но он был медленнее и
сложнее транзакций. Поэтому когда делался запуск тестов в несколько процессов, пришлось
приложить больше усилий, чтоб все наши тесты хорошо работали.</p>
<p><strong>Нужно стараться, чтоб в транзакциях работали максимум тестов.</strong> Транзакции хорошо
масштабируются на несколько процессов, а те тесты, в которых тестируются
именно транзакции, лучше пускать отдельно в одном процессе уже после основной пачки
тестов.</p>
</div>
<div class="section" id="section-2">
<h1>Запуск в несколько процессов</h1>
<p>Хорошие тесты - это те, которые запускаешь и через какое-то реальное время получаешь
фитбек прошли или не прошли, для этого вообще-то тесты и вводятся. Если тесты запускаются
долго, это плохо скажется на процессе тестирования в команде. Для меня реальное время - до
10 минут, меньше конечно лучше, но больше - это сигнал, что тесты нужно  срочно ускорять.
В какой-то момент количество тестов выросло до ~800, время 15 минут, всевозможные
локальные оптимизации кода в тестах произведены, а время еще нужно было сокращать, т.к.
тестов становилось все больше. Очень хороший скачек в ускорении тестов - это запуск в
несколько процессов, хотя этот метод добавляет определенную сложность тестовой среде.
Кроме изоляции базы на уровне процессов, также необходима изоляция всевозможных кешей,
redis. У нас вышло все еще сложнее. Реализация тестовой среды как-то была совсем не готова
к нескольким процессам и нужна была значительная переработка, которая в итоге состоялась.
Мы перенесли всю логику изоляции всего на уровень <tt class="docutils literal">TestCase</tt>, это нам дало возможность
не только запустить тесты в несколько потоков, но также и опробовать разные пускальщики:
<a class="reference external" href="http://nose2.readthedocs.org/en/latest/">nose2</a>, <a class="reference external" href="http://pytest.org/latest/">pytest</a>, но мы так и остались на <a class="reference external" href="http://nose.readthedocs.org/en/latest/">nose</a> первом. С каждым из пускальщиков были
какие-то проблемы. <tt class="docutils literal">pytest</tt> был очень медленным на наших тестах при распараллеливании.
<tt class="docutils literal">nose2</tt> у нас какое-то время поработал на CI, но он зависал при исключениях в
многопроцессорном режиме. У nose1 хоть и были проблемы с неработающими плагинами в
multiprocess, но <tt class="docutils literal">xmlunit</tt> отчет как-то прикрутили для CI, а без других обходились,
главное что тесты работали значительно быстрее. На одном хакатоне мы опробовали запускать
наши тесты на крутом серваке (с 24 ядрами, кажется), так у нас ~800 тестов проходили за
пару минут в 20 процессов. Но самый большой прирост заметен обычно при 2-4 процессах. На
моем ноутбуке в 4 процесса тесты проходили меньше 6 минут, но у меня реальных было 2 ядра
и два виртуальных :)</p>
<p><strong>Хорошо бы, чтобы тестовая среда была готова к запуску в несколько процессов,</strong> благо
если учесть, что используются транзакции в тестах, то решается это довольно легко,
<a class="reference external" href="https://github.com/naskoro/djtest-bootstrap">проект-шаблон доступен на github.</a></p>
</div>
<div class="section" id="mocks">
<h1>Mocks</h1>
<p>Опять же в моем мозгу сидит, что все внешние вызовы должны быть “замоканы” для того чтоб
тесты были предсказуемые и быстрые. Даже самый быстрый и надежный сторонний сервис может
дать сбой или ответить каким-то не совсем ожидаемым способом, и тогда тесты упадут. Но
тесты не должны падать из-за внешних условий, поэтому мы подменяем действительность моками
и прописываем те ответы, которые мы ожидаем в тестах. Есть еще ряд случаев, в которых без
моков не обойтись, это могут быть разные моменты со временем, проверка на прошлое или
будущее, проверка выпадения исключений и т.п. В общем в проекте с тестами скорее всего
будут моки, у нас их было много.</p>
<p>Хоть мы имеем дело с динамическим языком python и модуль <a class="reference external" href="https://pypi.python.org/pypi/mock">mock</a> очень помогает нам, но
моки - это в своем роде магия, и у модуля <tt class="docutils literal">mock</tt> есть свои нюансы и ограничения, про
которые не все разработчики знают. Время от времени моки становились проблемой, особенно
когда их сайд-эффекты сложно было определить и особенно у тех разработчиков, которые мало
общались с моками. <strong>К ним нужно привыкнуть, с опытом их использовать и чинить становится
проще.</strong></p>
</div>
<div class="section" id="section-3">
<h1>Зависимость тестов от фикстур</h1>
<p>Если над проектом постоянно работают, статические фикстуры - зло. Мы старались
использовать минимум фикстур, но совсем без них не обойтись. Все эти сторонние сервисы и
API требуют моков, а моки за собой тянут фикстуры. У нас статических фикстур было немного,
были даже скрипты, которые могли их обновлять. Но процесс обновления фикстур не был
встроен в CI, поэтому зависимость тестов от них стала одной из проблем, которую так до
конца и не решили.</p>
<p><strong>По-хорошему, фикстуры должны генерироваться динамически и этот процесс нужно встроить в
CI.</strong> Сторонние сервисы тоже разрабатываются, и придет время, когда нужно будет обновить
фикстуры до новой версии. А зависимых тестов может оказаться довольно много, когда в
команде пишут тесты несколько человек и не все знают про возможные проблемы, а тесты
писать нужно… Динамические фикстуры или постоянное обновление статических фикстур
препятствуют появлению зависимых тестов.</p>
<p>Вообще фикстуры, по моему мнению, одна из самых сложных задач при долгосрочном
тестировании, т.е. попасться в капкан зависимости от них довольно легко.</p>
</div>
<div class="section" id="section-4">
<h1>Следите за своими тестами</h1>
<p>Тесты - это как живой организм, который мы приручаем, а потом за ним нужно следить и
ухаживать, чтоб этот организм помогал нам в решении продуктовых задач. Да, конечно,
продуктовые задачи важнее, ведь не тесты делают пользователя счастливым, но они - часть
процесса разработки и помогают разработчикам быть уверенными в каждодневных изменениях,
которые они вносят для улучшения продукта. Тесты - это долгосрочная перспектива, а с любым
долговременным делом связано много сложностей и проблем, которые нужно помнить и
предугадывать, чтоб вовремя принимать корректирующие решения.</p>
<p>Следите за своими тестами и пусть они вам помогают писать хороший код.</p>
</div>
 ]]></content>
    </entry>
    <entry xml:base="https://pusto.org/post/feed.xml">
        <title type="text">Python. Подкоманды и argparse</title>
        <link href="https://pusto.org/post/python-argparse-subcommands/"/>
        <id>https://pusto.org/post/python-argparse-subcommands/</id>
        <updated>2013-10-27T08:00:00+02:00</updated>
        <author>
            <name>naspeh</name>
        </author>
        <content type="html"><![CDATA[ <!-- - введение
- неудобный интерфейс subcommands
- надстройки типа argh
- любимый метод
- выводы -->
<p>Поговорим об улучшении использования <a class="reference external" href="http://docs.python.org/2.7/library/argparse.html">argparse</a> и подкоманд в повседневной жизни.</p>
<p>В моей практике почти в каждом проекте есть интерфейс для командной строки, это может быть
<tt class="docutils literal">manage.py</tt> в веб проекте, просто скрипт бекапа или даже приложение GTK. В <cite>python 2.7</cite>
и <cite>3.2</cite> появился очень мощный модуль argparse для обработки параметров командной строки, и
в нем есть “из коробки” поддержка подкоманд и это очень круто. Но есть в этом модуле
маленький недостаток - интерфейс его использования немного избыточен.</p>
<!-- MORE -->
<p>Для начала нужно глянуть что уж такого плохого в интерфейсе, рассмотрим простой пример:</p>
<pre class="code py literal-block">
<span class="ch">#!/usr/bin/env python</span><span class="w">
</span><span class="kn">import</span> <span class="nn">argparse</span><span class="w">


</span><span class="k">def</span> <span class="nf">run_test</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
</span>    <span class="k">pass</span><span class="w">


</span><span class="k">def</span> <span class="nf">run_server</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="p">,</span> <span class="n">no_reload</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
</span>    <span class="k">pass</span><span class="w">


</span><span class="k">def</span> <span class="nf">parse_args</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
</span>    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">prog</span><span class="o">=</span><span class="s1">'app'</span><span class="p">)</span><span class="w">
</span>    <span class="n">cmds</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">add_subparsers</span><span class="p">(</span><span class="n">help</span><span class="o">=</span><span class="s1">'commands'</span><span class="p">)</span><span class="w">

</span>    <span class="n">cmd_run</span> <span class="o">=</span> <span class="n">cmds</span><span class="o">.</span><span class="n">add_parser</span><span class="p">(</span><span class="s1">'run'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'start dev server'</span><span class="p">)</span><span class="w">
</span>    <span class="n">cmd_run</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">'-s'</span><span class="p">,</span> <span class="s1">'--settings'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'application settings'</span><span class="p">)</span><span class="w">
</span>    <span class="n">cmd_run</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="w">
</span>        <span class="s1">'-P'</span><span class="p">,</span> <span class="s1">'--port'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">8000</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'server port'</span><span class="w">
</span>    <span class="p">)</span><span class="w">
</span>    <span class="n">cmd_run</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="w">
</span>        <span class="s1">'-H'</span><span class="p">,</span> <span class="s1">'--host'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s1">'localhost'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'server host'</span><span class="w">
</span>    <span class="p">)</span><span class="w">
</span>    <span class="n">cmd_run</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="w">
</span>        <span class="s1">'--no-reload'</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s1">'store_true'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'without reloading'</span><span class="w">
</span>    <span class="p">)</span><span class="w">
</span>    <span class="n">cmd_run</span><span class="o">.</span><span class="n">set_defaults</span><span class="p">(</span><span class="n">func</span><span class="o">=</span><span class="k">lambda</span> <span class="n">a</span><span class="p">:</span> <span class="p">(</span><span class="w">
</span>        <span class="n">run_server</span><span class="p">(</span><span class="n">a</span><span class="o">.</span><span class="n">host</span><span class="p">,</span> <span class="n">a</span><span class="o">.</span><span class="n">port</span><span class="p">,</span> <span class="n">a</span><span class="o">.</span><span class="n">no_reload</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="n">a</span><span class="o">.</span><span class="n">settings</span><span class="p">)</span><span class="w">
</span>    <span class="p">))</span><span class="w">

</span>    <span class="n">cmd_test</span> <span class="o">=</span> <span class="n">cmds</span><span class="o">.</span><span class="n">add_parser</span><span class="p">(</span><span class="s1">'test'</span><span class="p">,</span> <span class="n">aliases</span><span class="o">=</span><span class="p">[</span><span class="s1">'t'</span><span class="p">,</span> <span class="s1">'te'</span><span class="p">],</span> <span class="n">help</span><span class="o">=</span><span class="s1">'run tests'</span><span class="p">)</span><span class="w">
</span>    <span class="n">cmd_test</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">'-s'</span><span class="p">,</span> <span class="s1">'--settings'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'application settings'</span><span class="p">)</span><span class="w">
</span>    <span class="n">cmd_test</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">'target'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s1">'.'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'python module or file'</span><span class="p">)</span><span class="w">
</span>    <span class="n">cmd_test</span><span class="o">.</span><span class="n">set_defaults</span><span class="p">(</span><span class="w">
</span>        <span class="n">func</span><span class="o">=</span><span class="k">lambda</span> <span class="n">a</span><span class="p">:</span> <span class="n">run_test</span><span class="p">(</span><span class="n">a</span><span class="o">.</span><span class="n">module</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="n">a</span><span class="o">.</span><span class="n">settings</span><span class="p">)</span><span class="w">
</span>    <span class="p">)</span><span class="w">

</span>    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">(</span><span class="n">args</span><span class="p">)</span><span class="w">
</span>    <span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="s1">'func'</span><span class="p">):</span><span class="w">
</span>        <span class="n">parser</span><span class="o">.</span><span class="n">print_usage</span><span class="p">()</span><span class="w">
</span>    <span class="k">else</span><span class="p">:</span><span class="w">
</span>        <span class="n">args</span><span class="o">.</span><span class="n">func</span><span class="p">(</span><span class="n">args</span><span class="p">)</span><span class="w">


</span><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span><span class="w">
</span>    <span class="n">parse_args</span><span class="p">()</span>
</pre>
<p>Вроде не так уж все и плохо, обычный интерфейс. Есть дублирование параметра
<tt class="docutils literal"><span class="pre">--settings</span></tt>, но чтоб он был привязан к каждой подкоманде его нельзя вешать на базовый
парсер. Также нам пришлось переносить строки для соблюдения <a class="reference external" href="https://peps.python.org/pep-0008">PEP 8</a>, при том что не
помещались считанные символы. Можно укоротить переменные <tt class="docutils literal">cmd_run</tt>, <tt class="docutils literal">cmd_test</tt> до
<tt class="docutils literal">run</tt>, <tt class="docutils literal">test</tt> или даже до <tt class="docutils literal">r</tt>, <tt class="docutils literal">t</tt>, но суть не в этом. Эти переменные, в принципе,
не нужны, если добавить цепочки вызовов:</p>
<pre class="code py literal-block">
<span class="n">cmds</span><span class="o">.</span><span class="n">add_parser</span><span class="p">(</span><span class="s1">'run'</span><span class="p">)</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">'port'</span><span class="p">)</span><span class="o">.</span><span class="n">set_defaults</span><span class="p">(</span><span class="n">func</span><span class="o">=</span><span class="n">run_server</span><span class="p">)</span>
</pre>
<p>На чистом argparse цепочек вызовов не получится, хотя может в каких-то случаях
использования они и не нужны. В моей практике чаще хочется цепочек.</p>
<p>В самом начале примера объявлена пара функций и есть проекты, которые превращают эти
функции в подкоманды, типа: <a class="reference external" href="http://opster.readthedocs.org/en/latest/">opster</a>, <a class="reference external" href="http://pythonhosted.org/argh/">argh</a>, <a class="reference external" href="https://pypi.python.org/pypi/komandr">komandr</a>. Последние два основаны на
argparse, а opster использует <a class="reference external" href="http://docs.python.org/2.7/library/getopt.html">getopt.</a></p>
<p>В некоторых случаях использование подобных улучшаторов выглядит очень клево, например,
использование argh:</p>
<pre class="code py literal-block">
<span class="ch">#!/usr/bin/env python</span><span class="w">
</span><span class="kn">import</span> <span class="nn">argh</span><span class="w">


</span><span class="nd">@argh</span><span class="o">.</span><span class="n">aliases</span><span class="p">(</span><span class="s1">'t'</span><span class="p">,</span> <span class="s1">'te'</span><span class="p">)</span><span class="w">
</span><span class="k">def</span> <span class="nf">test</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
    </span><span class="sd">'''run tests'''</span><span class="w">
</span>    <span class="k">pass</span><span class="w">


</span><span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="s1">'localhost'</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">8000</span><span class="p">,</span> <span class="n">no_reload</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
    </span><span class="sd">'''run dev server'''</span><span class="w">
</span>    <span class="k">pass</span><span class="w">

</span><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span><span class="w">
</span>    <span class="n">argh</span><span class="o">.</span><span class="n">dispatch_commands</span><span class="p">([</span><span class="n">run</span><span class="p">,</span> <span class="n">test</span><span class="p">])</span>
</pre>
<p>Вывод главного help такой же как из первого примера:</p>
<pre class="literal-block">
usage: example-argh.py [-h] {run,test,t,te} ...

positional arguments:
  {run,test,t,te}
    run            run dev server
    test (t, te)   run tests

optional arguments:
  -h, --help       show this help message and exit
</pre>
<p>А вот вывод help для определенной подкоманды отличается отсутствием описаний и различием
коротких аналогов для параметров:</p>
<pre class="literal-block">
===pure argparse===
usage: app run [-h] [-s SETTINGS] [-P PORT] [-H HOST] [--no-reload]

optional arguments:
  -h, --help            show this help message and exit
  -s SETTINGS, --settings SETTINGS
                        application settings
  -P PORT, --port PORT  server port
  -H HOST, --host HOST  server host
  --no-reload           without reloading


===argh===
usage: example-argh.py run [-h] [--host HOST] [-p PORT] [-n] [-s SETTINGS]

run dev server

optional arguments:
  -h, --help            show this help message and exit
  --host HOST
  -p PORT, --port PORT
  -n, --no-reload
  -s SETTINGS, --settings SETTINGS
</pre>
<p>В принципе, можно добиться полного соответствия help, но от этого уже будет страдать
предельная лаконичность второго примера. Вообще-то, если названия параметров не требуют
пояснений, то использовать argh очень заманчиво, тем более он, в принципе, позволяет
добраться до обычного argparse, если где-то сталкиваешься с ограничениями.</p>
<p>Вся прелесть argparse, что с python 2.7 и 3.2 он входит в стандартную библиотеку и реально
крут по сравнению с тем же getopt и <a class="reference external" href="http://docs.python.org/3/library/optparse.html">optparse</a>. А перечисленные выше улучшаторы - это
отдельные пакеты и таскать их зависимостями в каждый проект не прикольно, особенно если
проект минималистичный или небольшой скрипт с подкомандами. Еще в улучшаторах часто
присутствует немного магии, argparse же прямой как двери.</p>
<p>Хорошо бы использовать argparse, но как-то покрасивее, чем в первом примере.</p>
<p>Следующий пример - мой любимый способ:</p>
<pre class="code py literal-block">
<span class="ch">#!/usr/bin/env python</span><span class="w">
</span><span class="kn">import</span> <span class="nn">argparse</span><span class="w">


</span><span class="k">def</span> <span class="nf">run_test</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
</span>    <span class="k">pass</span><span class="w">


</span><span class="k">def</span> <span class="nf">run_server</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="p">,</span> <span class="n">no_reload</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
</span>    <span class="k">pass</span><span class="w">


</span><span class="k">def</span> <span class="nf">parse_args</span><span class="p">(</span><span class="n">args</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span><span class="w">
</span>    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">prog</span><span class="o">=</span><span class="s1">'app'</span><span class="p">)</span><span class="w">
</span>    <span class="n">cmds</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">add_subparsers</span><span class="p">(</span><span class="n">help</span><span class="o">=</span><span class="s1">'commands'</span><span class="p">)</span><span class="w">

</span>    <span class="k">def</span> <span class="nf">cmd</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">):</span><span class="w">
</span>        <span class="n">p</span> <span class="o">=</span> <span class="n">cmds</span><span class="o">.</span><span class="n">add_parser</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">)</span><span class="w">
</span>        <span class="n">p</span><span class="o">.</span><span class="n">set_defaults</span><span class="p">(</span><span class="n">cmd</span><span class="o">=</span><span class="n">name</span><span class="p">)</span><span class="w">
</span>        <span class="n">p</span><span class="o">.</span><span class="n">arg</span> <span class="o">=</span> <span class="k">lambda</span> <span class="o">*</span><span class="n">a</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">:</span> <span class="n">p</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="o">*</span><span class="n">a</span><span class="p">,</span> <span class="o">**</span><span class="n">kw</span><span class="p">)</span> <span class="ow">and</span> <span class="n">p</span><span class="w">
</span>        <span class="n">p</span><span class="o">.</span><span class="n">exe</span> <span class="o">=</span> <span class="k">lambda</span> <span class="n">f</span><span class="p">:</span> <span class="n">p</span><span class="o">.</span><span class="n">set_defaults</span><span class="p">(</span><span class="n">exe</span><span class="o">=</span><span class="n">f</span><span class="p">)</span> <span class="ow">and</span> <span class="n">p</span><span class="w">

</span>        <span class="c1"># global options</span><span class="w">
</span>        <span class="n">p</span><span class="o">.</span><span class="n">arg</span><span class="p">(</span><span class="s1">'-s'</span><span class="p">,</span> <span class="s1">'--settings'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'application settings'</span><span class="p">)</span><span class="w">
</span>        <span class="k">return</span> <span class="n">p</span><span class="w">

</span>    <span class="n">cmd</span><span class="p">(</span><span class="s1">'run'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'start dev server'</span><span class="p">)</span>\
        <span class="o">.</span><span class="n">arg</span><span class="p">(</span><span class="s1">'-P'</span><span class="p">,</span> <span class="s1">'--port'</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">8000</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'server port'</span><span class="p">)</span>\
        <span class="o">.</span><span class="n">arg</span><span class="p">(</span><span class="s1">'-H'</span><span class="p">,</span> <span class="s1">'--host'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s1">'localhost'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'server host'</span><span class="p">)</span>\
        <span class="o">.</span><span class="n">arg</span><span class="p">(</span><span class="s1">'--no-reload'</span><span class="p">,</span> <span class="n">action</span><span class="o">=</span><span class="s1">'store_true'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'without reloading'</span><span class="p">)</span>\
        <span class="o">.</span><span class="n">exe</span><span class="p">(</span><span class="k">lambda</span> <span class="n">a</span><span class="p">:</span> <span class="p">(</span><span class="w">
</span>            <span class="n">run_server</span><span class="p">(</span><span class="n">a</span><span class="o">.</span><span class="n">host</span><span class="p">,</span> <span class="n">a</span><span class="o">.</span><span class="n">port</span><span class="p">,</span> <span class="n">a</span><span class="o">.</span><span class="n">no_reload</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="n">a</span><span class="o">.</span><span class="n">settings</span><span class="p">)</span><span class="w">
</span>        <span class="p">))</span><span class="w">

</span>    <span class="n">cmd</span><span class="p">(</span><span class="s1">'test'</span><span class="p">,</span> <span class="n">aliases</span><span class="o">=</span><span class="p">[</span><span class="s1">'t'</span><span class="p">,</span> <span class="s1">'te'</span><span class="p">],</span> <span class="n">help</span><span class="o">=</span><span class="s1">'run tests'</span><span class="p">)</span>\
        <span class="o">.</span><span class="n">arg</span><span class="p">(</span><span class="s1">'target'</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s1">'.'</span><span class="p">,</span> <span class="n">nargs</span><span class="o">=</span><span class="s1">'?'</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">'python module or file'</span><span class="p">)</span>\
        <span class="o">.</span><span class="n">exe</span><span class="p">(</span><span class="k">lambda</span> <span class="n">a</span><span class="p">:</span> <span class="n">run_test</span><span class="p">(</span><span class="n">a</span><span class="o">.</span><span class="n">target</span><span class="p">,</span> <span class="n">settings</span><span class="o">=</span><span class="n">a</span><span class="o">.</span><span class="n">settings</span><span class="p">))</span><span class="w">

</span>    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">(</span><span class="n">args</span><span class="p">)</span><span class="w">
</span>    <span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">args</span><span class="p">,</span> <span class="s1">'exe'</span><span class="p">):</span><span class="w">
</span>        <span class="n">parser</span><span class="o">.</span><span class="n">print_usage</span><span class="p">()</span><span class="w">
</span>    <span class="k">else</span><span class="p">:</span><span class="w">
</span>        <span class="n">args</span><span class="o">.</span><span class="n">exe</span><span class="p">(</span><span class="n">args</span><span class="p">)</span><span class="w">


</span><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span><span class="w">
</span>    <span class="n">parse_args</span><span class="p">()</span>
</pre>
<dl class="docutils">
<dt>По-моему, выходит очень читабельно:</dt>
<dd><ul class="first last simple">
<li>это чистый argparse и если его интерфейс чаще использовать, то даже этот сложный
интерфейс запомнится;</li>
<li>глобальные опции, типа <tt class="docutils literal"><span class="pre">--settings</span></tt>, мы можем определять в одном месте без
дублирования;</li>
<li>чем больше подкоманд и параметров, тем оправданнее добавление вложенной функции
<tt class="docutils literal">cmd</tt>;</li>
<li>обратный слеш в цепочках вызовов мне больше нравится, хотя перенос строк больше люблю
делать внутри скобок.</li>
</ul>
</dd>
</dl>
<p>После использования в нескольких местах такого подхода мне все больше нравится
отделение интерфейса функции от вызовов командной строки, все таки это немного разные
вещи. Хотя раньше мне очень нравилось превращение функций в подкоманды.</p>
<p>Вывод довольно банальный: у python очень крутая стандартная библиотека, argparse - очень
мощный инструмент для работы с параметрами командной строки. И даже если есть какие-то
библиотеки с красивыми плюшками (argh, opster или <a class="reference external" href="http://docopt.org/">docopt</a>) у них скорее всего тоже
найдутся свои недостатки, поэтому мой выбор - подточить использование argparse и забыть
про дополнительные зависимости.</p>
 ]]></content>
    </entry>
    <entry xml:base="https://pusto.org/post/feed.xml">
        <title type="text">Gnome2 и маленький экран</title>
        <link href="https://pusto.org/post/gnome2-and-small-display/"/>
        <id>https://pusto.org/post/gnome2-and-small-display/</id>
        <updated>2011-09-28T08:00:00+03:00</updated>
        <author>
            <name>naspeh</name>
        </author>
        <content type="html"><![CDATA[ <!-- http://www.burtonini.com/blog/computers/devilspie
http://live.gnome.org/DevilsPie
http://help.ubuntu.ru/wiki/devilspie (ru) -->
<p>В статье покажу как оптимизирую рабочий стол для маленького разрешения ноутбука
(1280х800) и рассмотрю пару полезных утилит для работы с окнами в <strong>gnome</strong>
<strong>второй</strong> версии.</p>
<ul class="simple">
<li><a class="reference external" href="http://packages.debian.org/sid/maximus">maximus</a> убирает декорацию для максимизированного окна
(по умолчанию максимизирует все окна);</li>
<li><a class="reference external" href="http://www.foosel.org/linux/devilspie">devilspie</a> позволяет сделать определенные дейсвия (focus, maximize и т.д.)
с окном по его атрибутам.</li>
</ul>
<!-- MORE -->
<p>Так повелось, что <strong>gnome</strong> у меня стал рабочим столом в <strong>linux</strong>. И так
сложилось, что решил использовать ноутбук “по полной”, как рабочую лошадку, чтоб
не возится с синхронизацией данных на нескольких рабочих машинах. Поэтому работу
за ноутбуком нужно было сделать максимально удобной.</p>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">Возможно скоро променяю <strong>gnome</strong> на какой-нибудь <a class="reference external" href="http://ru.wikipedia.org/wiki/Фреймовый_оконный_менеджер_X_Window_System">тайловый оконный менеджер</a>  и
<a class="reference external" href="http://www.debian.org/">debian</a> на <a class="reference external" href="http://www.archlinux.org/">archlinux</a>, поэтому решил записать этот рецепт, которым пользуюсь уже
давно, может кому еще пригодится.</p>
</div>
<p>До этого любил сидеть за монитором с большим разрешением 1600x1200
(Samsung-SyncMaster-204b, 20 дюймов), и когда садился за ноут (1280х800)
мне было неудобно, что на экране мало всего помещается. <strong>Решение</strong>: нужно по
максимуму оптимизировать визуальное пространство, занимаемое приложениями.</p>
<p>Первым делом думал избавиться от одной из панелей (по умолчанию в <strong>gnome</strong>
их две, внизу и вверху экрана). Верхняя панелька мне очень понравилась еще при
первом запуске <strong>gnome</strong> и ее удалять совсем не хотелось. Но места на ней совсем
не хватало еще и для списка окон:</p>
<img alt="_img/panel-top.png" src="https://pusto.org/post/gnome2-and-small-display/_img/panel-top.png"/>
<p>Список окон мне тоже оказался очень нужен и желательно, чтоб места для него
было побольше:</p>
<img alt="_img/panel-bottom.png" src="https://pusto.org/post/gnome2-and-small-display/_img/panel-bottom.png"/>
<p>Панельки так и остались у меня на своих местах :) как в <strong>debian</strong> по умолчанию.
Я пробовал для них автоскрытие, но не пошло. Про режим на весь экран у
приложений тоже помнил, но мне нужны были панельки, т.к. постоянно их использую.</p>
<p>Нужно было оптимизировать пространство между этими панельками.</p>
<div class="section" id="maximus-1">
<h1><a class="reference external" href="http://packages.debian.org/sid/maximus">Maximus</a></h1>
<p><strong>Декорация окон</strong>. Когда окно не развернуто на весь рабочий стол, то в
декорациях есть смысл: потянуть за заголовок, растянуть окно. А вот когда окно
максимизировано, то частично смысл в декорациях теряется, да еще и место
занимают. Действия типа: скрыть окно <tt class="docutils literal">(Alt+F9)</tt>, закрыть окно <tt class="docutils literal">(Alt+F4)</tt>,
максимизировать <tt class="docutils literal">(Alt+F10)</tt> хорошо выполняются через быстрые клавиши.
Поэтому убираем декорацию окон, когда они максимизированы. Для этого есть
пакет <a class="reference external" href="http://packages.debian.org/sid/maximus">maximus</a> в <strong>debian</strong>:</p>
<pre class="literal-block">
$ aptitude install maximus
</pre>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">В <a class="reference external" href="http://ru.wikipedia.org/wiki/Openbox">openbox</a> есть такой пункт в меню окна: убрать декорацию, но у меня пока
стандартный <a class="reference external" href="http://ru.wikipedia.org/wiki/Metacity">metacity</a>.</p>
</div>
<p>По умолчанию <a class="reference external" href="http://packages.debian.org/sid/maximus">maximus</a> разворачивает все окна на весь экран. Есть <a class="reference external" href="http://www.zhart.ru/software/21-gnome-panel-minimize-in-ubuntu-linux">вариант</a>
прописывать <cite>exclude_class</cite>, но я поступил по-другому - отключил максимизацию:</p>
<pre class="literal-block">
$ gconftool -s /apps/maximus/no_maximize --type bool true
$ gconftool -R /apps/maximus
 undecorate = true
 binding = disabled
 exclude_class = [Totem,Gnome-system-monitor]
 no_maximize = true
</pre>
<p>Т.е. максимизировать окна буду вручную через <tt class="docutils literal">Alt+F10</tt>.</p>
<p>В общем уже неплохо, но ряд приложений после запуска нужно сразу
максимизировать, т.к. они не хотят запоминать своих размеров и положения…
Лишние телодвижения: запустить, нажать <tt class="docutils literal">Alt+F10</tt>, а хочется просто запустить.</p>
</div>
<div class="section" id="devilspie-1">
<h1><a class="reference external" href="http://www.foosel.org/linux/devilspie">Devilspie</a></h1>
<p>И тут на помощь приходит <a class="reference external" href="http://www.foosel.org/linux/devilspie">devilspie</a>. Он может работать не только с классами окон
(<cite>exclude_class</cite> из <a class="reference external" href="http://packages.debian.org/sid/maximus">maximus</a> - это список классов окон), но и может проверить
имя приложения, класс и имя окна <a class="reference external" href="http://www.foosel.org/linux/devilspie#matchers">и еще ряд атрибутов</a>. Причем может
<a class="reference external" href="http://www.foosel.org/linux/devilspie#string_tests">проверить атрибут</a> не только на соответствие, а и на содержание (contains) и
соответствие регулярному выражению. <a class="reference external" href="http://www.foosel.org/linux/devilspie#actions">Действия</a> над окнами тоже разные:
maximize, unmaximize, focus и undecorate даже :).</p>
<p>Инструмент нашли, дальше используем.</p>
<pre class="literal-block">
$ aptitude install devilspie
</pre>
<p>Создаем файл <tt class="docutils literal"><span class="pre">~/.devilspie/common.ds</span></tt>. И помещаем туда что-то типа:</p>
<pre class="code py literal-block">
<span class="ln"> 1 </span><span class="p">(</span><span class="n">begin</span><span class="w">
</span><span class="ln"> 2 </span><span class="w"/>    <span class="p">;(</span><span class="n">debug</span><span class="p">)</span><span class="w">
</span><span class="ln"> 3 </span><span class="w"/>    <span class="p">(</span><span class="k">if</span><span class="w">
</span><span class="ln"> 4 </span><span class="w"/>        <span class="p">(</span><span class="ow">or</span><span class="w">
</span><span class="ln"> 5 </span><span class="w"/>            <span class="p">(</span><span class="n">contains</span><span class="p">(</span><span class="n">application_name</span><span class="p">)</span> <span class="s2">"Vim"</span><span class="p">)</span><span class="w">
</span><span class="ln"> 6 </span><span class="w"/>            <span class="p">(</span><span class="n">contains</span><span class="p">(</span><span class="n">application_name</span><span class="p">)</span> <span class="s2">"Terminal"</span><span class="p">)</span><span class="w">
</span><span class="ln"> 7 </span><span class="w"/>            <span class="p">(</span><span class="n">contains</span><span class="p">(</span><span class="n">window_name</span><span class="p">)</span> <span class="s2">"New Tab - Google Chrome"</span><span class="p">)</span><span class="w">
</span><span class="ln"> 8 </span><span class="w"/>            <span class="p">(</span><span class="n">contains</span><span class="p">(</span><span class="n">window_name</span><span class="p">)</span> <span class="s2">"FatRat"</span><span class="p">)</span><span class="w">
</span><span class="ln"> 9 </span><span class="w"/>            <span class="p">(</span><span class="n">contains</span><span class="p">(</span><span class="n">window_name</span><span class="p">)</span> <span class="s2">"Document Viewer"</span><span class="p">)</span><span class="w">
</span><span class="ln">10 </span><span class="w"/>            <span class="p">(</span><span class="n">contains</span><span class="p">(</span><span class="n">window_name</span><span class="p">)</span> <span class="s2">"Clementine"</span><span class="p">)</span><span class="w">
</span><span class="ln">11 </span><span class="w"/>            <span class="p">(</span><span class="ow">is</span><span class="p">(</span><span class="n">window_name</span><span class="p">)</span> <span class="s2">"DreamPie"</span><span class="p">)</span><span class="w">
</span><span class="ln">12 </span><span class="w"/>        <span class="p">)</span><span class="w">
</span><span class="ln">13 </span><span class="w"/>        <span class="p">(</span><span class="n">maximize</span><span class="p">)</span><span class="w">
</span><span class="ln">14 </span><span class="w"/>    <span class="p">)</span><span class="w">
</span><span class="ln">15 </span><span class="w"/><span class="p">)</span>
</pre>
<p>И добавляем <tt class="docutils literal">devilspie</tt> в автозагрузку.</p>
<p>Обычно работаю с конфигом <tt class="docutils literal"><span class="pre">~/.devilspie/common.ds</span></tt> следующим образом.
Добавляю строку с дебагом (например: убрать “;” в начале строки №2 в приведенном
выше листинге), убиваю процесс <tt class="docutils literal">devilspie</tt> и запускаю его в терминале.
В терминал начинают писаться атрибуты окон. Пример сессии:</p>
<pre class="literal-block">
$ killall devilspie
$ devilspie

 Window Title: 'naspeh@free: '; Application Name: 'Terminal'; Class: 'Gnome-terminal'; Geometry: 1280x774+0+3
 Window Title: 'pusto.org: Edit for fun - Iceweasel'; Application Name: 'Iceweasel'; Class: 'Iceweasel'; Geometry: 1280x774+0+3
 Window Title: 'x-nautilus-desktop'; Application Name: 'File Manager'; Class: 'Nautilus'; Geometry: 1280x800+0+0
 Window Title: 'Bottom Expanded Edge Panel'; Application Name: 'Bottom Expanded Edge Panel'; Class: 'Gnome-panel'; Geometry: 1280x24+0+776
 Window Title: 'Top Expanded Edge Panel'; Application Name: 'Top Expanded Edge Panel'; Class: 'Gnome-panel'; Geometry: 1280x25+0+0
</pre>
<p>Потом открываю нужное мне окно, смотрю атрибуты, правлю конфиг, перезапускаю
<tt class="docutils literal">devilspie</tt> и так пока не будет все хорошо :).</p>
<p>Раз уж используем <a class="reference external" href="http://www.foosel.org/linux/devilspie">devilspie</a>, можно с его помощью еще что-то замутить.</p>
<p>Например, <strong>Skype</strong> очень жутко ведет себя в <strong>linux</strong>. Один из боков: хочется
чтоб окна чатов открывались в одном месте и одинакового размера. Если заниматься
этим вручную, то тут нужно подгонять каждое новое окно чата мышкой, изрядно
потыкав. И тут на помощь приходит действие <tt class="docutils literal">geometry</tt> из <a class="reference external" href="http://www.foosel.org/linux/devilspie">devilspie</a>.</p>
<p>Пример debug:</p>
<pre class="literal-block">
Window Title: 'Skype? 2.2 (Beta) for Linux'; Application Name: 'Skype? 2.2 (Beta) for Linux'; Class: 'Skype'; Geometry: 266x487+0+25
Window Title: 'Anastasie - Skype? Chat'; Application Name: 'Skype'; Class: 'Skype'; Geometry: 824x619+456+95
</pre>
<pre class="code py literal-block">
<span class="p">(</span><span class="k">if</span><span class="w">

</span>    <span class="p">(</span><span class="ow">and</span><span class="w">
</span>        <span class="p">(</span><span class="n">contains</span><span class="p">(</span><span class="n">window_name</span><span class="p">)</span> <span class="s2">"Skype"</span><span class="p">)</span><span class="w">
</span>        <span class="p">(</span><span class="n">matches</span><span class="p">(</span><span class="n">window_role</span><span class="p">)</span> <span class="s2">"ConversationsWindow"</span><span class="p">)</span><span class="w">
</span>    <span class="p">)</span><span class="w">
</span>    <span class="p">(</span><span class="n">geometry</span> <span class="s2">"800x675+365-0"</span><span class="p">)</span><span class="w">
</span><span class="p">)</span>
</pre>
<p>Для получения <tt class="docutils literal">window_role</tt> использовал <a class="reference external" href="http://www.x.org/archive/X11R7.5/doc/man/man1/xprop.1.html">xprop</a>, который содержится в <tt class="docutils literal"><span class="pre">x11-utils</span></tt>.</p>
</div>
<div class="section" id="section-1">
<h1>Итого</h1>
<p>Есть действия, которые каждодневно повторяются, и если на них потратить немного
времени и автоматизировать, то в конечном счете сэкономится пара ненужных
телодвижений в день :). Как говорится: настрой свой <strong>linux</strong> под себя.</p>
<p>Напоследок скриншот экрана:</p>
<img alt="_img/screenshot.png" src="https://pusto.org/post/gnome2-and-small-display/_img/screenshot.png"/>
</div>
 ]]></content>
    </entry>
    <entry xml:base="https://pusto.org/post/feed.xml">
        <title type="text">Python. Управление кодом из консоли</title>
        <link href="https://pusto.org/post/python-code-management/"/>
        <id>https://pusto.org/post/python-code-management/</id>
        <updated>2011-09-14T08:00:00+03:00</updated>
        <author>
            <name>naspeh</name>
        </author>
        <content type="html"><![CDATA[ <p>В разработке с python немалую роль играет консоль. Запуск сервера, запуск
тестов, работа с <a class="reference external" href="http://ru.wikipedia.org/wiki/Система_управления_версиями">VCS</a>, развертывание (deployment) и т.д. Есть конечно <a class="reference external" href="http://ru.wikipedia.org/wiki/Интегрированная_среда_разработки">IDE</a>,
которые предлагают много “плюшек”, достаточно сделать всего лишь пару кликов,
но это мы прошли.</p>
<p>Дальше поговорим про организацию наших телодвижений в консоли.</p>
<!-- MORE -->
<div class="section" id="makefile">
<h1><a class="reference external" href="http://ru.wikipedia.org/wiki/Make">Makefile</a></h1>
<p>Когда я работал в <a class="reference external" href="http://42coffeecups.com">42cc</a>, в практике у нас было добавление <a class="reference external" href="http://ru.wikipedia.org/wiki/Make">Makefile</a> в проект,
куда записывались часто используемые команды, в итоге работа с проектом
сводилась к:</p>
<pre class="literal-block">
make clean
make runserver
make test
make deploy
</pre>
<p>так сказать псевдонимы, а команды на самом деле были примерно такие:</p>
<pre class="code make literal-block">
<span class="nf">clean</span><span class="o">:</span><span class="w">
    </span>-rm<span class="w"> </span>*~*<span class="w">
    </span>-find<span class="w"> </span>.<span class="w"> </span>-name<span class="w"> </span><span class="s1">'*.pyc'</span><span class="w"> </span>-exec<span class="w"> </span>rm<span class="w"> </span><span class="o">{}</span><span class="w"> </span><span class="se">\;</span><span class="w">

</span><span class="nf">runserver</span><span class="o">:</span><span class="w">
    </span><span class="nv">PYTHONPATH</span><span class="o">=</span><span class="k">$(</span>PYTHONPATH<span class="k">)</span><span class="w"> </span>python<span class="w"> </span>django-project/manage.py<span class="w"> </span>runserver<span class="w">

</span><span class="nf">test</span><span class="o">:</span><span class="w">
    </span><span class="nv">PYTHONPATH</span><span class="o">=</span><span class="k">$(</span>PYTHONPATH<span class="k">)</span><span class="w"> </span>nosetests<span class="w"> </span>--with-django<span class="w"> </span>--django-settings<span class="o">=</span><span class="k">$(</span>test_settings<span class="k">)</span><span class="w"> </span><span class="k">$(</span>module<span class="k">)</span>
</pre>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">В <a class="reference external" href="http://sphinx.pocoo.org/">sphinx</a>  пошли тем же путем: <a class="reference external" href="https://bitbucket.org/birkenfeld/sphinx/src/cf794ec8a096/Makefile">раз</a>, <a class="reference external" href="https://bitbucket.org/birkenfeld/sphinx/src/cf794ec8a096/doc/Makefile">два</a></p>
</div>
<dl class="docutils">
<dt>Есть с <tt class="docutils literal">Makefile</tt> пара неприятных моментов:</dt>
<dd><ul class="first last simple">
<li>отступы в целях должны быть именно <cite>табами</cite>, а не 4 пробела. А с учетом
специфики python и его <a class="reference external" href="http://www.python.org/dev/peps/pep-0008/">рекомендаций</a> были случаи, когда редактор был
настроен на замену табов, и чтоб вставить таб, нужно было его копировать %);</li>
<li>в задачу можно передавать параметры только через правку файла или задание
переменных окружения, это основной минус.</li>
</ul>
</dd>
</dl>
<p>Но в общем подход по минимизации команд мне нравился. Можно переключиться на
проект, посмотреть <tt class="docutils literal">Makefile</tt> и понять что используется. Набирать команды
короче. А еще у <tt class="docutils literal">Makefile</tt> зачастую есть поддержка автодополнения, что тоже в
повседневной разработке упрощает жизнь.</p>
</div>
<div class="section" id="fabric">
<h1><a class="reference external" href="http://docs.fabfile.org/">Fabric</a></h1>
<p>Потом я услышал про чудо библиотеку для деплоймента <a class="reference external" href="http://docs.fabfile.org/">fabric</a> и стал ее
использовать. Но плодить <tt class="docutils literal">fabfile.py</tt>, <tt class="docutils literal">Makefile</tt> совсем не хотелось.
Решил, что если использовать <tt class="docutils literal">fabric</tt> для деплоймента, то почему бы не
использовать ее и для частых локальных команд:</p>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">fabric.api</span> <span class="kn">import</span> <span class="n">local</span><span class="p">,</span> <span class="n">run</span><span class="p">,</span> <span class="n">cd</span><span class="w">


</span><span class="k">def</span> <span class="nf">run</span><span class="p">():</span><span class="w">
    </span><span class="sd">'''Start development server'''</span><span class="w">
</span>    <span class="n">local</span><span class="p">(</span><span class="s1">'PYTHONPATH=. paster serve --reload development.ini'</span><span class="p">,</span> <span class="n">capture</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">


</span><span class="k">def</span> <span class="nf">pep8</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="s1">'.'</span><span class="p">):</span><span class="w">
    </span><span class="sd">'''Run pep8'''</span><span class="w">
</span>    <span class="n">local</span><span class="p">(</span><span class="s1">'pep8 --ignore=E202 </span><span class="si">%s</span><span class="s1">'</span> <span class="o">%</span> <span class="n">target</span><span class="p">,</span> <span class="n">capture</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">


</span><span class="nd">@hosts</span><span class="p">(</span><span class="s1">'root@pusto.org'</span><span class="p">)</span><span class="w">
</span><span class="k">def</span> <span class="nf">deploy</span><span class="p">(</span><span class="n">restart</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span><span class="w">
    </span><span class="sd">'''Deploy to remote server'''</span><span class="w">
</span>    <span class="n">local</span><span class="p">(</span><span class="s1">'hg push'</span><span class="p">,</span> <span class="n">capture</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span><span class="w">
</span>    <span class="k">with</span> <span class="n">cd</span><span class="p">(</span><span class="s1">'/var/www/horosh/'</span><span class="p">):</span><span class="w">
</span>        <span class="n">run</span><span class="p">(</span><span class="s1">'hg pull&amp;&amp;hg up'</span><span class="p">)</span><span class="w">
</span>        <span class="k">if</span> <span class="n">restart</span><span class="p">:</span><span class="w">
</span>            <span class="n">run</span><span class="p">(</span><span class="s1">'/etc/init.d/horosh force-reload'</span><span class="p">)</span>
</pre>
<p>Работа с консолью опять сводится к коротким командам:</p>
<pre class="literal-block">
fab clean pep8
fab run
fab deploy:True
</pre>
<p>В <tt class="docutils literal">fabric</tt> мы уже можем передавать параметры при вызове задачи и это клево.</p>
<dl class="docutils">
<dt>При использовании <tt class="docutils literal">fabric</tt> у меня появился ряд демотиваторов:</dt>
<dd><ul class="first last simple">
<li><a class="reference external" href="http://docs.fabfile.org/en/1.2.2/usage/fab.html#per-task-arguments">специфика</a> передачи параметров в задачи через двоеточие, не unix стиль;</li>
<li>в зависимостях две либы: <a class="reference external" href="http://www.lag.net/paramiko/">paramiko</a>, <a class="reference external" href="https://github.com/dlitz/pycrypto">pycrypto</a>;</li>
<li>пару раз пытался заюзать функции из <tt class="docutils literal">fabric</tt> просто в коде, как обычную
библиотеку (может так делать не надо было :), но все сводилось к тому,
что нужно юзать <tt class="docutils literal">fab</tt> команду или отказаться совсем;</li>
<li>для меня было удивительным, что <tt class="docutils literal">fabric</tt> (или <tt class="docutils literal">paramiko</tt>) не подхватывает
<tt class="docutils literal"><span class="pre">~/.ssh/config</span></tt> (хотя ключи и <tt class="docutils literal">known_hosts</tt> подхватывает). Т.е. в хостах
нужно явно прописывать пользователя, когда пользователь на сервере не
совпадает, чтоб задача отработала без паролей по ключам. Хотя этого
пользователя <a class="reference external" href="http://docs.fabfile.org/en/1.2.2/usage/fab.html#settings-files">можно прописать в настройках</a> или задать через параметры.</li>
</ul>
</dd>
</dl>
<p>В принципе это не критичные моменты, библиотека делает свое дело. Для разработки
на винде, возможно, это лучшее решение, т.к. тут свой ssh клиент <tt class="docutils literal">paramiko</tt>,
но я - не на винде :).</p>
</div>
<div class="section" id="python-1">
<h1>“Чистый” python</h1>
<p>Со временем понял, что из <tt class="docutils literal">fabric</tt> мне больше всего нужны функции <tt class="docutils literal">local</tt> и
<tt class="docutils literal">run</tt>, а мои методы деплоя простые и не нужна особенность <tt class="docutils literal">fabric</tt> для
работы с множеством серверов.</p>
<p>Итак, чтоб сделать <tt class="docutils literal">local</tt> c перехватом вывода и без, нужно всего-то:</p>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">subprocess</span> <span class="kn">import</span> <span class="n">call</span><span class="p">,</span> <span class="n">Popen</span><span class="p">,</span> <span class="n">PIPE</span><span class="p">,</span> <span class="n">STDOUT</span><span class="w">

</span><span class="c1"># With capture</span><span class="w">
</span><span class="n">cmd</span> <span class="o">=</span> <span class="n">Popen</span><span class="p">(</span><span class="s1">'ls -la'</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">stdout</span><span class="o">=</span><span class="n">PIPE</span><span class="p">,</span> <span class="n">stderr</span><span class="o">=</span><span class="n">STDOUT</span><span class="p">)</span><span class="w">
</span><span class="nb">print</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">communicate</span><span class="p">()[</span><span class="mi">0</span><span class="p">])</span><span class="w">

</span><span class="c1"># Without capture</span><span class="w">
</span><span class="n">call</span><span class="p">(</span><span class="s1">'ls -la'</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</pre>
<p>Теперь можно вспомнить про <a class="reference external" href="https://pusto.org/post/python-argparse-subcommands/">argparse и его сабкоманды</a> и уже можно создавать
свои <tt class="docutils literal">manage.py</tt> на чистой стандартной библиотеке.</p>
<p><strong>А что будем делать с деплоем?</strong></p>
<p>Все просто :) - использовать стандартный клиент <tt class="docutils literal">ssh</tt>.</p>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">subprocess</span> <span class="kn">import</span> <span class="n">call</span><span class="w">

</span><span class="n">commands</span> <span class="o">=</span> <span class="s1">'&amp;&amp;'</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="s1">'ls -la'</span><span class="p">,</span> <span class="s1">'uptime'</span><span class="p">])</span><span class="w">
</span><span class="n">call</span><span class="p">(</span><span class="s1">'ssh pusto.org "</span><span class="si">%s</span><span class="s1">"'</span> <span class="o">%</span> <span class="n">commands</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</pre>
<p>Т.е. мы можем делать развертывание проекта при помощи стандартной библиотеки
python и клиента ssh, который у меня точно есть под рукой.</p>
</div>
<div class="section" id="section-1">
<h1>Итого</h1>
<p>Минимизация команд - это классный подход. Хочется обратить внимание на
возможности стандартной библиотеки python и лишний раз задуматься, а стоит ли
добавлять в зависимости проекта <em>“жирную”</em> библиотеку (аля <tt class="docutils literal">fabric</tt>)…</p>
<p><strong>P.S.</strong> Еще пара ссылок на инструменты касающиеся темы: <a class="reference external" href="http://python-doit.sourceforge.net/">doit</a>, <a class="reference external" href="http://paver.github.com/paver/">paver</a>.</p>
</div>
 ]]></content>
    </entry>
    <entry xml:base="https://pusto.org/post/feed.xml">
        <title type="text">Python. Компактные тесты</title>
        <link href="https://pusto.org/post/python-compact-tests/"/>
        <id>https://pusto.org/post/python-compact-tests/</id>
        <updated>2011-01-15T08:00:00+02:00</updated>
        <author>
            <name>naspeh</name>
        </author>
        <content type="html"><![CDATA[ <p>Код в тестах обычно простой, т.к. выполняет довольно тривиальные операции
проверки, сравнения и т.д. Когда много похожего кода, то логично подумать о
его краткости.</p>
<!-- MORE -->
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last"><a class="reference external" href="http://packages.python.org/nose/"><strong>nose</strong></a> у меня давно вошел в набор обязательных инструментов для
тестирования, так что в примерах есть его влияние.</p>
</div>
<div class="section" id="unittest">
<h1>Подход 1. Класс-контейнер с тестами в виде методов, аля <a class="reference external" href="http://docs.python.org/library/unittest.html">unittest</a></h1>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">unittest</span> <span class="kn">import</span> <span class="n">TestCase</span><span class="w">

</span><span class="n">answer</span> <span class="o">=</span> <span class="mi">42</span><span class="w">


</span><span class="k">class</span> <span class="nc">TestAnswer</span><span class="p">(</span><span class="n">TestCase</span><span class="p">):</span><span class="w">
</span>    <span class="k">def</span> <span class="nf">setUp</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span>        <span class="nb">print</span><span class="p">(</span><span class="s1">'setup'</span><span class="p">)</span><span class="w">

</span>    <span class="k">def</span> <span class="nf">test</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span>        <span class="bp">self</span><span class="o">.</span><span class="n">assertEquals</span><span class="p">(</span><span class="n">answer</span><span class="p">,</span> <span class="mi">42</span><span class="p">)</span><span class="w">

</span>    <span class="k">def</span> <span class="nf">tearDown</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span><span class="w">
</span>        <span class="nb">print</span><span class="p">(</span><span class="s1">'teardown'</span><span class="p">)</span>
</pre>
<p><a class="reference external" href="http://ru.wikipedia.org/wiki/JUnit">Исторически сложилось</a>, что <tt class="docutils literal">unittest</tt> использует <a class="reference external" href="http://ru.wikipedia.org/wiki/CamelCase">верблюжью нотацию</a>
для <tt class="docutils literal">setUp</tt>, <tt class="docutils literal">tearDown</tt>, <tt class="docutils literal">assert*</tt> (<tt class="docutils literal">assertTrue</tt>, <tt class="docutils literal">assertEquals</tt>…)
методов. Но в python есть <a class="reference external" href="https://peps.python.org/pep-0008">PEP 8</a>, в котором принято использовать подчеркивание
в названиях функций (методов), и в <tt class="docutils literal">nose.tools</tt> можно найти аналогичные
функции, но с подчеркиванием (<tt class="docutils literal">assert_true</tt>, <tt class="docutils literal">assert_equals</tt>) для
любителей <a class="reference external" href="https://peps.python.org/pep-0008">PEP 8</a>.</p>
</div>
<div class="section" id="section-1">
<h1>Подход 2. Модуль с тестами в виде функций</h1>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">nose.tools</span> <span class="kn">import</span> <span class="n">assert_equal</span><span class="p">,</span> <span class="n">with_setup</span><span class="w">

</span><span class="n">answer</span> <span class="o">=</span> <span class="mi">42</span><span class="w">


</span><span class="k">def</span> <span class="nf">setup_func</span><span class="p">():</span><span class="w">
</span>    <span class="nb">print</span><span class="p">(</span><span class="s1">'setup'</span><span class="p">)</span><span class="w">


</span><span class="k">def</span> <span class="nf">teardown_func</span><span class="p">():</span><span class="w">
</span>    <span class="nb">print</span><span class="p">(</span><span class="s1">'teardown'</span><span class="p">)</span><span class="w">


</span><span class="nd">@with_setup</span><span class="p">(</span><span class="n">setup_func</span><span class="p">,</span> <span class="n">teardown_func</span><span class="p">)</span><span class="w">
</span><span class="k">def</span> <span class="nf">test_answer</span><span class="p">():</span><span class="w">
</span>    <span class="n">assert_equal</span><span class="p">(</span><span class="n">answer</span><span class="p">,</span> <span class="mi">42</span><span class="p">)</span>
</pre>
<p>В последнее время <strong>второй подход</strong> компоновки тестов мне все больше нравится.
Почему?</p>
<ul>
<li><p class="first">и в первом и во втором подходе приходится давать имя модулю, который будет
содержать наши тесты. Но в первом нужно еще придумывать имя классу-контейнеру,
а т.к. мне нравятся небольшие модули (в них проще ориентироваться), то в
большинстве случаев название класса - тавтология:</p>
<pre class="literal-block">
test_auth.py:TestAuth.test_login
test_auth.py.test_login
</pre>
</li>
<li><p class="first">в первом подходе вроде лучше выглядят <tt class="docutils literal">SetUp</tt>, <tt class="docutils literal">TearDown</tt> методы, во
втором приходится импортировать декоратор <tt class="docutils literal">with_setup</tt>. Но и тут можно
выделить плюс, обычно название класса подбираю по содержимым тестам</p>
<pre class="code python literal-block">
<span class="k">class</span> <span class="nc">TestAuth</span><span class="p">(</span><span class="n">TestCase</span><span class="p">):</span><span class="w">
</span>    <span class="k">def</span> <span class="nf">test_login</span><span class="p">()</span><span class="w">
</span>        <span class="o">...</span><span class="w">
</span>    <span class="k">def</span> <span class="nf">test_logout</span><span class="p">()</span><span class="w">
</span>        <span class="o">...</span>
</pre>
<p>но когда для <tt class="docutils literal">test_login</tt> нужен <tt class="docutils literal">setUp</tt> метод, а для <tt class="docutils literal">test_logout</tt> нет,
то тут приходится класс-контейнеры компоновать в зависимости от используемых
<tt class="docutils literal">SetUp</tt>, <tt class="docutils literal">TearDown</tt> методов. В общем присутствует неоднозначность и это
не очень хорошо :)</p>
</li>
<li><p class="first">в классе-контейнере забирается один отступ, а отступы ценны, когда соблюдаешь
ограничение в 80 символов;</p>
</li>
<li><p class="first">в первом подходе класс наследуется от <tt class="docutils literal">unittest.TestCase</tt>, при вызове
каждого <tt class="docutils literal">assert*</tt> метода логично обращаться к <tt class="docutils literal">self</tt> и тут опять у нас
крадут символы:</p>
<pre class="literal-block">
self.assertEqual
assert_equal...4
</pre>
</li>
</ul>
</div>
<div class="section" id="doctest">
<h1>Для написания тестов можно использовать <a class="reference external" href="http://docs.python.org/library/doctest.html">doctest</a></h1>
<pre class="code python literal-block">
<span class="ln"> 1 </span><span class="n">answer</span> <span class="o">=</span> <span class="mi">42</span><span class="w">
</span><span class="ln"> 2 </span><span class="w">
</span><span class="ln"> 3 </span><span class="w">
</span><span class="ln"> 4 </span><span class="w"/><span class="k">def</span> <span class="nf">test_answer</span><span class="p">():</span><span class="w">
</span><span class="ln"> 5 </span><span class="w">    </span><span class="sd">'''
</span><span class="ln"> 6 </span><span class="sd">    &gt;&gt;&gt; answer
</span><span class="ln"> 7 </span><span class="sd">    42
</span><span class="ln"> 8 </span><span class="sd">    '''</span><span class="w">
</span><span class="ln"> 9 </span><span class="w"/>    <span class="k">assert</span> <span class="kc">False</span>
</pre>
<p>Выглядит кратко, хотя конечно такой формат тестов не всегда подходит…</p>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">Если запускать через <a class="reference external" href="http://packages.python.org/nose/">nose</a> (<strong>$ nosetests –with-doctest</strong>), то строка <strong>9</strong>
не вызывается.</p>
</div>
</div>
<div class="section" id="assert">
<h1>Классная вещь assert</h1>
<pre class="code python literal-block">
<span class="n">answer</span> <span class="o">=</span> <span class="mi">43</span><span class="w">


</span><span class="k">def</span> <span class="nf">test_answer</span><span class="p">():</span><span class="w">
</span>    <span class="k">assert</span> <span class="n">answer</span> <span class="o">==</span> <span class="mi">42</span>
</pre>
<p>После запуска, вывод:</p>
<pre class="code pytb literal-block">
<span class="x">$ nosetest
======================================================================
FAIL: test.test_answer
----------------------------------------------------------------------
</span><span class="gt">Traceback (most recent call last):
</span><span class="c">...</span><span class="w">
    </span><span class="k">assert</span> <span class="n">answer</span> <span class="o">==</span> <span class="mi">42</span><span class="w">
</span><span class="gr">AssertionError</span>
</pre>
<p>Очень заманчиво: не нужен дополнительный импорт, лаконично. Но вот при выводе
не известно какое значение содержит переменная <tt class="docutils literal">answer</tt>. Правда тут может
порадовать <a class="reference external" href="http://packages.python.org/nose/">nose</a> и даже двумя вариантами:</p>
<pre class="code pytb literal-block">
<span class="x">$ nosetests --pdb-failures
...
-&gt; assert answer == 42
(Pdb) answer
43</span>
</pre>
<p>приходится вводить <strong>answer</strong> - лишние телодвижения :).</p>
<p>Следующий вариант еще красивее:</p>
<pre class="code pytb literal-block">
<span class="x">$ nosetest -d
======================================================================
FAIL: test.test_answer
----------------------------------------------------------------------
</span><span class="gt">Traceback (most recent call last):
</span><span class="c">...</span><span class="w">
    </span><span class="k">assert</span> <span class="n">answer</span> <span class="o">==</span> <span class="mi">42</span><span class="w">
</span><span class="gr">AssertionError</span><span class="w">:
</span><span class="x">&gt;&gt;  assert 43 == 42</span>
</pre>
<p>так что, в принципе, тесты можно писать через <strong>assert</strong> без потери
информативности вывода, нужно только использовать правильные “пускальщики”.</p>
</div>
<div class="section" id="section-2">
<h1>Более краткие сигнатуры</h1>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">nose.tools</span> <span class="kn">import</span> <span class="n">eq_</span><span class="w">

</span><span class="n">answer</span> <span class="o">=</span> <span class="mi">43</span><span class="w">


</span><span class="k">def</span> <span class="nf">test_answer</span><span class="p">():</span><span class="w">
</span>    <span class="n">eq_</span><span class="p">(</span><span class="n">answer</span><span class="p">,</span> <span class="mi">42</span><span class="p">)</span>
</pre>
<p>После запуска, вывод:</p>
<pre class="code pytb literal-block">
<span class="x">FAIL: test.test_answer
----------------------------------------------------------------------
</span><span class="gt">Traceback (most recent call last):
</span><span class="c">...</span><span class="w">
    </span><span class="n">eq_</span><span class="p">(</span><span class="n">answer</span><span class="p">,</span> <span class="mi">42</span><span class="p">)</span><span class="w">
</span><span class="gr">AssertionError</span>: <span class="n">43 != 42</span>
</pre>
<p>Заменили <tt class="docutils literal">assert_equal</tt> на более короткий вариант <tt class="docutils literal">eq_</tt>, вывод ошибки будет
полностью аналогичен. Т.е. при выводе увидим, что <tt class="docutils literal">answer</tt> на самом деле
<strong>43</strong> и пойдем сразу искать ошибку в коде. Один нюанс, что тесты не
заканчиваются проверкой на <tt class="docutils literal">eq_</tt> и <tt class="docutils literal">ok_</tt>, которые есть в <tt class="docutils literal">nose.tools</tt>,
набор методов нужен более обширный…</p>
</div>
<div class="section" id="section-3">
<h1>Интересное по теме</h1>
<ul>
<li><p class="first"><a class="reference external" href="http://pytest.org/">pytest</a> - это аналог <a class="reference external" href="http://packages.python.org/nose/">nose</a>, со своими “плюшками”, <a class="reference external" href="http://pytest.org/latest/nose.html">он умеет</a> запускать
большинство тестов написанных для <a class="reference external" href="http://packages.python.org/nose/">nose</a>.</p>
</li>
<li><p class="first"><a class="reference external" href="http://github.com/dag/attest">attest</a> - интересный подход (python way) от известной команды <a class="reference external" href="http://www.pocoo.org/">Pocoo</a>.
Пример из документации:</p>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">attest</span> <span class="kn">import</span> <span class="n">Tests</span><span class="w">
</span><span class="n">math</span> <span class="o">=</span> <span class="n">Tests</span><span class="p">()</span><span class="w">

</span><span class="nd">@math</span><span class="o">.</span><span class="n">test</span><span class="w">
</span><span class="k">def</span> <span class="nf">arithmetics</span><span class="p">():</span><span class="w">
    </span><span class="sd">"""Ensure that the laws of physics are in check."""</span><span class="w">
</span>    <span class="k">assert</span> <span class="mi">1</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">==</span> <span class="mi">2</span><span class="w">

</span><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">'__main__'</span><span class="p">:</span><span class="w">
</span>    <span class="n">math</span><span class="o">.</span><span class="n">run</span><span class="p">()</span>
</pre>
</li>
<li><p class="first"><a class="reference external" href="http://packages.python.org/Oktest/">Oktest</a> для лаконичности - идея прикольная. Пример из документации:</p>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">oktest</span> <span class="kn">import</span> <span class="n">ok</span><span class="w">

</span><span class="n">ok</span> <span class="p">(</span><span class="n">x</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span>                 <span class="c1"># same as assert_(x &gt; 0)</span><span class="w">
</span><span class="n">ok</span> <span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">==</span> <span class="s1">'foo'</span>            <span class="c1"># same as assertEqual(s, 'foo')</span><span class="w">
</span><span class="n">ok</span> <span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">!=</span> <span class="s1">'foo'</span>            <span class="c1"># same as assertNotEqual(s, 'foo')</span>
</pre>
</li>
</ul>
</div>
<div class="section" id="section-4">
<h1>Итого</h1>
<p>В <strong>python</strong> есть множество способов для написания и запуска тестов, в статье
упоминаются не все. Если задаться целью, то можно писать красивые и лаконичные
тесты.</p>
</div>
 ]]></content>
    </entry>
</feed>