diff --git a/.gitignore b/.gitignore index 7862503e..d829a984 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,6 @@ man/ #vscode .vscode/ pip-wheel-metadata + +#mypy +.mypy_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a19c2b4..0765154e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,10 +2,14 @@ repos: # NOTE: it is not recommended to use mutable revs e.g. "stable" - run pre-commit autoupdate instead # ref: https://github.com/ambv/black/issues/420 - repo: https://github.com/ambv/black - rev: 23.3.0 + rev: 23.7.0 hooks: - - id: black + - id: black - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: - - id: flake8 + - id: flake8 + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.4.1 + hooks: + - id: mypy diff --git a/examples/todo/generate_output.py b/examples/todo/generate_output.py index cda2d28c..fbad9d04 100644 --- a/examples/todo/generate_output.py +++ b/examples/todo/generate_output.py @@ -88,7 +88,7 @@ def main(): output.extend(["```", ""]) - print("Writing output to {}".format(todo_output_filepath)) + print(f"Writing output to {todo_output_filepath}") with open(todo_output_filepath, "w") as f: f.write("\n".join(output)) diff --git a/examples/todo/todo/schemas.py b/examples/todo/todo/schemas.py index cc8ca81e..3893ceda 100644 --- a/examples/todo/todo/schemas.py +++ b/examples/todo/todo/schemas.py @@ -19,7 +19,7 @@ def __init__(self, **kwargs): partial_arg = super_kwargs.pop("partial", True) # Note: if you only want to mark some fields as partial, pass partial= a collection of field names, e.g.,: # partial_arg = super_kwargs.pop('partial', ('description', )) - super(UpdateTodoSchema, self).__init__(partial=partial_arg, **super_kwargs) + super().__init__(partial=partial_arg, **super_kwargs) class GetTodoListSchema(RequestSchema): diff --git a/flask_rebar/__init__.py b/flask_rebar/__init__.py index 3fe00f55..bc9e7753 100644 --- a/flask_rebar/__init__.py +++ b/flask_rebar/__init__.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from flask_rebar.utils.request_utils import marshal, response from flask_rebar.rebar import ( diff --git a/flask_rebar/authenticators/base.py b/flask_rebar/authenticators/base.py index 66d11286..89e8b8d4 100644 --- a/flask_rebar/authenticators/base.py +++ b/flask_rebar/authenticators/base.py @@ -9,7 +9,7 @@ """ -class Authenticator(object): +class Authenticator: """ Abstract authenticator class. Custom authentication methods should extend this class. diff --git a/flask_rebar/errors.py b/flask_rebar/errors.py index 899e6cfc..21c569bb 100644 --- a/flask_rebar/errors.py +++ b/flask_rebar/errors.py @@ -7,7 +7,6 @@ :copyright: Copyright 2018 PlanGrid, Inc., see AUTHORS. :license: MIT, see LICENSE for details. """ -from __future__ import unicode_literals class HttpJsonError(Exception): @@ -28,13 +27,13 @@ class HttpJsonError(Exception): response, not nested under "additional_data". """ - default_message = None - http_status_code = None + default_message: str + http_status_code: int def __init__(self, msg=None, additional_data=None): self.error_message = msg or self.default_message self.additional_data = additional_data - super(HttpJsonError, self).__init__(self.error_message) + super().__init__(self.error_message) class BadRequest(HttpJsonError): diff --git a/flask_rebar/messages.py b/flask_rebar/messages.py index 2e4bafa0..3398c6c1 100644 --- a/flask_rebar/messages.py +++ b/flask_rebar/messages.py @@ -67,14 +67,14 @@ class ErrorCode: def required_field_missing(field_name): return ErrorMessage( - "Required field missing: {}".format(field_name), + f"Required field missing: {field_name}", ErrorCode.REQUIRED_FIELD_MISSING, ) def required_field_empty(field_name): return ErrorMessage( - "Value for required field cannot be None: {}".format(field_name), + f"Value for required field cannot be None: {field_name}", ErrorCode.REQUIRED_FIELD_EMPTY, ) diff --git a/flask_rebar/rebar.py b/flask_rebar/rebar.py index 552e1f26..26b1e6f2 100644 --- a/flask_rebar/rebar.py +++ b/flask_rebar/rebar.py @@ -8,11 +8,10 @@ :copyright: Copyright 2018 PlanGrid, Inc., see AUTHORS. :license: MIT, see LICENSE for details. """ -from __future__ import unicode_literals - import sys from collections import defaultdict from collections import namedtuple +from collections.abc import Mapping from copy import copy from functools import wraps from flask import current_app, g, jsonify, request @@ -35,12 +34,6 @@ from flask_rebar.swagger_generation import SwaggerV2Generator from flask_rebar.swagger_ui import create_swagger_ui_blueprint -# Deal with maintaining (for now at least) support for 2.7+: -try: - from collections.abc import Mapping # 3.3+ -except ImportError: - from collections import Mapping # 2.7+ - MOVED_PERMANENTLY_ERROR = RequestRedirect PERMANENT_REDIRECT_ERROR = RequestRedirect @@ -218,7 +211,7 @@ def prefix_url(prefix, url): """ prefix = normalize_prefix(prefix) url = url[1:] if url.startswith("/") else url - return "/{}/{}".format(prefix, url) + return f"/{prefix}/{url}" # Metadata about a declared handler function. This can be used to both @@ -252,7 +245,7 @@ class PathDefinition( ) ) def __new__(cls, *args, **kwargs): - return super(PathDefinition, cls).__new__(cls, *args, **kwargs) + return super().__new__(cls, *args, **kwargs) @property @deprecated("authenticator", "3.0") @@ -260,7 +253,7 @@ def authenticator(self): return self.authenticators[0] if self.authenticators else None -class HandlerRegistry(object): +class HandlerRegistry: """ Registry for request handlers. @@ -635,7 +628,7 @@ def _register_swagger_ui(self, app): ) -class Rebar(object): +class Rebar: """ The main entry point for the Flask-Rebar extension. diff --git a/flask_rebar/swagger_generation/authenticator_to_swagger.py b/flask_rebar/swagger_generation/authenticator_to_swagger.py index 63d3060f..ee37fdc6 100644 --- a/flask_rebar/swagger_generation/authenticator_to_swagger.py +++ b/flask_rebar/swagger_generation/authenticator_to_swagger.py @@ -1,4 +1,5 @@ from collections import namedtuple +from typing import Type from flask_rebar.authenticators import HeaderApiKeyAuthenticator, Authenticator from .marshmallow_to_swagger import ConverterRegistry @@ -14,7 +15,7 @@ ) -class AuthenticatorConverter(object): +class AuthenticatorConverter: """ Abstract class for objects that convert Authenticator objects to security JSONSchema. @@ -35,7 +36,7 @@ class AuthenticatorConverter(object): """ - AUTHENTICATOR_TYPE = None + AUTHENTICATOR_TYPE: Type[Authenticator] def get_security_schemes(self, obj, context): """ @@ -152,7 +153,7 @@ def register_types(self, converters): :param iterable[AuthenticatorConverter] converters: """ - super(AuthenticatorConverterRegistry, self).register_types(converters) + super().register_types(converters) def get_security_schemes(self, authenticator, openapi_version=2): """ diff --git a/flask_rebar/swagger_generation/generator_utils.py b/flask_rebar/swagger_generation/generator_utils.py index 716ae372..566dc89a 100644 --- a/flask_rebar/swagger_generation/generator_utils.py +++ b/flask_rebar/swagger_generation/generator_utils.py @@ -243,12 +243,12 @@ def get_unique_schema_definitions( def get_unique_authenticators(registry): - authenticators = set( + authenticators = { authenticator for d in iterate_path_definitions(paths=registry.paths) for authenticator in d.authenticators if authenticator is not None and authenticator is not USE_DEFAULT - ) + } for authenticator in registry.default_authenticators: if authenticator is not None: @@ -264,8 +264,7 @@ def iterate_path_definitions(paths): :return Iterator[PathDefinition] """ for methods in paths.values(): - for definition in methods.values(): - yield definition + yield from methods.values() def recursively_convert_dict_to_ordered_dict(obj): diff --git a/flask_rebar/swagger_generation/marshmallow_to_swagger.py b/flask_rebar/swagger_generation/marshmallow_to_swagger.py index 635469db..7263941a 100644 --- a/flask_rebar/swagger_generation/marshmallow_to_swagger.py +++ b/flask_rebar/swagger_generation/marshmallow_to_swagger.py @@ -8,13 +8,12 @@ :copyright: Copyright 2018 PlanGrid, Inc., see AUTHORS. :license: MIT, see LICENSE for details. """ -from __future__ import unicode_literals - import copy import inspect import logging import sys from collections import namedtuple +from typing import Any, Optional, Type, Union import marshmallow as m from marshmallow.validate import Range @@ -29,16 +28,16 @@ LoadDumpOptions = None try: - EnumField = m.fields.Enum + EnumField: Optional[Type[m.fields.Field]] = m.fields.Enum except AttributeError: try: - from marshmallow_enum import EnumField, LoadDumpOptions + from marshmallow_enum import EnumField, LoadDumpOptions # type: ignore except ImportError: EnumField = None # Special value to signify that a JSONSchema field should be left unset -class UNSET(object): +class UNSET: pass @@ -142,13 +141,13 @@ def get_schema_fields(schema): return sorted(fields) -class MarshmallowConverter(object): +class MarshmallowConverter: """ Abstract class for objects that convert Marshmallow objects to JSONSchema dictionaries. """ - MARSHMALLOW_TYPE = None + MARSHMALLOW_TYPE: Any = None def convert(self, obj, context): """ @@ -264,10 +263,10 @@ class FieldConverter(MarshmallowConverter): This should be extended for specific Field types. """ - MARSHMALLOW_TYPE = m.fields.Field + MARSHMALLOW_TYPE: Type[m.fields.Field] = m.fields.Field def convert(self, obj, context): - jsonschema_obj = super(FieldConverter, self).convert(obj, context) + jsonschema_obj = super().convert(obj, context) if obj.dump_only: jsonschema_obj["readOnly"] = True @@ -339,7 +338,7 @@ class ValidatorConverter(MarshmallowConverter): This should be extended for specific Validator types. """ - MARSHMALLOW_TYPE = Validator + MARSHMALLOW_TYPE: Union[Type[Validator], Type[OneOf]] = Validator class NestedConverter(FieldConverter): @@ -561,7 +560,7 @@ def get_maximum_length(self, obj, context): return UNSET -class ConverterRegistry(object): +class ConverterRegistry: """ Registry for MarshmallowConverters. @@ -650,7 +649,7 @@ def convert(self, obj, openapi_version=2): class EnumConverter(FieldConverter): - MARSHMALLOW_TYPE = EnumField + MARSHMALLOW_TYPE = EnumField # type: ignore @sets_swagger_attr(sw.type_) def get_type(self, obj, context): diff --git a/flask_rebar/swagger_generation/swagger_generator_base.py b/flask_rebar/swagger_generation/swagger_generator_base.py index e2afdd5c..a8ebbbf3 100644 --- a/flask_rebar/swagger_generation/swagger_generator_base.py +++ b/flask_rebar/swagger_generation/swagger_generator_base.py @@ -83,7 +83,7 @@ class SwaggerGenerator(SwaggerGeneratorI): :param marshmallow.Schema default_response_schema: Schema to use as the default of all responses """ - _open_api_version = None + _open_api_version: str def __init__( self, diff --git a/flask_rebar/swagger_generation/swagger_generator_v2.py b/flask_rebar/swagger_generation/swagger_generator_v2.py index bbebd011..7ef0be28 100644 --- a/flask_rebar/swagger_generation/swagger_generator_v2.py +++ b/flask_rebar/swagger_generation/swagger_generator_v2.py @@ -7,8 +7,6 @@ :copyright: Copyright 2019 PlanGrid, Inc., see AUTHORS. :license: MIT, see LICENSE for details. """ -from __future__ import unicode_literals - import copy from flask_rebar.swagger_generation import swagger_words as sw @@ -77,7 +75,7 @@ def __init__( default_response_schema=Error(), authenticator_converter_registry=None, ): - super(SwaggerV2Generator, self).__init__( + super().__init__( openapi_major_version=2, version=version, title=title, diff --git a/flask_rebar/swagger_generation/swagger_generator_v3.py b/flask_rebar/swagger_generation/swagger_generator_v3.py index 8bc544a0..255c36e5 100644 --- a/flask_rebar/swagger_generation/swagger_generator_v3.py +++ b/flask_rebar/swagger_generation/swagger_generator_v3.py @@ -7,8 +7,6 @@ :copyright: Copyright 2019 PlanGrid, Inc., see AUTHORS. :license: MIT, see LICENSE for details. """ -from __future__ import unicode_literals - from flask_rebar.utils.defaults import USE_DEFAULT from flask_rebar.swagger_generation import swagger_words as sw from flask_rebar.swagger_generation.swagger_generator import SwaggerGenerator @@ -66,7 +64,7 @@ def __init__( authenticator_converter_registry=None, include_hidden=False, ): - super(SwaggerV3Generator, self).__init__( + super().__init__( openapi_major_version=3, version=version, title=title, diff --git a/flask_rebar/swagger_generation/swagger_objects.py b/flask_rebar/swagger_generation/swagger_objects.py index 3e8169c9..7aa0b336 100644 --- a/flask_rebar/swagger_generation/swagger_objects.py +++ b/flask_rebar/swagger_generation/swagger_objects.py @@ -13,7 +13,7 @@ from flask_rebar.swagger_generation import swagger_words as sw -class ExternalDocumentation(object): +class ExternalDocumentation: """Represents a Swagger "External Documentation Object" :param str url: The URL for the target documentation. Value MUST be in the format of a URL @@ -35,7 +35,7 @@ def as_swagger(self): return doc -class Tag(object): +class Tag: """Represents a Swagger "Tag Object" :param str name: The name of the tag @@ -61,7 +61,7 @@ def as_swagger(self): return doc -class ServerVariable(object): +class ServerVariable: """Represents a Swagger "Server Variable Object" :param str default: @@ -87,7 +87,7 @@ def as_swagger(self): return doc -class Server(object): +class Server: """Represents a Swagger "Server Object" :param str url: diff --git a/flask_rebar/swagger_generation/swagger_words.py b/flask_rebar/swagger_generation/swagger_words.py index 2901eec3..d1bf837a 100644 --- a/flask_rebar/swagger_generation/swagger_words.py +++ b/flask_rebar/swagger_generation/swagger_words.py @@ -7,8 +7,6 @@ :copyright: Copyright 2018 PlanGrid, Inc., see AUTHORS. :license: MIT, see LICENSE for details. """ -from __future__ import unicode_literals - additional_properties = "additionalProperties" all_of = "allOf" allow_empty_value = "allowEmptyValue" diff --git a/flask_rebar/utils/__init__.py b/flask_rebar/utils/__init__.py index b92fe5b4..dfbb3511 100644 --- a/flask_rebar/utils/__init__.py +++ b/flask_rebar/utils/__init__.py @@ -1,2 +1,2 @@ -class USE_DEFAULT(object): +class USE_DEFAULT: pass diff --git a/flask_rebar/utils/defaults.py b/flask_rebar/utils/defaults.py index b92fe5b4..dfbb3511 100644 --- a/flask_rebar/utils/defaults.py +++ b/flask_rebar/utils/defaults.py @@ -1,2 +1,2 @@ -class USE_DEFAULT(object): +class USE_DEFAULT: pass diff --git a/flask_rebar/utils/deprecation.py b/flask_rebar/utils/deprecation.py index d23855cb..accf2b3b 100644 --- a/flask_rebar/utils/deprecation.py +++ b/flask_rebar/utils/deprecation.py @@ -134,9 +134,7 @@ def _remap_kwargs(func_name, kwargs, aliases): if alias in remapped_args: new, eol_version, coerce_func = _validated_deprecation_spec(new_spec) if new in remapped_args: - raise TypeError( - "{} received both {} and {}".format(func_name, alias, new) - ) + raise TypeError(f"{func_name} received both {alias} and {new}") else: _deprecation_warning(alias, new, eol_version, stacklevel=4) if new: @@ -148,9 +146,7 @@ def _remap_kwargs(func_name, kwargs, aliases): def _deprecation_warning(old_name, new_name, eol_version, stacklevel=1): - eol_clause = ( - " and may be removed in version {}".format(eol_version) if eol_version else "" - ) - replacement_clause = "; use {}".format(new_name) if new_name else "" - msg = "{} is deprecated{}{}".format(old_name, eol_clause, replacement_clause) + eol_clause = f" and may be removed in version {eol_version}" if eol_version else "" + replacement_clause = f"; use {new_name}" if new_name else "" + msg = f"{old_name} is deprecated{eol_clause}{replacement_clause}" warnings.warn(message=msg, category=config.warning_type, stacklevel=stacklevel) diff --git a/flask_rebar/utils/marshmallow_objects_helpers.py b/flask_rebar/utils/marshmallow_objects_helpers.py index 072c9920..3daf0673 100644 --- a/flask_rebar/utils/marshmallow_objects_helpers.py +++ b/flask_rebar/utils/marshmallow_objects_helpers.py @@ -19,17 +19,17 @@ def get_marshmallow_objects_schema(model): class NestedTitledModel(mo.NestedModel): """ - Use this class instead of mashmallow_object.NestedModel if you need to supply + Use this class instead of marshmallow_object.NestedModel if you need to supply __swagger_title__ to override the default of {MyModelClass}Schema """ def __init__(self, nested, title, **kwargs): - super(NestedTitledModel, self).__init__(nested, **kwargs) + super().__init__(nested, **kwargs) self.schema.__swagger_title__ = title else: - class NestedTitledModel(object): + class NestedTitledModel: # type: ignore """ This version of NestedTitledModel will exist if marshmallow-objects is not present """ diff --git a/flask_rebar/utils/request_utils.py b/flask_rebar/utils/request_utils.py index fddca6dc..37eee6e9 100644 --- a/flask_rebar/utils/request_utils.py +++ b/flask_rebar/utils/request_utils.py @@ -7,8 +7,6 @@ :copyright: Copyright 2018 PlanGrid, Inc., see AUTHORS. :license: MIT, see LICENSE for details. """ -from __future__ import unicode_literals - import collections import copy diff --git a/flask_rebar/validation.py b/flask_rebar/validation.py index 813916a3..8fe1cbac 100644 --- a/flask_rebar/validation.py +++ b/flask_rebar/validation.py @@ -84,10 +84,10 @@ class CommaSeparatedList(fields.List): def _deserialize(self, value, attr, data, **kwargs): if not isinstance(value, list): value = value.split(",") - return super(CommaSeparatedList, self)._deserialize(value, attr, data) + return super()._deserialize(value, attr, data) def _serialize(self, value, attr, obj, **kwargs): - items = super(CommaSeparatedList, self)._serialize(value, attr, obj) + items = super()._serialize(value, attr, obj) return ",".join([str(i) for i in items]) @@ -109,10 +109,10 @@ def _deserialize(self, value, attr, data, **kwargs): ) ) items = data.getlist(attr) - return super(QueryParamList, self)._deserialize(items, attr, data) + return super()._deserialize(items, attr, data) -class RequireOnDumpMixin(object): +class RequireOnDumpMixin: """ DEPRECATED AND MAY BE REMOVED IN VERSION 3.0 In previous versions, this mixin was used to force validation on dump. As of 2.0.1, that diff --git a/pyproject.toml b/pyproject.toml index bf9dd424..c4e26d07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,17 @@ exclude = ''' )/ ''' +[tool.mypy] +exclude = ['build', 'docs', 'examples'] + +[[tool.mypy.overrides]] +module = [ + "marshmallow_enum", + "marshmallow_objects", + "parametrize" +] +ignore_missing_imports = true + [tool.pytest.ini_options] filterwarnings = [ "error" diff --git a/setup.py b/setup.py index f3e37b16..9e99a958 100644 --- a/setup.py +++ b/setup.py @@ -4,19 +4,22 @@ # packages required for local development and testing development = [ - "black==23.3.0", - "bumpversion==0.5.3", + "black==23.7.0", + "bumpversion==0.6.0", "click>=8.1.3,<9.0.0", "flake8==6.0.0", "gitchangelog>=3.0.4,<4.0.0", "jsonschema==4.18.4", "marshmallow-objects~=2.3", + "mypy==1.4.1", "parametrize==0.1.1", "pre-commit>=1.14.4", - "pytest~=6.2", + "pytest~=7.4", "pytest-order~=1.0", - "Sphinx==1.7.0", - "sphinx_rtd_theme==0.2.4", + "Sphinx>=6.0.0,<7.0.0", + "sphinx_rtd_theme==1.2.2", + "types-jsonschema==4.17.0.10", + "types-setuptools==68.0.0.3", ] if __name__ == "__main__": diff --git a/tests/helpers.py b/tests/helpers.py index 4e21a345..1861a905 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,7 +2,7 @@ from werkzeug.utils import cached_property -class JsonResponseMixin(object): +class JsonResponseMixin: """ Mixin with testing helper methods """ diff --git a/tests/swagger_generation/test_generator_utils.py b/tests/swagger_generation/test_generator_utils.py index 09607602..099667e2 100644 --- a/tests/swagger_generation/test_generator_utils.py +++ b/tests/swagger_generation/test_generator_utils.py @@ -16,7 +16,7 @@ class TestFlatten(unittest.TestCase): def setUp(self): - super(TestFlatten, self).setUp() + super().setUp() self.maxDiff = None def test_flatten(self): diff --git a/tests/swagger_generation/test_swagger_generator.py b/tests/swagger_generation/test_swagger_generator.py index 14fa8243..4a038893 100644 --- a/tests/swagger_generation/test_swagger_generator.py +++ b/tests/swagger_generation/test_swagger_generator.py @@ -260,7 +260,7 @@ def test_swagger_generators(registry, swagger_generator, expected_swagger): elif open_api_version == "3.1.0": swagger_jsonschema = SWAGGER_V3_JSONSCHEMA else: - raise ValueError("Unknown swagger_version: {}".format(open_api_version)) + raise ValueError(f"Unknown swagger_version: {open_api_version}") validate_swagger(expected_swagger, schema=swagger_jsonschema) diff --git a/tests/swagger_generation/test_swagger_generator_hidden_api.py b/tests/swagger_generation/test_swagger_generator_hidden_api.py index feee71d3..7adefab1 100644 --- a/tests/swagger_generation/test_swagger_generator_hidden_api.py +++ b/tests/swagger_generation/test_swagger_generator_hidden_api.py @@ -52,7 +52,7 @@ def test_swagger_generators(registry, swagger_generator, expected_swagger): elif open_api_version == "3.1.0": swagger_jsonschema = SWAGGER_V3_JSONSCHEMA else: - raise ValueError("Unknown swagger_version: {}".format(open_api_version)) + raise ValueError(f"Unknown swagger_version: {open_api_version}") validate_swagger(expected_swagger, schema=swagger_jsonschema) diff --git a/tests/test_errors.py b/tests/test_errors.py index 6d0dbd58..aed60820 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -7,8 +7,6 @@ :copyright: Copyright 2018 PlanGrid, Inc., see AUTHORS. :license: MIT, see LICENSE for details. """ -from __future__ import unicode_literals - import json import unittest diff --git a/tests/test_validation.py b/tests/test_validation.py index a10b4f83..8715f84a 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -33,7 +33,7 @@ class NoRequireOnDumpMixinSchema(Schema): value_required = fields.Str(required=True, allow_none=False) validation_required = fields.DateTime(required=True, allow_none=False) one_of_validation = fields.String(required=True, validate=OneOf(["a", "b"])) - dump_only = fields.Integer(dump_only=True) + dump_only = fields.Integer(dump_only=True) # type: ignore class RequireOnDumpMixinSchema(NoRequireOnDumpMixinSchema, RequireOnDumpMixin): @@ -60,7 +60,7 @@ class OuterNestedNone(Schema, RequireOnDumpMixin): class RequireOutputMixinTest(TestCase): def setUp(self): - super(RequireOutputMixinTest, self).setUp() + super().setUp() self.validated_schema = normalize_schema(RequireOnDumpMixinSchema) self.unvalidated_schema = normalize_schema(NoRequireOnDumpMixinSchema) self.data = {