Chytřejší volání obsluhy view

... Petr Blahoš, 28. 3. 2017 Pyramid Python

View handler ve webovém frameworku dostane jako parametr request v nějaké formě. Z něj si musí vydolovat parametry, a s nimi pak pracuje. Takhle to funguje ve flasku, v djangu, v bottle, i v pyramid. Kdysi dávno jsem četl Supercharge Your Python Developers od Jeffa Knuppa, a napadlo mě, že bych měl mít něco podobného. Takže: Zařídíme, abychom v naší Pyramid aplikaci místo

def view_handler(context, request):
    name = request.params["name"]
    phone = request.params["address"]
    ...

používali:

@extractparams(["name", "phone"])
def view_handler(context, request, name, phone):
    ...

Prostředí

Budeme potřebovat malou aplikaci, na které si všechno vyzkoušíme. Vytvořte podle dokumentace projekt z pyramid-cookiecutter-starter. Já si ho nazvu třeba EXTPAR. Budu používat verzi s šablonami mako. Jakmile si projekt vytvoříme a cvičně odstartujeme (pserve --reload development.ini), začneme se změnami.

Budeme mít několik view, takže si budeme muset přidat informace o routingu. Do __init__.py přidáme:

    config.add_route('home', '/')  # tohle už tam je...
    config.add_route('test1', '/test1')
    config.add_route('test2', '/test2')

Zároveň si do views.py přidáme obsluhu těchto nových views:

@view_config(route_name='test1', renderer='templates/test.mako')
def t1_view(context, request):
    return {'project': 'EXTPAR'}


@view_config(route_name='test2', renderer='templates/test.mako')
def t2_view(context, request):
    return {'project': 'EXTPAR'}

A šablonu templates/test.mako, která nám bude zatím zobrazovat jen path_info:

<%inherit file="layout.mako"/>
<div class="content">
  <h1><span class="font-semi-bold">Pyramid</span>
  <span class="smaller">${ request.path_info }</span></h1>
</div>

Do mytemplate.mako si ještě přidáme odkazy na naše testovací view, a k nim si rovnou dáme i nějaké ty request parametry:

<%inherit file="layout.mako"/>
<div class="content">
  <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Starter project</span></h1>
  <a href="${ request.route_url("test1", _query={"number": 1231, "text": "I am some text"}) }">TEST1</a>
  <a href="${ request.route_url("test2", _query={"number": 1231, "text": "I am some text"}) }">TEST2</a>
</div>

Klasicky

V test1 to uděláme klasicky: použijeme request.params.

@view_config(route_name='test1', renderer='templates/test.mako')
def t1_view(context, request):
    text = request.params["text"]
    number = int(request.params["number"])
    print("text=%s, number=%s" % (text, number))
    return {'project': 'EXTPAR'}

S dekorátorem

Jak se vlastně volá view v pyramid? Jsou dvě verze. Jedna bere jako parametr context a request, druhá jen request. Náš dekorátor tohle obalí, takže ten vnitřní mechanizmus pyramid vlastně nebude volat přímo naši funkci, ale tu obalenou. Hmm, raději si to vyzkoušíme.

Dekorátor si zatím vložíme přímo do views.py, a použijeme na funkci t2_view:

from functools import wraps


def infodecorator(dummy):
    def info_decorator(func):
        @wraps(func)
        def func_wrapper(*args, **kwargs):
            print("ARGS:", args)
            print("KW:", kwargs)
            print("dummy:", dummy)
            return func(*args, **kwargs)
        return func_wrapper
    return info_decorator


@view_config(route_name='test2', renderer='templates/test.mako')
@infodecorator("dummy")
def t2_view(context, request):
    return {'project': 'EXTPAR'}

Když se teď podíváte na výpis na konzoli, uvidíte, že dekorovaná funkce dostává 2 poziční parametry:

  • DefaultRootFactory, což je context
  • Request, což je request

Pro zajímavost upravíme dekorátor a volání funkce takto:

def infodecorator(dummy):
    def info_decorator(func):
        @wraps(func)
        def func_wrapper(request):  # změna hlavičky - jen jeden parametr
            print("request:", request)
            print("dummy:", dummy)
            return func(request)  # předává se pouze request
        return func_wrapper
    return info_decorator

@view_config(route_name='test2', renderer='templates/test.mako')
@infodecorator("dummy")
def t2_view(request):  # změna hlavičky - jen jeden parametr
    return {'project': 'EXTPAR'}

Nedokážu teď vysvětlit, co se děje uvnitř, každopádně chová se to tak, že když má hlavička funkce více parametrů, volá pyramid s argumenty (context, request), když má jeden, volá pouze s request. My se vrátíme k předchozí verzi. Teď si na zkoušku přidáme vlastní parametry. Použijeme k tomu kwargs:

def paramsdecorator(dummy):
    def params_decorator(func):
        @wraps(func)
        def func_wrapper(*args, **kwargs):
            kwargs.update({"text": "text-parameter", "number": 123})
            return func(*args, **kwargs)
        return func_wrapper
    return params_decorator


@view_config(route_name='test2', renderer='templates/test.mako')
@paramsdecorator(None)
def t2_view(context, request, text, number):
    print("text=%s, number=%s" % (text, number))
    return {'project': 'EXTPAR'}

Jak vidíte, zaktualizovali jsme hlavičku funkce t2_view, a se správnými parametry ji voláme z dekorátoru. Teď ještě musíme být schopni parametry konfigurovat, a vytáhnout je z requestu:

def extractparams(names):
    def params_decorator(func):
        @wraps(func)
        def func_wrapper(*args, **kwargs):
            for n in names:
                kwargs[n] = args[1].params.get(n)
            return func(*args, **kwargs)
        return func_wrapper
    return params_decorator


@view_config(route_name='test2', renderer='templates/test.mako')
@extractparams(["text", "number"])
def t2_view(context, request, text, number):
    print("text=%s, number=%s" % (text, number))
    return {'project': 'EXTPAR'}

Můžete zvesela zkoušet a volat s upravenými parametry, nebo i bez nich (request.params.get(name) zařídí None).

Class-based views

Jak už jsem zmínil, máme více variant volání obsluhy view. Vyzkoušíme si teď variantu s class-based view. Uděláme si nový soubor cbview.py, ve kterém si view handler zrovna odekorujeme naším známým infodecorator (nezapomeňte si přidat route do __init__.py, a klidně i odkaz do šablony mytemplate.mako):

from pyramid.view import view_config
from extpar.views import (infodecorator, extractparams, )


class CBView(object):
    def __init__(self, context, request):
        self.context = context
        self.request = request

    @view_config(route_name='test3', renderer='templates/mytemplate.mako')
    @infodecorator(None)
    def handler(self):
        return {}

Vidíme, že odekorovaná funkce dostane, naprosto očekávaně, jeden argument, a to CBView objekt (tedy self funkce CBView.handler). S tím náš dekorátor extractparams nepočítá, takže nemůže fungovat. Můžeme si samozřejmě pomoct:

def extractparams(names):
    def params_decorator(func):
        @wraps(func)
        def func_wrapper(*args, **kwargs):
            if "request" in args[0].__dict__:
                params = args[0].request.params
            elif "params" in args[0].__dict__:
                params = args[0].params
            else:
                params = args[1].params
            for n in names:
                kwargs[n] = params.get(n)
            return func(*args, **kwargs)
        return func_wrapper
    return params_decorator


class CBView(object):
    def __init__(self, context, request):
        self.context = context
        self.request = request

    @view_config(route_name='test3', renderer='templates/test.mako')
    @extractparams(["text", "number"])
    def handler(self, text, number):
        print("text=%s, number=%s" % (text, number))
        return {}

Tento dekorátor sem uvádím spíš pro úplnost, ne proto, že bych jej sám chtěl takto použít. Cítím, že mít sadu if-elif-else na této úrovni je špatně. Řešením je používat jen jeden způsob definice view handlerů, a tím pádem mít i v dekorátoru jen jednu cestu, jak se dostat k parametrům. V tomto článku jsem se ale chtěl zabývat jenom technickou stránkou problému.

Závěr

Popsané řešení nám nabízí jisté zjednodušení práce s request parametry ve webovém frameworku Pyramid, a tím umožňuje snížit množství kódu a unifikovat jej. Neřeší ale otázku toho, jestli je předaný parametr očekávaného datového typu. Tím se budeme zabývat příště.