Install this theme
Магический “py”

Хочу поведать историю с участием werkzeug (0.6 и ниже) и py (1.4 и выше). То, что py у меня установлен я не знал.

Началось все с обычной задачи по работе. Открываю консоль, запускаю dev сервер с веб проектом и получаю вот такое:

Traceback (most recent call last):
  File "./manage.py", line 89, in 
    script.run()
  File "/Users/riffm/proj/third-party/werkzeug/script.py", line 168, in run
    return func(**arguments)
  File "/Users/riffm/proj/third-party/werkzeug/script.py", line 298, in action
    static_files=static_files)
  File "/Users/riffm/proj/third-party/werkzeug/serving.py", line 390, in run_simple
    run_with_reloader(inner, extra_files, reloader_interval)
  File "/Users/riffm/proj/third-party/werkzeug/serving.py", line 319, in run_with_reloader
    reloader_loop(extra_files, interval)
  File "/Users/riffm/proj/third-party/werkzeug/serving.py", line 284, in reloader_loop
    for filename in chain(iter_module_files(), extra_files or ()):
  File "/Users/riffm/proj/third-party/werkzeug/serving.py", line 271, in iter_module_files
    filename = getattr(module, '__file__', None)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/py/_apipkg.py", 
line 159, in __getattribute__
    return getattr(getmod(), name)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/py/_apipkg.py",
line 144, in getmod
    x = importobj(modpath, None)
  File "/opt/local/Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/py/_apipkg.py",
line 37, in importobj
    module = __import__(modpath, None, None, ['__doc__'])
ImportError: No module named pytest

Сторонние библиотеки в проекте храняться в папке third-party, которая добавляется в sys.path в едином месте — manage.py. Ну, может кто доставил зависимость и не углядел, подумал я. И начал grep’ать проект на предмет использования pytest. Никаких результатов. Т.е. модуль или пакет pytest никто не импортирует явно (!). В трейсбеке видно, что в этом безобразии учавствет некий пакет py. Перед тем как посмотреть в его исходник, я запустил shell с приложением и провел мелкое расследование.

proj riffm$ ./manage.py shell
Python shell
>>> import sys
>>> filter(lambda k: k.startswith('py'), sys.modules.keys())
['py.builtin', 'pytils.utils', 'py.sys', 'py.os', 'pytils.translit', 'py.error', 'pytils.sys', 
'pytils', 'py.iniconfig', 'py.test.collect', 'pytils.numeral', 'pytils.warnings', 'py.xml', 'py.types', 
'pytils.os', 'py.process', 'py.apipkg', 'pytils.re', 'pytils.third', 'pytils.third.re', 'pyexpat.errors', 
'pyexpat.model', 'py.io', 'py', 'pyexpat', 'py.code',
'py.path', 'py._apipkg', 'pytils.err', 'pytils.third.types', 'py.py', 'pytils.pytils',
'pytils.typo', 'pytils.third.inspect', 'py.log', 'py.test', 'pytils.decimal', 'pytils.dt',
'pytils.datetime', 'py.test.cmdline', 'pytils.third.aspn426123']

Нашелся некий py.test и еще куча модулей и пакетов из, непойми откуда взявшегося, py пакета. Но pytest нигде нет. При попытке запустить только интерпретатор таких вещей не обнаруживаю. Значит, кто-то все-таки импортирует py или что-то из py. grep показал, что это делает werkzeug. Он импортирует greenlet’ы (wtf?) и перехватывает все исключения (там есть комментарий который многое объясняет). Но как мы знаем, питон импортирует последовательно проходя все пакеты, значит мой путь лежал к py/__init__.py.

Тут даже бывалый и видавший виды питон разработчик ахнет и стечет с кресла под стол. В py/__init__.py в наглую патчится sys.modules. Для этого у автора есть специальный API (!), вызовы которого видны в трейсбеке. Без комментариев. Скажу только, что там и нашелся pytest.

Потом выяснилось откуда взялся py в моем питоне, он идет зависимостью для пакета tox, что не удивительно, ведь автор один =) А, сам tox используется для организации тестирования webob, которым я, за пару дней до этого, занимался.

Выводы:

  • Проверять используемые пакеты на отсутствие py в качестве зависимости
  • Если в сторонней библиотеке обнаруживаются импорты в конструкциях try except или try except ImportError, надо исследовать пакет на предмет модификаций глобальных переменных типа sys.modules, т.к. эти модификации могут происходить до вываливания исключений

P.S. В werkzeug 0.7 из py, вроде, ничего не импортируется. И кстати, в py есть такой тикет со статусом wontfix =)

tsnoom:

подумала я тут, кому жарче?  комбайнеру на кубанских полях в солнцепек. или водителю автобуса в московских пробках. или пожарному в горящих лесах якутии ….
и как-то мне стало прохладнее … с вентилятором под боком и душем в пяти метрах и в любой момент …
быть благодарным это правильнее. каждодневного нытья …

Истина.

tsnoom:

подумала я тут, кому жарче?  комбайнеру на кубанских полях в солнцепек. или водителю автобуса в московских пробках. или пожарному в горящих лесах якутии ….

и как-то мне стало прохладнее … с вентилятором под боком и душем в пяти метрах и в любой момент …

быть благодарным это правильнее. каждодневного нытья …

Истина.

Спасибо, Красное море!

Умник url_for

Без ссылочной целостности любой веб проект не представляет интереса. Раньше ссылки вбивались в код как есть. Потом в обиход вошел термин url reverse. Подход простой: функция (например url_for) принимала строку — имя url и параметры, требуемые для корректного построения адреса, на выходе получается строка url. Таким образом, в проекте можно переименовывать адреса ресурсов не теряя ссылочную целостность. Такой подход применяется в werkzeug, django, pyramid (+1), rails (2.3 paths and urls, 3.6 naming routes) и др.

Для примера возьмем следующее приложение написанное при помощи библиотеки insanities.


from insanities import web
import views

ns = web.namespace

app = web.cases(
  web.match('/', 'index') | views.index,
  web.prefix('/docs') | ns('docs') | web.cases(
    web.match('') | views.docs,
    web.match('/new', 'new') | views.new_doc,
    web.prefix('/<int:doc_id>') | ns('item') | view.get_doc | web.cases(
      web.match('') | views.show_doc,
      web.match('/update', 'update') | views.update_doc,
      web.match('/delete', 'delete') | views.delete_doc,
    )
  )
)

url_for = web.Reverse.from_handler(app)

url_for('index')                      # '/'
url_for('docs')                       # '/docs'
url_for('docs.new')                   # '/docs/new'
url_for('docs.item', doc_id=1)        # '/docs/1'
url_for('docs.item.update', doc_id=1) # '/docs/1/update'
url_for('docs.item.delete', doc_id=1) # '/docs/1/delete'

Тут все просто. url строится по названию адреса (второй атрибут web.match) с учетом web.namespace. Но что, если надо реализовать ресурс “события”? А еще опубликовать этот ресурс в нескольких местах? Прикинем следующую функцию-фабрику. (Пример не очень хорош)


from insanities import web
import views

ns = web.namespace

def event_factory():
  return web.cases(
    web.match('') | views.events,
    web.match('/new', 'new') | views.new_event,
    web.prefix('/<int:event_id>') | ns('item') | views.get_event | web.cases(
      web.match('') | views.show_event,
      web.match('/update', 'update') | views.update_event,
      web.match('/delete', 'delete') | views.delete_event,
    )
  )

И пристроим ее в двух разных местах нашего приложения


...

app = web.cases(
  web.match('/', 'index') | views.index,
  web.prefix('/docs') | ns('docs') | web.cases(
    web.match('') | views.docs,
    web.match('/new', 'new') | views.new_doc,
    web.prefix('/<int:doc_id>') | ns('item') | view.get_doc | web.cases(
      web.match('') | views.show_doc,
      web.match('/update', 'update') | views.update_doc,
      web.match('/delete', 'delete') | views.delete_doc,
      web.prefix('/events') | ns('events') | event_factory(),
    )
  ),
  web.prefix('/events') | ns('events') | event_factory(),
)

С учетом всего написаного url_for будет работать так


...
url_for = web.Reverse.from_handler(app)

url_for('docs.item.events', doc_id=1)                         # '/docs/1/events'
url_for('docs.item.events.item', doc_id=1, event_id=1)        # '/docs/1/events/1'
url_for('docs.item.events.item.update', doc_id=1, event_id=1) # '/docs/1/events/1/update'
url_for('events')                                             # '/events'
url_for('events.item', event_id=1)                            # '/events/1'
url_for('events.item.update', event_id=1)                     # '/events/1/update'


Проблема заключается в следующем: наверняка, в views.show_event надо будет дать ссылку на обновление или удаление данного элемента. На момент вызова учитывается текущий namespace и можно делать url относительно его. Напиример если мы попали в views.show_event с namespace равеным ‘events’.


url_for('.')                          # '/events'
url_for('.item', event_id=1)          # '/events/1'
url_for('.item.update', event_id=1)   # '/events/1/update'

Но в случае с ‘docs.item.events’ такой трюк не пройдет, т.к. для построения url нужен так же и doc_id (по правде говоря, он попадает во views.show_event).

Как удобнее решать такие ситуации — не знаю. Можно использовать предопределенный url reverse объект.


url_for.events()                         # '/events'
url_for.events.item(event_id=1)          # '/events/1'
url_for.events.item.update(event_id=1)   # '/events/1/update'

url_for.docs.item.events(doc_id=1)          # '/docs/1/events'

predefined = url_for.docs.item(doc_id=1)
predefined.events()                         # '/docs/1/events'
predefined.events.item(event_id=1)          # '/docs/1/events/1'
predefined.events.item.update(event_id=1)   # '/docs/1/events/1/update'


Т.е. url_for атрибуты которого возвращают новый объект url_for частично заполненный. Синтаксис приведенный выше — неоднозначен. Жду предложений по этому поводу.

tsnoom:

у нас есть новая кухня … и нет денег и сил
:)

tsnoom:

у нас есть новая кухня … и нет денег и сил

:)

tsnoom:

хочется весны и почему-то арбузов … с черешней.

tsnoom:

хочется весны и почему-то арбузов … с черешней.

git log
  • Появился форк insanities под названием insanities-testing. Было много экспериментов с API форм, веб составляющей, а так же успешное применение в нескольких небольших проектах. В итоге к концу года появилась реализация, от которой меня не коробит, за которую мне не стыдно. 
  • Появился mint, который тоже уже применяется и очень мне нравится. Интересно то, что большое количество плюсов было выявленно уже в процессе использования. Так же есть еще несколько моментов, которые хотелось бы реализовать к версии 1.0
  • В этом году познакомился с большим количеством людей из мира питона и веба. Спасибо всем, кто со мной сотрудничал, подкидывал идеи и критиковал.

git checkout -b 2011