Repository: Scille/umongo
Pull request #165 (2019-02-05 09:43:14)

Add Schema cache to as_marshmallow_schema
This adds a cache to as_marshmallow_schema.

My use case is the following:

My app calls `as_marshmallow_schema` on all top-level schemas (one schema per resource). Some low-level schemas are used in many places throughout the code, embedded, sometimes deeply, in top-level schemas.

Without this change, the marshmallow schemas generated for those embedded umongo schemas are duplicated. Functionally, it makes no difference, but then they also get duplicated in the auto-generated OpenAPI specification that I generate with apispec, which makes the doc a bit ugly.

This is just one use case, but more generally, it's a pity to duplicate stuff.

The caching mechanism here is two-fold. Hashable parameters are used as a key in the cache dictionary. Dictionary parameters are not hashable, so they can't be used as key in the cache dict. They are stored as is and compared manually. Comparing all parameters manually would save one or two lines of code but would be much less efficient.

@touilleMan if that's fine for you, I'd like to release a new version (1.2.0) with this and #162. If you want to review but don't have time right now, please tell me, I can put this on hold.


Patch:
diff --git a/umongo/abstract.py b/umongo/abstract.py
index c5af707..e313abf 100644
--- a/umongo/abstract.py
+++ b/umongo/abstract.py
@@ -17,6 +17,8 @@ class BaseSchema(MaSchema):
     __check_unknown_fields = validates_schema(pass_original=True)(
         schema_validator_check_unknown_fields)
 
+    _marshmallow_schemas_cache = {}
+
     def map_to_field(self, func):
         """
         Apply a function to every field in the schema
@@ -44,6 +46,13 @@ class BaseSchema(MaSchema):
         :param meta: Optional dict with attributes for the schema's Meta class.
         """
         params = params or {}
+        # Use hashable parameters as cache dict key and dict parameters for manual comparison
+        cache_key = (self.__class__, base_schema_cls, check_unknown_fields, mongo_world)
+        cache_modifiers = (params, meta)
+        if cache_key in self._marshmallow_schemas_cache:
+            for modifiers, ma_schema in self._marshmallow_schemas_cache[cache_key]:
+                if modifiers == cache_modifiers:
+                    return ma_schema
         nmspc = {
             name: field.as_marshmallow_field(
                 params=params.get(name),
@@ -57,12 +66,15 @@ class BaseSchema(MaSchema):
             nmspc['_%s__check_unknown_fields' % name] = validates_schema(
                 pass_original=True)(schema_validator_check_unknown_fields)
         # By default OO world returns `missing` fields as `None`,
-        # disable this behavior here to let marshmallow deals with it
+        # disable this behavior here to let marshmallow deal with it
         if not mongo_world:
             nmspc['get_attribute'] = schema_from_umongo_get_attribute
         if meta:
             nmspc['Meta'] = type('Meta', (base_schema_cls.Meta,), meta)
-        return type(name, (base_schema_cls, ), nmspc)
+        ma_schema = type(name, (base_schema_cls, ), nmspc)
+        self._marshmallow_schemas_cache.setdefault(cache_key, []).append(
+            (cache_modifiers, ma_schema))
+        return ma_schema
 
 
 class I18nErrorDict(dict):


Test patch:
diff --git a/tests/test_marshmallow.py b/tests/test_marshmallow.py
index c8dbb24..ae83d52 100644
--- a/tests/test_marshmallow.py
+++ b/tests/test_marshmallow.py
@@ -133,6 +133,35 @@ class TestMarshmallow(BaseTest):
         assert ma_custom_base_schema.Meta.exclude == ('content',)
         assert ma_custom_base_schema.Meta.dump_only == ('id',)
 
+    def test_as_marshmallow_schema_cache(self):
+        ma_schema_cls = self.User.schema.as_marshmallow_schema()
+
+        new_ma_schema_cls = self.User.schema.as_marshmallow_schema(
+            params={'name': {'load_only': True}})
+        assert new_ma_schema_cls != ma_schema_cls
+
+        new_ma_schema_cls = self.User.schema.as_marshmallow_schema(
+            meta={'exclude': ('name',)})
+        assert new_ma_schema_cls != ma_schema_cls
+
+        new_ma_schema_cls = self.User.schema.as_marshmallow_schema(
+            check_unknown_fields=False)
+        assert new_ma_schema_cls != ma_schema_cls
+
+        new_ma_schema_cls = self.User.schema.as_marshmallow_schema(
+            mongo_world=True)
+        assert new_ma_schema_cls != ma_schema_cls
+
+        class MyBaseSchema(marshmallow.Schema):
+            pass
+
+        new_ma_schema_cls = self.User.schema.as_marshmallow_schema(
+            base_schema_cls=MyBaseSchema)
+        assert new_ma_schema_cls != ma_schema_cls
+
+        new_ma_schema_cls = self.User.schema.as_marshmallow_schema()
+        assert new_ma_schema_cls == ma_schema_cls
+
     def test_keep_attributes(self):
         @self.instance.register
         class Vehicle(Document):