Пару-тройку месяцев назад мне нужна была идея для проекта и в итоге решил написать webmail клиент с gmail-подобными цепочками писем. Это то чего мне не хватает, и чем бы пользовался регулярно на ежедневной основе.
Mailr на ранней стадии разработки, еще очень многое предстоит сделать.
Сейчас есть демо концепта, которое довольно быстро работает. Из того что сделано большая часть не очень видна, так как она связана с IMAP общением, отложенной синхронизацией, парсингом писем, а видимая часть – немного рабочего интерфейса. Можно послать письмо на demo[at]pusto.org и оно появится в Inbox.
Рабочее название проекта Mailr. Код на github.
Mailr будет иметь быстрый и удобный веб интерфейс, которым будет удобно пользоваться на небольшом экране ноутбука, на большом мониторе и на iPad Mini, все эти девайсы у меня есть и хочется иметь единый настраиваемый интерфейс для них.
Mailr будет иметь gmail-совместимый режим через IMAP, чтоб можно было вернуться в любое время на gmail. Так как пока версия под мобильные телефоны не планируется, то этот режим будет тоже полезен, можно будет параллельно коннектиться к gmail привычным мобильным клиентом, если нужно. Кроме прочего с gmail за спиной проще начинать разработку и сконцентрироваться на удобном интерфейсе.
Многие функции из gmail нужно реализовать: удобные цепочки писем, метки, быстрый поиск, фильтры для сортировки входящей почты, хороший механизм схлопывания цитируемых писем, горячие клавиши, поддержка SSL…
Также будут дополнительные функции.
Объединение цепочек писем. Google хорошо находит соответствие писем и цепочек, но иногда его алгоритмы не работают:
Возможность вручную объединить цепочки – это выход в таких ситуациях.
Markdown для написания писем. Мне нравится Markdown и reStructuredText и мне бы хотелось писать письма используя эти текстовые языки разметки, после конвертации они выдают отличный для чтения HTML. Текущий редактор писем в gmail для меня очень неудобный.
Две панели. Это моя любимая функция :), две панели видно на скриншоте. Давно уже использую двухпанельный режим в своем текстовом редакторе VIM и мне уже не комфортно в однопанельных редакторах. Вторая панель расширяет обычно контекст, когда работаешь в первой. В будущем будет возможность отключить этот режим.
Настраеваемый интерфейс. Как говорил выше, мне нужен удобный интерфейс на разнообразных разрешениях экрана, темы и настройки интерфейса будут решать эту задачу.
Вся почта в одном табе. Я использую Chrome и мне нравится, что он открывает “Настройки”, “Скачанные файлы”, “Расширения” в новых вкладках, а не окнах (раньше использовал Firefox – он многое открывает в новых окнах). У меня весь серфинг интернета живет в одном окне браузера, а вся почта, в идеале, хочется, чтоб жила в одной вкладке (включая все аккаунты)
Простой backup. Это важно для Open Source продукта, чтоб была возможность взять все данные (аккаунты, фильтры, цепочки) и перенести с одной инсталяции на свою локальную или на сервер своего проверенного друга-гика.
Когда можно будет использовать Mailr c gmail в качестве IMAP сервера, то дальше мне хочется уйти все таки от gmail и использовать свой email адрес. И, скорее всего, следующим шагом будет интеграция с Mailgun. Поднять свой правильный почтовый сервер с антиспам-фильтром – дело не самое легкое, с Mailgun будет проще, тем более они не хранят письма у себя.
Дальше много мыслей для продолжения: поддержка других IMAP серверов, множественные аккаунты в одном табе, PGP шифрование, списки рассылок для друзей…
Хочется в этом проекте использовать минимум зависимостей и непереусложнить с кодом, ведь потом все нужно будет поддерживать.
Да, только jquery – из-за архитектурного решения. Мне больше нравится писать Python код, а JavaScript хочется очень минимизировать, поэтому вместо модного REST и рендеринга на стороне клиента мне захотелось генерировать семантичный HTML на стороне сервера. Это полезно, например, для iPad Mini, в нем процессор слабее и памяти меньше, чем обычно на ноутбуках и десктопах. В будущем эта ситуация может измениться.
В этом проекте еще нужно многое придумать, многое реализовать, многое оптимизировать. В последнее время я занимался им очень интенсивно, но мой отпуск заканчивается и нужно возвращаться к работе, то есть времени на проект будет намного меньше. Очень хочется его довести до стадии, чтоб заменить наконец gmail :).
Open Source – это круто и мне всегда хотелось отдать дань этому сообществу. Если задуманное мной в этом email клиенте удастся реализовать и получится хороший продукт, то это будет отличный вклад.
Статья была на хабре, но им не понравилась ссылка на демо %).
И, конечно, смотрел на mailpile.is, но они пошли странным путем.
Note
Слово бекап (англ. backup) использую в тексте потому, что оно короче и привычнее русского аналога “резервная копия”.
В июле купил SSD диск для ноута и решил, что нужно поставить Arch Linux кошерно с нуля. До этого у меня проработала инсталяция около года без переустановки, казусы случались, но их удавалось решить на существующей системе. Ну и хотелось этот новенький чистенький Arch забекапить и вообще сделать хорошую регулярную систему бекапов для моих рабочих машин.
До этого уже занимался бекапами. В одной из версий использовал duplicity. Последняя была основана на rsync, а дельты упаковывались в tar, на Debian Testing у меня эта система работала регулярно по крону, а в Arch Linux использовал нерегулярно вручную, хотелось ее переделать.
Перед тем как делать систему бекапов, опять стал смотреть на заточенные для этого инструменты. Внимательно смотрел на rdup, bup и obnam. Последние две используют свои репозитории, в которых реализуют дедупликацию файлов. У bup формат совместимый с git. Смотрел на них потому, что можно было отказаться от разделения на latest и delta, ведь они бы сами все красиво раскладывали. Проблема с нестандартными форматами репозиториев такая же как и для архивов, то что нельзя просто посмотреть ls. У bup есть даже своя FUSE файловая система, но в ней не видны атрибуты файлов (можно посмотреть список файлов, но дату изменения или владельца файла посмотреть нельзя). rdup можно было бы использовать, но все таки вернулся к старому способу с rsync. В свое время отказался от duplicity и от архивов в целом опять же из-за простоты доступа к файлам. Хоть с обычными архивами умеет прозрачно работать mc, но делать инкрементный бекап на архиве невозможно (ну или слишком сложно), а разархивирование нескольких гигов данных даже без компрессии - операция не самая быстрая.
Note
От шифрования и архивов отказался, потому что не храню бекапы в интернете.
К схеме с latest & delta пришел потому, что реально мне важно было сохранить последнее состояние системы, а дельта нужна была для возможности посмотреть на старые файлы, которые случайно удалил, или посмотреть на файлы с какой-то важной информацией, которой нет в последней версии. Мне чаще нужно посмотреть что изменилось в системе, и дельта тут тоже очень кстати, ведь если сохранять снимок всех данных, то нужно производить лишние действия, чтоб получить дельту . Еще заморозкой дельты решаю вопрос данных, которые хорошо бы сохранить и которые хочется почистить в текущей файловой системе (то есть удаляю файлы, а потом архивирую дельту с определенной меткой). В общем схема с latest & delta мне очень подходит.
Схема вроде ясная, сделано так, чтоб к ноуту не нужно было ничего подключать для сохранения копии бекапов, а так как в домашней сети нахожусь очень часто, то достаточно чтоб был включен неттоп для получения двух свежих копий.
На внешний винчестер ставлю тоже Arch (называю его LiveHard, по аналогии с LiveCD), чтоб иметь под рукой загрузочный винт с привычно настроенным окружением. Кроме того у меня есть еще один внешний винчестер (более старый с USB 2.0), из него делаю тоже LiveHard, чтоб таскать с собой, ведь первый постоянно висит на неттопе.
Note
Для бекапа системы в первую очередь нужно сохранить правильный список пакетов. Обычный "pacman -Q" не подходит, потому что мне нужны пакеты установленные мной и не включая две базовые группы пакетов base и base-dev, потому что обе группы обычно ставятся в начале установки нового Arch. В принципе, с pacman это делается несложно:
$ comm -23 <(pacman -Qeq) <(pacman -Qgq base base-devel | sort) > /backup/pkglist.txt
Полезность этого списка не только в целях бекапа, но и просто посмотреть пакеты, которые не используются и можно удалить, чтоб система была максимально чистой.
Дальше нужно сохранить все важные файлы:
$ rsync -aAXHvh --delete \ > --backup --backup-dir=/backup/delta/ \ > -f '+ /boot/' -f '+ /etc/' -f '+ /home/' -f '- /**/*.pyc' -f '- /*' \ > / /backup/latest/
Появляется latest & delta, при этом delta у меня не сохраняется каждый раз, а накапливается в одной папке. А tar архив создается отдельной командой, которая запускается реже, чем регулярный бекап:
$ cd /backup && tar -cvf $(date '+%Y-%m-%d').tar --directory=delta .
Или с меткой о каком-то намеренном удалении:
$ cd /backup && tar -cvf $(date '+%Y-%m-%d')--cleanup-home.tar --directory=delta .
Потом все перекидываем по SSH на неттоп:
$ rsync -aAXHvhx --delete /backup/ box:/backups/pad/
В целом это уже рабочая система бекапов. Вначале не думал про полный бекап и новую систему установил без LVM. Но потом решил, что полный бекап нужен, и выбрал снепшоты LVM, потому что Btrfs никогда не использовал и репутация у этой файловой системы не самая лучшая. Я скопировал через dd необходимые разделы и записал их на внешний винт, чтоб сделать новую разметку диска для LVM. Когда закончил с новой разметкой, то обнаружил, что главный раздел у меня не записался полностью o_O, не хватило места на винте, а на сообщение не обратил внимание. Но не все так плохо, у меня ведь был бекап, который как раз для подобных неожиданных случаев и создан.
Немного про разметку диска. Раньше любил выносить home на отдельный раздел, но по сути home у меня - это набор конфигурационных файлов, а downloads, music и рабочие директории выношу на отдельный большой раздел. В итоге схема следующая:
- /dev/sda1 EFI System /boot 100-200 MB + /dev/sda2 Linux LVM 128GB - /dev/pad/root /root 30GB - /dev/pad/arch /arch 50GB - остальное место для снепшотов или для увеличения разделов
Отдельный раздел для загрузки обязателен для UEFI загрузчика, а остальное отдается для LVM. Так как за ноутбуком обычно работаю, то 50GB на /arch раздел мне достаточно. Для мультимедия у меня есть неттоп с хорошим большим монитором и хорошими колонками.
Режим востановления из неполного бекапа следующий:
# гружусь с 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, перегружаюсь
Система готова и находится в полном соответствии со старой. В принципе, шагов не много, но было бы проще и быстрее с полным бекапом.
Полный бекап делаю через LVM снепшот плюс опять же rsync:
$ lvcreate --size 10G --snapshot --name snap /dev/pad/root \ > && mount /dev/pad/snap /backups/mnt \ > && rsync -aAXHvhyx \ > --exclude="{/dev/*,/proc/*,/sys/*,/tmp/*,/run/*,/mnt/*,/media/*,/lost+found}" > && umount /backups/mnt > && lvremove -f /dev/pad/snap
Для регулярного запуска бекапов использую асинхронный cron. Асинхронный потому, что хотя и работаю за ноутбуком регулярно, но работаю в разное время. fcron может запускать команды в зависимости от времени работы ноутбука, например каждые шесть часов работы. Обычный cron рассчитан на то, что машина все время работает.
Теперь если вспомнить про мой backup скрипт и добавить, что он был написан с оглядкой на крон и в нем реализовано логирование, то fcrontab будет выглядеть очень просто:
SHELL=/bin/zsh PATH="/usr/bin:/root/bin" BACKUP_LOG=1 @ 6h backup run && backup call pad_to_box @ 2d1h backup tar @ 2d2h backup full @ 2d4h backup full
Таким образом, у меня каждые шесть часов делается неполный бекап, а раз в два дня - полный бекап и архивирование delta.
Кроме всего, полный бекап используется для разворачивания новых Arch Linux, например на неттопе и LiveHards. Теперь развернуть привычно настроенный Arch очень просто и быстро.
]]>С апреля уже не работаю в ostrovok.ru, но опыт по внедрению тестов в разработку, полученный в этой команде, очень хороший. Хочу записать по горячим следам ряд практических советов и замечаний по поводу внедрения тестов и django тестов в частности.
Внедрять тесты в уже существующий проект с кучей всесторонних зависимостей от внешних сервисов и API - задача, требующая довольно много времени. Плюс возвращаться и дорабатывать их нужно будет не один раз. Хорошо что в ostrovok.ru все понимали, что тесты нужны, просто не знали с какой стороны к ним подойти и нужен был человек, который “заражен” тестами и возьмется за внедрение.
Про первые шаги писал на хабре (26.06.2012), потом позже рассказывал на Pycon Russia (24.02.2013) про оптимизацию и запуск тестов в несколько процессов. Когда уходил из ostrovok.ru тестов было ~1000 (6 мин на моем ноутбуке, 4 процесса), через полгода сказали, что уже ~1500 (6 мин на CI сервере, больше 10 процессов). Тесты удались, хотя некоторые места хотелось бы сделать лучше.
Поговорим о некоторых проблемах и способах их не допустить.
В моем мозгу изоляция окружения для конкретного теста - это обязательное условие для существования хороших тестов. Для изоляции базы самый верный способ - это транзакции, они быстрые. В начале теста мы открываем транзакцию, а после прохождения теста делаем ее откат (rollback), ну и условие напрашивается само - не должно быть прерывания открытой транзакции. У нас вышло так, что в процессе бронирования было ручное управление транзакциями (это и есть прерывание), а тестов вокруг бронирования у нас было очень много (больше всего хотелось покрыть тестами именно этот процесс), поэтому мне пришлось придумывать свой механизм изоляции базы на основе ее копирования из шаблона. Хотя этот механизм был быстрее стандартного в django для этого случая, но он был медленнее и сложнее транзакций. Поэтому когда делался запуск тестов в несколько процессов, пришлось приложить больше усилий, чтоб все наши тесты хорошо работали.
Нужно стараться, чтоб в транзакциях работали максимум тестов. Транзакции хорошо масштабируются на несколько процессов, а те тесты, в которых тестируются именно транзакции, лучше пускать отдельно в одном процессе уже после основной пачки тестов.
Хорошие тесты - это те, которые запускаешь и через какое-то реальное время получаешь фитбек прошли или не прошли, для этого вообще-то тесты и вводятся. Если тесты запускаются долго, это плохо скажется на процессе тестирования в команде. Для меня реальное время - до 10 минут, меньше конечно лучше, но больше - это сигнал, что тесты нужно срочно ускорять. В какой-то момент количество тестов выросло до ~800, время 15 минут, всевозможные локальные оптимизации кода в тестах произведены, а время еще нужно было сокращать, т.к. тестов становилось все больше. Очень хороший скачек в ускорении тестов - это запуск в несколько процессов, хотя этот метод добавляет определенную сложность тестовой среде. Кроме изоляции базы на уровне процессов, также необходима изоляция всевозможных кешей, redis. У нас вышло все еще сложнее. Реализация тестовой среды как-то была совсем не готова к нескольким процессам и нужна была значительная переработка, которая в итоге состоялась. Мы перенесли всю логику изоляции всего на уровень TestCase, это нам дало возможность не только запустить тесты в несколько потоков, но также и опробовать разные пускальщики: nose2, pytest, но мы так и остались на nose первом. С каждым из пускальщиков были какие-то проблемы. pytest был очень медленным на наших тестах при распараллеливании. nose2 у нас какое-то время поработал на CI, но он зависал при исключениях в многопроцессорном режиме. У nose1 хоть и были проблемы с неработающими плагинами в multiprocess, но xmlunit отчет как-то прикрутили для CI, а без других обходились, главное что тесты работали значительно быстрее. На одном хакатоне мы опробовали запускать наши тесты на крутом серваке (с 24 ядрами, кажется), так у нас ~800 тестов проходили за пару минут в 20 процессов. Но самый большой прирост заметен обычно при 2-4 процессах. На моем ноутбуке в 4 процесса тесты проходили меньше 6 минут, но у меня реальных было 2 ядра и два виртуальных :)
Хорошо бы, чтобы тестовая среда была готова к запуску в несколько процессов, благо если учесть, что используются транзакции в тестах, то решается это довольно легко, проект-шаблон доступен на github.
Опять же в моем мозгу сидит, что все внешние вызовы должны быть “замоканы” для того чтоб тесты были предсказуемые и быстрые. Даже самый быстрый и надежный сторонний сервис может дать сбой или ответить каким-то не совсем ожидаемым способом, и тогда тесты упадут. Но тесты не должны падать из-за внешних условий, поэтому мы подменяем действительность моками и прописываем те ответы, которые мы ожидаем в тестах. Есть еще ряд случаев, в которых без моков не обойтись, это могут быть разные моменты со временем, проверка на прошлое или будущее, проверка выпадения исключений и т.п. В общем в проекте с тестами скорее всего будут моки, у нас их было много.
Хоть мы имеем дело с динамическим языком python и модуль mock очень помогает нам, но моки - это в своем роде магия, и у модуля mock есть свои нюансы и ограничения, про которые не все разработчики знают. Время от времени моки становились проблемой, особенно когда их сайд-эффекты сложно было определить и особенно у тех разработчиков, которые мало общались с моками. К ним нужно привыкнуть, с опытом их использовать и чинить становится проще.
Если над проектом постоянно работают, статические фикстуры - зло. Мы старались использовать минимум фикстур, но совсем без них не обойтись. Все эти сторонние сервисы и API требуют моков, а моки за собой тянут фикстуры. У нас статических фикстур было немного, были даже скрипты, которые могли их обновлять. Но процесс обновления фикстур не был встроен в CI, поэтому зависимость тестов от них стала одной из проблем, которую так до конца и не решили.
По-хорошему, фикстуры должны генерироваться динамически и этот процесс нужно встроить в CI. Сторонние сервисы тоже разрабатываются, и придет время, когда нужно будет обновить фикстуры до новой версии. А зависимых тестов может оказаться довольно много, когда в команде пишут тесты несколько человек и не все знают про возможные проблемы, а тесты писать нужно… Динамические фикстуры или постоянное обновление статических фикстур препятствуют появлению зависимых тестов.
Вообще фикстуры, по моему мнению, одна из самых сложных задач при долгосрочном тестировании, т.е. попасться в капкан зависимости от них довольно легко.
Тесты - это как живой организм, который мы приручаем, а потом за ним нужно следить и ухаживать, чтоб этот организм помогал нам в решении продуктовых задач. Да, конечно, продуктовые задачи важнее, ведь не тесты делают пользователя счастливым, но они - часть процесса разработки и помогают разработчикам быть уверенными в каждодневных изменениях, которые они вносят для улучшения продукта. Тесты - это долгосрочная перспектива, а с любым долговременным делом связано много сложностей и проблем, которые нужно помнить и предугадывать, чтоб вовремя принимать корректирующие решения.
Следите за своими тестами и пусть они вам помогают писать хороший код.
Поговорим об улучшении использования argparse и подкоманд в повседневной жизни.
В моей практике почти в каждом проекте есть интерфейс для командной строки, это может быть manage.py в веб проекте, просто скрипт бекапа или даже приложение GTK. В python 2.7 и 3.2 появился очень мощный модуль argparse для обработки параметров командной строки, и в нем есть “из коробки” поддержка подкоманд и это очень круто. Но есть в этом модуле маленький недостаток - интерфейс его использования немного избыточен.
Для начала нужно глянуть что уж такого плохого в интерфейсе, рассмотрим простой пример:
#!/usr/bin/env python import argparse def run_test(module, settings=None): pass def run_server(host, port, no_reload=False, settings=None): pass def parse_args(args=None): parser = argparse.ArgumentParser(prog='app') cmds = parser.add_subparsers(help='commands') cmd_run = cmds.add_parser('run', help='start dev server') cmd_run.add_argument('-s', '--settings', help='application settings') cmd_run.add_argument( '-P', '--port', type=int, default=8000, help='server port' ) cmd_run.add_argument( '-H', '--host', default='localhost', help='server host' ) cmd_run.add_argument( '--no-reload', action='store_true', help='without reloading' ) cmd_run.set_defaults(func=lambda a: ( run_server(a.host, a.port, a.no_reload, settings=a.settings) )) cmd_test = cmds.add_parser('test', aliases=['t', 'te'], help='run tests') cmd_test.add_argument('-s', '--settings', help='application settings') cmd_test.add_argument('target', default='.', help='python module or file') cmd_test.set_defaults( func=lambda a: run_test(a.module, settings=a.settings) ) args = parser.parse_args(args) if not hasattr(args, 'func'): parser.print_usage() else: args.func(args) if __name__ == '__main__': parse_args()
Вроде не так уж все и плохо, обычный интерфейс. Есть дублирование параметра --settings, но чтоб он был привязан к каждой подкоманде его нельзя вешать на базовый парсер. Также нам пришлось переносить строки для соблюдения PEP 8, при том что не помещались считанные символы. Можно укоротить переменные cmd_run, cmd_test до run, test или даже до r, t, но суть не в этом. Эти переменные, в принципе, не нужны, если добавить цепочки вызовов:
cmds.add_parser('run').add_argument('port').set_defaults(func=run_server)
На чистом argparse цепочек вызовов не получится, хотя может в каких-то случаях использования они и не нужны. В моей практике чаще хочется цепочек.
В самом начале примера объявлена пара функций и есть проекты, которые превращают эти функции в подкоманды, типа: opster, argh, komandr. Последние два основаны на argparse, а opster использует getopt.
В некоторых случаях использование подобных улучшаторов выглядит очень клево, например, использование argh:
#!/usr/bin/env python import argh @argh.aliases('t', 'te') def test(module, settings=None): '''run tests''' pass def run(host='localhost', port=8000, no_reload=False, settings=None): '''run dev server''' pass if __name__ == '__main__': argh.dispatch_commands([run, test])
Вывод главного help такой же как из первого примера:
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
А вот вывод help для определенной подкоманды отличается отсутствием описаний и различием коротких аналогов для параметров:
===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
В принципе, можно добиться полного соответствия help, но от этого уже будет страдать предельная лаконичность второго примера. Вообще-то, если названия параметров не требуют пояснений, то использовать argh очень заманчиво, тем более он, в принципе, позволяет добраться до обычного argparse, если где-то сталкиваешься с ограничениями.
Вся прелесть argparse, что с python 2.7 и 3.2 он входит в стандартную библиотеку и реально крут по сравнению с тем же getopt и optparse. А перечисленные выше улучшаторы - это отдельные пакеты и таскать их зависимостями в каждый проект не прикольно, особенно если проект минималистичный или небольшой скрипт с подкомандами. Еще в улучшаторах часто присутствует немного магии, argparse же прямой как двери.
Хорошо бы использовать argparse, но как-то покрасивее, чем в первом примере.
Следующий пример - мой любимый способ:
#!/usr/bin/env python import argparse def run_test(module, settings=None): pass def run_server(host, port, no_reload=False, settings=None): pass def parse_args(args=None): parser = argparse.ArgumentParser(prog='app') cmds = parser.add_subparsers(help='commands') def cmd(name, **kw): p = cmds.add_parser(name, **kw) p.set_defaults(cmd=name) p.arg = lambda *a, **kw: p.add_argument(*a, **kw) and p p.exe = lambda f: p.set_defaults(exe=f) and p # global options p.arg('-s', '--settings', help='application settings') return p cmd('run', help='start dev server')\ .arg('-P', '--port', type=int, default=8000, help='server port')\ .arg('-H', '--host', default='localhost', help='server host')\ .arg('--no-reload', action='store_true', help='without reloading')\ .exe(lambda a: ( run_server(a.host, a.port, a.no_reload, settings=a.settings) )) cmd('test', aliases=['t', 'te'], help='run tests')\ .arg('target', default='.', nargs='?', help='python module or file')\ .exe(lambda a: run_test(a.target, settings=a.settings)) args = parser.parse_args(args) if not hasattr(args, 'exe'): parser.print_usage() else: args.exe(args) if __name__ == '__main__': parse_args()
После использования в нескольких местах такого подхода мне все больше нравится отделение интерфейса функции от вызовов командной строки, все таки это немного разные вещи. Хотя раньше мне очень нравилось превращение функций в подкоманды.
Вывод довольно банальный: у python очень крутая стандартная библиотека, argparse - очень мощный инструмент для работы с параметрами командной строки. И даже если есть какие-то библиотеки с красивыми плюшками (argh, opster или docopt) у них скорее всего тоже найдутся свои недостатки, поэтому мой выбор - подточить использование argparse и забыть про дополнительные зависимости.
]]>В статье покажу как оптимизирую рабочий стол для маленького разрешения ноутбука (1280х800) и рассмотрю пару полезных утилит для работы с окнами в gnome второй версии.
Так повелось, что gnome у меня стал рабочим столом в linux. И так сложилось, что решил использовать ноутбук “по полной”, как рабочую лошадку, чтоб не возится с синхронизацией данных на нескольких рабочих машинах. Поэтому работу за ноутбуком нужно было сделать максимально удобной.
Note
Возможно скоро променяю gnome на какой-нибудь тайловый оконный менеджер и debian на archlinux, поэтому решил записать этот рецепт, которым пользуюсь уже давно, может кому еще пригодится.
До этого любил сидеть за монитором с большим разрешением 1600x1200 (Samsung-SyncMaster-204b, 20 дюймов), и когда садился за ноут (1280х800) мне было неудобно, что на экране мало всего помещается. Решение: нужно по максимуму оптимизировать визуальное пространство, занимаемое приложениями.
Первым делом думал избавиться от одной из панелей (по умолчанию в gnome их две, внизу и вверху экрана). Верхняя панелька мне очень понравилась еще при первом запуске gnome и ее удалять совсем не хотелось. Но места на ней совсем не хватало еще и для списка окон:
Список окон мне тоже оказался очень нужен и желательно, чтоб места для него было побольше:
Панельки так и остались у меня на своих местах :) как в debian по умолчанию. Я пробовал для них автоскрытие, но не пошло. Про режим на весь экран у приложений тоже помнил, но мне нужны были панельки, т.к. постоянно их использую.
Нужно было оптимизировать пространство между этими панельками.
Декорация окон. Когда окно не развернуто на весь рабочий стол, то в декорациях есть смысл: потянуть за заголовок, растянуть окно. А вот когда окно максимизировано, то частично смысл в декорациях теряется, да еще и место занимают. Действия типа: скрыть окно (Alt+F9), закрыть окно (Alt+F4), максимизировать (Alt+F10) хорошо выполняются через быстрые клавиши. Поэтому убираем декорацию окон, когда они максимизированы. Для этого есть пакет maximus в debian:
$ aptitude install maximus
По умолчанию maximus разворачивает все окна на весь экран. Есть вариант прописывать exclude_class, но я поступил по-другому - отключил максимизацию:
$ 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
Т.е. максимизировать окна буду вручную через Alt+F10.
В общем уже неплохо, но ряд приложений после запуска нужно сразу максимизировать, т.к. они не хотят запоминать своих размеров и положения… Лишние телодвижения: запустить, нажать Alt+F10, а хочется просто запустить.
И тут на помощь приходит devilspie. Он может работать не только с классами окон (exclude_class из maximus - это список классов окон), но и может проверить имя приложения, класс и имя окна и еще ряд атрибутов. Причем может проверить атрибут не только на соответствие, а и на содержание (contains) и соответствие регулярному выражению. Действия над окнами тоже разные: maximize, unmaximize, focus и undecorate даже :).
Инструмент нашли, дальше используем.
$ aptitude install devilspie
Создаем файл ~/.devilspie/common.ds. И помещаем туда что-то типа:
1 (begin 2 ;(debug) 3 (if 4 (or 5 (contains(application_name) "Vim") 6 (contains(application_name) "Terminal") 7 (contains(window_name) "New Tab - Google Chrome") 8 (contains(window_name) "FatRat") 9 (contains(window_name) "Document Viewer") 10 (contains(window_name) "Clementine") 11 (is(window_name) "DreamPie") 12 ) 13 (maximize) 14 ) 15 )
И добавляем devilspie в автозагрузку.
Обычно работаю с конфигом ~/.devilspie/common.ds следующим образом. Добавляю строку с дебагом (например: убрать “;” в начале строки №2 в приведенном выше листинге), убиваю процесс devilspie и запускаю его в терминале. В терминал начинают писаться атрибуты окон. Пример сессии:
$ 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
Потом открываю нужное мне окно, смотрю атрибуты, правлю конфиг, перезапускаю devilspie и так пока не будет все хорошо :).
Раз уж используем devilspie, можно с его помощью еще что-то замутить.
Например, Skype очень жутко ведет себя в linux. Один из боков: хочется чтоб окна чатов открывались в одном месте и одинакового размера. Если заниматься этим вручную, то тут нужно подгонять каждое новое окно чата мышкой, изрядно потыкав. И тут на помощь приходит действие geometry из devilspie.
Пример debug:
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
(if (and (contains(window_name) "Skype") (matches(window_role) "ConversationsWindow") ) (geometry "800x675+365-0") )
Для получения window_role использовал xprop, который содержится в x11-utils.
Есть действия, которые каждодневно повторяются, и если на них потратить немного времени и автоматизировать, то в конечном счете сэкономится пара ненужных телодвижений в день :). Как говорится: настрой свой linux под себя.
Напоследок скриншот экрана:
Дальше поговорим про организацию наших телодвижений в консоли.
Когда я работал в 42cc, в практике у нас было добавление Makefile в проект, куда записывались часто используемые команды, в итоге работа с проектом сводилась к:
make clean make runserver make test make deploy
так сказать псевдонимы, а команды на самом деле были примерно такие:
clean: -rm *~* -find . -name '*.pyc' -exec rm {} \; runserver: PYTHONPATH=$(PYTHONPATH) python django-project/manage.py runserver test: PYTHONPATH=$(PYTHONPATH) nosetests --with-django --django-settings=$(test_settings) $(module)
Но в общем подход по минимизации команд мне нравился. Можно переключиться на проект, посмотреть Makefile и понять что используется. Набирать команды короче. А еще у Makefile зачастую есть поддержка автодополнения, что тоже в повседневной разработке упрощает жизнь.
Потом я услышал про чудо библиотеку для деплоймента fabric и стал ее использовать. Но плодить fabfile.py, Makefile совсем не хотелось. Решил, что если использовать fabric для деплоймента, то почему бы не использовать ее и для частых локальных команд:
from fabric.api import local, run, cd def run(): '''Start development server''' local('PYTHONPATH=. paster serve --reload development.ini', capture=False) def pep8(target='.'): '''Run pep8''' local('pep8 --ignore=E202 %s' % target, capture=False) @hosts('root@pusto.org') def deploy(restart=False): '''Deploy to remote server''' local('hg push', capture=False) with cd('/var/www/horosh/'): run('hg pull&&hg up') if restart: run('/etc/init.d/horosh force-reload')
Работа с консолью опять сводится к коротким командам:
fab clean pep8 fab run fab deploy:True
В fabric мы уже можем передавать параметры при вызове задачи и это клево.
В принципе это не критичные моменты, библиотека делает свое дело. Для разработки на винде, возможно, это лучшее решение, т.к. тут свой ssh клиент paramiko, но я - не на винде :).
Со временем понял, что из fabric мне больше всего нужны функции local и run, а мои методы деплоя простые и не нужна особенность fabric для работы с множеством серверов.
Итак, чтоб сделать local c перехватом вывода и без, нужно всего-то:
from subprocess import call, Popen, PIPE, STDOUT # With capture cmd = Popen('ls -la', shell=True, stdout=PIPE, stderr=STDOUT) print(cmd.communicate()[0]) # Without capture call('ls -la', shell=True)
Теперь можно вспомнить про argparse и его сабкоманды и уже можно создавать свои manage.py на чистой стандартной библиотеке.
А что будем делать с деплоем?
Все просто :) - использовать стандартный клиент ssh.
from subprocess import call commands = '&&'.join(['ls -la', 'uptime']) call('ssh pusto.org "%s"' % commands, shell=True)
Т.е. мы можем делать развертывание проекта при помощи стандартной библиотеки python и клиента ssh, который у меня точно есть под рукой.
Note
nose у меня давно вошел в набор обязательных инструментов для тестирования, так что в примерах есть его влияние.
from unittest import TestCase answer = 42 class TestAnswer(TestCase): def setUp(self): print('setup') def test(self): self.assertEquals(answer, 42) def tearDown(self): print('teardown')
Исторически сложилось, что unittest использует верблюжью нотацию для setUp, tearDown, assert* (assertTrue, assertEquals…) методов. Но в python есть PEP 8, в котором принято использовать подчеркивание в названиях функций (методов), и в nose.tools можно найти аналогичные функции, но с подчеркиванием (assert_true, assert_equals) для любителей PEP 8.
from nose.tools import assert_equal, with_setup answer = 42 def setup_func(): print('setup') def teardown_func(): print('teardown') @with_setup(setup_func, teardown_func) def test_answer(): assert_equal(answer, 42)
В последнее время второй подход компоновки тестов мне все больше нравится. Почему?
и в первом и во втором подходе приходится давать имя модулю, который будет содержать наши тесты. Но в первом нужно еще придумывать имя классу-контейнеру, а т.к. мне нравятся небольшие модули (в них проще ориентироваться), то в большинстве случаев название класса - тавтология:
test_auth.py:TestAuth.test_login test_auth.py.test_login
в первом подходе вроде лучше выглядят SetUp, TearDown методы, во втором приходится импортировать декоратор with_setup. Но и тут можно выделить плюс, обычно название класса подбираю по содержимым тестам
class TestAuth(TestCase): def test_login() ... def test_logout() ...
но когда для test_login нужен setUp метод, а для test_logout нет, то тут приходится класс-контейнеры компоновать в зависимости от используемых SetUp, TearDown методов. В общем присутствует неоднозначность и это не очень хорошо :)
в классе-контейнере забирается один отступ, а отступы ценны, когда соблюдаешь ограничение в 80 символов;
в первом подходе класс наследуется от unittest.TestCase, при вызове каждого assert* метода логично обращаться к self и тут опять у нас крадут символы:
self.assertEqual assert_equal...4
1 answer = 42 2 3 4 def test_answer(): 5 ''' 6 >>> answer 7 42 8 ''' 9 assert False
Выглядит кратко, хотя конечно такой формат тестов не всегда подходит…
Note
Если запускать через nose ($ nosetests –with-doctest), то строка 9 не вызывается.
answer = 43 def test_answer(): assert answer == 42
После запуска, вывод:
$ nosetest ====================================================================== FAIL: test.test_answer ---------------------------------------------------------------------- Traceback (most recent call last): ... assert answer == 42 AssertionError
Очень заманчиво: не нужен дополнительный импорт, лаконично. Но вот при выводе не известно какое значение содержит переменная answer. Правда тут может порадовать nose и даже двумя вариантами:
$ nosetests --pdb-failures
...
-> assert answer == 42
(Pdb) answer
43
приходится вводить answer - лишние телодвижения :).
Следующий вариант еще красивее:
$ nosetest -d ====================================================================== FAIL: test.test_answer ---------------------------------------------------------------------- Traceback (most recent call last): ... assert answer == 42 AssertionError: >> assert 43 == 42
так что, в принципе, тесты можно писать через assert без потери информативности вывода, нужно только использовать правильные “пускальщики”.
from nose.tools import eq_ answer = 43 def test_answer(): eq_(answer, 42)
После запуска, вывод:
FAIL: test.test_answer ---------------------------------------------------------------------- Traceback (most recent call last): ... eq_(answer, 42) AssertionError: 43 != 42
Заменили assert_equal на более короткий вариант eq_, вывод ошибки будет полностью аналогичен. Т.е. при выводе увидим, что answer на самом деле 43 и пойдем сразу искать ошибку в коде. Один нюанс, что тесты не заканчиваются проверкой на eq_ и ok_, которые есть в nose.tools, набор методов нужен более обширный…
pytest - это аналог nose, со своими “плюшками”, он умеет запускать большинство тестов написанных для nose.
attest - интересный подход (python way) от известной команды Pocoo. Пример из документации:
from attest import Tests math = Tests() @math.test def arithmetics(): """Ensure that the laws of physics are in check.""" assert 1 + 1 == 2 if __name__ == '__main__': math.run()
Oktest для лаконичности - идея прикольная. Пример из документации:
from oktest import ok ok (x) > 0 # same as assert_(x > 0) ok (s) == 'foo' # same as assertEqual(s, 'foo') ok (s) != 'foo' # same as assertNotEqual(s, 'foo')
В python есть множество способов для написания и запуска тестов, в статье упоминаются не все. Если задаться целью, то можно писать красивые и лаконичные тесты.