2025, Sep 30 13:16
Как правильно использовать _cp_dispatch в CherryPy для REST
Разбираем, как работает _cp_dispatch в CherryPy: REST-маршрутизация, возврат обработчика вместо self, управление vpath, предсказуемые 404 и пример кода подробно.
_cp_dispatch в CherryPy — это низкоуровневый хук, который позволяет гибко управлять маршрутизацией URL. Он особенно полезен, когда нужны аккуратные REST-пути, не совпадающие один к одному с именами методов. Подводный камень: если бездумно менять сегменты пути и постоянно возвращать self, CherryPy может продолжать обход так, будто у вас есть под-объекты, и ваш обработчик так и не будет вызван. Ниже — компактный разбор ловушки и способ её обойти.
Постановка задачи
Задача проста: сопоставить /batch/ с конкретным методом, а /batch/{id}/product/ или /batch/{id}/product/{sku} — с другим. Всё остальное должно возвращать 404. В первой попытке переписывается vpath и возвращается self, ожидая, что CherryPy перезапустит разрешение по изменённому пути.
import cherrypy
class GoodsApi(object):
def __init__(self, data_store):
self.data_store = data_store
def _cp_dispatch(self, vpath):
# /batch/
if len(vpath) == 1 and vpath[0] == "batch":
vpath[0] = "batch_entry"
return self
# /batch/{id}/product/
if len(vpath) == 3 and vpath[0] == "batch":
vpath.pop(0) # удалить "batch"
cherrypy.request.params["batch_id"] = vpath.pop(0)
return self
# /batch/{id}/product/{sku}
if len(vpath) == 4 and vpath[0] == "batch" and vpath[2] == "product":
vpath.pop(0) # удалить "batch"
cherrypy.request.params["batch_id"] = vpath.pop(0)
vpath.pop(0) # удалить "product"
cherrypy.request.params["sku"] = vpath[0]
vpath[0] = "item"
return self
# обход по умолчанию
return self
@cherrypy.expose
@cherrypy.tools.json_out()
def batch_entry(self):
# предназначен для /batch/
pass
@cherrypy.expose
@cherrypy.tools.json_out()
def item(self, batch_id, sku=None):
# предназначен для /batch/{id}/product[/ {sku}]
pass
Что происходит на самом деле
Сначала CherryPy ищет открытые (exposed) обработчики, которые соответствуют пути. Если подходящих нет, он вызывает _cp_dispatch(self, vpath) с оставшимися сегментами. Внутри этого хука есть две корректные стратегии. Можно изменять vpath и возвращать self, продолжая обход, либо вернуть конкретный привязанный метод, чтобы завершить разрешение сразу. Если хук вернёт None, CherryPy ответит 404.
Переписывание сегмента вроде vpath[0] = "batch_entry" не заставляет CherryPy вызвать метод напрямую; результат по-прежнему воспринимается как следующий шаг обхода. Поэтому обработчик для /batch/ в первой попытке так и не срабатывает. Ещё одна тонкость: оставшиеся сегменты в vpath могут «перетекать» в разрешение аргументов, из-за чего возникают проблемы вроде «multiple values for batch_id». Извлечение параметров в _cp_dispatch и контроль, чтобы не оставалось лишних сегментов, снимают это недоразумение.
Решение: возвращать обработчик напрямую
Надёжный подход — не переписывать путь, а возвращать целевой обработчик из _cp_dispatch. Заполните cherrypy.request.params по мере необходимости, «съешьте» использованные части пути и позвольте CherryPy вызвать возвращённый метод. Пути, которые не соответствуют ожиданиям, должны приводить к None, чтобы отдать 404.
import cherrypy
class GoodsApi(object):
def __init__(self, data_store):
self.data_store = data_store
def _cp_dispatch(self, vpath):
# /batch/
if len(vpath) == 1 and vpath[0] == "batch":
return getattr(self, "batch_entry")
# /batch/{id}/product/
if len(vpath) == 3 and vpath[0] == "batch":
vpath.pop(0) # удалить "batch"
cherrypy.request.params["batch_id"] = vpath.pop(0)
return getattr(self, "item")
# /batch/{id}/product/{sku}
if len(vpath) == 4 and vpath[0] == "batch" and vpath[2] == "product":
vpath.pop(0) # удалить "batch"
cherrypy.request.params["batch_id"] = vpath.pop(0)
vpath.pop(0) # удалить "product"
cherrypy.request.params["sku"] = vpath.pop(0)
return getattr(self, "item")
# всё остальное → 404
return None
@cherrypy.expose
@cherrypy.tools.json_out()
def batch_entry(self):
return {"msg": "batch list or create batch here"}
@cherrypy.expose
@cherrypy.tools.json_out()
def item(self, batch_id, sku=None):
return {"batch": batch_id, "sku": sku}
if __name__ == "__main__":
cherrypy.quickstart(GoodsApi({}), "/rest-products")
Такая схема даёт ожидаемое поведение. Запросы к /rest-products/batch/ вызывают batch_entry() как для GET, так и для POST. Запросы к /rest-products/batch/123/product/ вызывают item(batch_id=123). Запросы к /rest-products/batch/123/product/ABC123 вызывают item(batch_id=123, sku="ABC123"). Любые другие пути приводят к 404.
Почему это важно
Пользовательская REST-маршрутизация часто находится на грани между формой URL и сигнатурами обработчиков. Понимание того, что _cp_dispatch либо продолжает обход с изменённым vpath, либо немедленно завершает его, возвращая обработчик, помогает избежать неоднозначностей. Это также предотвращает тонкие конфликты параметров — например, когда одно и то же значение попадает в аргумент как из остатка vpath, так и из request.params.
Выводы
Используйте _cp_dispatch, чтобы собрать из URL ровно те части, которые нужны, сохраните их в cherrypy.request.params и верните либо обработчик, либо None. Не переименовывайте сегменты пути, пытаясь имитировать имена методов, и не возвращайте self — CherryPy воспримет это как обычные шаги обхода, а не прямую цель. Если важны ясность и строгие 404 для неподходящих путей, явный возврат обработчика делает маршрутизацию детерминированной и простой для понимания.