From 3defae8d74056f22604d985007f71e627587cc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Lafr=C3=A9choux?= Date: Wed, 27 Oct 2021 14:48:31 +0200 Subject: [PATCH 1/8] Use OrderedSet as default set_class --- src/marshmallow/schema.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py index cde8c080f..7d5c7d1b9 100644 --- a/src/marshmallow/schema.py +++ b/src/marshmallow/schema.py @@ -319,6 +319,8 @@ class AlbumSchema(Schema): OPTIONS_CLASS = SchemaOpts # type: type + set_class = OrderedSet + # These get set by SchemaMeta opts = None # type: SchemaOpts _declared_fields = {} # type: typing.Dict[str, ma_fields.Field] @@ -419,10 +421,6 @@ def __repr__(self) -> str: def dict_class(self) -> type: return OrderedDict if self.ordered else dict - @property - def set_class(self) -> type: - return OrderedSet if self.ordered else set - @classmethod def from_dict( cls, From 634f36a6dbec1007ed5148ad50e06e435040ef5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Lafr=C3=A9choux?= Date: Sat, 31 Dec 2022 00:05:24 +0100 Subject: [PATCH 2/8] Fix typing issues --- src/marshmallow/schema.py | 6 ++++-- src/marshmallow/types.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py index 7d5c7d1b9..6f7f7f98e 100644 --- a/src/marshmallow/schema.py +++ b/src/marshmallow/schema.py @@ -388,7 +388,9 @@ def __init__( self.declared_fields = copy.deepcopy(self._declared_fields) self.many = many self.only = only - self.exclude = set(self.opts.exclude) | set(exclude) + self.exclude: set[typing.Any] | typing.MutableSet[typing.Any] = set( + self.opts.exclude + ) | set(exclude) self.ordered = self.opts.ordered self.load_only = set(load_only) or set(self.opts.load_only) self.dump_only = set(dump_only) or set(self.opts.dump_only) @@ -968,7 +970,7 @@ def _init_fields(self) -> None: if self.only is not None: # Return only fields specified in only option - field_names = self.set_class(self.only) + field_names: typing.AbstractSet[typing.Any] = self.set_class(self.only) invalid_fields |= field_names - available_field_names else: diff --git a/src/marshmallow/types.py b/src/marshmallow/types.py index 7626994a1..4ba66d95b 100644 --- a/src/marshmallow/types.py +++ b/src/marshmallow/types.py @@ -6,6 +6,8 @@ """ import typing -StrSequenceOrSet = typing.Union[typing.Sequence[str], typing.Set[str]] +StrSequenceOrSet = typing.Union[ + typing.Sequence[str], typing.Set[str], typing.MutableSet[str] +] Tag = typing.Union[str, typing.Tuple[str, bool]] Validator = typing.Callable[[typing.Any], typing.Any] From 05b2e187cece9d4d139d150ff25714d4c8f9c696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Lafr=C3=A9choux?= Date: Sat, 31 Dec 2022 00:06:45 +0100 Subject: [PATCH 3/8] Use typing.AbstractSet rather than typing.Set --- src/marshmallow/types.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/marshmallow/types.py b/src/marshmallow/types.py index 4ba66d95b..7c7b6b5b3 100644 --- a/src/marshmallow/types.py +++ b/src/marshmallow/types.py @@ -6,8 +6,6 @@ """ import typing -StrSequenceOrSet = typing.Union[ - typing.Sequence[str], typing.Set[str], typing.MutableSet[str] -] +StrSequenceOrSet = typing.Union[typing.Sequence[str], typing.AbstractSet[str]] Tag = typing.Union[str, typing.Tuple[str, bool]] Validator = typing.Callable[[typing.Any], typing.Any] From d94ecbbaec5161f2f05a3b68d3c7d68d6800e7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Lafr=C3=A9choux?= Date: Sat, 31 Dec 2022 00:21:49 +0100 Subject: [PATCH 4/8] Docs: remove "Ordering Output" section from quickstart --- docs/quickstart.rst | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 7d106228b..c6bc24262 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -524,37 +524,6 @@ Note that ``name`` will be automatically formatted as a :class:`String Date: Sun, 2 Jul 2023 19:15:41 +0200 Subject: [PATCH 5/8] Rework a few tests due to OrderedSet being default --- tests/test_fields.py | 9 +++++---- tests/test_options.py | 6 +----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 79fd01727..1296cd6d6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -9,6 +9,7 @@ RAISE, missing, ) +from marshmallow.orderedset import OrderedSet from marshmallow.exceptions import StringNotCollectionError from tests.base import ALL_FIELDS @@ -380,14 +381,14 @@ class MySchema(Schema): @pytest.mark.parametrize( ("param", "fields_list"), [("only", ["foo"]), ("exclude", ["bar"])] ) - def test_ordered_instanced_nested_schema_only_and_exclude(self, param, fields_list): + def test_nested_schema_only_and_exclude(self, param, fields_list): class NestedSchema(Schema): + # We mean to test the use of OrderedSet to specify it explicitly + # even if it is default + set_class = OrderedSet foo = fields.String() bar = fields.String() - class Meta: - ordered = True - class MySchema(Schema): nested = fields.Nested(NestedSchema(), **{param: fields_list}) diff --git a/tests/test_options.py b/tests/test_options.py index 02652d5fb..e24d9445f 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -178,9 +178,6 @@ def test_nested_field_order_with_only_arg_is_maintained_on_load(self): def test_nested_field_order_with_exclude_arg_is_maintained(self, user): class HasNestedExclude(Schema): - class Meta: - ordered = True - user = fields.Nested(KeepOrder, exclude=("birthdate",)) ser = HasNestedExclude() @@ -231,7 +228,7 @@ def test_fields_are_added(self): result = s.load({"name": "Steve", "from": "Oskosh"}) assert result == in_data - def test_ordered_included(self): + def test_included_fields_ordered_after_declared_fields(self): class AddFieldsOrdered(Schema): name = fields.Str() email = fields.Str() @@ -242,7 +239,6 @@ class Meta: "in": fields.Str(), "@at": fields.Str(), } - ordered = True s = AddFieldsOrdered() in_data = { From 002637585fe1f70f0dd9926349569bce7247e3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Lafr=C3=A9choux?= Date: Sun, 2 Jul 2023 19:25:27 +0200 Subject: [PATCH 6/8] Remove Field._creation_index and ordered arg in _get_fields --- src/marshmallow/fields.py | 4 ---- src/marshmallow/schema.py | 21 +++++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 118662b92..d258b977a 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -137,7 +137,6 @@ class Field(FieldABC): # to exist as attributes on the objects to serialize. Set this to False # for those fields _CHECK_ATTRIBUTE = True - _creation_index = 0 # Used for sorting #: Default error messages for various kinds of errors. The keys in this dictionary #: are passed to `Field.make_error`. The values are error messages passed to @@ -227,9 +226,6 @@ def __init__( stacklevel=2, ) - self._creation_index = Field._creation_index - Field._creation_index += 1 - # Collect default error message from self and parent classes messages = {} # type: dict[str, str] for cls in reversed(self.__class__.__mro__): diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py index 6f7f7f98e..e6868d72b 100644 --- a/src/marshmallow/schema.py +++ b/src/marshmallow/schema.py @@ -42,25 +42,21 @@ _T = typing.TypeVar("_T") -def _get_fields(attrs, ordered=False): - """Get fields from a class. If ordered=True, fields will sorted by creation index. +def _get_fields(attrs): + """Get fields from a class :param attrs: Mapping of class attributes - :param bool ordered: Sort fields by creation index """ - fields = [ + return [ (field_name, field_value) for field_name, field_value in attrs.items() if is_instance_or_subclass(field_value, base.FieldABC) ] - if ordered: - fields.sort(key=lambda pair: pair[1]._creation_index) - return fields # This function allows Schemas to inherit from non-Schema classes and ensures # inheritance according to the MRO -def _get_fields_by_mro(klass, ordered=False): +def _get_fields_by_mro(klass): """Collect fields from a class, following its method resolution order. The class itself is excluded from the search; only its parents are checked. Get fields from ``_declared_fields`` if available, else use ``__dict__``. @@ -73,7 +69,6 @@ class itself is excluded from the search; only its parents are checked. Get ( _get_fields( getattr(base, "_declared_fields", base.__dict__), - ordered=ordered, ) for base in mro[:0:-1] ), @@ -102,13 +97,13 @@ def __new__(mcs, name, bases, attrs): break else: ordered = False - cls_fields = _get_fields(attrs, ordered=ordered) + cls_fields = _get_fields(attrs) # Remove fields from list of class attributes to avoid shadowing # Schema attributes/methods in case of name conflict for field_name, _ in cls_fields: del attrs[field_name] klass = super().__new__(mcs, name, bases, attrs) - inherited_fields = _get_fields_by_mro(klass, ordered=ordered) + inherited_fields = _get_fields_by_mro(klass) meta = klass.Meta # Set klass.opts in __new__ rather than __init__ so that it is accessible in @@ -352,9 +347,7 @@ class Meta: - ``timeformat``: Default format for `Time ` fields. - ``render_module``: Module to use for `loads ` and `dumps `. Defaults to `json` from the standard library. - - ``ordered``: If `True`, order serialization output according to the - order in which fields were declared. Output of `Schema.dump` will be a - `collections.OrderedDict`. + - ``ordered``: If `True`, output of `Schema.dump` will be a `collections.OrderedDict`. - ``index_errors``: If `True`, errors dictionaries will include the index of invalid items in a collection. - ``load_only``: Tuple or list of fields to exclude from serialized results. From 684fe3690a20a726ed2730d5df16f4c8f53fdeba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Lafr=C3=A9choux?= Date: Sun, 2 Jul 2023 19:36:53 +0200 Subject: [PATCH 7/8] get_declared_fields: default to dict output --- src/marshmallow/schema.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py index e6868d72b..5a3b601e7 100644 --- a/src/marshmallow/schema.py +++ b/src/marshmallow/schema.py @@ -112,13 +112,11 @@ def __new__(mcs, name, bases, attrs): # Add fields specified in the `include` class Meta option cls_fields += list(klass.opts.include.items()) - dict_cls = OrderedDict if ordered else dict # Assign _declared_fields on class klass._declared_fields = mcs.get_declared_fields( klass=klass, cls_fields=cls_fields, inherited_fields=inherited_fields, - dict_cls=dict_cls, ) return klass @@ -128,7 +126,7 @@ def get_declared_fields( klass: type, cls_fields: list, inherited_fields: list, - dict_cls: type, + dict_cls: type = dict, ): """Returns a dictionary of field_name => `Field` pairs declared on the class. This is exposed mainly so that plugins can add additional fields, e.g. fields @@ -138,8 +136,7 @@ def get_declared_fields( :param cls_fields: The fields declared on the class, including those added by the ``include`` class Meta option. :param inherited_fields: Inherited fields. - :param dict_cls: Either `dict` or `OrderedDict`, depending on whether - the user specified `ordered=True`. + :param dict_cls: dict-like class to use for dict output Default to ``dict``. """ return dict_cls(inherited_fields + cls_fields) From 6abbfca3697d71eb19340ecda08450d30856ab99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Lafr=C3=A9choux?= Date: Sun, 16 Jul 2023 22:26:40 +0200 Subject: [PATCH 8/8] Add test_set_dict_class --- tests/test_schema.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_schema.py b/tests/test_schema.py index 4943fbd8f..bca3b78dc 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -2933,3 +2933,16 @@ class Meta: MySchema(unknown="badval") else: MySchema().load({"foo": "bar"}, unknown="badval") + + +@pytest.mark.parametrize("dict_cls", (dict, OrderedDict)) +def test_set_dict_class(dict_cls): + """Demonstrate how to specify dict_class as class attribute""" + + class MySchema(Schema): + dict_class = dict_cls + foo = fields.String() + + result = MySchema().dump({"foo": "bar"}) + assert result == {"foo": "bar"} + assert isinstance(result, dict_cls)