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 для неподходящих путей, явный возврат обработчика делает маршрутизацию детерминированной и простой для понимания.

Статья основана на вопросе с сайта StackOverflow от Bart Friederichs и ответе Madhav Singh Rana.