From df362dd7b65d08d48b53f9ad3a229ec5214040b2 Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 15:12:29 +0800 Subject: [PATCH 01/11] add EnumSetMeta: auto generate load and dump func for EnumField --- hobbit_core/flask_hobbit/schemas.py | 50 ++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/hobbit_core/flask_hobbit/schemas.py b/hobbit_core/flask_hobbit/schemas.py index f21ccf3..42e7586 100644 --- a/hobbit_core/flask_hobbit/schemas.py +++ b/hobbit_core/flask_hobbit/schemas.py @@ -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): @@ -87,3 +91,47 @@ class PagedUserSchema(PagedSchema): class Meta: strict = True + + +class EnumSetMeta(ModelSchemaMeta): + """Auto generate load and dump func for EnumField. + """ + + @classmethod + def gen_func(cls, decorator, field_name, enum): + + @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['label']) + if decorator is post_dump: + data[field_name] = enum.dump(data['label']) + 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) + + 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)) + + return schema + + +@six.add_metaclass(EnumSetMeta) +class ModelSchema(ORMSchema, SchemaMixin): + pass From 519daa9cfeed0fea31c43043b66168ab372df09f Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 15:28:17 +0800 Subject: [PATCH 02/11] default dateformat is '%Y-%m-%d %H:%M:%S' --- hobbit_core/flask_hobbit/schemas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hobbit_core/flask_hobbit/schemas.py b/hobbit_core/flask_hobbit/schemas.py index 42e7586..6bf2c83 100644 --- a/hobbit_core/flask_hobbit/schemas.py +++ b/hobbit_core/flask_hobbit/schemas.py @@ -129,6 +129,8 @@ def __new__(cls, name, bases, attrs): setattr(schema, 'dump_{}'.format(field_name), cls.gen_func(post_dump, field_name, declared.enum)) + setattr(schema.Meta, 'dateformat', '%Y-%m-%d %H:%M:%S') + return schema From 9082f341b8795126f3f33fca0b2d2f2fe76d0997 Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 15:31:15 +0800 Subject: [PATCH 03/11] verbose default is True --- hobbit_core/flask_hobbit/schemas.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hobbit_core/flask_hobbit/schemas.py b/hobbit_core/flask_hobbit/schemas.py index 6bf2c83..4ee7da1 100644 --- a/hobbit_core/flask_hobbit/schemas.py +++ b/hobbit_core/flask_hobbit/schemas.py @@ -98,7 +98,7 @@ class EnumSetMeta(ModelSchemaMeta): """ @classmethod - def gen_func(cls, decorator, field_name, enum): + def gen_func(cls, decorator, field_name, enum, verbose=True): @decorator def wrapper(self, data): @@ -108,7 +108,7 @@ def wrapper(self, data): if decorator is pre_load: data[field_name] = enum.load(data['label']) if decorator is post_dump: - data[field_name] = enum.dump(data['label']) + data[field_name] = enum.dump(data['label'], verbose) else: raise Exception( 'hobbit_core: decorator `{}` not support'.format( @@ -127,7 +127,9 @@ def __new__(cls, name, bases, attrs): 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)) + cls.gen_func( + post_dump, field_name, declared.enum, + verbose=getattr(schema.Meta, 'verbose', True))) setattr(schema.Meta, 'dateformat', '%Y-%m-%d %H:%M:%S') From 1105ebf6474f9ea84dff87a467b98659a8e45bec Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 16:12:40 +0800 Subject: [PATCH 04/11] add test for ModelSchema --- .gitignore | 2 ++ hobbit_core/flask_hobbit/schemas.py | 4 ++-- pytest.ini | 3 +++ tests/__init__.py | 20 ++++++++++++++++++++ tests/models.py | 8 ++++++++ tests/test_schemas.py | 26 ++++++++++++++++++++++++++ tests/views.py | 4 ++-- 7 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tests/test_schemas.py diff --git a/.gitignore b/.gitignore index f0db4e5..9542c55 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ docs/_templates/* hobbit_core.egg-info/* dist/* build/* + +tests/tst_app.sqlite diff --git a/hobbit_core/flask_hobbit/schemas.py b/hobbit_core/flask_hobbit/schemas.py index 4ee7da1..e5d1a2e 100644 --- a/hobbit_core/flask_hobbit/schemas.py +++ b/hobbit_core/flask_hobbit/schemas.py @@ -106,9 +106,9 @@ def wrapper(self, data): return data if decorator is pre_load: - data[field_name] = enum.load(data['label']) + data[field_name] = enum.load(data[field_name]) if decorator is post_dump: - data[field_name] = enum.dump(data['label'], verbose) + data[field_name] = enum.dump(data[field_name], verbose) else: raise Exception( 'hobbit_core: decorator `{}` not support'.format( diff --git a/pytest.ini b/pytest.ini index 93961b6..801df28 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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 diff --git a/tests/__init__.py b/tests/__init__.py index e55889b..75a70f6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -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): diff --git a/tests/models.py b/tests/models.py index 79be7ff..421575c 100644 --- a/tests/models.py +++ b/tests/models.py @@ -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='角色') diff --git a/tests/test_schemas.py b/tests/test_schemas.py new file mode 100644 index 0000000..320a203 --- /dev/null +++ b/tests/test_schemas.py @@ -0,0 +1,26 @@ +# -*- 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 + + 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': '管理员'} diff --git a/tests/views.py b/tests/views.py index 267a835..7392f55 100644 --- a/tests/views.py +++ b/tests/views.py @@ -8,12 +8,12 @@ @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()}) @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()}) From da683640d6206c35847ae497c1bc53db818ed18f Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 16:16:37 +0800 Subject: [PATCH 05/11] enhance test: assert dateformat is '%Y-%m-%d %H:%M:%S' & verbose worked --- tests/test_schemas.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 320a203..e7b6400 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -18,9 +18,21 @@ class UserSchema(ModelSchema): 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': '管理员'} From aecd79d3bce053b9eaab0e8a31712d4d36ddbbd0 Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 16:29:47 +0800 Subject: [PATCH 06/11] add doc for ModelSchema --- hobbit_core/flask_hobbit/schemas.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/hobbit_core/flask_hobbit/schemas.py b/hobbit_core/flask_hobbit/schemas.py index e5d1a2e..a3e433c 100644 --- a/hobbit_core/flask_hobbit/schemas.py +++ b/hobbit_core/flask_hobbit/schemas.py @@ -94,7 +94,8 @@ class Meta: class EnumSetMeta(ModelSchemaMeta): - """Auto generate load and dump func for EnumField. + """EnumSetMeta is a metaclass that can be used to auto generate load and + dump func for EnumField. """ @classmethod @@ -138,4 +139,24 @@ def __new__(cls, name, bases, attrs): @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 From 1b71bd6b06741a9095ce547be6e3265f1813876f Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 17:22:47 +0800 Subject: [PATCH 07/11] add test for pre_load --- hobbit_core/flask_hobbit/db.py | 6 +++++- hobbit_core/flask_hobbit/schemas.py | 17 ++++++++--------- tests/test_schemas.py | 9 +++++++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/hobbit_core/flask_hobbit/db.py b/hobbit_core/flask_hobbit/db.py index 0c87094..496eb0b 100644 --- a/hobbit_core/flask_hobbit/db.py +++ b/hobbit_core/flask_hobbit/db.py @@ -115,10 +115,11 @@ 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' @@ -126,6 +127,9 @@ def load(cls, val): 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: diff --git a/hobbit_core/flask_hobbit/schemas.py b/hobbit_core/flask_hobbit/schemas.py index a3e433c..58f73c0 100644 --- a/hobbit_core/flask_hobbit/schemas.py +++ b/hobbit_core/flask_hobbit/schemas.py @@ -108,7 +108,7 @@ def wrapper(self, data): if decorator is pre_load: data[field_name] = enum.load(data[field_name]) - if decorator is post_dump: + elif decorator is post_dump: data[field_name] = enum.dump(data[field_name], verbose) else: raise Exception( @@ -120,19 +120,18 @@ def wrapper(self, data): 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=getattr(schema.Meta, 'verbose', True))) - - setattr(schema.Meta, 'dateformat', '%Y-%m-%d %H:%M:%S') + 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 diff --git a/tests/test_schemas.py b/tests/test_schemas.py index e7b6400..0ba9464 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -36,3 +36,12 @@ class Meta: 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', + } From 302ee5adc6b35aefc78b698173984c2f2d410515 Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 17:25:29 +0800 Subject: [PATCH 08/11] add test for EnumExt.load(label) --- tests/test_db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_db.py b/tests/test_db.py index 6fa3418..8764000 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -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 From 61dc39cc06dff0ac8f2d45b7db1b73d9b382924b Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Mon, 29 Oct 2018 18:30:53 +0800 Subject: [PATCH 09/11] release 1.2.5a2 --- docs/changelog.rst | 7 ++++++- hobbit_core/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5324b29..d7e2276 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,9 +1,14 @@ Change history ============== +1.2.5a2 (2018-10-29) + +* Add ModelSchema(Auto generate load and dump func for EnumField). +* Add logging config file. + 1.2.5a1 (2018-10-25) -* Added EnumExt implementation +* Add EnumExt implementation. 1.2.5a0 (2018-10-22) diff --git a/hobbit_core/__init__.py b/hobbit_core/__init__.py index ad51bc8..e7c4f9e 100644 --- a/hobbit_core/__init__.py +++ b/hobbit_core/__init__.py @@ -1 +1 @@ -VERSION = [1, 2, 5, 'a1'] +VERSION = [1, 2, 5, 'a2'] From b33809c61775a8e050dfc866c207d1e3c227994a Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Tue, 30 Oct 2018 09:35:49 +0800 Subject: [PATCH 10/11] fix use_kwargs with fileds.missing=None --- hobbit_core/flask_hobbit/utils.py | 5 +++-- tests/test_utils.py | 25 +++++++++++++++++++-- tests/views.py | 36 +++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/hobbit_core/flask_hobbit/utils.py b/hobbit_core/flask_hobbit/utils.py index 0679be4..3c04c8e 100644 --- a/hobbit_core/flask_hobbit/utils.py +++ b/hobbit_core/flask_hobbit/utils.py @@ -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`' @@ -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, diff --git a/tests/test_utils.py b/tests/test_utils.py index 4b414cf..a9675d6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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'} diff --git a/tests/views.py b/tests/views.py index 7392f55..6aa442e 100644 --- a/tests/views.py +++ b/tests/views.py @@ -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 @@ -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, 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(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)) From 0dd77f72c6d988e86fbdfa5a192b7db257b945f6 Mon Sep 17 00:00:00 2001 From: Legolas Bloom Date: Tue, 30 Oct 2018 09:36:33 +0800 Subject: [PATCH 11/11] sync doc --- docs/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d7e2276..7374d44 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,10 +1,11 @@ Change history ============== -1.2.5a2 (2018-10-29) +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)