воскресенье, 4 мая 2014 г.

Range для не целых чисел

Почему-то никогда не думал, что rangexrange) понимают только целые числа (в Python 3 ситуация такая же) и функцией нельзя воспользоваться, как например:

range(0., 5., .5)
range(Decimal('0'), Decimal('10'), Decimal('1.5'))

Поэтому пришлось сделать свою замену:

import operator


def arange(start, stop=None, step=None):
    """
    Implement range function not only for integers as Python's builtin
    function, but for Decimals and floats as well.

    Returns generator with arithmetic progession, not list.
    """
    klass = type(start)
    lt_func = operator.lt

    stop = start if stop is None else stop
    start = klass(0) if start == stop else start
    step = klass(1 if step is None else step)

    assert isinstance(stop, klass), (
        'Start and stop limits have different types, {0!r} != {1!r}.'.
        format(type(start).__name__, type(stop).__name__)
    )
    assert step, "Step shouldn't be a zero: {0!r}.".format(step)

    if start < stop and step < 0 or start > stop and step > 0:
        raise StopIteration
    elif start > stop and step < 0:
        lt_func = operator.gt

    while lt_func(start, stop):
        yield start
        start += step

В отличии от range в Python 2 arange отдает генератор, а не список, что вообщем-то удобней и правильней, ну и функция всегда вернет элементы такого же типа, как были переданы в start и stop аргументы, даже если шаг - это целое число или он не указан вовсе.

Как всегда код оформлен как Gist на GitHub'е, там еще доктесты и юниттесты.

четверг, 25 апреля 2013 г.

Миграция с Posterous на Blogger

Как вы, наверное, знаете Twitter сначала купил Posterous, а потом его закрыл. И так как на Posterous'е был мой блог, я перенес все посты из него сюда. Сорри, если RSS фид обновился слишком рьяно.

Если у кого есть схожая проблема и он еще не перевез никуда свой Posterous, вот есть маленький скриптик, который делает всю работу.

зы. Так как этот блог стал моим единственным, то я сменил его название на более честное. Впредь тут будут появлятся информация и по Flask'у, и по Django, но в большинстве случаев думаю просто по Python'у.

четверг, 20 декабря 2012 г.

Запускаем gunicorn из Python'а

Иногда бывает надо запустить gunicorn внутри Python скрипта, например, в manage.py. Конечно всегда можно воспользоваться subprocess.call:

import subprocess


app = 'package.module:app'
host, port = '0.0.0.0', 8000
subprocess.call('gunicorn -b {}:{:d} -w 4 {}'.format(host, port, app))

Но как-то это не комильфо подумал я и решил найти более труЪ-способ :)

Решение пришло не сразу, но пришло, надо всего лишь переопределить sys.argv и вызвать метод run,

import sys

from gunicorn.app.wsgiapp import run


app = 'package.module:app'
host, port = '0.0.0.0', 8000
sys.argv = [
    sys.argv[0],
    '-b', '{}:{:d}'.format(host, port),
    '-w', '4',
    app
]
run()

Не очень круто вышло, не находите? А все потому что gunicorn еще использует устаревший optparse для парсинга аргументов с коммандной строки, а там задавать список аргументов в parse_args совсем не обязательно, ведь по дефолту берется список sys.argv[1:].

Так что на таком простом примере лично мне ясно видно, насколько optparse устарел и как вашему приложению, которое его еще использует надо сломя голову переезжать на argparse! И это я еще не рассказал вам о бесподобном управлении под-коммандами в argparse :)

четверг, 29 ноября 2012 г.

Чиним gunicorn'овский Internal Server Error

Если вдруг, после запуска gunicorn перестал работать и показывает ошибку похожую на:

Internal Server Error

Traceback:

Traceback (most recent call last):
  File "/Users/playpauseandstop/Projects/project/env/lib/python2.7/site-packages/gunicorn/workers/async.py", line 45, in handle
    self.handle_request(req, client, addr)
  File "/Users/playpauseandstop/Projects/project/env/lib/python2.7/site-packages/gunicorn/workers/async.py", line 73, in handle_request
    resp, environ = wsgi.create(req, sock, addr, self.address, self.cfg)
  File "/Users/playpauseandstop/Projects/project/env/lib/python2.7/site-packages/gunicorn/http/wsgi.py", line 161, in create
    path_info = path_info.split(script_name, 1)[1]
IndexError: list index out of range

то это говорит о том, что какая-то сволочьое-то приложение установило переменную окружения SCRIPT_NAME, которую gunicorn не может нормально обработать. Чтобы пофиксить, просто удаляем переменную окружения,

$ unset SCRIPT_NAME

и перегружаем страницу (ну можно еще перегрузить gunicorn для пущей важности). That's all!

четверг, 8 ноября 2012 г.

Валидация Django моделей, пять советов

Не думаю, что для кого-то открою Америку, сказав что в Django есть валидация не только для форм, а и для моделей :) Однако судя по моей практике это чуть спорное, но весьма полезное решение не спешат повсеместно использовать и городят свои велосипеды для проверки значений при создании/обновлении моделей. Поэтому хочу поделиться несколькими простыми советами по этой теме.

Во-первых, храните валидаторы отдельно от моделей. Хранение всех валидаторов в models.py неимоверно раздувает и без того не маленький модуль моделей (а иногда это и пакет), хранение же валидаторов в validators.py решает эту проблему и логично выносит все операции по проверке значений/уникальности модели в подходящее для этого место. Ну и сюда же, не пишите сложные проверки в Model.clean, Model.validate_unique методах, просто вызывайте необходимые функции валидации из validators.py, например:

models.py

from django.db import models

from .validators import validate_complex_case, validate_unique


class Card(models.Model):
    ...
    def clean(self):
        validate_complex_case(instance)

    def validate_unique(self, exclude=None):
        validate_unique(self)
        return super(Card, self).validate_unique(exclude)

validators.py

from django.core.exceptions import ValidationError


def validate_complex_case(instance):
    if instance.name == 'XXX' and instance.path != '/path/to/XXX':
        raise ValidationError('Please provide proper path for the card.')


def validate_unique(instance):
    manager = instance._default_manager
    other = manager.filter(name=instance.name)

    if other.count():
        message = 'Card with this Name already exists.'
        raise ValidationError({'__all__': [message]})

Во-вторых, непонятно зачем, но ошибки в проверке на уникальность модели нужно обворачивать в message_dict, использование простого сообщения приводит к AttributeError. Хотя с другой стороны это повышает возможности по написанию ошибок в разных полях моделей. Можно, например, подсветить какие именно поля не уникальны и уже сохранены в БД.

В-третьих, не забывайте, что валидаторов может быть сколько угодно много. Старайтесь разбивать большие валидаторы на маленькие атомарные функции и выносить их в core.validators или project.validators, чтоб иметь возможность использовать их не только для текущего приложения, а и для любых других приложений в проекте. Аттрибут validators у поля модели принимает список валидаторов, так что Вы вполне можете писать,

from django.core.validators import MaxLengthValidator, MinLengthValidator
from django.db import models

from .validators import validate_markup


class Card(models.Model):
    ...
    comment = models.TextField(validators=[
        MinLengthValidator(10), MaxLengthValidator(1000), validate_markup
    ])

В-четвертых, валидация модели автоматически не вызывается, когда вы хотите сохранить модель не из админ-панели или ModelForm'ы. Т.е., просто добавление валидаторов в поля модели и задание методов clean или validate_unique не гарантирует, что неправильные данные не сохранятся в базе данных после Model.objects.create или instance.save(). Для того, чтобы обезопасить себя и включить валидацию и для моделей, используйте сигналы:

from django.db import models
from django.db.models import signals
from django.dispatch import receiver


class Card(models.Model)
    ...


@receiver(signals.pre_save, sender=Card)
def validate_card(instance):
    instance.full_clean()

Ну и в-пятых,

  • Не забывайте про стандартные валидаторы Django
  • Импортируйте исключение ошибки валидации как from django.core.exceptions import ValidationError
  • Для моделей нельзя писать методы clean_FIELD как для форм
  • Валидация модели при вызове instance.full_clean() идет в следующей последовательности: валидация полей (instance.clean_fields()), вызов instance.clean(), проверка на уникальность (instance.validate_unique())

зы. Код и принципы данного материала актуальны для Django 1.3 ветки (да я так и не пересел на 1.4 или 1.5 потому что не вижу в этом особого смысла), может в новых версиях что-то поменялось, уточняйте в документации :)