Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ docs/_templates/*
hobbit_core.egg-info/*
dist/*
build/*

tests/tst_app.sqlite
8 changes: 7 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
Change history
==============

1.2.5a2 (2018-10-30)

* Add ModelSchema(Auto generate load and dump func for EnumField).
* Add logging config file.
* Fix use_kwargs with fileds.missing=None.

1.2.5a1 (2018-10-25)

* Added EnumExt implementation
* Add EnumExt implementation.

1.2.5a0 (2018-10-22)

Expand Down
2 changes: 1 addition & 1 deletion hobbit_core/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
VERSION = [1, 2, 5, 'a1']
VERSION = [1, 2, 5, 'a2']
6 changes: 5 additions & 1 deletion hobbit_core/flask_hobbit/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,17 +115,21 @@ def dump(cls, label, verbose=False):

@classmethod
def load(cls, val):
"""Get label by key or value.
"""Get label by key or value. Return val when val is label.

Examples::

TaskState.load('FINISHED') # 'FINISHED'
TaskState.load(4) # 'FINISHED'
TaskState.load('新建') # 'CREATED'

Returns:
str|None: Label.
"""

if val in cls.__members__:
return val

pos = 1 if isinstance(val, six.string_types) else 0
for elem in cls:
if elem.value[pos] == val:
Expand Down
74 changes: 73 additions & 1 deletion hobbit_core/flask_hobbit/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# -*- encoding: utf-8 -*-
from marshmallow import Schema, fields, post_load
import six

from marshmallow import Schema, fields, pre_load, post_load, post_dump
from marshmallow_sqlalchemy.schema import ModelSchemaMeta
from flask_marshmallow.sqla import ModelSchema
from marshmallow_enum import EnumField


class ORMSchema(ModelSchema):
Expand Down Expand Up @@ -87,3 +91,71 @@ class PagedUserSchema(PagedSchema):

class Meta:
strict = True


class EnumSetMeta(ModelSchemaMeta):
"""EnumSetMeta is a metaclass that can be used to auto generate load and
dump func for EnumField.
"""

@classmethod
def gen_func(cls, decorator, field_name, enum, verbose=True):

@decorator
def wrapper(self, data):
if data.get(field_name) is None:
return data

if decorator is pre_load:
data[field_name] = enum.load(data[field_name])
elif decorator is post_dump:
data[field_name] = enum.dump(data[field_name], verbose)
else:
raise Exception(
'hobbit_core: decorator `{}` not support'.format(
decorator))

return data
return wrapper

def __new__(cls, name, bases, attrs):
schema = ModelSchemaMeta.__new__(cls, name, tuple(bases), attrs)
verbose = getattr(schema.Meta, 'verbose', True)

setattr(schema.Meta, 'dateformat', '%Y-%m-%d %H:%M:%S')

for field_name, declared in schema._declared_fields.items():
if not isinstance(declared, EnumField):
continue

setattr(schema, 'load_{}'.format(field_name), cls.gen_func(
pre_load, field_name, declared.enum))
setattr(schema, 'dump_{}'.format(field_name), cls.gen_func(
post_dump, field_name, declared.enum, verbose=verbose))

return schema


@six.add_metaclass(EnumSetMeta)
class ModelSchema(ORMSchema, SchemaMixin):
"""Base ModelSchema for ``class Model(db.SurrogatePK)``.

* Auto generate load and dump func for EnumField.
* Auto dump_only for ``id``, ``created_at``, ``updated_at`` fields.
* Auto set dateformat to ``'%Y-%m-%d %H:%M:%S'``.
* Auto use verbose for dump EnumField. See ``db.EnumExt``. You can define
verbose in ``Meta``.

Example::

class UserSchema(ModelSchema):
role = EnumField(RoleEnum)

class Meta:
model = User

data = UserSchema().dump(user).data
assert data['role'] == {'key': 1, 'label': 'admin', 'value': '管理员'}

"""
pass
5 changes: 3 additions & 2 deletions hobbit_core/flask_hobbit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ def _get_init_args(instance, base_class):

kwargs = {k: getattr(instance, k) for k in no_defaults
if k != 'self' and hasattr(instance, k)}
kwargs.update({k: getattr(instance, k, argspec.defaults[i])
kwargs.update({k: getattr(instance, k) if hasattr(instance, k) else
getattr(instance, k, argspec.defaults[i])
for i, k in enumerate(has_defaults)})

assert len(kwargs) == len(argspec.args) - 1, 'exclude `self`'
Expand Down Expand Up @@ -169,7 +170,7 @@ def factory(request):
only = parser.parse(argmap, request).keys()

argmap_kwargs.update({
'partial': True,
'partial': False, # fix missing=None not work
'only': only or None,
'context': {"request": request},
'strict': True,
Expand Down
3 changes: 3 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[pytest]
env =
FLASK_APP=tests/run.py
FLASK_ENV=testing
addopts = --cov hobbit_core --cov=tests --cov-report term-missing -s -x -vv -p no:warnings
20 changes: 20 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,31 @@
import functools

import pytest
from flask_sqlalchemy import model

from .run import app, db


class BaseTest(object):
root_path = os.path.split(os.path.abspath(__name__))[0]

@classmethod
def setup_class(cls):
with app.app_context():
db.create_all()

@classmethod
def teardown_class(cls):
with app.app_context():
db.drop_all()

def teardown_method(self, method):
with app.app_context():
for m in [m for m in db.Model._decl_class_registry.values()
if isinstance(m, model.DefaultMeta)]:
db.session.query(m).delete()
db.session.commit()


def rmdir(path):
if os.path.exists(path):
Expand Down
8 changes: 8 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
# -*- encoding: utf-8 -*-
from hobbit_core.flask_hobbit.db import Column, SurrogatePK
from hobbit_core.flask_hobbit.db import EnumExt

from .exts import db


class RoleEnum(EnumExt):
admin = (1, '管理员')
normal = (2, '普通用户')


class User(SurrogatePK, db.Model):
username = Column(db.String(50), nullable=True, unique=True)
email = Column(db.String(50), nullable=True, unique=True)
password = Column(db.String(255), nullable=False, server_default='')
role = Column(db.Enum(RoleEnum), doc='角色')
1 change: 1 addition & 0 deletions tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_dump(self, TaskState):
assert {'key': 0, 'value': u'新建'} == TaskState.dump('CREATED')

def test_load(self, TaskState):
assert 'FINISHED' == TaskState.load('FINISHED')
assert 'FINISHED' == TaskState.load(1)
assert 'CREATED' == TaskState.load(u'新建')
assert TaskState.load(100) is None
Expand Down
47 changes: 47 additions & 0 deletions tests/test_schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# -*- encoding: utf-8 -*-
from marshmallow_enum import EnumField

from hobbit_core.flask_hobbit.schemas import ModelSchema

from .exts import db
from . import BaseTest
from .models import User, RoleEnum


class TestSchema(BaseTest):

def test_model_schema(self, client):

class UserSchema(ModelSchema):
role = EnumField(RoleEnum)

class Meta:
model = User

assert UserSchema.Meta.dateformat == '%Y-%m-%d %H:%M:%S'

user = User(username='name', email='admin@test', role=RoleEnum.admin)
db.session.add(user)
db.session.commit()

data = UserSchema().dump(user).data
assert data['role'] == {'key': 1, 'label': 'admin', 'value': '管理员'}

class UserSchema(ModelSchema):
role = EnumField(RoleEnum)

class Meta:
model = User
verbose = False

data = UserSchema().dump(user).data
assert data['role'] == {'key': 1, 'value': '管理员'}

payload = {'username': 'name', 'email': 'admin@test'}
for role in (RoleEnum.admin.name, RoleEnum.admin.value[0],
RoleEnum.admin.value[1]):
payload['role'] = role
assert UserSchema().load(payload).data == {
'role': RoleEnum.admin, 'email': 'admin@test',
'username': 'name',
}
25 changes: 23 additions & 2 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,38 @@ def test_secure_filename_py3(self):
'i contain cool \xfcml\xe4uts.txt') == \
'i_contain_cool_umlauts.txt'

def test_use_kwargs(self, client):

class TestUseKwargs(BaseTest):

def test_use_kwargs_with_partial(self, client):
payload = {'username': 'username', 'email': 'email'}
resp = client.post('/use_kwargs_with_partial/', json=payload)
assert resp.json == payload

def test_use_kwargs_without_partial(self, client):
payload = {'username': 'username', 'email': 'email'}
resp = client.post('/use_kwargs_without_partial/', json=payload)
assert resp.json == payload

def test_use_kwargs_with_partial2(self, client):
payload = {'username': 'username'}
resp = client.post('/use_kwargs_with_partial/', json=payload)
assert resp.json == payload

def test_use_kwargs_without_partial2(self, client):
payload = {'username': 'username'}
resp = client.post('/use_kwargs_without_partial/', json=payload)
assert resp.json == {'username': 'username', 'email': None}
assert resp.json == {'username': 'username', 'email': 'missing'}

def test_use_kwargs_dictargmap_partial(self, client):
resp = client.post('/use_kwargs_dictargmap_partial/', json={})
assert resp.json == {'username': None}

def test_use_kwargs_dictargmap_partial2(self, client):
resp = client.post('/use_kwargs_dictargmap_partial/', json={
'username': None})
assert resp.json == {'username': None}

def test_base_use_kwargs_dictargmap_whitout_partial(self, client):
resp = client.post('/base_use_kwargs_dictargmap_partial/', json={})
assert resp.json == {'username': None, 'password': 'missing'}
40 changes: 36 additions & 4 deletions tests/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from flask import Blueprint, jsonify
from marshmallow import fields
from marshmallow.utils import missing
from webargs.flaskparser import use_kwargs as base_use_kwargs

from hobbit_core.flask_hobbit.utils import use_kwargs

Expand All @@ -7,13 +10,42 @@
bp = Blueprint('test', __name__)


def wrapper_kwargs(kwargs):
res = {}
for k, v in kwargs.items():
if v is missing:
res[k] = 'missing'
continue
res[k] = v
return res


@bp.route('/use_kwargs_with_partial/', methods=['POST'])
@use_kwargs(UserSchema(partial=True))
@use_kwargs(UserSchema(partial=True, exclude=['role']))
def use_kwargs_with_partial(**kwargs):
return jsonify({k: v or None for k, v in kwargs.items()})
return jsonify(wrapper_kwargs(kwargs))


@bp.route('/use_kwargs_without_partial/', methods=['POST'])
@use_kwargs(UserSchema())
@use_kwargs(UserSchema(exclude=['role']))
def use_kwargs_without_partial(**kwargs):
return jsonify({k: v or None for k, v in kwargs.items()})
return jsonify(wrapper_kwargs(kwargs))


@bp.route('/use_kwargs_dictargmap_partial/', methods=['POST'])
@use_kwargs({
'username': fields.Str(missing=None),
'password': fields.Str(allow_none=True),
}, schema_kwargs={'partial': True})
def use_kwargs_dictargmap_partial(**kwargs):
print(kwargs)
return jsonify(wrapper_kwargs(kwargs))


@bp.route('/base_use_kwargs_dictargmap_partial/', methods=['POST'])
@base_use_kwargs({
'username': fields.Str(missing=None),
'password': fields.Str(allow_none=True),
})
def base_use_kwargs_dictargmap_partial(**kwargs):
return jsonify(wrapper_kwargs(kwargs))