Хочу поведать историю с участием 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, которым я, за пару дней до этого, занимался.
Выводы:
P.S. В werkzeug 0.7 из py, вроде, ничего не импортируется. И кстати, в py есть такой тикет со статусом wontfix =)
подумала я тут, кому жарче? комбайнеру на кубанских полях в солнцепек. или водителю автобуса в московских пробках. или пожарному в горящих лесах якутии ….
и как-то мне стало прохладнее … с вентилятором под боком и душем в пяти метрах и в любой момент …
быть благодарным это правильнее. каждодневного нытья …
Истина.
Спасибо, Красное море!
Без ссылочной целостности любой веб проект не представляет интереса. Раньше ссылки вбивались в код как есть. Потом в обиход вошел термин 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 частично заполненный. Синтаксис приведенный выше — неоднозначен. Жду предложений по этому поводу.
у нас есть новая кухня … и нет денег и сил
:)
хочется весны и почему-то арбузов … с черешней.
git checkout -b 2011