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]
