From 68109530148d87800cab0abe4bc288a0d9248550 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Mon, 27 Feb 2017 09:55:50 +0000 Subject: [PATCH 1/9] added OrderedDynamicField class to store data in the defined order because of #203 --- mongoengine/fields.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 11425095..4b6a1a9b 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -5,6 +5,7 @@ import re import time import uuid import warnings +from collections import OrderedDict from operator import itemgetter from bson import Binary, DBRef, ObjectId, SON @@ -49,7 +50,7 @@ __all__ = ( 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField', - 'MultiPolygonField', 'GeoJsonBaseField' + 'MultiPolygonField', 'GeoJsonBaseField', 'OrderedDynamicField' ) RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -644,7 +645,7 @@ class DynamicField(BaseField): is_list = True value = {k: v for k, v in enumerate(value)} - data = {} + data = self._container_type() if hasattr(self, '_container_type') else {} for k, v in value.iteritems(): data[k] = self.to_mongo(v, use_db_field, fields) @@ -675,6 +676,16 @@ class DynamicField(BaseField): value.validate(clean=clean) +class OrderedDynamicField(DynamicField): + """A field that wraps DynamicField. This uses OrderedDict class + to guarantee to store data in the defined order instead of dict. + """ + + def __init__(self, *args, **kwargs): + super(OrderedDynamicField, self).__init__(*args, **kwargs) + self._container_type = OrderedDict + + class ListField(ComplexBaseField): """A list field that wraps a standard field, allowing multiple instances of the field to be used as a list in the database. From 84a8f1eb2b0c21f3e245469a469e14194c8c2d2a Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Tue, 28 Feb 2017 03:35:39 +0000 Subject: [PATCH 2/9] added OrderedDocument class to decode BSON data to OrderedDict for retrieving data in order --- mongoengine/document.py | 28 +++++++++++++++++++++++++++- mongoengine/queryset/base.py | 4 ++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index b79e5e97..a2a5e156 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,6 +1,7 @@ import re import warnings +from collections import OrderedDict from bson.dbref import DBRef import pymongo from pymongo.read_preferences import ReadPreference @@ -21,7 +22,8 @@ from mongoengine.queryset import (NotUniqueError, OperationError, __all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument', 'DynamicEmbeddedDocument', 'OperationError', - 'InvalidCollectionError', 'NotUniqueError', 'MapReduceDocument') + 'InvalidCollectionError', 'NotUniqueError', 'MapReduceDocument', + 'OrderedDocument') def includes_cls(fields): @@ -1036,3 +1038,27 @@ class MapReduceDocument(object): self._key_object = self._document.objects.with_id(self.key) return self._key_object return self._key_object + + +class OrderedDocument(Document): + """A document that is almost same with Document except for returning + results in OrderedDict instead of dict. + """ + + # The __metaclass__ attribute is removed by 2to3 when running with Python3 + # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 + my_metaclass = TopLevelDocumentMetaclass + __metaclass__ = TopLevelDocumentMetaclass + + @classmethod + def _get_collection(cls): + collection = super(OrderedDocument, cls)._get_collection() + + if IS_PYMONGO_3: + # returns collection object which is set OrderedDict class to be decoded from BSON document + from bson import CodecOptions + return collection.with_options(codec_options=CodecOptions(document_class=OrderedDict)) + else: + # set attribute to specify the class to be decoeded + cls.decoded_class = OrderedDict + return collection diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7e485686..4738f5f7 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1501,6 +1501,10 @@ class BaseQuerySet(object): cursor_args['read_preference'] = self._read_preference else: cursor_args['slave_okay'] = self._slave_okay + + # set decode format if needed + if hasattr(self._document, 'decoded_class'): + cursor_args['as_class'] = self._document.decoded_class else: fields_name = 'projection' # snapshot is not handled at all by PyMongo 3+ From e32a9777d794103a562ed6c087751dea665b7fdc Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Tue, 28 Feb 2017 03:35:53 +0000 Subject: [PATCH 3/9] added test for OrderedDynamicField and OrderedDocument --- tests/fields/fields.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 318c0c59..3516471f 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -7,6 +7,7 @@ import itertools import re from nose.plugins.skip import SkipTest +from collections import OrderedDict import six try: @@ -4499,6 +4500,40 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertTrue(hasattr(CustomData.c_field, 'custom_data')) self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) + def test_ordered_dynamic_fields_class(self): + """ + Tests that OrderedDynamicFields interits features of the DynamicFields + and saves/retrieves data in order. + """ + class Member(Document): + name = StringField() + age = IntField() + + class Team(OrderedDocument): + members = OrderedDynamicField() + + Member.drop_collection() + Team.drop_collection() + + member_info = [ + ('Martin McFly', 17), + ('Emmett Brown', 65), + ('George McFly', 47) + ] + members = OrderedDict() + for name, age in member_info: + members[name] = Member(name=name, age=age) + members[name].save() + + Team(members=members).save() + + index = 0 + team = Team.objects.get() + for member in team.members: + print("%s == %s" % (member, member_info[index][0])) + self.assertEqual(member, member_info[index][0]) + index += 1 + if __name__ == '__main__': unittest.main() From 5957dc72eb209982a13821f6afe3e74ca39719a8 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 1 Mar 2017 09:20:57 +0000 Subject: [PATCH 4/9] To achive storing object data in order with minimum implementation, I changed followings. - added optional parameter `container_class` which enables to choose intermediate class at encoding Python data, instead of additional field class. - removed OrderedDocument class because the equivalent feature could be implemented by the outside of Mongoengine. --- mongoengine/document.py | 28 +---------------------- mongoengine/fields.py | 23 ++++++++----------- mongoengine/queryset/base.py | 4 ---- tests/fields/fields.py | 44 +++++++++++++++--------------------- 4 files changed, 29 insertions(+), 70 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index a2a5e156..b79e5e97 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,7 +1,6 @@ import re import warnings -from collections import OrderedDict from bson.dbref import DBRef import pymongo from pymongo.read_preferences import ReadPreference @@ -22,8 +21,7 @@ from mongoengine.queryset import (NotUniqueError, OperationError, __all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument', 'DynamicEmbeddedDocument', 'OperationError', - 'InvalidCollectionError', 'NotUniqueError', 'MapReduceDocument', - 'OrderedDocument') + 'InvalidCollectionError', 'NotUniqueError', 'MapReduceDocument') def includes_cls(fields): @@ -1038,27 +1036,3 @@ class MapReduceDocument(object): self._key_object = self._document.objects.with_id(self.key) return self._key_object return self._key_object - - -class OrderedDocument(Document): - """A document that is almost same with Document except for returning - results in OrderedDict instead of dict. - """ - - # The __metaclass__ attribute is removed by 2to3 when running with Python3 - # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 - my_metaclass = TopLevelDocumentMetaclass - __metaclass__ = TopLevelDocumentMetaclass - - @classmethod - def _get_collection(cls): - collection = super(OrderedDocument, cls)._get_collection() - - if IS_PYMONGO_3: - # returns collection object which is set OrderedDict class to be decoded from BSON document - from bson import CodecOptions - return collection.with_options(codec_options=CodecOptions(document_class=OrderedDict)) - else: - # set attribute to specify the class to be decoeded - cls.decoded_class = OrderedDict - return collection diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 4b6a1a9b..70061e08 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -5,7 +5,6 @@ import re import time import uuid import warnings -from collections import OrderedDict from operator import itemgetter from bson import Binary, DBRef, ObjectId, SON @@ -50,7 +49,7 @@ __all__ = ( 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField', - 'MultiPolygonField', 'GeoJsonBaseField', 'OrderedDynamicField' + 'MultiPolygonField', 'GeoJsonBaseField' ) RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -620,6 +619,14 @@ class DynamicField(BaseField): Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data""" + def __init__(self, container_class=dict, *args, **kwargs): + self._container_cls = container_class + if not issubclass(self._container_cls, dict): + self.error('The class that is specified in `container_class` parameter ' + 'must be a subclass of `dict`.') + + super(DynamicField, self).__init__(*args, **kwargs) + def to_mongo(self, value, use_db_field=True, fields=None): """Convert a Python type to a MongoDB compatible type. """ @@ -645,7 +652,7 @@ class DynamicField(BaseField): is_list = True value = {k: v for k, v in enumerate(value)} - data = self._container_type() if hasattr(self, '_container_type') else {} + data = self._container_cls() for k, v in value.iteritems(): data[k] = self.to_mongo(v, use_db_field, fields) @@ -676,16 +683,6 @@ class DynamicField(BaseField): value.validate(clean=clean) -class OrderedDynamicField(DynamicField): - """A field that wraps DynamicField. This uses OrderedDict class - to guarantee to store data in the defined order instead of dict. - """ - - def __init__(self, *args, **kwargs): - super(OrderedDynamicField, self).__init__(*args, **kwargs) - self._container_type = OrderedDict - - class ListField(ComplexBaseField): """A list field that wraps a standard field, allowing multiple instances of the field to be used as a list in the database. diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 4738f5f7..7e485686 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1501,10 +1501,6 @@ class BaseQuerySet(object): cursor_args['read_preference'] = self._read_preference else: cursor_args['slave_okay'] = self._slave_okay - - # set decode format if needed - if hasattr(self._document, 'decoded_class'): - cursor_args['as_class'] = self._document.decoded_class else: fields_name = 'projection' # snapshot is not handled at all by PyMongo 3+ diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 3516471f..b73eb0af 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -4500,39 +4500,31 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertTrue(hasattr(CustomData.c_field, 'custom_data')) self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) - def test_ordered_dynamic_fields_class(self): + def test_dynamicfield_with_container_class(self): """ - Tests that OrderedDynamicFields interits features of the DynamicFields - and saves/retrieves data in order. + Tests that object can be stored in order by DynamicField class + with container_class parameter. """ - class Member(Document): - name = StringField() - age = IntField() + raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)] - class Team(OrderedDocument): - members = OrderedDynamicField() + class Doc(Document): + ordered_data = DynamicField(container_class=OrderedDict) + unordered_data = DynamicField() - Member.drop_collection() - Team.drop_collection() + Doc.drop_collection() - member_info = [ - ('Martin McFly', 17), - ('Emmett Brown', 65), - ('George McFly', 47) - ] - members = OrderedDict() - for name, age in member_info: - members[name] = Member(name=name, age=age) - members[name].save() + doc = Doc(ordered_data=OrderedDict(raw_data), + unordered_data=dict(raw_data)).save() - Team(members=members).save() + self.assertEqual(type(doc.ordered_data), OrderedDict) + self.assertEqual(type(doc.unordered_data), dict) + self.assertEqual([k for k,_ in doc.ordered_data.items()], ['d', 'c', 'b', 'a']) + self.assertNotEqual([k for k,_ in doc.unordered_data.items()], ['d', 'c', 'b', 'a']) - index = 0 - team = Team.objects.get() - for member in team.members: - print("%s == %s" % (member, member_info[index][0])) - self.assertEqual(member, member_info[index][0]) - index += 1 + def test_dynamicfield_with_wrong_container_class(self): + with self.assertRaises(ValidationError): + class DocWithInvalidField: + data = DynamicField(container_class=list) if __name__ == '__main__': From 6aaf9ba470edcba89489b95f0302884730bf70ca Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 1 Mar 2017 09:32:28 +0000 Subject: [PATCH 5/9] removed a checking of dict order because this order is not cared (some implementation might be in ordered, but other one is not) --- tests/fields/fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index b73eb0af..2db37317 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -4519,7 +4519,6 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertEqual(type(doc.ordered_data), OrderedDict) self.assertEqual(type(doc.unordered_data), dict) self.assertEqual([k for k,_ in doc.ordered_data.items()], ['d', 'c', 'b', 'a']) - self.assertNotEqual([k for k,_ in doc.unordered_data.items()], ['d', 'c', 'b', 'a']) def test_dynamicfield_with_wrong_container_class(self): with self.assertRaises(ValidationError): From f2fe58c3c5d01e49accc7c48356adbb6694d8693 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 8 Mar 2017 10:25:41 +0000 Subject: [PATCH 6/9] Added a condition to store data to ObjectDict when the items type is it Previous dereference implementation re-contains data as `dict` except for the predicted type. But the OrderedDict is not predicted, so the its data would be converted `dict` implicitly. As the result, the order of stored data get wrong. And this patch prevents it. --- mongoengine/dereference.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 59204d4d..c11449b9 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from bson import DBRef, SON import six @@ -201,6 +202,10 @@ class DeReference(object): as_tuple = isinstance(items, tuple) iterator = enumerate(items) data = [] + elif type(items) == OrderedDict: + is_list = False + iterator = items.iteritems() + data = OrderedDict() else: is_list = False iterator = items.iteritems() From 9cd3dcdebf36728fc1ba8958bd69ccd3edff1d24 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 8 Mar 2017 14:45:43 +0000 Subject: [PATCH 7/9] Added a test for the change of the condition in DeReference processing This checks DBRef conversion using DynamicField with the ordering guarantee. --- tests/test_dereference.py | 70 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/test_dereference.py b/tests/test_dereference.py index 7f58a85b..9a976611 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -2,10 +2,15 @@ import unittest from bson import DBRef, ObjectId +from collections import OrderedDict from mongoengine import * from mongoengine.connection import get_db from mongoengine.context_managers import query_counter +from mongoengine.python_support import IS_PYMONGO_3 +from mongoengine.base import TopLevelDocumentMetaclass +if IS_PYMONGO_3: + from bson import CodecOptions class FieldTest(unittest.TestCase): @@ -1287,5 +1292,70 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) + def test_dynamic_field_dereference(self): + class Merchandise(Document): + name = StringField() + price = IntField() + + class Store(Document): + merchandises = DynamicField() + + Merchandise.drop_collection() + Store.drop_collection() + + merchandises = { + '#1': Merchandise(name='foo', price=100).save(), + '#2': Merchandise(name='bar', price=120).save(), + '#3': Merchandise(name='baz', price=110).save(), + } + Store(merchandises=merchandises).save() + + store = Store.objects().first() + for obj in store.merchandises.values(): + self.assertFalse(isinstance(obj, Merchandise)) + + store.select_related() + for obj in store.merchandises.values(): + self.assertTrue(isinstance(obj, Merchandise)) + + def test_dynamic_field_dereference_with_ordering_guarantee_on_pymongo3(self): + # This is because 'codec_options' is supported on pymongo3 or later + if IS_PYMONGO_3: + class OrderedDocument(Document): + my_metaclass = TopLevelDocumentMetaclass + __metaclass__ = TopLevelDocumentMetaclass + + @classmethod + def _get_collection(cls): + collection = super(OrderedDocument, cls)._get_collection() + opts = CodecOptions(document_class=OrderedDict) + + return collection.with_options(codec_options=opts) + + class Merchandise(Document): + name = StringField() + price = IntField() + + class Store(OrderedDocument): + merchandises = DynamicField(container_class=OrderedDict) + + Merchandise.drop_collection() + Store.drop_collection() + + merchandises = OrderedDict() + merchandises['#1'] = Merchandise(name='foo', price=100).save() + merchandises['#2'] = Merchandise(name='bar', price=120).save() + merchandises['#3'] = Merchandise(name='baz', price=110).save() + + Store(merchandises=merchandises).save() + + store = Store.objects().first() + + store.select_related() + + # confirms that the load data order is same with the one at storing + self.assertTrue(type(store.merchandises), OrderedDict) + self.assertEqual(','.join(store.merchandises.keys()), '#1,#2,#3') + if __name__ == '__main__': unittest.main() From ffbb2c96896a184032807ab00dbcc0ca89ece77a Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 8 Mar 2017 14:46:04 +0000 Subject: [PATCH 8/9] This is Additional tests for the container_class parameter of DynamicField This tests DynamicField dereference with ordering guarantee. --- tests/fields/fields.py | 50 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 2db37317..2c0d1228 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -5,6 +5,7 @@ import uuid import math import itertools import re +import pymongo from nose.plugins.skip import SkipTest from collections import OrderedDict @@ -26,9 +27,12 @@ except ImportError: from mongoengine import * from mongoengine.connection import get_db from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList, - _document_registry) + _document_registry, TopLevelDocumentMetaclass) -from tests.utils import MongoDBTestCase +from tests.utils import MongoDBTestCase, MONGO_TEST_DB +from mongoengine.python_support import IS_PYMONGO_3 +if IS_PYMONGO_3: + from bson import CodecOptions __all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase") @@ -4513,18 +4517,54 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): Doc.drop_collection() - doc = Doc(ordered_data=OrderedDict(raw_data), - unordered_data=dict(raw_data)).save() + doc = Doc(ordered_data=OrderedDict(raw_data), unordered_data=dict(raw_data)).save() + # checks that the data is in order self.assertEqual(type(doc.ordered_data), OrderedDict) self.assertEqual(type(doc.unordered_data), dict) - self.assertEqual([k for k,_ in doc.ordered_data.items()], ['d', 'c', 'b', 'a']) + self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a') + + # checks that the data is stored to the database in order + pymongo_db = pymongo.MongoClient()[MONGO_TEST_DB] + if IS_PYMONGO_3: + codec_option = CodecOptions(document_class=OrderedDict) + db_doc = pymongo_db.doc.with_options(codec_options=codec_option).find_one() + else: + db_doc = pymongo_db.doc.find_one(as_class=OrderedDict) + + self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a') def test_dynamicfield_with_wrong_container_class(self): with self.assertRaises(ValidationError): class DocWithInvalidField: data = DynamicField(container_class=list) + def test_dynamicfield_with_wrong_container_class_and_reload_docuemnt(self): + # This is because 'codec_options' is supported on pymongo3 or later + if IS_PYMONGO_3: + class OrderedDocument(Document): + my_metaclass = TopLevelDocumentMetaclass + __metaclass__ = TopLevelDocumentMetaclass + + @classmethod + def _get_collection(cls): + collection = super(OrderedDocument, cls)._get_collection() + opts = CodecOptions(document_class=OrderedDict) + + return collection.with_options(codec_options=opts) + + raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)] + + class Doc(OrderedDocument): + data = DynamicField(container_class=OrderedDict) + + Doc.drop_collection() + + doc = Doc(data=OrderedDict(raw_data)).save() + doc.reload() + + self.assertEqual(type(doc.data), OrderedDict) + self.assertEqual(','.join(doc.data.keys()), 'd,c,b,a') if __name__ == '__main__': unittest.main() From d8b238d5f172ec058952046b50921d879e7bc826 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Thu, 6 Apr 2017 00:42:11 +0000 Subject: [PATCH 9/9] Refactored the implementation of DynamicField extension for storing data in order --- mongoengine/dereference.py | 2 +- mongoengine/fields.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index c11449b9..f30b2c15 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -202,7 +202,7 @@ class DeReference(object): as_tuple = isinstance(items, tuple) iterator = enumerate(items) data = [] - elif type(items) == OrderedDict: + elif isinstance(items, OrderedDict): is_list = False iterator = items.iteritems() data = OrderedDict() diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 70061e08..a3b0f883 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -5,6 +5,7 @@ import re import time import uuid import warnings +from collections import Mapping from operator import itemgetter from bson import Binary, DBRef, ObjectId, SON @@ -621,7 +622,7 @@ class DynamicField(BaseField): def __init__(self, container_class=dict, *args, **kwargs): self._container_cls = container_class - if not issubclass(self._container_cls, dict): + if not issubclass(self._container_cls, Mapping): self.error('The class that is specified in `container_class` parameter ' 'must be a subclass of `dict`.')