Наконец-то, django-discover-runner от Янниса Лейдла

Эта была долгая история полная костылей, но кажется лед тронулся и совсем скоро и Django'вский дефолтный тест раннер будет иметь поддержку автоматического поиска тестов в проекте и мы наконец забудем про from test_* import * как про страшный сон.

Само решение и соответствующий тикет появились довольно давно, а вчера Яннис Лейдл (jezdez, один из Django core-team) оформил решение как отдельный пакет, назвав его django-discover-runner. Шаг в верную сторону, но для меня до сих пор не понятно, почему это очевидное изменение не было сделано сразу после включения unittest2 в проект.

Постоянные сессии во Flask'е, один из способов

По умолчанию, все содержимое flask.session будет очищено при закрытии браузера. Однако много когда нам нужно, чтоб данные сессии хранились и после рестарта браузера. Для этих случаев есть аттрибут permanent и следующий простой сниппет:

import datetime

from flask import Flask, session


app = Flask(__name__)
app.before_request(lambda: setattr(session, 'permanent', True))
app.permanent_session_lifetime = datetime.timedelta(days=14)

Последняя строчка сниппета выставляет длину сессии в 14 дней, во Flask'е же по дефолту используется 31 день для хранения постоянной сессии. Также эту настройку можно указать как PERMANENT_SESSION_LIFETIME в вашем settings.py.

зы. Однако также не забывайте, что Flask хранит все данные сессии в кукисах, а не как, например, Django только ключ сессии, а все данные уже считывает с базы данных или другого источника. Так что уместно будет использовать flask.session как хранилище каких-то ключей, например, токена текущего залогинненого пользователя.

Окончательно дружим Flask и nosetests

Не секрет, что Flask и так хорошо дружит с nosetests, но до сегодняшнего дня был один очень раздражющий момент в их взаимоотношениях :)

Как мы все знаем nosetests по дефолту захватывает все из stdout/stderr и логгинга, чтоб при запуске тестов вывод не засорялся ненужной нам информацией. Однако в дебаг-моде Flask кладет на всех и устанавливает с помощью flask.logging.create_logger функции хэндлер, который начинает срать в консоль при каждом удобном случае, причем минуя все ранее установленные хэндлеры. Итог: куча ненужной логгинг информации при запуске тестов как:

(env)$ TESTING=1 nosetets -c -v -w <package>

Не хорошо, но In mock we trust, так что все что надо - это замокать упомянутую выше функцию в случае, когда мы запускаем тесты в дебаг-моде:

if TESTING and DEBUG:
    from flask import logging as flask_logging

    def mock_create_logger(app):
        return logging.getLogger(app.logger_name)

    flask_logging.create_logger = mock_create_logger

Помещаем этот сниппет в settings.py, затем не забываем загрузить настройки как import settings; app.config.from_object(settings) в нашем app.py - и получаем счастье, nosetests уверенно захватывает все нужное и вывод тестов чист и аккуратен.

Полный гист доступен на Гитхабе, если кто-то готов предложить более красивый вариант решения проблемы - жду в комментариях.

Flask-Dropbox

За что нравится Flask, так это за его концепцию reusable apps. По сравнению с Django на создание по настоящему реюзабельного приложения уходит КУДА меньше времени.

Вот, например, вчера вечером захотелось поиграться с Dropbox API, а сегодня уже готов Flask-Dropbox :) Причем готов с тестовым проектом, который позволит вам загружать файлы в Dropbox, просматривать их и удалять.

Пример использования как всегда прост и неказист: импортируем главный класс и блюпринт, инициализируем их, регистрируем блюпринт с указанием префикса для урлов:

from flask import Flask
from flask.ext.dropbox import Dropbox, DropboxBlueprint

import settings


app = Flask(__name__)
app.config.from_object(settings)

dropbox = Dropbox(app)
dropbox_blueprint = DropboxBlueprint(dropbox)
app.register_blueprint(dropbox_blueprint, url_prefix='/dropbox')

Единственное на чем следует детально остановится - это настройки. Так как мы имеем дело с API, без них никуда :)

Во-первых, обязательно нужно будет настроить app.secret_key или просто SECRET_KEY, чтобы иметь возможность использовать flask.session (там будут храниться нужные нам токены).

Во-вторых, нужно будет создать какое-то приложение в Dropbox developers site, если еще нет такого и получить там DROPBOX_KEY, DROPBOX_SECRET и DROPBOX_ACCESS_TYPE. Без указания этих значений - кина не будет и Dropbox(app) отдаст вам ValueError :)

После укзания настроек все просто :)

Аутентификация. dropbox.is_authenticated проверит нет ли валидного access token'а в текущей сессии, dropbox.login_url сгенерирует урл для логина при помощи Dropbox, и наконец dropbox.logout_url отдаст урл, переход по которому вылогинит дропбокс пользователя. После успешного логина получить данные о пользователе можно из словаря dropbox.account_info.

Работа с дропбоксом. После того, как пользователь залогинился вся работа с его дропбоксом будет проходить через проперти dropbox.client, который является прокси к инициализированному инстансу DropboxClient. Полный список доступных методов последнего доступен в документации.

В остальном код доступен на ГитХабе, установить можно с PyPI. Пользуйтесь!

Flask-LazyViews

Мне не нравится использовать декоратор @app.route или метод app.add_url_route для регистрации функций отображения во Flask приложениях и блюпринтах, потому что мне намного больше по душе паттерн ленивой загрузки этих функций :)

Именно так и родился Flask-LazyViews. Пример использования тривиальный, для приложений:

from flask import Flask
from flask.ext.lazyviews import LazyViews


app = Flask(__name__)
views = LazyViews(app)

views.add('/', 'views.home')
views.add('/page/', 'views.page')

Или для блюпринтов:

from flask import Blueprint
from flask.ext.lazyviews import LazyViews


blueprint = Blueprint('test', __name__)
views = LazyViews(blueprint, '.views')

views.add('/', 'test')
views.add('/advanced', 'advanced_test', methods=('GET', 'POST'))

Больше информации доступно как всегда на ГитХабе, установить можно с PyPI.

Flask-And-Redis

Понадобилось на днях во Flask проекте докрутить поддержку Redis'а. Плюс, хотелось иметь возможность задавать любые настройки в settings модуле, в итоге появился Flask-And-Redis, так как в Flaks-Redis далеко не все можно настроить и оно не Flask-way инициализируется.

Пример использования есть в репозитории, по быстрому повторю и здесь:

from flask import Flask
from flask.ext.redis import Redis


app = Flask(__name__)
redis = Redis(app)

Вуаля :)

Один маленький совет для ускорения бутстрапа проектов

Конечно, использование git:// урлов в файле зависимостей проекта не есть отличная идея, но если вы таки решились на нее и даже делаете как-то так:

-e git://github.com/user/repo.git@commit#egg=package

то я спешу вас облагоразумить! Не делайте так! НИКОГДА :)

В случае в репозиториями GitHub'а, используйте zipball'ы (tarball'ы нормально не распознаются pip),

https://github.com/user/repo/zipball/commit#egg=package

в других случаях просто архивируйте при помощи git archive необходимый вам коммит или тег, и грузите его на свой cdn. И теперь вам не надо будет ожидать пока пип склонирует репо и поставит хедом необходимый вам коммит. Ускорение бутстрапа на жирных зависимостях (например, ask/celery, django/django будет очень ощутимым)!

зы. И да, даже для мастера (любого другого бранча) вам не нужно использовать git:// в случае GitHub'а. Используем такую же технику и получаем:

https://github.com/user/repo/zipball/master#egg=package

вместо:

-e git://github.com/user/repo.git#egg=package

Одной строкой: проверяем кто удалился или отфрендил тебя на Facebook'е

Когда у тебя не два друга на ФБ, не возможно уследить за всеми изменениями в ростере друзей. И хоть какое-то время фб-таймлайн позвалял посмотреть, кто отфрендил тебя - это не особо помогало в случаях, когда кто-то решил самоликвидироваться из соц сети.

По этому, я на коленке написал скриптик, который заходит на фб по xmpp (по этому требует xmpppy), читает список друзей и сравнивает его с сохраненным на диске. Если есть различия в списках, он их показывает в простом формате Статус | Профиль | Имя.

Код доступен как гист на GitHub, может по-позже таки заморочусь с FB auth и сделаю из скрипта еще одно никому не нужное фб приложение :)

Пару заметок о coverage, nosetests и lettuce

Думаю ни для кого не секрет, что в nosetests уже есть встроенная поддержка coverage, однако там нет очень важной фичи coverage, а именно возможности дополнять файл данных после каждого следующего запуска тестов (опция -a --append в coverage run).

Зачем это может понадобится? Самый простой пример: если в проекте есть и юнит тесты, и интеграционные, и не дай бог тесты, работающие с реальными данными :) Т.е. если мы запускаем все тесты перед деплоем не просто nosetests ..., а связкой из nosetests ... && nosetests ... && nosetests ..., в таком случае добавление --with-coverage в каждую итерацию nosetests даст нам три совсем ненужных таблицы покрытия, где каждая таблица будет отличаться от предыдущей и смерджить их воедино не выйдет - а это совсем не то, что нам надо.

Что делать? На самом деле ничего сложного, просто меняем:

$ nosetests --with-coverage ... && \
  nosetests --with-coverage ... && \
  nosetests --with-coverage ...

на:

$ coverage run `which nosetests` ... && \
  coverage run -a `which nosetests` ... && \
  coverage run -a `which nosetests` ... && \
  coverage report -m

В итоге получим то, чего добивались, а именно таблицу покрытия кода всеми тестами.

И да, раз уже заговорили про coverage, вы не забываете про использование .coveragerc? Очень полезная вещь!

Так что теперь, если в проекте используются и lettuce, и nosetests - посчитать покрытие кода не составит особого труда, используя coverage run `which lettuce` ... && coverage run -a `which nosetests` ...

И последнее на сегодня, при запуске lettuce тестов, не забывайте указывать путь не к директории, в которой есть features, а к самой директории features. Я на этом моменте очень сильно злился!

Добавляем поддержку optgroup в wtforms.fields.SelectField

Сейчас по работе начал использовать WTForms и все бы в них просто и понятно, и можно легко и быстро перейти к ним от Django forms, но один момент меня очень разочаровывал, отсутствие поддержки optgroup в SelectField для, например, следующих choices:

ROLE_CHOICES = (
    ('Content Creation', (
        ('project_manager', 'Project Manager'),
        ('writer', 'Writer'),
        ('editor', 'Editor'),
        ('senior_editor', 'Senior Editor'),
    )),
    ('Content Distribution', (
        ('dist_project_manager', 'Project Manager'),
        ('dist_content_publisher', 'Publisher'),
        ('dist_content_reviewer', 'Reviewer'),
    ))
)

Точнее есть, но какая-то уж очень неверная :) Ибо следующая форма,

from wtforms import fields, form


class SettingsForm(form.Form):

    default_role = fields.SelectField(choices=ROLE_CHOICES, ...)
    decimal_value = fields.DecimalField(...)
    int_value = fields.IntegerField(...)

отображалась вот таким корявым способом:

WTForms SelectField

Быстрый поиск по интернету и багам WTForms показал, что проблема уже не нова, но авторы ее пока фиксить не собираются, да и предложенное решение было явно over-designed. Так что я не долго думая написал кастомный виджет и самую малость подправил SelectField для того, чтобы ROLE_CHOICES в итоге рендерился так:

WTForms extended SelectField

Сразу предупреждаю, что задача решалась в лоб и возможно существует более универсальное решение, которое подойдет не только для SelectField, но и для SelectMultipleField, но так как последнее поле не используется в моем проекте, то я на него даже не смотрел :) В целом, код доступен как гист на гитхабе, удачного использования :)

update. Хм, начинаю сомневаться в адекватности автора WTForms и разумности позиционирования этой библиотеки взамен Django forms, ибо даже не могу найти слов как интерпретировать позицию по закрытию как invalid бага #47 Required IntegerField misvalidates "0". Это же клиника или каким образом 0 не валидное целое число?