Repository: Scille/umongo
Pull request #301 (2020-09-24 08:58:59)

Remove umongo.Schema
Schemas should inherit from umongo.abstract.BaseSchema
Expose RemoveMissingSchema as umongo.RemoveMissingSchema
Simplify as_marshmallow_schema cache


Patch:
diff --git a/umongo/__init__.py b/umongo/__init__.py
index caa60a6..2a70378 100644
--- a/umongo/__init__.py
+++ b/umongo/__init__.py
@@ -25,11 +25,10 @@ from .exceptions import (
     UnknownFieldInDBError,
 )
 from . import fields, validate
-from .schema import Schema
 from .data_objects import Reference
 from .embedded_document import EmbeddedDocument
 from .mixin import MixinDocument
-from .expose_missing import ExposeMissing
+from .expose_missing import ExposeMissing, RemoveMissingSchema
 from .i18n import set_gettext
 
 
@@ -54,6 +53,7 @@ __all__ = (
     'EmbeddedDocument',
     'MixinDocument',
     'ExposeMissing',
+    'RemoveMissingSchema',
 
     'UMongoError',
     'ValidationError',
@@ -66,8 +66,6 @@ __all__ = (
 
     'fields',
 
-    'Schema',
-
     'Reference',
 
     'set_gettext',
diff --git a/umongo/abstract.py b/umongo/abstract.py
index 6fd3c66..1dea0d3 100644
--- a/umongo/abstract.py
+++ b/umongo/abstract.py
@@ -22,6 +22,7 @@ class BaseSchema(ma.Schema):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         self.error_messages = I18nErrorDict(self.error_messages)
+        self._ma_schema = None
 
     def map_to_field(self, func):
         """
@@ -36,6 +37,27 @@ class BaseSchema(ma.Schema):
             if hasattr(field, 'map_to_field'):
                 field.map_to_field(mongo_path, name, func)
 
+    def as_marshmallow_schema(self):
+        """Return a pure-marshmallow version of this schema class"""
+        # Use a cache to avoid generating several times the same schema
+        if self._ma_schema is not None:
+            return self._ma_schema
+
+        # Create schema if not found in cache
+        nmspc = {
+            name: field.as_marshmallow_field()
+            for name, field in self.fields.items()
+        }
+        name = 'Marshmallow%s' % type(self).__name__
+        m_schema = type(name, (self.MA_BASE_SCHEMA_CLS, ), nmspc)
+        # Add i18n support to the schema
+        # We can't use I18nErrorDict here because __getitem__ is not called
+        # when error_messages is updated with _default_error_messages.
+        m_schema._default_error_messages = {
+            k: _(v) for k, v in m_schema._default_error_messages.items()}
+        self._ma_schema = m_schema
+        return m_schema
+
 
 class BaseField(ma.fields.Field):
     """
diff --git a/umongo/builder.py b/umongo/builder.py
index a9f1118..de9f69f 100644
--- a/umongo/builder.py
+++ b/umongo/builder.py
@@ -10,6 +10,7 @@ from copy import copy
 
 import marshmallow as ma
 
+from .abstract import BaseSchema
 from .template import Template, Implementation
 from .data_proxy import data_proxy_factory
 from .document import DocumentTemplate, DocumentOpts, DocumentImplementation
@@ -17,7 +18,6 @@ from .embedded_document import (
     EmbeddedDocumentTemplate, EmbeddedDocumentOpts, EmbeddedDocumentImplementation)
 from .mixin import MixinDocumentTemplate, MixinDocumentOpts, MixinDocumentImplementation
 from .exceptions import DocumentDefinitionError, NotRegisteredDocumentError
-from .schema import Schema
 from .indexes import parse_index
 from . import fields
 
@@ -283,7 +283,7 @@ class BaseBuilder:
             if issubclass(base, Implementation) and hasattr(base, 'Schema')
         )
         if not schema_bases:
-            schema_bases = (Schema, )
+            schema_bases = (BaseSchema, )
         if base_tmpl_cls is DocumentTemplate:
             nmspc['pk_field'] = _on_need_add_id_field(schema_bases, schema_fields)
 
diff --git a/umongo/expose_missing.py b/umongo/expose_missing.py
index 4454e35..a9d3535 100644
--- a/umongo/expose_missing.py
+++ b/umongo/expose_missing.py
@@ -3,11 +3,18 @@
 Allows the user to let umongo document return missing rather than None for
 empty fields.
 """
-
-
 from contextvars import ContextVar
 from contextlib import AbstractContextManager
 
+import marshmallow as ma
+
+
+__all__ = (
+    'ExposeMissing',
+    'RemoveMissingSchema',
+)
+
+
 EXPOSE_MISSING = ContextVar("expose_missing", default=False)
 
 
@@ -24,3 +31,14 @@ class ExposeMissing(AbstractContextManager):
 
     def __exit__(self, *args, **kwargs):
         EXPOSE_MISSING.reset(self.token)
+
+
+class RemoveMissingSchema(ma.Schema):
+    """
+    Custom :class:`marshmallow.Schema` subclass that skips missing fields
+    rather than returning None for missing fields when dumping umongo
+    :class:`umongo.Document`s.
+    """
+    def dump(self, *args, **kwargs):
+        with ExposeMissing():
+            return super().dump(*args, **kwargs)
diff --git a/umongo/schema.py b/umongo/schema.py
deleted file mode 100644
index 75a40b6..0000000
--- a/umongo/schema.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""Schema used in Document"""
-import marshmallow as ma
-
-from .abstract import BaseSchema
-from .i18n import gettext as _
-from .expose_missing import ExposeMissing
-
-__all__ = (
-    'Schema',
-    'RemoveMissingSchema',
-)
-
-
-class RemoveMissingSchema(ma.Schema):
-    """
-    Custom :class:`marshmallow.Schema` subclass that skips missing fields
-    rather than returning None for missing fields when dumping umongo
-    :class:`umongo.Document`s.
-    """
-    def dump(self, *args, **kwargs):
-        with ExposeMissing():
-            return super().dump(*args, **kwargs)
-
-
-class Schema(BaseSchema):
-    """Schema used in Document"""
-
-    _marshmallow_schemas_cache = {}
-
-    def as_marshmallow_schema(self):
-        """Return a pure-marshmallow version of this schema class"""
-        # Use a cache to avoid generating several times the same schema
-        cache_key = (self.__class__, self.MA_BASE_SCHEMA_CLS)
-        if cache_key in self._marshmallow_schemas_cache:
-            return self._marshmallow_schemas_cache[cache_key]
-
-        # Create schema if not found in cache
-        nmspc = {
-            name: field.as_marshmallow_field()
-            for name, field in self.fields.items()
-        }
-        name = 'Marshmallow%s' % type(self).__name__
-        m_schema = type(name, (self.MA_BASE_SCHEMA_CLS, ), nmspc)
-        # Add i18n support to the schema
-        # We can't use I18nErrorDict here because __getitem__ is not called
-        # when error_messages is updated with _default_error_messages.
-        m_schema._default_error_messages = {
-            k: _(v) for k, v in m_schema._default_error_messages.items()}
-        self._marshmallow_schemas_cache[cache_key] = m_schema
-        return m_schema


Test patch:
diff --git a/tests/test_data_proxy.py b/tests/test_data_proxy.py
index bc409b6..4dbe1b9 100644
--- a/tests/test_data_proxy.py
+++ b/tests/test_data_proxy.py
@@ -3,8 +3,9 @@ import pytest
 from bson import ObjectId
 import marshmallow as ma
 
+from umongo import fields, EmbeddedDocument, validate, exceptions
+from umongo.abstract import BaseSchema
 from umongo.data_proxy import data_proxy_factory, BaseDataProxy, BaseNonStrictDataProxy
-from umongo import Schema, fields, EmbeddedDocument, validate, exceptions
 
 from .common import BaseTest
 
@@ -13,7 +14,7 @@ class TestDataProxy(BaseTest):
 
     def test_repr(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             field_a = fields.IntField(attribute='mongo_field_a')
             field_b = fields.StrField()
 
@@ -27,7 +28,7 @@ class TestDataProxy(BaseTest):
 
     def test_simple(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.IntField()
             b = fields.IntField()
 
@@ -45,7 +46,7 @@ class TestDataProxy(BaseTest):
 
     def test_load(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.IntField()
             b = fields.IntField(attribute='in_mongo_b')
 
@@ -66,7 +67,7 @@ class TestDataProxy(BaseTest):
 
     def test_modify(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.IntField()
             b = fields.IntField(attribute='in_mongo_b')
 
@@ -89,7 +90,7 @@ class TestDataProxy(BaseTest):
 
     def test_list_field_modify(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.ListField(fields.IntField())
             b = fields.ListField(fields.IntField(), attribute='in_mongo_b')
 
@@ -135,7 +136,7 @@ class TestDataProxy(BaseTest):
         class MyEmbedded(EmbeddedDocument):
             aa = fields.IntField()
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             # EmbeddedField need instance to retrieve implementation
             a = fields.EmbeddedField(MyEmbedded, instance=self.instance)
             b = fields.ListField(fields.IntField)
@@ -154,7 +155,7 @@ class TestDataProxy(BaseTest):
 
     def test_set(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.IntField()
             b = fields.IntField(attribute='in_mongo_b')
             c = fields.StrField(
@@ -188,7 +189,7 @@ class TestDataProxy(BaseTest):
 
     def test_del(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.IntField()
             b = fields.IntField(attribute='in_mongo_b')
 
@@ -206,7 +207,7 @@ class TestDataProxy(BaseTest):
 
     def test_route_naming(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             in_front = fields.IntField(attribute='in_mongo')
 
         MyDataProxy = data_proxy_factory('My', MySchema())
@@ -225,7 +226,7 @@ class TestDataProxy(BaseTest):
 
     def test_from_mongo(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             in_front = fields.IntField(attribute='in_mongo')
 
         MyDataProxy = data_proxy_factory('My', MySchema())
@@ -237,7 +238,7 @@ class TestDataProxy(BaseTest):
 
     def test_equality(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.IntField()
             b = fields.IntField(attribute='in_mongo_b')
 
@@ -257,7 +258,7 @@ class TestDataProxy(BaseTest):
 
     def test_share_ressources(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.IntField()
             b = fields.IntField(attribute='in_mongo_b')
 
@@ -272,7 +273,7 @@ class TestDataProxy(BaseTest):
 
     def test_set_to_missing_fields(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.IntField()
             b = fields.IntField(attribute='in_mongo_b')
 
@@ -291,7 +292,7 @@ class TestDataProxy(BaseTest):
         default_value = ObjectId('507f1f77bcf86cd799439011')
         default_callable = lambda: ObjectId('507f1f77bcf86cd799439012')
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             no_default = fields.ObjectIdField()
             with_default = fields.ObjectIdField(default=default_value)
             with_callable_default = fields.ObjectIdField(default=default_callable)
@@ -322,7 +323,7 @@ class TestDataProxy(BaseTest):
 
     def test_validate(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             with_max = fields.IntField(validate=validate.Range(max=99))
 
         MyDataProxy = data_proxy_factory('My', MySchema())
@@ -340,7 +341,7 @@ class TestDataProxy(BaseTest):
         class MyEmbedded(EmbeddedDocument):
             required = fields.IntField(required=True)
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             # EmbeddedField need instance to retrieve implementation
             required = fields.IntField(required=True)
             embedded = fields.EmbeddedField(MyEmbedded, instance=self.instance)
@@ -385,7 +386,7 @@ class TestDataProxy(BaseTest):
         }
 
     def test_unkown_field_in_db(self):
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             field = fields.IntField(attribute='mongo_field')
 
         DataProxy = data_proxy_factory('My', MySchema())
@@ -396,7 +397,7 @@ class TestDataProxy(BaseTest):
             d.from_mongo({'mongo_field': 42, 'xxx': 'foo'})
 
     def test_iterators(self):
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             field_a = fields.IntField(attribute='mongo_field_a')
             field_b = fields.IntField(attribute='mongo_field_b')
 
@@ -418,7 +419,7 @@ class TestNonStrictDataProxy(BaseTest):
 
     def test_build(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             pass
 
         strict_proxy = data_proxy_factory('My', MySchema(), strict=True)
@@ -427,7 +428,7 @@ class TestNonStrictDataProxy(BaseTest):
         assert issubclass(non_strict_proxy, BaseNonStrictDataProxy)
 
     def test_basic(self):
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             field_a = fields.IntField(attribute='mongo_field_a')
 
         NonStrictDataProxy = data_proxy_factory('My', MySchema(), strict=False)
diff --git a/tests/test_document.py b/tests/test_document.py
index 0fac665..e5d9e13 100644
--- a/tests/test_document.py
+++ b/tests/test_document.py
@@ -8,8 +8,9 @@ import marshmallow as ma
 
 from umongo import (
     Document, EmbeddedDocument, MixinDocument,
-    Schema, fields, exceptions, post_dump, pre_load, validates_schema, ExposeMissing
+    fields, exceptions, post_dump, pre_load, validates_schema, ExposeMissing
 )
+from umongo.abstract import BaseSchema
 from .common import BaseTest
 
 
@@ -566,7 +567,7 @@ class TestConfig(BaseTest):
             pass
 
         d = Doc()
-        assert isinstance(d.schema, Schema)
+        assert isinstance(d.schema, BaseSchema)
 
     def test_base_config(self):
 
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 11d933e..8ad5e57 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -8,8 +8,9 @@ from bson import ObjectId, DBRef, Decimal128
 import marshmallow as ma
 
 from umongo.data_proxy import data_proxy_factory
-from umongo import Document, EmbeddedDocument, Schema, fields, Reference, validate
+from umongo import Document, EmbeddedDocument, fields, Reference, validate
 from umongo.data_objects import List, Dict
+from umongo.abstract import BaseSchema
 
 from .common import BaseTest
 
@@ -106,7 +107,7 @@ class TestFields(BaseTest):
 
     def test_basefields(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             string = fields.StringField()
             uuid = fields.UUIDField()
             number = fields.NumberField()
@@ -173,7 +174,7 @@ class TestFields(BaseTest):
 
     def test_datetime(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.DateTimeField()
 
         s = MySchema()
@@ -205,7 +206,7 @@ class TestFields(BaseTest):
 
         timezone_2h = dt.timezone(dt.timedelta(hours=2), "test")
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.AwareDateTimeField()
             b = fields.AwareDateTimeField(default_timezone=timezone_2h)
 
@@ -235,7 +236,7 @@ class TestFields(BaseTest):
 
     def test_date(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             a = fields.DateField()
 
         s = MySchema()
@@ -256,7 +257,7 @@ class TestFields(BaseTest):
 
     def test_dict(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             dict = fields.DictField(attribute='in_mongo_dict', allow_none=True)
             kdict = fields.DictField(keys=fields.StringField(validate=validate.Length(0, 1)))
             vdict = fields.DictField(values=fields.IntField(validate=validate.Range(max=5)))
@@ -334,7 +335,7 @@ class TestFields(BaseTest):
 
     def test_dict_default(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             # Passing a mutable as default is a bad idea in real life
             d_dict = fields.DictField(values=fields.IntField, default={'1': 1, '2': 2})
             c_dict = fields.DictField(values=fields.IntField, default=lambda: {'1': 1, '2': 2})
@@ -422,7 +423,7 @@ class TestFields(BaseTest):
 
     def test_list(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             list = fields.ListField(fields.IntField(), attribute='in_mongo_list', allow_none=True)
 
         MyDataProxy = data_proxy_factory('My', MySchema())
@@ -507,7 +508,7 @@ class TestFields(BaseTest):
 
     def test_list_default(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             d_list = fields.ListField(fields.IntField(), default=(1, 2, 3))
             c_list = fields.ListField(fields.IntField(), default=lambda: (1, 2, 3))
 
@@ -596,7 +597,7 @@ class TestFields(BaseTest):
 
     def test_objectid(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             objid = fields.ObjectIdField(attribute='in_mongo_objid')
 
         MyDataProxy = data_proxy_factory('My', MySchema())
@@ -767,7 +768,7 @@ class TestFields(BaseTest):
 
     def test_decimal(self):
 
-        class MySchema(Schema):
+        class MySchema(BaseSchema):
             price = fields.DecimalField(attribute='in_mongo_price')
 
         MyDataProxy = data_proxy_factory('My', MySchema())
diff --git a/tests/test_marshmallow.py b/tests/test_marshmallow.py
index c0a5d08..d68d8d3 100644
--- a/tests/test_marshmallow.py
+++ b/tests/test_marshmallow.py
@@ -6,10 +6,11 @@ import pytest
 from bson import ObjectId
 import marshmallow as ma
 
-from umongo import Document, EmbeddedDocument, fields, set_gettext, validate, missing
-from umongo import marshmallow_bonus as ma_bonus_fields, Schema
-from umongo.abstract import BaseField
-from umongo.schema import RemoveMissingSchema
+from umongo import (
+    Document, EmbeddedDocument, fields, set_gettext, validate, missing, RemoveMissingSchema
+)
+from umongo import marshmallow_bonus as ma_bonus_fields
+from umongo.abstract import BaseField, BaseSchema
 
 from .common import BaseTest
 
@@ -37,7 +38,7 @@ class TestMarshmallow(BaseTest):
     def test_by_schema(self):
         ma_schema_cls = self.User.schema.as_marshmallow_schema()
         assert issubclass(ma_schema_cls, ma.Schema)
-        assert not issubclass(ma_schema_cls, Schema)
+        assert not issubclass(ma_schema_cls, BaseSchema)
 
     def test_custom_ma_base_schema_cls(self):