marshmallow-formにform-factoryが誕生しました。Nestedなフィールドのmany=Trueが有効になりました

marshmallow-formにform-factoryが誕生しました。Nestedなフィールドのmany=Trueが有効になりました。

今回の主な変更点は以下の3つです

  • 必要なら 各フィールドに描画関数を設定することができるようになりました
  • form_factoryが使えるようになりました(experimental)
  • nestedなフィールドのmany=Trueが有効になりました

必要なら 各フィールドに描画関数を設定することができるようになりました

metadataに__call__という名前の関数があるとこれを描画関数として使うことができます。

import marshmallow_form as mf


def input_tag(self):
    fmt = '<input name="{self.name}" type="{type}" value="{self.value}">'
    return fmt.format(self=self, type=self["type"])


class PersonForm(mf.Form):
    name = mf.String(__call__=input_tag, type="text")
    age = mf.Int(__call__=input_tag, type="number")


form = PersonForm(initial={"name": "foo", "age": 0})
print(form.name())
# <input name="name" type="text" value="foo">
print(form.age())
# <input name="age" type="number" value="0">

使い方としては、直接使ってももちろん良いのですが。自分で以下のようなフィールドを定義するイメージです。 partialを使って部分適用してあげるとメタデータの更新だけですが、継承などを使ったかのようにメタデータの初期値を設定する事ができます。

# your module
from functools import partial


String = partial(mf.String, type="text", __call__=input_tag)
Int = partial(mf.Int, type="number", __call__=input_tag)

今度は以下のような定義で済むようになります。

class PersonForm(mf.Form):
    name = String()
    age = Int()

form = PersonForm(initial={"name": "foo", "age": 0})
print(form.name())
# <input name="name" type="text" value="foo">
print(form.age())
# <input name="age" type="number" value="0">

form_factoryが使えるようになりました(experimental)

このStack Over Flowの質問 を見てschemaからformへの変換はあったほうが良いのかなと思いました。

そこで暫定的にですがform_factoryを作りました*1

以下の様にして使います。

from marshmallow import Schema, fields
import marshmallow_form as mf


class CommentSchema(Schema):
    id = fields.Int(doc="ID")
    text = fields.String(doc="TEXT")
    created_at = fields.DateTime(doc="CREATED_AT")
    parent_id = fields.Int(doc="PARENT_ID")

    class Meta:
        ordered = True

CommentForm = mf.form_factory("CommentForm", CommentSchema)

form = CommentForm()

for f in form:
    print(f.name, f.value, f["doc"])
# id 0 ID
# text  TEXT
# created_at None CREATED_AT
# parent_id 0 PARENT_ID

metadataをschema側に書くことも可能です。これはmarshmallow-formのmetadataもデータの在処はmarshmallowのschema側のmetadataだからです。 また、schemaを生成する時にはorderedオプションをTrueにするのが必須です。 コレを忘れた場合には、iterateした時の順序がおかしくなってしまいます。(フィールドの順序を気にする必要があるというのがFormとSchemaの違いの1つのような気がします。)

但し以下のようなfieldsオプションを使った定義には対応できていません(TODO)。

class CommentSchema(Schema):
    class Meta:
        ordered = True
        fields = ("id", "text", "created_at", "parent_id")

nestedなフィールドのmany=Trueが有効になりました

これもまた 先のStack Over Flowの質問 を見た時に思ったことなのですが。

例えばmarshmallowを使って以下のようなjson出力を生成できます。

from marshmallow import Schema, fields
from collections import namedtuple
from datetime import datetime
from mako.template import Template


# Parents -> {[Like], [Comment]} という関係

class CommentSchema(Schema):
    id = fields.Int()
    text = fields.String()
    created_at = fields.DateTime()
    parent_id = fields.Int()


class LikeSchema(Schema):
    id = fields.Int()
    created_at = fields.DateTime()
    parent_id = fields.Int()


class ParentSchema(Schema):
    comments = fields.Nested(CommentSchema, many=True)
    likes = fields.Nested(LikeSchema, many=True)
    description = fields.Str()
    title = fields.Str()


Parent = namedtuple("Parent", "id title description comments likes")
Like = namedtuple("Like", "id created_at parent_id")
Comment = namedtuple("Comment", "id text created_at parent_id")

now = datetime.now()

parent = Parent(
    id=1,
    title="this is title of article",
    description="long long text",
    comments=[
        Comment(id=1, text="hmm", created_at=now, parent_id=1),
        Comment(id=2, text="...", created_at=now, parent_id=1)
    ],
    likes=[
        Like(id=1, created_at=now, parent_id=1),
        Like(id=2, created_at=now, parent_id=1),
        Like(id=3, created_at=now, parent_id=1),
    ],
)

schema = ParentSchema()

pprint(schema.dump(parent).data, indent=2)
# { 'comments': [ { 'created_at': '2015-03-25T18:32:40.901066+00:00',
#                   'id': 1,
#                   'parent_id': 1,
#                   'text': 'hmm'},
#                 { 'created_at': '2015-03-25T18:32:40.901066+00:00',
#                   'id': 2,
#                   'parent_id': 1,
#                   'text': '...'}],
#   'description': 'long long text',
#   'id': 1,
#   'likes': [ { 'created_at': '2015-03-25T18:32:40.901066+00:00',
#                'id': 1,
#                'parent_id': 1},
#              { 'created_at': '2015-03-25T18:32:40.901066+00:00',
#                'id': 2,
#                'parent_id': 1},
#              { 'created_at': '2015-03-25T18:32:40.901066+00:00',
#                'id': 3,
#                'parent_id': 1}],
#   'title': 'this is title of article'}

jsonAPI等の場合には、既にあるデータをそのままdumpすれば良いとされる一方で、フォームを使いたい場合には大抵HTMLをレンダリングしたいことが多いです。 その際には、他に付随したメタデータもラップした形で取得したいという思いが出てきます。 また、relationには1:1だけではなく1:Nの関係が出てきます。これをmarshmallowではNestedにmany=Trueというオプションを与えて設定するのですが、コレに対応していませんでした。現在は以下のようなことが可能です。

import marshmallow_form as mf


class ParentForm(mf.form_factory("_ParentForm", ParentSchema)):
    class Meta:
        overrides = {
            "description": {"ja": "概要"},
            "comments": {"ja": "コメント"}
        }


template = Template("""
${form.description['ja']}: ${form.description.value}
${form.comments['ja']}:
%for c in form.comments:
 - ${c.id.value}(${c.created_at.value})
%endfor
${form.likes['ja'] or form.likes._name}:
%for c in form.likes:
 - ${c.id.value}(${c.created_at.value})
%endfor
""")


form = ParentForm.from_object(parent)
print(template.render(form=form))

# 概要: long long text
# コメント:
#  - 1(2015-03-25T22:20:56.635734+00:00)
#  - 2(2015-03-25T22:20:56.635734+00:00)
# likes:
#  - 1(2015-03-25T22:20:56.635734+00:00)
#  - 2(2015-03-25T22:20:56.635734+00:00)
#  - 3(2015-03-25T22:20:56.635734+00:00)

但し注意点としてNested(many=True)を要素として持ったフォームは、通常のiterateで常にfieldが返ってくるという前提が崩れます。

for f in form:
   #ここでfはfieldかもしれないし。many=Trueに対応したsubform的な何かかもしれない。

したがって以下の様に個別に利用してレンダリングするかレンダリング関数側でケアしてあげる必要があります。

for f in form:
    if isinstance(f, NestedListBoundField):
        for sf in f:
             # do something
    else:
        # do something

*1:ロジック的に現状metaclassのnewと重複してしまっている部分がある