marshmallow 3.21.3__tar.gz → 3.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {marshmallow-3.21.3 → marshmallow-3.22.0}/CHANGELOG.rst +10 -0
  2. {marshmallow-3.21.3 → marshmallow-3.22.0}/PKG-INFO +17 -14
  3. {marshmallow-3.21.3 → marshmallow-3.22.0}/README.rst +13 -10
  4. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/index.rst +13 -0
  5. {marshmallow-3.21.3 → marshmallow-3.22.0}/pyproject.toml +4 -4
  6. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/decorators.py +13 -11
  7. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/schema.py +24 -26
  8. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/types.py +0 -1
  9. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_options.py +16 -0
  10. {marshmallow-3.21.3 → marshmallow-3.22.0}/CONTRIBUTING.rst +0 -0
  11. {marshmallow-3.21.3 → marshmallow-3.22.0}/LICENSE +0 -0
  12. {marshmallow-3.21.3 → marshmallow-3.22.0}/NOTICE +0 -0
  13. {marshmallow-3.21.3 → marshmallow-3.22.0}/SECURITY.md +0 -0
  14. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/.gitignore +0 -0
  15. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/_static/css/versionwarning.css +0 -0
  16. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/_static/marshmallow-logo.png +0 -0
  17. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/_templates/donate.html +0 -0
  18. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/_templates/useful-links.html +0 -0
  19. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/about.rst.inc +0 -0
  20. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/api_reference.rst +0 -0
  21. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/authors.rst +0 -0
  22. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/changelog.rst +0 -0
  23. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/code_of_conduct.rst +0 -0
  24. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/conf.py +0 -0
  25. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/contributing.rst +0 -0
  26. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/custom_fields.rst +0 -0
  27. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/dashing.json +0 -0
  28. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/ecosystem.rst +0 -0
  29. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/examples.rst +0 -0
  30. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/extending.rst +0 -0
  31. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/install.rst +0 -0
  32. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/kudos.rst +0 -0
  33. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/license.rst +0 -0
  34. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/marshmallow.class_registry.rst +0 -0
  35. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/marshmallow.decorators.rst +0 -0
  36. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/marshmallow.error_store.rst +0 -0
  37. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/marshmallow.exceptions.rst +0 -0
  38. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/marshmallow.fields.rst +0 -0
  39. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/marshmallow.schema.rst +0 -0
  40. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/marshmallow.utils.rst +0 -0
  41. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/marshmallow.validate.rst +0 -0
  42. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/nesting.rst +0 -0
  43. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/quickstart.rst +0 -0
  44. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/upgrading.rst +0 -0
  45. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/whos_using.rst +0 -0
  46. {marshmallow-3.21.3 → marshmallow-3.22.0}/docs/why.rst +0 -0
  47. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/__init__.py +0 -0
  48. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/base.py +0 -0
  49. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/class_registry.py +0 -0
  50. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/error_store.py +0 -0
  51. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/exceptions.py +0 -0
  52. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/fields.py +0 -0
  53. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/orderedset.py +0 -0
  54. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/py.typed +0 -0
  55. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/utils.py +0 -0
  56. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/validate.py +0 -0
  57. {marshmallow-3.21.3 → marshmallow-3.22.0}/src/marshmallow/warnings.py +0 -0
  58. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/__init__.py +0 -0
  59. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/base.py +0 -0
  60. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/conftest.py +0 -0
  61. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/foo_serializer.py +0 -0
  62. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/mypy_test_cases/test_validation_error.py +0 -0
  63. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_decorators.py +0 -0
  64. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_deserialization.py +0 -0
  65. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_error_store.py +0 -0
  66. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_exceptions.py +0 -0
  67. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_fields.py +0 -0
  68. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_registry.py +0 -0
  69. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_schema.py +0 -0
  70. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_serialization.py +0 -0
  71. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_utils.py +0 -0
  72. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_validate.py +0 -0
  73. {marshmallow-3.21.3 → marshmallow-3.22.0}/tests/test_version_attributes.py +0 -0
  74. {marshmallow-3.21.3 → marshmallow-3.22.0}/tox.ini +0 -0
@@ -1,6 +1,16 @@
1
1
  Changelog
2
2
  ---------
3
3
 
4
+ 3.22.0 (2024-08-20)
5
+ *******************
6
+
7
+ Features:
8
+
9
+ - Add ``many`` Meta option to ``Schema`` so it expects a collection by default (:issue:`2270`).
10
+ Thanks :user:`himalczyk` for reporting and :user:`deckar01` for the PR.
11
+ - Refactor hooks (:pr:`2279`).
12
+ Thanks :user:`deckar01` for the PR.
13
+
4
14
  3.21.3 (2024-06-05)
5
15
  *******************
6
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: marshmallow
3
- Version: 3.21.3
3
+ Version: 3.22.0
4
4
  Summary: A lightweight library for converting complex datatypes to and from native Python datatypes.
5
5
  Author-email: Steven Loria <sloria1@gmail.com>
6
6
  Maintainer-email: Steven Loria <sloria1@gmail.com>, Jérôme Lafréchoux <jerome@jolimont.fr>, Jared Deckard <jared@shademaps.com>
@@ -19,11 +19,11 @@ Requires-Dist: packaging>=17.0
19
19
  Requires-Dist: marshmallow[tests] ; extra == "dev"
20
20
  Requires-Dist: tox ; extra == "dev"
21
21
  Requires-Dist: pre-commit~=3.5 ; extra == "dev"
22
- Requires-Dist: sphinx==7.3.7 ; extra == "docs"
22
+ Requires-Dist: sphinx==8.0.2 ; extra == "docs"
23
23
  Requires-Dist: sphinx-issues==4.1.0 ; extra == "docs"
24
- Requires-Dist: alabaster==0.7.16 ; extra == "docs"
24
+ Requires-Dist: alabaster==1.0.0 ; extra == "docs"
25
25
  Requires-Dist: sphinx-version-warning==1.1.2 ; extra == "docs"
26
- Requires-Dist: autodocsumm==0.2.12 ; extra == "docs"
26
+ Requires-Dist: autodocsumm==0.2.13 ; extra == "docs"
27
27
  Requires-Dist: pytest ; extra == "tests"
28
28
  Requires-Dist: pytz ; extra == "tests"
29
29
  Requires-Dist: simplejson ; extra == "tests"
@@ -40,19 +40,21 @@ Provides-Extra: tests
40
40
  marshmallow: simplified object serialization
41
41
  ********************************************
42
42
 
43
- .. image:: https://badgen.net/pypi/v/marshmallow
43
+ |pypi| |build-status| |pre-commit| |docs|
44
+
45
+ .. |pypi| image:: https://badgen.net/pypi/v/marshmallow
44
46
  :target: https://pypi.org/project/marshmallow/
45
47
  :alt: Latest version
46
48
 
47
- .. image:: https://github.com/marshmallow-code/marshmallow/actions/workflows/build-release.yml/badge.svg
49
+ .. |build-status| image:: https://github.com/marshmallow-code/marshmallow/actions/workflows/build-release.yml/badge.svg
48
50
  :target: https://github.com/marshmallow-code/marshmallow/actions/workflows/build-release.yml
49
51
  :alt: Build status
50
52
 
51
- .. image:: https://results.pre-commit.ci/badge/github/marshmallow-code/marshmallow/dev.svg
53
+ .. |pre-commit| image:: https://results.pre-commit.ci/badge/github/marshmallow-code/marshmallow/dev.svg
52
54
  :target: https://results.pre-commit.ci/latest/github/marshmallow-code/marshmallow/dev
53
55
  :alt: pre-commit.ci status
54
56
 
55
- .. image:: https://readthedocs.org/projects/marshmallow/badge/
57
+ .. |docs| image:: https://readthedocs.org/projects/marshmallow/badge/
56
58
  :target: https://marshmallow.readthedocs.io/
57
59
  :alt: Documentation
58
60
 
@@ -152,15 +154,16 @@ Thank you to all our backers! [`Become a backer`_]
152
154
  Sponsors
153
155
  --------
154
156
 
155
- Support this project by becoming a sponsor (or ask your company to support this project by becoming a sponsor).
156
- Your logo will show up here with a link to your website. [`Become a sponsor`_]
157
+ marshmallow is sponsored by `Route4Me <https://route4me.com>`_.
157
158
 
158
- .. _`Become a sponsor`: https://opencollective.com/marshmallow#sponsor
159
+ .. image:: https://github.com/user-attachments/assets/018c2e23-032e-4a11-98da-8b6dc25b9054
160
+ :target: https://route4me.com
161
+ :alt: Routing Planner
159
162
 
160
- .. image:: https://opencollective.com/static/images/become_sponsor.svg
161
- :target: https://opencollective.com/marshmallow#sponsor
162
- :alt: Become a sponsor
163
+ Support this project by becoming a sponsor (or ask your company to support this project by becoming a sponsor).
164
+ Your logo will be displayed here with a link to your website. [`Become a sponsor`_]
163
165
 
166
+ .. _`Become a sponsor`: https://opencollective.com/marshmallow#sponsor
164
167
 
165
168
  Professional Support
166
169
  ====================
@@ -2,19 +2,21 @@
2
2
  marshmallow: simplified object serialization
3
3
  ********************************************
4
4
 
5
- .. image:: https://badgen.net/pypi/v/marshmallow
5
+ |pypi| |build-status| |pre-commit| |docs|
6
+
7
+ .. |pypi| image:: https://badgen.net/pypi/v/marshmallow
6
8
  :target: https://pypi.org/project/marshmallow/
7
9
  :alt: Latest version
8
10
 
9
- .. image:: https://github.com/marshmallow-code/marshmallow/actions/workflows/build-release.yml/badge.svg
11
+ .. |build-status| image:: https://github.com/marshmallow-code/marshmallow/actions/workflows/build-release.yml/badge.svg
10
12
  :target: https://github.com/marshmallow-code/marshmallow/actions/workflows/build-release.yml
11
13
  :alt: Build status
12
14
 
13
- .. image:: https://results.pre-commit.ci/badge/github/marshmallow-code/marshmallow/dev.svg
15
+ .. |pre-commit| image:: https://results.pre-commit.ci/badge/github/marshmallow-code/marshmallow/dev.svg
14
16
  :target: https://results.pre-commit.ci/latest/github/marshmallow-code/marshmallow/dev
15
17
  :alt: pre-commit.ci status
16
18
 
17
- .. image:: https://readthedocs.org/projects/marshmallow/badge/
19
+ .. |docs| image:: https://readthedocs.org/projects/marshmallow/badge/
18
20
  :target: https://marshmallow.readthedocs.io/
19
21
  :alt: Documentation
20
22
 
@@ -114,15 +116,16 @@ Thank you to all our backers! [`Become a backer`_]
114
116
  Sponsors
115
117
  --------
116
118
 
117
- Support this project by becoming a sponsor (or ask your company to support this project by becoming a sponsor).
118
- Your logo will show up here with a link to your website. [`Become a sponsor`_]
119
+ marshmallow is sponsored by `Route4Me <https://route4me.com>`_.
119
120
 
120
- .. _`Become a sponsor`: https://opencollective.com/marshmallow#sponsor
121
+ .. image:: https://github.com/user-attachments/assets/018c2e23-032e-4a11-98da-8b6dc25b9054
122
+ :target: https://route4me.com
123
+ :alt: Routing Planner
121
124
 
122
- .. image:: https://opencollective.com/static/images/become_sponsor.svg
123
- :target: https://opencollective.com/marshmallow#sponsor
124
- :alt: Become a sponsor
125
+ Support this project by becoming a sponsor (or ask your company to support this project by becoming a sponsor).
126
+ Your logo will be displayed here with a link to your website. [`Become a sponsor`_]
125
127
 
128
+ .. _`Become a sponsor`: https://opencollective.com/marshmallow#sponsor
126
129
 
127
130
  Professional Support
128
131
  ====================
@@ -18,6 +18,19 @@ Why another library?
18
18
 
19
19
  See :doc:`this document <why>` to learn about what makes marshmallow unique.
20
20
 
21
+ Sponsors
22
+ ========
23
+
24
+ marshmallow is sponsored by `Route4Me <https://route4me.com>`_.
25
+
26
+ .. image:: https://github.com/user-attachments/assets/018c2e23-032e-4a11-98da-8b6dc25b9054
27
+ :target: https://route4me.com
28
+ :alt: Routing Planner
29
+
30
+ Support this project by becoming a sponsor (or ask your company to support this project by becoming a sponsor).
31
+ Your logo will be displayed here with a link to your website. [`Become a sponsor`_]
32
+
33
+ .. _`Become a sponsor`: https://opencollective.com/marshmallow#sponsor
21
34
 
22
35
  Guide
23
36
  =====
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "marshmallow"
3
- version = "3.21.3"
3
+ version = "3.22.0"
4
4
  description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
5
5
  readme = "README.rst"
6
6
  license = { file = "LICENSE" }
@@ -33,11 +33,11 @@ Tidelift = "https://tidelift.com/subscription/pkg/pypi-marshmallow?utm_source=py
33
33
 
34
34
  [project.optional-dependencies]
35
35
  docs = [
36
- "sphinx==7.3.7",
36
+ "sphinx==8.0.2",
37
37
  "sphinx-issues==4.1.0",
38
- "alabaster==0.7.16",
38
+ "alabaster==1.0.0",
39
39
  "sphinx-version-warning==1.1.2",
40
- "autodocsumm==0.2.12",
40
+ "autodocsumm==0.2.13",
41
41
  ]
42
42
  tests = ["pytest", "pytz", "simplejson"]
43
43
  dev = ["marshmallow[tests]", "tox", "pre-commit~=3.5"]
@@ -68,6 +68,7 @@ Example: ::
68
68
  from __future__ import annotations
69
69
 
70
70
  import functools
71
+ from collections import defaultdict
71
72
  from typing import Any, Callable, cast
72
73
 
73
74
  PRE_DUMP = "pre_dump"
@@ -79,7 +80,7 @@ VALIDATES_SCHEMA = "validates_schema"
79
80
 
80
81
 
81
82
  class MarshmallowHook:
82
- __marshmallow_hook__: dict[tuple[str, bool] | str, Any] | None = None
83
+ __marshmallow_hook__: dict[str, list[tuple[bool, Any]]] | None = None
83
84
 
84
85
 
85
86
  def validates(field_name: str) -> Callable[..., Any]:
@@ -117,7 +118,8 @@ def validates_schema(
117
118
  """
118
119
  return set_hook(
119
120
  fn,
120
- (VALIDATES_SCHEMA, pass_many),
121
+ VALIDATES_SCHEMA,
122
+ many=pass_many,
121
123
  pass_original=pass_original,
122
124
  skip_on_field_errors=skip_on_field_errors,
123
125
  )
@@ -136,7 +138,7 @@ def pre_dump(
136
138
  .. versionchanged:: 3.0.0
137
139
  ``many`` is always passed as a keyword arguments to the decorated method.
138
140
  """
139
- return set_hook(fn, (PRE_DUMP, pass_many))
141
+ return set_hook(fn, PRE_DUMP, many=pass_many)
140
142
 
141
143
 
142
144
  def post_dump(
@@ -157,7 +159,7 @@ def post_dump(
157
159
  .. versionchanged:: 3.0.0
158
160
  ``many`` is always passed as a keyword arguments to the decorated method.
159
161
  """
160
- return set_hook(fn, (POST_DUMP, pass_many), pass_original=pass_original)
162
+ return set_hook(fn, POST_DUMP, many=pass_many, pass_original=pass_original)
161
163
 
162
164
 
163
165
  def pre_load(
@@ -174,7 +176,7 @@ def pre_load(
174
176
  ``partial`` and ``many`` are always passed as keyword arguments to
175
177
  the decorated method.
176
178
  """
177
- return set_hook(fn, (PRE_LOAD, pass_many))
179
+ return set_hook(fn, PRE_LOAD, many=pass_many)
178
180
 
179
181
 
180
182
  def post_load(
@@ -196,11 +198,11 @@ def post_load(
196
198
  ``partial`` and ``many`` are always passed as keyword arguments to
197
199
  the decorated method.
198
200
  """
199
- return set_hook(fn, (POST_LOAD, pass_many), pass_original=pass_original)
201
+ return set_hook(fn, POST_LOAD, many=pass_many, pass_original=pass_original)
200
202
 
201
203
 
202
204
  def set_hook(
203
- fn: Callable[..., Any] | None, key: tuple[str, bool] | str, **kwargs: Any
205
+ fn: Callable[..., Any] | None, tag: str, many: bool = False, **kwargs: Any
204
206
  ) -> Callable[..., Any]:
205
207
  """Mark decorated function as a hook to be picked up later.
206
208
  You should not need to use this method directly.
@@ -214,7 +216,7 @@ def set_hook(
214
216
  """
215
217
  # Allow using this as either a decorator or a decorator factory.
216
218
  if fn is None:
217
- return functools.partial(set_hook, key=key, **kwargs)
219
+ return functools.partial(set_hook, tag=tag, many=many, **kwargs)
218
220
 
219
221
  # Set a __marshmallow_hook__ attribute instead of wrapping in some class,
220
222
  # because I still want this to end up as a normal (unbound) method.
@@ -222,10 +224,10 @@ def set_hook(
222
224
  try:
223
225
  hook_config = function.__marshmallow_hook__
224
226
  except AttributeError:
225
- function.__marshmallow_hook__ = hook_config = {}
227
+ function.__marshmallow_hook__ = hook_config = defaultdict(list)
226
228
  # Also save the kwargs for the tagged function on
227
- # __marshmallow_hook__, keyed by (<tag>, <pass_many>)
229
+ # __marshmallow_hook__, keyed by <tag>
228
230
  if hook_config is not None:
229
- hook_config[key] = kwargs
231
+ hook_config[tag].append((many, kwargs))
230
232
 
231
233
  return fn
@@ -148,7 +148,7 @@ class SchemaMeta(ABCMeta):
148
148
  class_registry.register(name, cls)
149
149
  cls._hooks = cls.resolve_hooks()
150
150
 
151
- def resolve_hooks(cls) -> dict[types.Tag, list[str]]:
151
+ def resolve_hooks(cls) -> dict[str, list[tuple[str, bool, dict]]]:
152
152
  """Add in the decorated processors
153
153
 
154
154
  By doing this after constructing the class, we let standard inheritance
@@ -156,7 +156,7 @@ class SchemaMeta(ABCMeta):
156
156
  """
157
157
  mro = inspect.getmro(cls)
158
158
 
159
- hooks = defaultdict(list) # type: typing.Dict[types.Tag, typing.List[str]]
159
+ hooks = defaultdict(list) # type: typing.Dict[str, typing.List[typing.Tuple[str, bool, dict]]]
160
160
 
161
161
  for attr_name in dir(cls):
162
162
  # Need to look up the actual descriptor, not whatever might be
@@ -176,14 +176,16 @@ class SchemaMeta(ABCMeta):
176
176
  continue
177
177
 
178
178
  try:
179
- hook_config = attr.__marshmallow_hook__
179
+ hook_config = attr.__marshmallow_hook__ # type: typing.Dict[str, typing.List[typing.Tuple[bool, dict]]]
180
180
  except AttributeError:
181
181
  pass
182
182
  else:
183
- for key in hook_config.keys():
183
+ for tag, config in hook_config.items():
184
184
  # Use name here so we can get the bound method later, in
185
185
  # case the processor was a descriptor or something.
186
- hooks[key].append(attr_name)
186
+ hooks[tag].extend(
187
+ (attr_name, many, kwargs) for many, kwargs in config
188
+ )
187
189
 
188
190
  return hooks
189
191
 
@@ -226,6 +228,7 @@ class SchemaOpts:
226
228
  self.dump_only = getattr(meta, "dump_only", ())
227
229
  self.unknown = validate_unknown_parameter_value(getattr(meta, "unknown", RAISE))
228
230
  self.register = getattr(meta, "register", True)
231
+ self.many = getattr(meta, "many", False)
229
232
 
230
233
 
231
234
  class Schema(base.SchemaABC, metaclass=SchemaMeta):
@@ -319,7 +322,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
319
322
  # These get set by SchemaMeta
320
323
  opts = None # type: SchemaOpts
321
324
  _declared_fields = {} # type: typing.Dict[str, ma_fields.Field]
322
- _hooks = {} # type: typing.Dict[types.Tag, typing.List[str]]
325
+ _hooks = {} # type: typing.Dict[str, typing.List[typing.Tuple[str, bool, dict]]]
323
326
 
324
327
  class Meta:
325
328
  """Options object for a Schema.
@@ -342,6 +345,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
342
345
  `OrderedDict`.
343
346
  - ``exclude``: Tuple or list of fields to exclude in the serialized result.
344
347
  Nested fields can be represented with dot delimiters.
348
+ - ``many``: Whether the data is a collection by default.
345
349
  - ``dateformat``: Default format for `Date <fields.Date>` fields.
346
350
  - ``datetimeformat``: Default format for `DateTime <fields.DateTime>` fields.
347
351
  - ``timeformat``: Default format for `Time <fields.Time>` fields.
@@ -365,7 +369,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
365
369
  *,
366
370
  only: types.StrSequenceOrSet | None = None,
367
371
  exclude: types.StrSequenceOrSet = (),
368
- many: bool = False,
372
+ many: bool | None = None,
369
373
  context: dict | None = None,
370
374
  load_only: types.StrSequenceOrSet = (),
371
375
  dump_only: types.StrSequenceOrSet = (),
@@ -379,7 +383,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
379
383
  raise StringNotCollectionError('"exclude" should be a list of strings')
380
384
  # copy declared fields from metaclass
381
385
  self.declared_fields = copy.deepcopy(self._declared_fields)
382
- self.many = many
386
+ self.many = self.opts.many if many is None else many
383
387
  self.only = only
384
388
  self.exclude: set[typing.Any] | typing.MutableSet[typing.Any] = set(
385
389
  self.opts.exclude
@@ -539,7 +543,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
539
543
  Validation no longer occurs upon serialization.
540
544
  """
541
545
  many = self.many if many is None else bool(many)
542
- if self._has_processors(PRE_DUMP):
546
+ if self._hooks[PRE_DUMP]:
543
547
  processed_obj = self._invoke_dump_processors(
544
548
  PRE_DUMP, obj, many=many, original_data=obj
545
549
  )
@@ -548,7 +552,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
548
552
 
549
553
  result = self._serialize(processed_obj, many=many)
550
554
 
551
- if self._has_processors(POST_DUMP):
555
+ if self._hooks[POST_DUMP]:
552
556
  result = self._invoke_dump_processors(
553
557
  POST_DUMP, result, many=many, original_data=obj
554
558
  )
@@ -846,7 +850,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
846
850
  if partial is None:
847
851
  partial = self.partial
848
852
  # Run preprocessors
849
- if self._has_processors(PRE_LOAD):
853
+ if self._hooks[PRE_LOAD]:
850
854
  try:
851
855
  processed_data = self._invoke_load_processors(
852
856
  PRE_LOAD, data, many=many, original_data=data, partial=partial
@@ -870,7 +874,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
870
874
  error_store=error_store, data=result, many=many
871
875
  )
872
876
  # Run schema-level validation
873
- if self._has_processors(VALIDATES_SCHEMA):
877
+ if self._hooks[VALIDATES_SCHEMA]:
874
878
  field_errors = bool(error_store.errors)
875
879
  self._invoke_schema_validators(
876
880
  error_store=error_store,
@@ -892,7 +896,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
892
896
  )
893
897
  errors = error_store.errors
894
898
  # Run post processors
895
- if not errors and postprocess and self._has_processors(POST_LOAD):
899
+ if not errors and postprocess and self._hooks[POST_LOAD]:
896
900
  try:
897
901
  result = self._invoke_load_processors(
898
902
  POST_LOAD,
@@ -1055,9 +1059,6 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
1055
1059
  raise error
1056
1060
  self.on_bind_field(field_name, field_obj)
1057
1061
 
1058
- def _has_processors(self, tag) -> bool:
1059
- return bool(self._hooks[(tag, True)] or self._hooks[(tag, False)])
1060
-
1061
1062
  def _invoke_dump_processors(
1062
1063
  self, tag: str, data, *, many: bool, original_data=None
1063
1064
  ):
@@ -1102,9 +1103,8 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
1102
1103
  return data
1103
1104
 
1104
1105
  def _invoke_field_validators(self, *, error_store: ErrorStore, data, many: bool):
1105
- for attr_name in self._hooks[VALIDATES]:
1106
+ for attr_name, _, validator_kwargs in self._hooks[VALIDATES]:
1106
1107
  validator = getattr(self, attr_name)
1107
- validator_kwargs = validator.__marshmallow_hook__[VALIDATES]
1108
1108
  field_name = validator_kwargs["field_name"]
1109
1109
 
1110
1110
  try:
@@ -1159,11 +1159,10 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
1159
1159
  partial: bool | types.StrSequenceOrSet | None,
1160
1160
  field_errors: bool = False,
1161
1161
  ):
1162
- for attr_name in self._hooks[(VALIDATES_SCHEMA, pass_many)]:
1162
+ for attr_name, hook_many, validator_kwargs in self._hooks[VALIDATES_SCHEMA]:
1163
+ if hook_many != pass_many:
1164
+ continue
1163
1165
  validator = getattr(self, attr_name)
1164
- validator_kwargs = validator.__marshmallow_hook__[
1165
- (VALIDATES_SCHEMA, pass_many)
1166
- ]
1167
1166
  if field_errors and validator_kwargs["skip_on_field_errors"]:
1168
1167
  continue
1169
1168
  pass_original = validator_kwargs.get("pass_original", False)
@@ -1201,12 +1200,11 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
1201
1200
  original_data=None,
1202
1201
  **kwargs,
1203
1202
  ):
1204
- key = (tag, pass_many)
1205
- for attr_name in self._hooks[key]:
1203
+ for attr_name, hook_many, processor_kwargs in self._hooks[tag]:
1204
+ if hook_many != pass_many:
1205
+ continue
1206
1206
  # This will be a bound method.
1207
1207
  processor = getattr(self, attr_name)
1208
-
1209
- processor_kwargs = processor.__marshmallow_hook__[key]
1210
1208
  pass_original = processor_kwargs.get("pass_original", False)
1211
1209
 
1212
1210
  if many and not pass_many:
@@ -8,5 +8,4 @@
8
8
  import typing
9
9
 
10
10
  StrSequenceOrSet = typing.Union[typing.Sequence[str], typing.AbstractSet[str]]
11
- Tag = typing.Union[str, typing.Tuple[str, bool]]
12
11
  Validator = typing.Callable[[typing.Any], typing.Any]
@@ -262,3 +262,19 @@ class TestIncludeOption:
262
262
  assert "email" in s._declared_fields.keys()
263
263
  assert "from" in s._declared_fields.keys()
264
264
  assert isinstance(s._declared_fields["from"], fields.Str)
265
+
266
+
267
+ class TestManyOption:
268
+ class ManySchema(Schema):
269
+ foo = fields.Str()
270
+
271
+ class Meta:
272
+ many = True
273
+
274
+ def test_many_by_default(self):
275
+ test = self.ManySchema()
276
+ assert test.load([{"foo": "bar"}]) == [{"foo": "bar"}]
277
+
278
+ def test_explicit_single(self):
279
+ test = self.ManySchema(many=False)
280
+ assert test.load({"foo": "bar"}) == {"foo": "bar"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes