Skip to content

Commit 03889c6

Browse files
authored
Merge pull request #1896 from marshmallow-code/ordered_set_default
Use OrderedSet as default set_class
2 parents 91147b2 + 6abbfca commit 03889c6

File tree

7 files changed

+35
-70
lines changed

7 files changed

+35
-70
lines changed

docs/quickstart.rst

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -524,37 +524,6 @@ Note that ``name`` will be automatically formatted as a :class:`String <marshmal
524524
# No need to include 'uppername'
525525
additional = ("name", "email", "created_at")
526526
527-
Ordering Output
528-
---------------
529-
530-
To maintain field ordering, set the ``ordered`` option to `True`. This will instruct marshmallow to serialize data to a `collections.OrderedDict`.
531-
532-
.. code-block:: python
533-
534-
from collections import OrderedDict
535-
from pprint import pprint
536-
537-
from marshmallow import Schema, fields
538-
539-
540-
class UserSchema(Schema):
541-
first_name = fields.String()
542-
last_name = fields.String()
543-
email = fields.Email()
544-
545-
class Meta:
546-
ordered = True
547-
548-
549-
u = User("Charlie", "Stones", "charlie@stones.com")
550-
schema = UserSchema()
551-
result = schema.dump(u)
552-
assert isinstance(result, OrderedDict)
553-
pprint(result, indent=2)
554-
#  OrderedDict([('first_name', 'Charlie'),
555-
# ('last_name', 'Stones'),
556-
# ('email', 'charlie@stones.com')])
557-
558527
Next Steps
559528
----------
560529

src/marshmallow/fields.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ class Field(FieldABC):
137137
# to exist as attributes on the objects to serialize. Set this to False
138138
# for those fields
139139
_CHECK_ATTRIBUTE = True
140-
_creation_index = 0 # Used for sorting
141140

142141
#: Default error messages for various kinds of errors. The keys in this dictionary
143142
#: are passed to `Field.make_error`. The values are error messages passed to
@@ -227,9 +226,6 @@ def __init__(
227226
stacklevel=2,
228227
)
229228

230-
self._creation_index = Field._creation_index
231-
Field._creation_index += 1
232-
233229
# Collect default error message from self and parent classes
234230
messages = {} # type: dict[str, str]
235231
for cls in reversed(self.__class__.__mro__):

src/marshmallow/schema.py

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,25 +42,21 @@
4242
_T = typing.TypeVar("_T")
4343

4444

45-
def _get_fields(attrs, ordered=False):
46-
"""Get fields from a class. If ordered=True, fields will sorted by creation index.
45+
def _get_fields(attrs):
46+
"""Get fields from a class
4747
4848
:param attrs: Mapping of class attributes
49-
:param bool ordered: Sort fields by creation index
5049
"""
51-
fields = [
50+
return [
5251
(field_name, field_value)
5352
for field_name, field_value in attrs.items()
5453
if is_instance_or_subclass(field_value, base.FieldABC)
5554
]
56-
if ordered:
57-
fields.sort(key=lambda pair: pair[1]._creation_index)
58-
return fields
5955

6056

6157
# This function allows Schemas to inherit from non-Schema classes and ensures
6258
# inheritance according to the MRO
63-
def _get_fields_by_mro(klass, ordered=False):
59+
def _get_fields_by_mro(klass):
6460
"""Collect fields from a class, following its method resolution order. The
6561
class itself is excluded from the search; only its parents are checked. Get
6662
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
7369
(
7470
_get_fields(
7571
getattr(base, "_declared_fields", base.__dict__),
76-
ordered=ordered,
7772
)
7873
for base in mro[:0:-1]
7974
),
@@ -102,13 +97,13 @@ def __new__(mcs, name, bases, attrs):
10297
break
10398
else:
10499
ordered = False
105-
cls_fields = _get_fields(attrs, ordered=ordered)
100+
cls_fields = _get_fields(attrs)
106101
# Remove fields from list of class attributes to avoid shadowing
107102
# Schema attributes/methods in case of name conflict
108103
for field_name, _ in cls_fields:
109104
del attrs[field_name]
110105
klass = super().__new__(mcs, name, bases, attrs)
111-
inherited_fields = _get_fields_by_mro(klass, ordered=ordered)
106+
inherited_fields = _get_fields_by_mro(klass)
112107

113108
meta = klass.Meta
114109
# Set klass.opts in __new__ rather than __init__ so that it is accessible in
@@ -117,13 +112,11 @@ def __new__(mcs, name, bases, attrs):
117112
# Add fields specified in the `include` class Meta option
118113
cls_fields += list(klass.opts.include.items())
119114

120-
dict_cls = OrderedDict if ordered else dict
121115
# Assign _declared_fields on class
122116
klass._declared_fields = mcs.get_declared_fields(
123117
klass=klass,
124118
cls_fields=cls_fields,
125119
inherited_fields=inherited_fields,
126-
dict_cls=dict_cls,
127120
)
128121
return klass
129122

@@ -133,7 +126,7 @@ def get_declared_fields(
133126
klass: type,
134127
cls_fields: list,
135128
inherited_fields: list,
136-
dict_cls: type,
129+
dict_cls: type = dict,
137130
):
138131
"""Returns a dictionary of field_name => `Field` pairs declared on the class.
139132
This is exposed mainly so that plugins can add additional fields, e.g. fields
@@ -143,8 +136,7 @@ def get_declared_fields(
143136
:param cls_fields: The fields declared on the class, including those added
144137
by the ``include`` class Meta option.
145138
:param inherited_fields: Inherited fields.
146-
:param dict_cls: Either `dict` or `OrderedDict`, depending on whether
147-
the user specified `ordered=True`.
139+
:param dict_cls: dict-like class to use for dict output Default to ``dict``.
148140
"""
149141
return dict_cls(inherited_fields + cls_fields)
150142

@@ -319,6 +311,8 @@ class AlbumSchema(Schema):
319311

320312
OPTIONS_CLASS = SchemaOpts # type: type
321313

314+
set_class = OrderedSet
315+
322316
# These get set by SchemaMeta
323317
opts = None # type: SchemaOpts
324318
_declared_fields = {} # type: typing.Dict[str, ma_fields.Field]
@@ -350,9 +344,7 @@ class Meta:
350344
- ``timeformat``: Default format for `Time <fields.Time>` fields.
351345
- ``render_module``: Module to use for `loads <Schema.loads>` and `dumps <Schema.dumps>`.
352346
Defaults to `json` from the standard library.
353-
- ``ordered``: If `True`, order serialization output according to the
354-
order in which fields were declared. Output of `Schema.dump` will be a
355-
`collections.OrderedDict`.
347+
- ``ordered``: If `True`, output of `Schema.dump` will be a `collections.OrderedDict`.
356348
- ``index_errors``: If `True`, errors dictionaries will include the index
357349
of invalid items in a collection.
358350
- ``load_only``: Tuple or list of fields to exclude from serialized results.
@@ -386,7 +378,9 @@ def __init__(
386378
self.declared_fields = copy.deepcopy(self._declared_fields)
387379
self.many = many
388380
self.only = only
389-
self.exclude = set(self.opts.exclude) | set(exclude)
381+
self.exclude: set[typing.Any] | typing.MutableSet[typing.Any] = set(
382+
self.opts.exclude
383+
) | set(exclude)
390384
self.ordered = self.opts.ordered
391385
self.load_only = set(load_only) or set(self.opts.load_only)
392386
self.dump_only = set(dump_only) or set(self.opts.dump_only)
@@ -419,10 +413,6 @@ def __repr__(self) -> str:
419413
def dict_class(self) -> type:
420414
return OrderedDict if self.ordered else dict
421415

422-
@property
423-
def set_class(self) -> type:
424-
return OrderedSet if self.ordered else set
425-
426416
@classmethod
427417
def from_dict(
428418
cls,
@@ -970,7 +960,7 @@ def _init_fields(self) -> None:
970960

971961
if self.only is not None:
972962
# Return only fields specified in only option
973-
field_names = self.set_class(self.only)
963+
field_names: typing.AbstractSet[typing.Any] = self.set_class(self.only)
974964

975965
invalid_fields |= field_names - available_field_names
976966
else:

src/marshmallow/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
"""
77
import typing
88

9-
StrSequenceOrSet = typing.Union[typing.Sequence[str], typing.Set[str]]
9+
StrSequenceOrSet = typing.Union[typing.Sequence[str], typing.AbstractSet[str]]
1010
Tag = typing.Union[str, typing.Tuple[str, bool]]
1111
Validator = typing.Callable[[typing.Any], typing.Any]

tests/test_fields.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
RAISE,
1010
missing,
1111
)
12+
from marshmallow.orderedset import OrderedSet
1213
from marshmallow.exceptions import StringNotCollectionError
1314

1415
from tests.base import ALL_FIELDS
@@ -380,14 +381,14 @@ class MySchema(Schema):
380381
@pytest.mark.parametrize(
381382
("param", "fields_list"), [("only", ["foo"]), ("exclude", ["bar"])]
382383
)
383-
def test_ordered_instanced_nested_schema_only_and_exclude(self, param, fields_list):
384+
def test_nested_schema_only_and_exclude(self, param, fields_list):
384385
class NestedSchema(Schema):
386+
# We mean to test the use of OrderedSet to specify it explicitly
387+
# even if it is default
388+
set_class = OrderedSet
385389
foo = fields.String()
386390
bar = fields.String()
387391

388-
class Meta:
389-
ordered = True
390-
391392
class MySchema(Schema):
392393
nested = fields.Nested(NestedSchema(), **{param: fields_list})
393394

tests/test_options.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,6 @@ def test_nested_field_order_with_only_arg_is_maintained_on_load(self):
178178

179179
def test_nested_field_order_with_exclude_arg_is_maintained(self, user):
180180
class HasNestedExclude(Schema):
181-
class Meta:
182-
ordered = True
183-
184181
user = fields.Nested(KeepOrder, exclude=("birthdate",))
185182

186183
ser = HasNestedExclude()
@@ -231,7 +228,7 @@ def test_fields_are_added(self):
231228
result = s.load({"name": "Steve", "from": "Oskosh"})
232229
assert result == in_data
233230

234-
def test_ordered_included(self):
231+
def test_included_fields_ordered_after_declared_fields(self):
235232
class AddFieldsOrdered(Schema):
236233
name = fields.Str()
237234
email = fields.Str()
@@ -242,7 +239,6 @@ class Meta:
242239
"in": fields.Str(),
243240
"@at": fields.Str(),
244241
}
245-
ordered = True
246242

247243
s = AddFieldsOrdered()
248244
in_data = {

tests/test_schema.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2933,3 +2933,16 @@ class Meta:
29332933
MySchema(unknown="badval")
29342934
else:
29352935
MySchema().load({"foo": "bar"}, unknown="badval")
2936+
2937+
2938+
@pytest.mark.parametrize("dict_cls", (dict, OrderedDict))
2939+
def test_set_dict_class(dict_cls):
2940+
"""Demonstrate how to specify dict_class as class attribute"""
2941+
2942+
class MySchema(Schema):
2943+
dict_class = dict_cls
2944+
foo = fields.String()
2945+
2946+
result = MySchema().dump({"foo": "bar"})
2947+
assert result == {"foo": "bar"}
2948+
assert isinstance(result, dict_cls)

0 commit comments

Comments
 (0)