Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-marshmallow for 
openSUSE:Factory checked in at 2026-04-25 21:35:26
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-marshmallow (Old)
 and      /work/SRC/openSUSE:Factory/.python-marshmallow.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-marshmallow"

Sat Apr 25 21:35:26 2026 rev:30 rq:1349128 version:4.3.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-marshmallow/python-marshmallow.changes    
2026-03-30 18:30:29.049037504 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-marshmallow.new.11940/python-marshmallow.changes
 2026-04-25 21:35:42.113697656 +0200
@@ -1,0 +2,15 @@
+Fri Apr 24 09:00:42 UTC 2026 - John Paul Adrian Glaubitz 
<[email protected]>
+
+- Update to 4.3.0
+  * Add ``pre_load`` and ``post_load`` parameters to `marshmallow.fields.Field`
+    for field-level pre- and post-processing (:issue:`2787`).
+  * Typing: improvements to `marshmallow.validate` (:pr:`2940`).
+- from version 4.2.4
+  * `marshmallow.validate.URL` and `marshmallow.validate.Email` accept
+    Internationalized Domain Names (IDNs) (:issue:`2821`, :issue:`2936`).
+  * `marshmallow.validate.Email` also correctly rejects IDN domains with
+    leading/trailing hyphens.
+    Thanks :user:`touhidurrr` for the report.
+  * Typing: Fix typing of ``nested`` in `marshmallow.fields.Nested` 
(:pr:`2935`).
+
+-------------------------------------------------------------------

Old:
----
  marshmallow-4.2.3.tar.gz

New:
----
  marshmallow-4.3.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-marshmallow.spec ++++++
--- /var/tmp/diff_new_pack.13AhRz/_old  2026-04-25 21:35:42.581716700 +0200
+++ /var/tmp/diff_new_pack.13AhRz/_new  2026-04-25 21:35:42.585716862 +0200
@@ -26,7 +26,7 @@
 %endif
 %{?sle15_python_module_pythons}
 Name:           python-marshmallow
-Version:        4.2.3
+Version:        4.3.0
 Release:        0
 Summary:        ORM/ODM/framework-agnostic library to convert datatypes 
from/to Python types
 License:        BSD-3-Clause AND MIT

++++++ marshmallow-4.2.3.tar.gz -> marshmallow-4.3.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/CHANGELOG.rst 
new/marshmallow-4.3.0/CHANGELOG.rst
--- old/marshmallow-4.2.3/CHANGELOG.rst 2026-03-26 00:26:40.587345400 +0100
+++ new/marshmallow-4.3.0/CHANGELOG.rst 2026-04-03 23:45:50.049361000 +0200
@@ -1,6 +1,25 @@
 Changelog
 =========
 
+4.3.0 (2026-04-03)
+------------------
+
+Features:
+
+- Add ``pre_load`` and ``post_load`` parameters to `marshmallow.fields.Field` 
for
+  field-level pre- and post-processing (:issue:`2787`).
+- Typing: improvements to `marshmallow.validate` (:pr:`2940`).
+
+4.2.4 (2026-04-02)
+------------------
+
+Bug fixes:
+
+- `marshmallow.validate.URL` and `marshmallow.validate.Email` accept 
Internationalized Domain Names (IDNs) (:issue:`2821`, :issue:`2936`).
+  `marshmallow.validate.Email` also correctly rejects IDN domains with 
leading/trailing hyphens.
+  Thanks :user:`touhidurrr` for the report.
+- Typing: Fix typing of ``nested`` in `marshmallow.fields.Nested` (:pr:`2935`).
+
 4.2.3 (2026-03-25)
 ------------------
 
@@ -12,13 +31,13 @@
   Thanks :user:`nosnickid` for the report and :user:`worksbyfriday` for the PR.
 - Fix `marshmallow.validate.OneOf` emitting extra pairs when labels outnumber 
choices (:issue:`2869`).
   Thanks: user:`T90REAL` for the report and :user:`rstar327` for the PR.
-- Fix behavior when passing a dot-delited attribute name to ``partial`` for a 
key with ``data_key`` set (:pr:`2903`).
+- Fix behavior when passing a dot-delimited attribute name to ``partial`` for 
a key with ``data_key`` set (:pr:`2903`).
   Thanks :user:`bysiber` for the PR.
 - Fix Enum field by-name lookup to only return actual members (:pr:`2902`).
   Thanks :user:`bysiber` for the PR.
 - `marshmallow.fields.DateTime` with ``format="timestamp_ms"`` properly
   rejects bool values (:pr:`2904`). Thanks :user:`bysiber` for the PR.
-- Fix typing of ``error_essages`` argument to `marshmallow.fields.Field` 
(:pr:`1636`).
+- Fix typing of ``error_messages`` argument to `marshmallow.fields.Field` 
(:pr:`1636`).
   Thanks :user:`repole` for reporting and :user:`dhruvildarji` for the PR.
 
 Other changes:
@@ -112,10 +131,12 @@
   The typing of `data` is also updated to be more accurate.
   Thanks :user:`ziplokk1` for reporting.
 - *Backwards-incompatible*: Use `datetime.date.fromisoformat`, 
`datetime.time.fromisoformat`, and `datetime.datetime.fromisoformat` from the 
standard library to deserialize dates, times and datetimes (:pr:`2078`).
-As a consequence of this change:
+  As a consequence of this change:
+
   - Time with time offsets are now supported.
   - YYYY-MM-DD is now accepted as a datetime and deserialized as naive 00:00 
AM.
   - `from_iso_date`, `from_iso_time` and `from_iso_datetime` are removed from 
`marshmallow.utils`.
+
 - Remove `isoformat`, `to_iso_time` and `to_iso_datetime` from 
`marshmallow.utils` (:pr:`2766`).
 - Remove `from_rfc`, and `rfcformat` from `marshmallow.utils` (:pr:`2767`).
 - Remove `is_keyed_tuple` from `marshmallow.utils` (:pr:`2768`).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/CONTRIBUTING.rst 
new/marshmallow-4.3.0/CONTRIBUTING.rst
--- old/marshmallow-4.2.3/CONTRIBUTING.rst      2026-03-26 00:26:40.587345400 
+0100
+++ new/marshmallow-4.3.0/CONTRIBUTING.rst      2026-04-03 23:45:50.049361000 
+0200
@@ -46,21 +46,19 @@
     $ git clone https://github.com/marshmallow-code/marshmallow.git
     $ cd marshmallow
 
-2. Install development requirements. **It is highly recommended that you use a 
virtualenv.**
-   Use the following command to install an editable version of
-   marshmallow along with its development requirements.
+2. Install `uv <https://docs.astral.sh/uv/getting-started/installation/>`_.
+
+3. Install development requirements.
 
 .. code-block:: shell-session
 
-    # After activating your virtualenv
-    $ pip install -e '.[dev]'
+    $ uv sync
 
-3. Install the pre-commit hooks, which will format and lint your git staged 
files.
+4. (Optional but recommended) Install the pre-commit hooks, which will format 
and lint your git staged files.
 
 .. code-block:: shell-session
 
-    # The pre-commit CLI was installed above
-    $ pre-commit install --allow-missing-config
+    $ uv run pre-commit install --allow-missing-config
 
 Git branch structure
 ++++++++++++++++++++
@@ -113,19 +111,19 @@
 
 .. code-block:: shell-session
 
-    $ pytest
+    $ uv run pytest
 
 To run formatting and syntax checks:
 
 .. code-block:: shell-session
 
-    $ tox -e lint
+    $ uv run tox -e lint
 
 (Optional) To run tests in all supported Python versions in their own virtual 
environments (must have each interpreter installed):
 
 .. code-block:: shell-session
 
-    $ tox
+    $ uv run tox
 
 .. _contributing_documentation:
 
@@ -138,7 +136,7 @@
 
 .. code-block:: shell-session
 
-   $ tox -e docs-serve
+   $ uv run tox -e docs-serve
 
 Changes to documentation will automatically trigger a rebuild.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/PKG-INFO 
new/marshmallow-4.3.0/PKG-INFO
--- old/marshmallow-4.2.3/PKG-INFO      1970-01-01 01:00:00.000000000 +0100
+++ new/marshmallow-4.3.0/PKG-INFO      1970-01-01 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: marshmallow
-Version: 4.2.3
+Version: 4.3.0
 Summary: A lightweight library for converting complex datatypes to and from 
native Python datatypes.
 Author-email: Steven Loria <[email protected]>
 Maintainer-email: Steven Loria <[email protected]>, Jérôme Lafréchoux 
<[email protected]>, Jared Deckard <[email protected]>
@@ -18,25 +18,11 @@
 License-File: LICENSE
 Requires-Dist: backports-datetime-fromisoformat; python_version < '3.11'
 Requires-Dist: typing-extensions; python_version < '3.11'
-Requires-Dist: marshmallow[tests] ; extra == "dev"
-Requires-Dist: tox ; extra == "dev"
-Requires-Dist: pre-commit>=3.5,<5.0 ; extra == "dev"
-Requires-Dist: autodocsumm==0.2.14 ; extra == "docs"
-Requires-Dist: furo==2025.12.19 ; extra == "docs"
-Requires-Dist: sphinx-copybutton==0.5.2 ; extra == "docs"
-Requires-Dist: sphinx-issues==6.0.0 ; extra == "docs"
-Requires-Dist: sphinx==8.2.3 ; extra == "docs"
-Requires-Dist: sphinxext-opengraph==0.13.0 ; extra == "docs"
-Requires-Dist: pytest ; extra == "tests"
-Requires-Dist: simplejson ; extra == "tests"
 Project-URL: Changelog, 
https://marshmallow.readthedocs.io/en/latest/changelog.html
 Project-URL: Funding, https://opencollective.com/marshmallow
 Project-URL: Issues, https://github.com/marshmallow-code/marshmallow/issues
 Project-URL: Source, https://github.com/marshmallow-code/marshmallow
 Project-URL: Tidelift, 
https://tidelift.com/subscription/pkg/pypi-marshmallow?utm_source=pypi-marshmallow&utm_medium=pypi
-Provides-Extra: dev
-Provides-Extra: docs
-Provides-Extra: tests
 
 ********************************************
 marshmallow: simplified object serialization
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/docs/conf.py 
new/marshmallow-4.3.0/docs/conf.py
--- old/marshmallow-4.2.3/docs/conf.py  2026-03-26 00:26:40.588555800 +0100
+++ new/marshmallow-4.3.0/docs/conf.py  2026-04-03 23:45:50.050572200 +0200
@@ -41,8 +41,7 @@
     "source_directory": "docs/",
     "sidebar_hide_name": True,
     "light_css_variables": {
-        # Serif system font stack: https://systemfontstack.com/
-        "font-stack": "Iowan Old Style, Apple Garamond, Baskerville, Times New 
Roman, Droid Serif, Times, Source Serif Pro, serif, Apple Color Emoji, Segoe UI 
Emoji, Segoe UI Symbol;",
+        "font-stack": "Charter, Iowan Old Style, Palatino Linotype, Palatino, 
Georgia, serif;",
     },
     "top_of_page_buttons": ["view", "edit"],
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/docs/extending/index.rst 
new/marshmallow-4.3.0/docs/extending/index.rst
--- old/marshmallow-4.2.3/docs/extending/index.rst      2026-03-26 
00:26:40.589648200 +0100
+++ new/marshmallow-4.3.0/docs/extending/index.rst      2026-04-03 
23:45:50.051643000 +0200
@@ -6,7 +6,7 @@
 .. toctree::
   :maxdepth: 1
 
-  pre_and_post_processing_methods
+  pre_and_post_processing
   schema_validation
   using_original_input_data
   overriding_attribute_access
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/marshmallow-4.2.3/docs/extending/pre_and_post_processing.rst 
new/marshmallow-4.3.0/docs/extending/pre_and_post_processing.rst
--- old/marshmallow-4.2.3/docs/extending/pre_and_post_processing.rst    
1970-01-01 01:00:00.000000000 +0100
+++ new/marshmallow-4.3.0/docs/extending/pre_and_post_processing.rst    
2026-04-03 23:45:50.051643000 +0200
@@ -0,0 +1,247 @@
+Pre-processing and post-processing
+===================================
+
+Decorator API
+-------------
+
+Data pre-processing and post-processing methods can be registered using the 
`pre_load <marshmallow.decorators.pre_load>`, `post_load 
<marshmallow.decorators.post_load>`, `pre_dump 
<marshmallow.decorators.pre_dump>`, and `post_dump 
<marshmallow.decorators.post_dump>` decorators.
+
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields, post_load
+
+
+    class UserSchema(Schema):
+        name = fields.Str()
+        slug = fields.Str()
+
+        @post_load
+        def slugify_name(self, in_data, **kwargs):
+            in_data["slug"] = in_data["slug"].lower().strip().replace(" ", "-")
+            return in_data
+
+
+    schema = UserSchema()
+    result = schema.load({"name": "Steve", "slug": "Steve Loria "})
+    result["slug"]  # => 'steve-loria'
+
+Passing "many"
+--------------
+
+By default, pre- and post-processing methods receive one object/datum at a 
time, transparently handling the ``many`` parameter passed to the ``Schema``'s 
`~marshmallow.Schema.dump`/`~marshmallow.Schema.load` method at runtime.
+
+In cases where your pre- and post-processing methods needs to handle the input 
collection when processing multiple objects, add ``pass_many=True`` to the 
method decorators.
+
+Your method will then receive the input data (which may be a single datum or a 
collection, depending on the dump/load call).
+
+.. _enveloping_1:
+
+Example: Enveloping
+-------------------
+
+One common use case is to wrap data in a namespace upon serialization and 
unwrap the data during deserialization.
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields, pre_load, post_load, post_dump
+
+
+    class BaseSchema(Schema):
+        # Custom options
+        __envelope__ = {"single": None, "many": None}
+        __model__ = User
+
+        def get_envelope_key(self, many):
+            """Helper to get the envelope key."""
+            key = self.__envelope__["many"] if many else 
self.__envelope__["single"]
+            assert key is not None, "Envelope key undefined"
+            return key
+
+        @pre_load(pass_many=True)
+        def unwrap_envelope(self, data, many, **kwargs):
+            key = self.get_envelope_key(many)
+            return data[key]
+
+        @post_dump(pass_many=True)
+        def wrap_with_envelope(self, data, many, **kwargs):
+            key = self.get_envelope_key(many)
+            return {key: data}
+
+        @post_load
+        def make_object(self, data, **kwargs):
+            return self.__model__(**data)
+
+
+    class UserSchema(BaseSchema):
+        __envelope__ = {"single": "user", "many": "users"}
+        __model__ = User
+        name = fields.Str()
+        email = fields.Email()
+
+
+    user_schema = UserSchema()
+
+    user = User("Mick", email="[email protected]")
+    user_data = user_schema.dump(user)
+    # {'user': {'email': '[email protected]', 'name': 'Mick'}}
+
+    users = [
+        User("Keith", email="[email protected]"),
+        User("Charlie", email="[email protected]"),
+    ]
+    users_data = user_schema.dump(users, many=True)
+    # {'users': [{'email': '[email protected]', 'name': 'Keith'},
+    #            {'email': '[email protected]', 'name': 'Charlie'}]}
+
+    user_objs = user_schema.load(users_data, many=True)
+    # [<User(name='Keith Richards')>, <User(name='Charlie Watts')>]
+
+.. _field_level_processing:
+
+Field-level pre- and post-processing
+-------------------------------------
+
+For field-level processing, pass ``pre_load`` and ``post_load``
+callables directly to individual fields. This is useful for simple, 
field-specific
+transformations that don't need access to the full schema data.
+
+Each callable receives the field value and returns a transformed value.
+You can pass a single callable or a list of callables, which are applied in 
order.
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields
+
+
+    class UserSchema(Schema):
+        name = fields.Str(pre_load=str.strip)
+        birthday = fields.Date(post_load=lambda value: value.year)
+
+
+    schema = UserSchema()
+    result = schema.load({"name": "  Steve  ", "birthday": "1994-05-12"})
+    result["name"]  # => 'Steve'
+    result["birthday"]  # => 1994
+
+
+``pre_load`` callables run before the field's deserialization (and before 
``allow_none`` is checked),
+while ``post_load`` callables run after validation and deserialization.
+
+Like validators, ``pre_load`` and ``post_load`` callables may raise a
+`ValidationError <marshmallow.exceptions.ValidationError>`, which will be
+stored under the field's key in the errors dictionary.
+
+Raising errors in pre-/post-processor methods
+---------------------------------------------
+
+Pre- and post-processing methods may raise a `ValidationError 
<marshmallow.exceptions.ValidationError>`. By default, errors will be stored on 
the ``"_schema"`` key in the errors dictionary.
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields, ValidationError, pre_load
+
+
+    class BandSchema(Schema):
+        name = fields.Str()
+
+        @pre_load
+        def unwrap_envelope(self, data, **kwargs):
+            if "data" not in data:
+                raise ValidationError('Input data must have a "data" key.')
+            return data["data"]
+
+
+    sch = BandSchema()
+    try:
+        sch.load({"name": "The Band"})
+    except ValidationError as err:
+        err.messages
+    # {'_schema': ['Input data must have a "data" key.']}
+
+If you want to store and error on a different key, pass the key name as the 
second argument to `ValidationError <marshmallow.exceptions.ValidationError>`.
+
+.. code-block:: python
+
+    from marshmallow import Schema, fields, ValidationError, pre_load
+
+
+    class BandSchema(Schema):
+        name = fields.Str()
+
+        @pre_load
+        def unwrap_envelope(self, data, **kwargs):
+            if "data" not in data:
+                raise ValidationError(
+                    'Input data must have a "data" key.', "_preprocessing"
+                )
+            return data["data"]
+
+
+    sch = BandSchema()
+    try:
+        sch.load({"name": "The Band"})
+    except ValidationError as err:
+        err.messages
+    # {'_preprocessing': ['Input data must have a "data" key.']}
+
+Pre-/post-processor invocation order
+------------------------------------
+
+In summary, the processing pipeline for deserialization is as follows:
+
+1. ``@pre_load(pass_many=True)`` methods
+2. ``@pre_load(pass_many=False)`` methods
+3. Field-level ``pre_load`` callables
+4. Field deserialization (``_deserialize``)
+5. Field-level ``validate`` callables and ``@validates`` methods
+6. Field-level ``post_load`` callables
+7. ``@validates_schema`` methods (schema validators)
+8. ``@post_load(pass_many=False)`` methods
+9. ``@post_load(pass_many=True)`` methods
+
+The pipeline for serialization is similar, except that the ``pass_many=True`` 
processors are invoked *after* the ``pass_many=False`` processors and there are 
no validators.
+
+1. ``@pre_dump(pass_many=False)`` methods
+2. ``@pre_dump(pass_many=True)`` methods
+3. ``dump(obj, many)`` (serialization)
+4. ``@post_dump(pass_many=False)`` methods
+5. ``@post_dump(pass_many=True)`` methods
+
+
+.. warning::
+
+    You may register multiple processor methods on a Schema. Keep in mind, 
however, that **the invocation order of decorated methods of the same type is 
not guaranteed**. If you need to guarantee order of processing steps, you 
should put them in the same method.
+
+
+    .. code-block:: python
+
+        from marshmallow import Schema, fields, pre_load
+
+
+        # YES
+        class MySchema(Schema):
+            field_a = fields.Raw()
+
+            @pre_load
+            def preprocess(self, data, **kwargs):
+                step1_data = self.step1(data)
+                step2_data = self.step2(step1_data)
+                return step2_data
+
+            def step1(self, data): ...
+
+            # Depends on step1
+            def step2(self, data): ...
+
+
+        # NO
+        class MySchema(Schema):
+            field_a = fields.Raw()
+
+            @pre_load
+            def step1(self, data, **kwargs): ...
+
+            # Depends on step1
+            @pre_load
+            def step2(self, data, **kwargs): ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/marshmallow-4.2.3/docs/extending/pre_and_post_processing_methods.rst 
new/marshmallow-4.3.0/docs/extending/pre_and_post_processing_methods.rst
--- old/marshmallow-4.2.3/docs/extending/pre_and_post_processing_methods.rst    
2026-03-26 00:26:40.589648200 +0100
+++ new/marshmallow-4.3.0/docs/extending/pre_and_post_processing_methods.rst    
1970-01-01 01:00:00.000000000 +0100
@@ -1,210 +0,0 @@
-Pre-processing and post-processing methods
-==========================================
-
-Decorator API
--------------
-
-Data pre-processing and post-processing methods can be registered using the 
`pre_load <marshmallow.decorators.pre_load>`, `post_load 
<marshmallow.decorators.post_load>`, `pre_dump 
<marshmallow.decorators.pre_dump>`, and `post_dump 
<marshmallow.decorators.post_dump>` decorators.
-
-
-.. code-block:: python
-
-    from marshmallow import Schema, fields, post_load
-
-
-    class UserSchema(Schema):
-        name = fields.Str()
-        slug = fields.Str()
-
-        @post_load
-        def slugify_name(self, in_data, **kwargs):
-            in_data["slug"] = in_data["slug"].lower().strip().replace(" ", "-")
-            return in_data
-
-
-    schema = UserSchema()
-    result = schema.load({"name": "Steve", "slug": "Steve Loria "})
-    result["slug"]  # => 'steve-loria'
-
-Passing "many"
---------------
-
-By default, pre- and post-processing methods receive one object/datum at a 
time, transparently handling the ``many`` parameter passed to the ``Schema``'s 
`~marshmallow.Schema.dump`/`~marshmallow.Schema.load` method at runtime.
-
-In cases where your pre- and post-processing methods needs to handle the input 
collection when processing multiple objects, add ``pass_many=True`` to the 
method decorators.
-
-Your method will then receive the input data (which may be a single datum or a 
collection, depending on the dump/load call).
-
-.. _enveloping_1:
-
-Example: Enveloping
--------------------
-
-One common use case is to wrap data in a namespace upon serialization and 
unwrap the data during deserialization.
-
-.. code-block:: python
-
-    from marshmallow import Schema, fields, pre_load, post_load, post_dump
-
-
-    class BaseSchema(Schema):
-        # Custom options
-        __envelope__ = {"single": None, "many": None}
-        __model__ = User
-
-        def get_envelope_key(self, many):
-            """Helper to get the envelope key."""
-            key = self.__envelope__["many"] if many else 
self.__envelope__["single"]
-            assert key is not None, "Envelope key undefined"
-            return key
-
-        @pre_load(pass_many=True)
-        def unwrap_envelope(self, data, many, **kwargs):
-            key = self.get_envelope_key(many)
-            return data[key]
-
-        @post_dump(pass_many=True)
-        def wrap_with_envelope(self, data, many, **kwargs):
-            key = self.get_envelope_key(many)
-            return {key: data}
-
-        @post_load
-        def make_object(self, data, **kwargs):
-            return self.__model__(**data)
-
-
-    class UserSchema(BaseSchema):
-        __envelope__ = {"single": "user", "many": "users"}
-        __model__ = User
-        name = fields.Str()
-        email = fields.Email()
-
-
-    user_schema = UserSchema()
-
-    user = User("Mick", email="[email protected]")
-    user_data = user_schema.dump(user)
-    # {'user': {'email': '[email protected]', 'name': 'Mick'}}
-
-    users = [
-        User("Keith", email="[email protected]"),
-        User("Charlie", email="[email protected]"),
-    ]
-    users_data = user_schema.dump(users, many=True)
-    # {'users': [{'email': '[email protected]', 'name': 'Keith'},
-    #            {'email': '[email protected]', 'name': 'Charlie'}]}
-
-    user_objs = user_schema.load(users_data, many=True)
-    # [<User(name='Keith Richards')>, <User(name='Charlie Watts')>]
-
-Raising errors in pre-/post-processor methods
----------------------------------------------
-
-Pre- and post-processing methods may raise a `ValidationError 
<marshmallow.exceptions.ValidationError>`. By default, errors will be stored on 
the ``"_schema"`` key in the errors dictionary.
-
-.. code-block:: python
-
-    from marshmallow import Schema, fields, ValidationError, pre_load
-
-
-    class BandSchema(Schema):
-        name = fields.Str()
-
-        @pre_load
-        def unwrap_envelope(self, data, **kwargs):
-            if "data" not in data:
-                raise ValidationError('Input data must have a "data" key.')
-            return data["data"]
-
-
-    sch = BandSchema()
-    try:
-        sch.load({"name": "The Band"})
-    except ValidationError as err:
-        err.messages
-    # {'_schema': ['Input data must have a "data" key.']}
-
-If you want to store and error on a different key, pass the key name as the 
second argument to `ValidationError <marshmallow.exceptions.ValidationError>`.
-
-.. code-block:: python
-
-    from marshmallow import Schema, fields, ValidationError, pre_load
-
-
-    class BandSchema(Schema):
-        name = fields.Str()
-
-        @pre_load
-        def unwrap_envelope(self, data, **kwargs):
-            if "data" not in data:
-                raise ValidationError(
-                    'Input data must have a "data" key.', "_preprocessing"
-                )
-            return data["data"]
-
-
-    sch = BandSchema()
-    try:
-        sch.load({"name": "The Band"})
-    except ValidationError as err:
-        err.messages
-    # {'_preprocessing': ['Input data must have a "data" key.']}
-
-Pre-/post-processor invocation order
-------------------------------------
-
-In summary, the processing pipeline for deserialization is as follows:
-
-1. ``@pre_load(pass_many=True)`` methods
-2. ``@pre_load(pass_many=False)`` methods
-3. ``load(in_data, many)`` (validation and deserialization)
-4. ``@validates`` methods (field validators)
-5. ``@validates_schema`` methods (schema validators)
-6. ``@post_load(pass_many=True)`` methods
-7. ``@post_load(pass_many=False)`` methods
-
-The pipeline for serialization is similar, except that the ``pass_many=True`` 
processors are invoked *after* the ``pass_many=False`` processors and there are 
no validators.
-
-1. ``@pre_dump(pass_many=False)`` methods
-2. ``@pre_dump(pass_many=True)`` methods
-3. ``dump(obj, many)`` (serialization)
-4. ``@post_dump(pass_many=False)`` methods
-5. ``@post_dump(pass_many=True)`` methods
-
-
-.. warning::
-
-    You may register multiple processor methods on a Schema. Keep in mind, 
however, that **the invocation order of decorated methods of the same type is 
not guaranteed**. If you need to guarantee order of processing steps, you 
should put them in the same method.
-
-
-    .. code-block:: python
-
-        from marshmallow import Schema, fields, pre_load
-
-
-        # YES
-        class MySchema(Schema):
-            field_a = fields.Raw()
-
-            @pre_load
-            def preprocess(self, data, **kwargs):
-                step1_data = self.step1(data)
-                step2_data = self.step2(step1_data)
-                return step2_data
-
-            def step1(self, data): ...
-
-            # Depends on step1
-            def step2(self, data): ...
-
-
-        # NO
-        class MySchema(Schema):
-            field_a = fields.Raw()
-
-            @pre_load
-            def step1(self, data, **kwargs): ...
-
-            # Depends on step1
-            @pre_load
-            def step2(self, data, **kwargs): ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/docs/nesting.rst 
new/marshmallow-4.3.0/docs/nesting.rst
--- old/marshmallow-4.2.3/docs/nesting.rst      2026-03-26 00:26:40.590085000 
+0100
+++ new/marshmallow-4.3.0/docs/nesting.rst      2026-04-03 23:45:50.051643000 
+0200
@@ -274,8 +274,8 @@
         name = fields.String()
         email = fields.Email()
         # Use the 'exclude' argument to avoid infinite recursion
-        employer = fields.Nested(lambda: UserSchema(exclude=("employer",)))
-        friends = fields.List(fields.Nested(lambda: UserSchema()))
+        employer = fields.Nested(lambda: UserSchema(exclude=("employer", 
"friends")))
+        friends = fields.List(fields.Nested(lambda: UserSchema("employer", 
"friends")))
 
 
     user = User("Steve", "[email protected]")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/docs/quickstart.rst 
new/marshmallow-4.3.0/docs/quickstart.rst
--- old/marshmallow-4.2.3/docs/quickstart.rst   2026-03-26 00:26:40.590085000 
+0100
+++ new/marshmallow-4.3.0/docs/quickstart.rst   2026-04-03 23:45:50.051643000 
+0200
@@ -286,6 +286,12 @@
 
     If you need to validate multiple fields within a single validator, see 
:ref:`schema_validation`.
 
+.. seealso::
+
+    Need to *transform* a field's value?
+    Use the ``pre_load`` and ``post_load`` field parameters.
+    See :ref:`field_level_processing`.
+
 
 Field validators as methods
 +++++++++++++++++++++++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/docs/upgrading.rst 
new/marshmallow-4.3.0/docs/upgrading.rst
--- old/marshmallow-4.2.3/docs/upgrading.rst    2026-03-26 00:26:40.590085000 
+0100
+++ new/marshmallow-4.3.0/docs/upgrading.rst    2026-04-03 23:45:50.051643000 
+0200
@@ -1750,7 +1750,7 @@
             data["field_a"] -= 1
             return data
 
-See the :doc:`extending/pre_and_post_processing_methods` page for more 
information on the ``pre_*`` and ``post_*`` decorators.
+See the :doc:`extending/pre_and_post_processing` page for more information on 
the ``pre_*`` and ``post_*`` decorators.
 
 Schema validators
 *****************
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/pyproject.toml 
new/marshmallow-4.3.0/pyproject.toml
--- old/marshmallow-4.2.3/pyproject.toml        2026-03-26 00:26:40.591085200 
+0100
+++ new/marshmallow-4.3.0/pyproject.toml        2026-04-03 23:45:50.052954000 
+0200
@@ -1,6 +1,6 @@
 [project]
 name = "marshmallow"
-version = "4.2.3"
+version = "4.3.0"
 description = "A lightweight library for converting complex datatypes to and 
from native Python datatypes."
 readme = "README.rst"
 license = "MIT"
@@ -33,17 +33,20 @@
 Source = "https://github.com/marshmallow-code/marshmallow";
 Tidelift = 
"https://tidelift.com/subscription/pkg/pypi-marshmallow?utm_source=pypi-marshmallow&utm_medium=pypi";
 
-[project.optional-dependencies]
+[dependency-groups]
 docs = [
-  "autodocsumm==0.2.14",
-  "furo==2025.12.19",
-  "sphinx-copybutton==0.5.2",
-  "sphinx-issues==6.0.0",
-  "sphinx==8.2.3",
-  "sphinxext-opengraph==0.13.0",
+  "autodocsumm",
+  "furo",
+  "sphinx-copybutton",
+  "sphinx-issues",
+  "sphinx>=8.1",
+  "sphinxext-opengraph",
 ]
 tests = ["pytest", "simplejson"]
-dev = ["marshmallow[tests]", "tox", "pre-commit>=3.5,<5.0"]
+dev = [{ include-group = "tests" }, "tox", "tox-uv", "pre-commit>=3.5,<5.0"]
+
+[tool.uv]
+default-groups = ["dev"]
 
 [build-system]
 requires = ["flit_core>=3.12,<4"]
@@ -125,7 +128,7 @@
 [tool.mypy]
 files = ["src", "tests", "examples"]
 ignore_missing_imports = true
-warn_unreachable = false
+warn_unreachable = true
 warn_unused_ignores = true
 warn_redundant_casts = true
 no_implicit_optional = true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/src/marshmallow/fields.py 
new/marshmallow-4.3.0/src/marshmallow/fields.py
--- old/marshmallow-4.2.3/src/marshmallow/fields.py     2026-03-26 
00:26:40.591728200 +0100
+++ new/marshmallow-4.3.0/src/marshmallow/fields.py     2026-04-03 
23:45:50.053634000 +0200
@@ -82,6 +82,10 @@
 ]
 
 _InternalT = typing.TypeVar("_InternalT")
+_ProcessorT = typing.TypeVar(
+    "_ProcessorT",
+    bound=types.PostLoadCallable | types.PreLoadCallable | types.Validator,
+)
 
 
 class _BaseFieldKwargs(typing.TypedDict, total=False):
@@ -90,6 +94,8 @@
     data_key: str | None
     attribute: str | None
     validate: types.Validator | typing.Iterable[types.Validator] | None
+    pre_load: types.PreLoadCallable | typing.Iterable[types.PreLoadCallable] | 
None
+    post_load: types.PostLoadCallable | 
typing.Iterable[types.PostLoadCallable] | None
     required: bool
     allow_none: bool | None
     load_only: bool
@@ -135,6 +141,12 @@
         during deserialization. Validator takes a field's input value as
         its only parameter and returns a boolean.
         If it returns `False`, an :exc:`ValidationError` is raised.
+    :param pre_load: Callable or collection of callables that are applied to 
the
+        raw input value before deserialization. Each callable receives the 
value
+        and returns a transformed value.
+    :param post_load: Callable or collection of callables that are applied to 
the
+        deserialized value after validation. Each callable receives the value
+        and returns a transformed value.
     :param required: Raise a :exc:`ValidationError` if the field value
         is not supplied during deserialization.
     :param allow_none: Set this to `True` if `None` should be considered a 
valid value during
@@ -159,6 +171,8 @@
         Use `Raw <marshmallow.fields.Raw>` or another `Field 
<marshmallow.fields.Field>` subclass instead.
     .. versionchanged:: 4.0.0
         Remove ``context`` property.
+    .. versionchanged:: 4.3.0
+        Add ``pre_load`` and ``post_load``.
     """
 
     # Some fields, such as Method fields and Function fields, are not expected
@@ -183,6 +197,12 @@
         data_key: str | None = None,
         attribute: str | None = None,
         validate: types.Validator | typing.Iterable[types.Validator] | None = 
None,
+        pre_load: (
+            types.PreLoadCallable | typing.Iterable[types.PreLoadCallable] | 
None
+        ) = None,
+        post_load: (
+            types.PostLoadCallable | typing.Iterable[types.PostLoadCallable] | 
None
+        ) = None,
         required: bool = False,
         allow_none: bool | None = None,
         load_only: bool = False,
@@ -196,17 +216,9 @@
         self.attribute = attribute
         self.data_key = data_key
         self.validate = validate
-        if validate is None:
-            self.validators = []
-        elif callable(validate):
-            self.validators = [validate]
-        elif utils.is_iterable_but_not_string(validate):
-            self.validators = list(validate)
-        else:
-            raise ValueError(
-                "The 'validate' parameter must be a callable "
-                "or a collection of callables."
-            )
+        self.validators = self._normalize_processors(validate, 
param="validate")
+        self.pre_load = self._normalize_processors(pre_load, param="pre_load")
+        self.post_load = self._normalize_processors(post_load, 
param="post_load")
 
         # If allow_none is None and load_default is None
         # None should be considered valid by default
@@ -369,10 +381,21 @@
         if value is missing_:
             _miss = self.load_default
             return _miss() if callable(_miss) else _miss
+
+        # Apply pre_load functions
+        for func in self.pre_load:
+            value = func(value)
+
         if self.allow_none and value is None:
             return None
+
         output = self._deserialize(value, attr, data, **kwargs)
+        # Apply validators
         self._validate(output)
+
+        # Apply post_load functions
+        for func in self.post_load:
+            output = func(output)
         return output
 
     # Methods for concrete classes to override.
@@ -433,6 +456,22 @@
         """
         return value
 
+    @staticmethod
+    def _normalize_processors(
+        processors: _ProcessorT | typing.Iterable[_ProcessorT] | None,
+        *,
+        param: str,
+    ) -> list[_ProcessorT]:
+        if processors is None:
+            return []
+        if callable(processors):
+            return [processors]
+        if utils.is_iterable_but_not_string(processors):
+            return list(processors)
+        raise ValueError(
+            f"The '{param}' parameter must be a callable or an iterable of 
callables."
+        )
+
 
 class Raw(Field[typing.Any]):
     """Field that applies no formatting."""
@@ -500,7 +539,7 @@
             | SchemaMeta
             | str
             | dict[str, Field]
-            | typing.Callable[[], Schema | SchemaMeta | dict[str, Field]]
+            | typing.Callable[[], Schema | SchemaMeta | str | dict[str, Field]]
         ),
         *,
         only: types.StrSequenceOrSet | None = None,
@@ -528,12 +567,12 @@
     def schema(self) -> Schema:
         """The nested Schema object."""
         if not self._schema:
-            if callable(self.nested) and not isinstance(self.nested, type):
-                nested = self.nested()
-            else:
-                nested = typing.cast("Schema", self.nested)
             # defer the import of `marshmallow.schema` to avoid circular 
imports
-            from marshmallow.schema import Schema  # noqa: PLC0415
+            from marshmallow.schema import Schema, SchemaMeta  # noqa: PLC0415
+
+            nested = self.nested
+            if callable(nested) and not isinstance(nested, SchemaMeta):
+                nested = nested()
 
             if isinstance(nested, dict):
                 nested = Schema.from_dict(nested)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/src/marshmallow/types.py 
new/marshmallow-4.3.0/src/marshmallow/types.py
--- old/marshmallow-4.2.3/src/marshmallow/types.py      2026-03-26 
00:26:40.592085100 +0100
+++ new/marshmallow-4.3.0/src/marshmallow/types.py      2026-04-03 
23:45:50.053634000 +0200
@@ -9,6 +9,8 @@
 
 import typing
 
+T = typing.TypeVar("T")
+
 #: A type that can be either a sequence of strings or a set of strings
 StrSequenceOrSet: typing.TypeAlias = typing.Sequence[str] | 
typing.AbstractSet[str]
 
@@ -24,6 +26,11 @@
 #: A valid option for the ``unknown`` schema option and argument
 UnknownOption: typing.TypeAlias = typing.Literal["exclude", "include", "raise"]
 
+#: Type for field-level pre-load functions
+PreLoadCallable = typing.Callable[[typing.Any], typing.Any]
+#: Type for field-level post-load functions
+PostLoadCallable = typing.Callable[[T], T]
+
 
 class SchemaValidator(typing.Protocol):
     def __call__(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/src/marshmallow/validate.py 
new/marshmallow-4.3.0/src/marshmallow/validate.py
--- old/marshmallow-4.2.3/src/marshmallow/validate.py   2026-03-26 
00:26:40.592085100 +0100
+++ new/marshmallow-4.3.0/src/marshmallow/validate.py   2026-04-03 
23:45:50.053634000 +0200
@@ -13,6 +13,7 @@
     from marshmallow import types
 
 _T = typing.TypeVar("_T")
+_UNICODE_LETTERS = "\u00a1-\uffff"
 
 
 class Validator(ABC):
@@ -98,17 +99,28 @@
 
     class RegexMemoizer:
         def __init__(self):
-            self._memoized = {}
+            self._memoized: dict[tuple[bool, bool, bool], re.Pattern[str]] = {}
 
         def _regex_generator(
             self, *, relative: bool, absolute: bool, require_tld: bool
-        ) -> typing.Pattern:
+        ) -> re.Pattern[str]:
             hostname_variants = [
-                # a normal domain name, expressed in [A-Z0-9] chars with 
hyphens allowed only in the middle
+                # a normal domain name, expressed in [A-Z0-9] chars (plus 
unicode letters)
+                # with hyphens allowed only in the middle
                 # note that the regex will be compiled with IGNORECASE, so 
these are upper and lowercase chars
                 (
-                    r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+"
-                    r"(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)"
+                    r"(?:[A-Z0-9"
+                    + _UNICODE_LETTERS
+                    + r"](?:[A-Z0-9"
+                    + _UNICODE_LETTERS
+                    + r"-]{0,61}[A-Z0-9"
+                    + _UNICODE_LETTERS
+                    + r"])?\.)+"
+                    r"(?:[A-Z"
+                    + _UNICODE_LETTERS
+                    + r"]{2,6}\.?|[A-Z0-9"
+                    + _UNICODE_LETTERS
+                    + r"-]{2,}\.?)"
                 ),
                 # or the special string 'localhost'
                 r"localhost",
@@ -119,7 +131,15 @@
             ]
             if not require_tld:
                 # allow dotless hostnames
-                
hostname_variants.append(r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.?)")
+                hostname_variants.append(
+                    r"(?:[A-Z0-9"
+                    + _UNICODE_LETTERS
+                    + r"](?:[A-Z0-9"
+                    + _UNICODE_LETTERS
+                    + r"-]{0,61}[A-Z0-9"
+                    + _UNICODE_LETTERS
+                    + r"])?\.?)"
+                )
 
             absolute_part = "".join(
                 (
@@ -156,7 +176,7 @@
 
         def __call__(
             self, *, relative: bool, absolute: bool, require_tld: bool
-        ) -> typing.Pattern:
+        ) -> re.Pattern[str]:
             key = (relative, absolute, require_tld)
             if key not in self._memoized:
                 self._memoized[key] = self._regex_generator(
@@ -192,7 +212,7 @@
     def _repr_args(self) -> str:
         return f"relative={self.relative!r}, absolute={self.absolute!r}"
 
-    def _format_error(self, value) -> str:
+    def _format_error(self, value: str) -> str:
         return self.error.format(input=value)
 
     def __call__(self, value: str) -> str:
@@ -241,8 +261,11 @@
 
     DOMAIN_REGEX = re.compile(
         # domain
-        r"(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+"
-        r"(?:[A-Z]{2,6}|[A-Z0-9-]{2,})\Z"
+        r"(?:[A-Z0-9" + _UNICODE_LETTERS + r"]"
+        r"(?:[A-Z0-9" + _UNICODE_LETTERS + r"-]{0,61}"
+        r"[A-Z0-9" + _UNICODE_LETTERS + r"])?\.)+"
+        r"(?:[A-Z" + _UNICODE_LETTERS + r"]{2,6}"
+        r"|[A-Z0-9" + _UNICODE_LETTERS + r"-]{2,})\Z"
         # literal form, ipv4 address (SMTP 4.1.3)
         r"|^\[(25[0-5]|2[0-4]\d|[0-1]?\d?\d)"
         r"(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}\]\Z",
@@ -272,13 +295,6 @@
 
         if domain_part not in self.DOMAIN_WHITELIST:
             if not self.DOMAIN_REGEX.match(domain_part):
-                try:
-                    domain_part = domain_part.encode("idna").decode("ascii")
-                except UnicodeError:
-                    pass
-                else:
-                    if self.DOMAIN_REGEX.match(domain_part):
-                        return value
                 raise ValidationError(message)
 
         return value
@@ -314,8 +330,8 @@
 
     def __init__(
         self,
-        min=None,  # noqa: A002
-        max=None,  # noqa: A002
+        min: typing.Any = None,  # noqa: A002
+        max: typing.Any = None,  # noqa: A002
         *,
         min_inclusive: bool = True,
         max_inclusive: bool = True,
@@ -476,7 +492,7 @@
 
     def __init__(
         self,
-        regex: str | bytes | typing.Pattern,
+        regex: str | bytes | re.Pattern[str] | re.Pattern[bytes],
         flags: int = 0,
         *,
         error: str | None = None,
@@ -557,7 +573,7 @@
     def _repr_args(self) -> str:
         return f"iterable={self.iterable!r}"
 
-    def _format_error(self, value) -> str:
+    def _format_error(self, value: typing.Any) -> str:
         return self.error.format(input=value, values=self.values_text)
 
     def __call__(self, value: typing.Any) -> typing.Any:
@@ -597,7 +613,7 @@
     def _repr_args(self) -> str:
         return f"choices={self.choices!r}, labels={self.labels!r}"
 
-    def _format_error(self, value) -> str:
+    def _format_error(self, value: typing.Any) -> str:
         return self.error.format(
             input=value, choices=self.choices_text, labels=self.labels_text
         )
@@ -656,7 +672,7 @@
 
     default_message = "One or more of the choices you made was not in: 
{choices}."
 
-    def _format_error(self, value) -> str:
+    def _format_error(self, value: typing.Sequence[typing.Any]) -> str:
         value_text = ", ".join(str(val) for val in value)
         return super()._format_error(value_text)
 
@@ -681,7 +697,7 @@
 
     default_message = "One or more of the choices you made was in: {values}."
 
-    def _format_error(self, value) -> str:
+    def _format_error(self, value: typing.Sequence[typing.Any]) -> str:
         value_text = ", ".join(str(val) for val in value)
         return super()._format_error(value_text)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/tests/test_fields.py 
new/marshmallow-4.3.0/tests/test_fields.py
--- old/marshmallow-4.2.3/tests/test_fields.py  2026-03-26 00:26:40.593085000 
+0100
+++ new/marshmallow-4.3.0/tests/test_fields.py  2026-04-03 23:45:50.054662700 
+0200
@@ -631,3 +631,125 @@
                     "daughter": {"value": {"age": ["Missing data for required 
field."]}}
                 }
             }
+
+
+class TestFieldPreAndPostLoad:
+    def test_field_pre_load(self):
+        class UserSchema(Schema):
+            name = fields.Str(pre_load=str)
+
+        schema = UserSchema()
+        result = schema.load({"name": 808})
+        assert result["name"] == "808"
+
+    def test_field_pre_load_multiple(self):
+        def decrement(value):
+            return value - 1
+
+        def add_prefix(value):
+            return "test_" + value
+
+        class UserSchema(Schema):
+            name = fields.Str(pre_load=[decrement, str, add_prefix])
+
+        schema = UserSchema()
+        result = schema.load({"name": 809})
+        assert result["name"] == "test_808"
+
+    def test_field_post_load(self):
+        class UserSchema(Schema):
+            age = fields.Int(post_load=str)
+
+        schema = UserSchema()
+        result = schema.load({"age": 42})
+        assert result["age"] == "42"
+
+    def test_field_post_load_multiple(self):
+        def multiply_by_2(value):
+            return value * 2
+
+        def decrement(value):
+            return value - 1
+
+        class UserSchema(Schema):
+            age = fields.Float(post_load=[multiply_by_2, decrement])
+
+        schema = UserSchema()
+        result = schema.load({"age": 21.5})
+        assert result["age"] == 42.0
+
+    def test_field_pre_and_post_load(self):
+        def multiply_by_2(value):
+            return value * 2
+
+        class UserSchema(Schema):
+            age = fields.Int(pre_load=[str.strip, int], 
post_load=[multiply_by_2])
+
+        schema = UserSchema()
+        result = schema.load({"age": " 21 "})
+        assert result["age"] == 42
+
+    def test_field_pre_load_validation_error(self):
+        def always_fail(value):
+            raise ValidationError("oops")
+
+        class UserSchema(Schema):
+            age = fields.Int(pre_load=always_fail)
+
+        schema = UserSchema()
+        with pytest.raises(ValidationError) as exc:
+            schema.load({"age": 42})
+        assert exc.value.messages == {"age": ["oops"]}
+
+    def test_field_post_load_validation_error(self):
+        def always_fail(value):
+            raise ValidationError("oops")
+
+        class UserSchema(Schema):
+            age = fields.Int(post_load=always_fail)
+
+        schema = UserSchema()
+        with pytest.raises(ValidationError) as exc:
+            schema.load({"age": 42})
+        assert exc.value.messages == {"age": ["oops"]}
+
+    def test_field_pre_load_none(self):
+        def handle_none(value):
+            if value is None:
+                return 0
+            return value
+
+        class UserSchema(Schema):
+            age = fields.Int(pre_load=handle_none, allow_none=True)
+
+        schema = UserSchema()
+        result = schema.load({"age": None})
+        assert result["age"] == 0
+
+    def test_field_post_load_not_called_with_none_input_when_not_allowed(self):
+        def handle_none(value):
+            if value is None:
+                return 0
+            return value
+
+        class UserSchema(Schema):
+            age = fields.Int(post_load=handle_none, allow_none=False)
+
+        schema = UserSchema()
+        with pytest.raises(ValidationError) as exc:
+            schema.load({"age": None})
+        assert exc.value.messages == {"age": ["Field may not be null."]}
+
+    def test_invalid_type_passed_to_pre_load(self):
+        with pytest.raises(
+            ValueError,
+            match="The 'pre_load' parameter must be a callable or an iterable 
of callables.",
+        ):
+            fields.Int(pre_load="not_callable")  # type: ignore[arg-type]
+
+    def test_invalid_type_passed_to_post_load(self):
+        with pytest.raises(
+            ValueError,
+            match="The 'post_load' parameter must be a callable or an iterable 
of callables.",
+        ):
+            fields.Int(post_load="not_callable")  # type: ignore[arg-type]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/tests/test_validate.py 
new/marshmallow-4.3.0/tests/test_validate.py
--- old/marshmallow-4.2.3/tests/test_validate.py        2026-03-26 
00:26:40.593085000 +0100
+++ new/marshmallow-4.3.0/tests/test_validate.py        2026-04-03 
23:45:50.054662700 +0200
@@ -293,6 +293,35 @@
 
 
 @pytest.mark.parametrize(
+    "valid_url",
+    [
+        "https://তৌহিদুর.বাংলা";,
+        "https://münchen.de";,
+        "https://例え.jp/path";,
+        "http://مثال.إختبار";,
+        "https://üñîçödé.com/path?q=1#frag";,
+        "http://www.اختبار.com:8080/path";,
+    ],
+)
+def test_url_idn_valid(valid_url):
+    validator = validate.URL()
+    assert validator(valid_url) == valid_url
+
+
[email protected](
+    "invalid_url",
+    [
+        "münchen.de",
+        "তৌহিদুর.বাংলা",
+    ],
+)
+def test_url_idn_invalid(invalid_url):
+    validator = validate.URL()
+    with pytest.raises(ValidationError):
+        validator(invalid_url)
+
+
[email protected](
     "valid_email",
     [
         "[email protected]",
@@ -337,6 +366,37 @@
     validator = validate.Email()
     with pytest.raises(ValidationError):
         validator(invalid_email)
+
+
[email protected](
+    "valid_email",
+    [
+        "user@münchen.de",
+        "user@例え.jp",
+        "user@مثال.إختبار",
+        "user@пример.испытание",
+        "user@üñîçödé.com",
+        "δοκ.ιμή@παράδειγμα.δοκιμή",
+        "[email protected]ünchen.de",
+    ],
+)
+def test_email_idn_valid(valid_email):
+    validator = validate.Email()
+    assert validator(valid_email) == valid_email
+
+
[email protected](
+    "invalid_email",
+    [
+        "user@-münchen.de",
+        "user@münchen-.de",
+        "user@münchen",
+    ],
+)
+def test_email_idn_invalid(invalid_email):
+    validator = validate.Email()
+    with pytest.raises(ValidationError):
+        validator(invalid_email)
 
 
 def test_email_custom_message():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/marshmallow-4.2.3/tox.ini 
new/marshmallow-4.3.0/tox.ini
--- old/marshmallow-4.2.3/tox.ini       2026-03-26 00:26:40.593085000 +0100
+++ new/marshmallow-4.3.0/tox.ini       2026-04-03 23:45:50.054662700 +0200
@@ -2,7 +2,7 @@
 envlist = lint,mypy,py{310,311,312,313,314},docs
 
 [testenv]
-extras = tests
+dependency_groups = tests
 commands = pytest {posargs}
 
 [testenv:lint]
@@ -17,14 +17,14 @@
 commands = mypy --show-error-codes
 
 [testenv:docs]
-extras = docs
+dependency_groups = docs
 commands = sphinx-build --fresh-env docs/ docs/_build {posargs}
 
 ; Below tasks are for development only (not run in CI)
 
 [testenv:docs-serve]
 deps = sphinx-autobuild
-extras = docs
+dependency_groups = docs
 commands = sphinx-autobuild --port=0 --open-browser --delay=2 docs/ 
docs/_build {posargs} --watch src --watch CONTRIBUTING.rst --watch README.rst
 
 [testenv:readme-serve]

Reply via email to