Skip to content

Commit cda386b

Browse files
authored
Merge pull request #34 from TTWShell/feature/meta-enum-fields
add EnumSetMeta: auto generate load and dump func for EnumField
2 parents 6efc0bc + 0dd77f7 commit cda386b

File tree

13 files changed

+229
-12
lines changed

13 files changed

+229
-12
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ docs/_templates/*
1515
hobbit_core.egg-info/*
1616
dist/*
1717
build/*
18+
19+
tests/tst_app.sqlite

docs/changelog.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
Change history
22
==============
33

4+
1.2.5a2 (2018-10-30)
5+
6+
* Add ModelSchema(Auto generate load and dump func for EnumField).
7+
* Add logging config file.
8+
* Fix use_kwargs with fileds.missing=None.
9+
410
1.2.5a1 (2018-10-25)
511

6-
* Added EnumExt implementation
12+
* Add EnumExt implementation.
713

814
1.2.5a0 (2018-10-22)
915

hobbit_core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = [1, 2, 5, 'a1']
1+
VERSION = [1, 2, 5, 'a2']

hobbit_core/flask_hobbit/db.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,17 +115,21 @@ def dump(cls, label, verbose=False):
115115

116116
@classmethod
117117
def load(cls, val):
118-
"""Get label by key or value.
118+
"""Get label by key or value. Return val when val is label.
119119
120120
Examples::
121121
122+
TaskState.load('FINISHED') # 'FINISHED'
122123
TaskState.load(4) # 'FINISHED'
123124
TaskState.load('新建') # 'CREATED'
124125
125126
Returns:
126127
str|None: Label.
127128
"""
128129

130+
if val in cls.__members__:
131+
return val
132+
129133
pos = 1 if isinstance(val, six.string_types) else 0
130134
for elem in cls:
131135
if elem.value[pos] == val:

hobbit_core/flask_hobbit/schemas.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# -*- encoding: utf-8 -*-
2-
from marshmallow import Schema, fields, post_load
2+
import six
3+
4+
from marshmallow import Schema, fields, pre_load, post_load, post_dump
5+
from marshmallow_sqlalchemy.schema import ModelSchemaMeta
36
from flask_marshmallow.sqla import ModelSchema
7+
from marshmallow_enum import EnumField
48

59

610
class ORMSchema(ModelSchema):
@@ -87,3 +91,71 @@ class PagedUserSchema(PagedSchema):
8791

8892
class Meta:
8993
strict = True
94+
95+
96+
class EnumSetMeta(ModelSchemaMeta):
97+
"""EnumSetMeta is a metaclass that can be used to auto generate load and
98+
dump func for EnumField.
99+
"""
100+
101+
@classmethod
102+
def gen_func(cls, decorator, field_name, enum, verbose=True):
103+
104+
@decorator
105+
def wrapper(self, data):
106+
if data.get(field_name) is None:
107+
return data
108+
109+
if decorator is pre_load:
110+
data[field_name] = enum.load(data[field_name])
111+
elif decorator is post_dump:
112+
data[field_name] = enum.dump(data[field_name], verbose)
113+
else:
114+
raise Exception(
115+
'hobbit_core: decorator `{}` not support'.format(
116+
decorator))
117+
118+
return data
119+
return wrapper
120+
121+
def __new__(cls, name, bases, attrs):
122+
schema = ModelSchemaMeta.__new__(cls, name, tuple(bases), attrs)
123+
verbose = getattr(schema.Meta, 'verbose', True)
124+
125+
setattr(schema.Meta, 'dateformat', '%Y-%m-%d %H:%M:%S')
126+
127+
for field_name, declared in schema._declared_fields.items():
128+
if not isinstance(declared, EnumField):
129+
continue
130+
131+
setattr(schema, 'load_{}'.format(field_name), cls.gen_func(
132+
pre_load, field_name, declared.enum))
133+
setattr(schema, 'dump_{}'.format(field_name), cls.gen_func(
134+
post_dump, field_name, declared.enum, verbose=verbose))
135+
136+
return schema
137+
138+
139+
@six.add_metaclass(EnumSetMeta)
140+
class ModelSchema(ORMSchema, SchemaMixin):
141+
"""Base ModelSchema for ``class Model(db.SurrogatePK)``.
142+
143+
* Auto generate load and dump func for EnumField.
144+
* Auto dump_only for ``id``, ``created_at``, ``updated_at`` fields.
145+
* Auto set dateformat to ``'%Y-%m-%d %H:%M:%S'``.
146+
* Auto use verbose for dump EnumField. See ``db.EnumExt``. You can define
147+
verbose in ``Meta``.
148+
149+
Example::
150+
151+
class UserSchema(ModelSchema):
152+
role = EnumField(RoleEnum)
153+
154+
class Meta:
155+
model = User
156+
157+
data = UserSchema().dump(user).data
158+
assert data['role'] == {'key': 1, 'label': 'admin', 'value': '管理员'}
159+
160+
"""
161+
pass

hobbit_core/flask_hobbit/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ def _get_init_args(instance, base_class):
130130

131131
kwargs = {k: getattr(instance, k) for k in no_defaults
132132
if k != 'self' and hasattr(instance, k)}
133-
kwargs.update({k: getattr(instance, k, argspec.defaults[i])
133+
kwargs.update({k: getattr(instance, k) if hasattr(instance, k) else
134+
getattr(instance, k, argspec.defaults[i])
134135
for i, k in enumerate(has_defaults)})
135136

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

171172
argmap_kwargs.update({
172-
'partial': True,
173+
'partial': False, # fix missing=None not work
173174
'only': only or None,
174175
'context': {"request": request},
175176
'strict': True,

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
[pytest]
2+
env =
3+
FLASK_APP=tests/run.py
4+
FLASK_ENV=testing
25
addopts = --cov hobbit_core --cov=tests --cov-report term-missing -s -x -vv -p no:warnings

tests/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,31 @@
44
import functools
55

66
import pytest
7+
from flask_sqlalchemy import model
8+
9+
from .run import app, db
710

811

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

15+
@classmethod
16+
def setup_class(cls):
17+
with app.app_context():
18+
db.create_all()
19+
20+
@classmethod
21+
def teardown_class(cls):
22+
with app.app_context():
23+
db.drop_all()
24+
25+
def teardown_method(self, method):
26+
with app.app_context():
27+
for m in [m for m in db.Model._decl_class_registry.values()
28+
if isinstance(m, model.DefaultMeta)]:
29+
db.session.query(m).delete()
30+
db.session.commit()
31+
1232

1333
def rmdir(path):
1434
if os.path.exists(path):

tests/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1+
# -*- encoding: utf-8 -*-
12
from hobbit_core.flask_hobbit.db import Column, SurrogatePK
3+
from hobbit_core.flask_hobbit.db import EnumExt
24

35
from .exts import db
46

57

8+
class RoleEnum(EnumExt):
9+
admin = (1, '管理员')
10+
normal = (2, '普通用户')
11+
12+
613
class User(SurrogatePK, db.Model):
714
username = Column(db.String(50), nullable=True, unique=True)
815
email = Column(db.String(50), nullable=True, unique=True)
916
password = Column(db.String(255), nullable=False, server_default='')
17+
role = Column(db.Enum(RoleEnum), doc='角色')

tests/test_db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def test_dump(self, TaskState):
2323
assert {'key': 0, 'value': u'新建'} == TaskState.dump('CREATED')
2424

2525
def test_load(self, TaskState):
26+
assert 'FINISHED' == TaskState.load('FINISHED')
2627
assert 'FINISHED' == TaskState.load(1)
2728
assert 'CREATED' == TaskState.load(u'新建')
2829
assert TaskState.load(100) is None

0 commit comments

Comments
 (0)