Repository: marshmallow-code/marshmallow
Pull request #2226 (2024-01-17 17:23:59)

Dev chores 
- Migrate to pyproject.toml
  - Reference: https://packaging.python.org/en/latest/guides/modernize-setup-py-project/
  - Mostly borrows from https://github.com/pallets/flask/blob/main/pyproject.toml
- Migrate to ruff
  - Because it's faster and consolidates tooling we already use 
- Update release workflow based on https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/

I've already done these in a few of the other marshmallow-code projects and my personal repos without issue so far.

see

https://github.com/marshmallow-code/flask-marshmallow/pull/285
https://github.com/marshmallow-code/flask-marshmallow/pull/287

sloria: @lafrech tagged you for review since there's some tooling changes here. no need to review if it's not interesting to you; just wanted you to be aware before i do the same for the other repos (marshmallow-sqlalchemy, webargs, apispec, etc)

Patch:
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index d01d7dba..5d6d10c0 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -27,7 +27,7 @@ jobs:
           python-version: "3.11"
       - run: python -m pip install --upgrade pip wheel
       - run: pip install tox
-      - run: tox -edocs
+      - run: tox -e docs
   tests:
     name: ${{ matrix.name }}
     runs-on: ubuntu-latest
@@ -46,10 +46,26 @@ jobs:
       - run: python -m pip install --upgrade pip
       - run: python -m pip install tox
       - run: python -m tox -e ${{ matrix.tox }}
+  build:
+    if: startsWith(github.ref, 'refs/tags')
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v4
+        with:
+          python-version: "3.11"
+      - name: Install pypa/build
+        run: python3 -m pip install build --user
+      - name: Build a binary wheel and a source tarball
+        run: python3 -m build
+      - name: Store the distribution packages
+        uses: actions/upload-artifact@v3
+        with:
+          name: python-package-distributions
+          path: dist/
   # this duplicates pre-commit.ci, so only run it on tags
   # it guarantees that linting is passing prior to a release
   lint-pre-release:
-    name: lint
     if: startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-latest
     steps:
@@ -60,19 +76,21 @@ jobs:
       - run: python -m pip install --upgrade pip
       - run: python -m pip install tox
       - run: python -m tox -e lint
-  release:
-    needs: [mypy, tests, lint-pre-release]
+  publish-to-pypi:
     name: PyPI release
-    if: startsWith(github.ref, 'refs/tags')
+    if: startsWith(github.ref, 'refs/tags/')
+    needs: [mypy, build, tests, lint-pre-release]
     runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/marshmallow
+    permissions:
+      id-token: write
     steps:
-      - uses: actions/checkout@v4.0.0
-      - uses: actions/setup-python@v4.7.0
-      - name: install requirements
-        run: python -m pip install build twine
-      - name: build dists
-        run: python -m build
-      - name: check package metadata
-        run: twine check dist/*
-      - name: publish
-        run: twine upload -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} dist/*
+      - name: Download all the dists
+        uses: actions/download-artifact@v3
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Publish distribution to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
diff --git a/.gitignore b/.gitignore
index 3d06386a..ed08d834 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,3 +79,6 @@ venv
 .mypy_cache/
 .dmypy.json
 dmypy.json
+
+# ruff
+.ruff_cache
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e05b665c..bb4e9b6a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,19 +1,19 @@
 repos:
-- repo: https://github.com/asottile/pyupgrade
-  rev: v3.15.0
+- repo: https://github.com/astral-sh/ruff-pre-commit
+  rev: v0.1.13
   hooks:
-  - id: pyupgrade
-    args: [--py38-plus]
-- repo: https://github.com/psf/black
-  rev: 23.12.1
+    - id: ruff
+    - id: ruff-format
+- repo: https://github.com/python-jsonschema/check-jsonschema
+  rev: 0.27.3
   hooks:
-  - id: black
-    language_version: python3
-- repo: https://github.com/pycqa/flake8
-  rev: 7.0.0
+    - id: check-github-workflows
+    - id: check-readthedocs
+- repo: https://github.com/asottile/blacken-docs
+  rev: 1.16.0
   hooks:
-  - id: flake8
-    additional_dependencies: [flake8-bugbear==23.12.2]
+  - id: blacken-docs
+    additional_dependencies: [black==23.12.1]
 - repo: https://github.com/pre-commit/mirrors-mypy
   rev: v1.8.0
   hooks:
@@ -21,8 +21,3 @@ repos:
     additional_dependencies: [types-simplejson, types-pytz, packaging]
     # these files are checked under `tox -e mypy-test`
     exclude: ^tests/mypy_test_cases/.*$
-- repo: https://github.com/asottile/blacken-docs
-  rev: 1.16.0
-  hooks:
-  - id: blacken-docs
-    additional_dependencies: [black==23.12.1]
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index fb77fdd0..00000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,13 +0,0 @@
-include LICENSE
-include *.rst NOTICE
-include src/marshmallow/py.typed
-recursive-include tests *
-recursive-include examples *
-recursive-include docs *
-recursive-exclude docs *.pyc
-recursive-exclude docs *.pyo
-recursive-exclude tests *.pyc
-recursive-exclude tests *.pyo
-recursive-exclude examples *.pyc
-recursive-exclude examples *.pyo
-prune docs/_build
diff --git a/README.rst b/README.rst
index 22601651..fdd36c01 100644
--- a/README.rst
+++ b/README.rst
@@ -17,11 +17,6 @@ marshmallow: simplified object serialization
 .. image:: https://readthedocs.org/projects/marshmallow/badge/
    :target: https://marshmallow.readthedocs.io/
    :alt: Documentation
-   
-.. image:: https://badgen.net/badge/code%20style/black/000
-    :target: https://github.com/ambv/black
-    :alt: code style: black
-
 
 **marshmallow** is an ORM/ODM/framework-agnostic library for converting complex datatypes, such as objects, to and from native Python datatypes.
 
diff --git a/RELEASING.md b/RELEASING.md
index 7659fc07..b2465ca6 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -1,8 +1,8 @@
 # Releasing
 
-1. Bump version in `src/marshmallow/__init__.py` and update the changelog
+1. Bump version in `pyproject.toml` and update the changelog
    with today's date.
-2. Commit:  `git commit -m "Bump version and update changelog"`
+2. Commit: `git commit -m "Bump version and update changelog"`
 3. Tag the commit: `git tag x.y.z`
 4. Push: `git push --tags origin dev`. CI will take care of the
    PyPI release.
diff --git a/docs/conf.py b/docs/conf.py
index 4a79de31..538fcc42 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,13 +1,10 @@
-import sys
+import datetime as dt
+import importlib.metadata
 import os
 import time
-import datetime as dt
 
 import alabaster
 
-sys.path.insert(0, os.path.abspath(os.path.join("..", "src")))
-import marshmallow  # noqa: E402
-
 extensions = [
     "sphinx.ext.autodoc",
     "sphinx.ext.intersphinx",
@@ -42,7 +39,7 @@ copyright = (
     )
 )
 
-version = release = marshmallow.__version__
+version = release = importlib.metadata.version("marshmallow")
 
 exclude_patterns = ["_build"]
 
@@ -57,7 +54,6 @@ html_show_sourcelink = False
 
 html_theme_options = {
     "logo": "marshmallow-logo.png",
-    "logo_name": None,
     "description": "Object serialization and deserialization, lightweight and fluffy.",
     "description_font_style": "italic",
     "github_user": "marshmallow-code",
diff --git a/examples/flask_example.py b/examples/flask_example.py
index bb1cf635..0d8b9e8d 100644
--- a/examples/flask_example.py
+++ b/examples/flask_example.py
@@ -3,7 +3,8 @@ import datetime
 from flask import Flask, request
 from flask_sqlalchemy import SQLAlchemy
 from sqlalchemy.exc import NoResultFound
-from marshmallow import Schema, fields, ValidationError, pre_load
+
+from marshmallow import Schema, ValidationError, fields, pre_load
 
 app = Flask(__name__)
 app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/quotes.db"
diff --git a/examples/package_json_example.py b/examples/package_json_example.py
index 2593b473..03a3d3b7 100644
--- a/examples/package_json_example.py
+++ b/examples/package_json_example.py
@@ -1,9 +1,10 @@
-import sys
 import json
-from packaging import version
+import sys
 from pprint import pprint
 
-from marshmallow import Schema, fields, INCLUDE, ValidationError
+from packaging import version
+
+from marshmallow import INCLUDE, Schema, ValidationError, fields
 
 
 class Version(fields.Field):
diff --git a/examples/peewee_example.py b/examples/peewee_example.py
index 7e2bc55e..eda1647c 100644
--- a/examples/peewee_example.py
+++ b/examples/peewee_example.py
@@ -1,16 +1,17 @@
 import datetime as dt
 from functools import wraps
 
-from flask import Flask, request, g, jsonify
 import peewee as pw
+from flask import Flask, g, jsonify, request
+
 from marshmallow import (
     Schema,
+    ValidationError,
     fields,
-    validate,
-    pre_load,
     post_dump,
     post_load,
-    ValidationError,
+    pre_load,
+    validate,
 )
 
 app = Flask(__name__)
diff --git a/examples/textblob_example.py b/examples/textblob_example.py
index 54d36282..b5da2c23 100644
--- a/examples/textblob_example.py
+++ b/examples/textblob_example.py
@@ -1,5 +1,6 @@
-from bottle import route, request, run
+from bottle import request, route, run
 from textblob import TextBlob
+
 from marshmallow import Schema, fields
 
 
diff --git a/performance/benchmark.py b/performance/benchmark.py
index 9a30a1e2..8e669af3 100644
--- a/performance/benchmark.py
+++ b/performance/benchmark.py
@@ -4,11 +4,11 @@ Uses the `timeit` module to benchmark serializing an object through Marshmallow.
 """
 import argparse
 import cProfile
+import datetime
 import gc
 import timeit
-import datetime
 
-from marshmallow import Schema, fields, ValidationError, post_dump
+from marshmallow import Schema, ValidationError, fields, post_dump
 
 
 # Custom validator
diff --git a/pyproject.toml b/pyproject.toml
index 4c6215c0..dae434f3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,90 @@
-[tool.black]
-line-length = 88
-target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
+[project]
+name = "marshmallow"
+version = "3.20.2"
+description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
+readme = "README.rst"
+license = { file = "LICENSE" }
+authors = [{ name = "Steven Loria", email = "sloria1@gmail.com" }]
+maintainers = [
+  { name = "Steven Loria", email = "sloria1@gmail.com" },
+  { name = "Jérôme Lafréchoux", email = "jerome@jolimont.fr" },
+  { name = "Jared Deckard", email = "jared@shademaps.com" },
+]
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Intended Audience :: Developers",
+  "License :: OSI Approved :: MIT License",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3.8",
+  "Programming Language :: Python :: 3.9",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Programming Language :: Python :: 3.12",
+]
+requires-python = ">=3.8"
+dependencies = ["packaging>=17.0"]
+
+[project.urls]
+Changelog = "https://marshmallow.readthedocs.io/en/latest/changelog.html"
+Funding = "https://opencollective.com/marshmallow"
+Issues = "https://github.com/marshmallow-code/marshmallow/issues"
+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]
+docs = [
+  "sphinx==7.2.6",
+  "sphinx-issues==3.0.1",
+  "alabaster==0.7.16",
+  "sphinx-version-warning==1.1.2",
+  "autodocsumm==0.2.12",
+]
+tests = ["pytest", "pytz", "simplejson"]
+dev = ["marshmallow[tests]", "tox", "pre-commit~=3.5"]
+
+[build-system]
+requires = ["flit_core<4"]
+build-backend = "flit_core.buildapi"
+
+[tool.flit.sdist]
+include = [
+  "docs/",
+  "tests/",
+  "CHANGELOG.rst",
+  "CONTRIBUTING.rst",
+  "SECURITY.md",
+  "NOTICE",
+  "tox.ini",
+]
+exclude = ["docs/_build/"]
+
+[tool.ruff]
+src = ["src"]
+fix = true
+show-fixes = true
+show-source = true
+
+[tool.ruff.lint]
+ignore = ["E203", "E266", "E501", "E731"]
+select = [
+  "B",  # flake8-bugbear
+  "E",  # pycodestyle error
+  "F",  # pyflakes
+  "I",  # isort
+  "UP", # pyupgrade
+  "W",  # pycodestyle warning
+]
+
+[tool.ruff.per-file-ignores]
+"tests/*" = ["E721"]
+
+[tool.mypy]
+ignore_missing_imports = true
+warn_unreachable = true
+warn_unused_ignores = true
+warn_redundant_casts = true
+no_implicit_optional = true
+
+[tool.pytest.ini_options]
+norecursedirs = ".git .ropeproject .tox docs env venv tests/mypy_test_cases"
+addopts = "-v --tb=short"
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 72024443..00000000
--- a/setup.cfg
+++ /dev/null
@@ -1,18 +0,0 @@
-[metadata]
-license_files = LICENSE
-
-[flake8]
-max-line-length = 90
-max-complexity = 18
-extend-ignore = E203, E266, E501, E731, B903
-
-[tool:pytest]
-norecursedirs = .git .ropeproject .tox docs env venv tests/mypy_test_cases
-addopts = -v --tb=short
-
-[mypy]
-ignore_missing_imports = true
-warn_unreachable = true
-warn_unused_ignores = true
-warn_redundant_casts = true
-no_implicit_optional = true
diff --git a/setup.py b/setup.py
deleted file mode 100644
index e309bfb0..00000000
--- a/setup.py
+++ /dev/null
@@ -1,92 +0,0 @@
-import re
-from setuptools import setup, find_packages
-
-EXTRAS_REQUIRE = {
-    "tests": ["pytest", "pytz", "simplejson"],
-    "lint": [
-        "pre-commit~=3.5",
-    ],
-    "docs": [
-        "sphinx==7.2.6",
-        "sphinx-issues==3.0.1",
-        "alabaster==0.7.16",
-        "sphinx-version-warning==1.1.2",
-        "autodocsumm==0.2.12",
-    ],
-}
-EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
-
-
-def find_version(fname):
-    """Attempts to find the version number in the file names fname.
-    Raises RuntimeError if not found.
-    """
-    version = ""
-    with open(fname) as fp:
-        reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]')
-        for line in fp:
-            m = reg.match(line)
-            if m:
-                version = m.group(1)
-                break
-    if not version:
-        raise RuntimeError("Cannot find version information")
-    return version
-
-
-def read(fname):
-    with open(fname) as fp:
-        content = fp.read()
-    return content
-
-
-setup(
-    name="marshmallow",
-    version=find_version("src/marshmallow/__init__.py"),
-    description=(
-        "A lightweight library for converting complex "
-        "datatypes to and from native Python datatypes."
-    ),
-    long_description=read("README.rst"),
-    author="Steven Loria",
-    author_email="sloria1@gmail.com",
-    url="https://github.com/marshmallow-code/marshmallow",
-    packages=find_packages("src", exclude=("test*", "examples")),
-    package_dir={"": "src"},
-    package_data={"marshmallow": ["py.typed"]},
-    include_package_data=True,
-    install_requires=["packaging>=17.0"],
-    extras_require=EXTRAS_REQUIRE,
-    license="MIT",
-    zip_safe=False,
-    keywords=[
-        "serialization",
-        "rest",
-        "json",
-        "api",
-        "marshal",
-        "marshalling",
-        "deserialization",
-        "validation",
-        "schema",
-    ],
-    python_requires=">=3.8",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-    ],
-    test_suite="tests",
-    project_urls={
-        "Changelog": "https://marshmallow.readthedocs.io/en/latest/changelog.html",
-        "Issues": "https://github.com/marshmallow-code/marshmallow/issues",
-        "Funding": "https://opencollective.com/marshmallow",
-        "Tidelift": "https://tidelift.com/subscription/pkg/pypi-marshmallow?utm_source=pypi-marshmallow&utm_medium=pypi",  # noqa
-    },
-)
diff --git a/src/marshmallow/__init__.py b/src/marshmallow/__init__.py
index a8ef9d14..dd061ff4 100644
--- a/src/marshmallow/__init__.py
+++ b/src/marshmallow/__init__.py
@@ -1,5 +1,7 @@
 from __future__ import annotations
 
+import importlib.metadata
+
 from packaging.version import Version
 
 from marshmallow.decorators import (
@@ -16,7 +18,8 @@ from marshmallow.utils import EXCLUDE, INCLUDE, RAISE, missing, pprint
 
 from . import fields
 
-__version__ = "3.20.2"
+# TODO: Deprecate __version__?
+__version__ = importlib.metadata.version("marshmallow")
 __parsed_version__ = Version(__version__)
 __version_info__: tuple[int, int, int] | tuple[
     int, int, int, str, int
diff --git a/src/marshmallow/base.py b/src/marshmallow/base.py
index 97916ff9..0e927a0e 100644
--- a/src/marshmallow/base.py
+++ b/src/marshmallow/base.py
@@ -8,6 +8,7 @@ These are necessary to avoid circular imports between schema.py and fields.py.
     Users should not need to use this module directly.
 """
 from __future__ import annotations
+
 from abc import ABC, abstractmethod
 
 
diff --git a/src/marshmallow/class_registry.py b/src/marshmallow/class_registry.py
index 5097cfd8..f20f0565 100644
--- a/src/marshmallow/class_registry.py
+++ b/src/marshmallow/class_registry.py
@@ -10,6 +10,7 @@ class:`fields.Nested <marshmallow.fields.Nested>`.
 from __future__ import annotations
 
 import typing
+
 from marshmallow.exceptions import RegistryError
 
 if typing.TYPE_CHECKING:
@@ -76,16 +77,16 @@ def get_class(classname: str, all: bool = False) -> list[SchemaType] | SchemaTyp
         classes = _registry[classname]
     except KeyError as error:
         raise RegistryError(
-            "Class with name {!r} was not found. You may need "
-            "to import the class.".format(classname)
+            f"Class with name {classname!r} was not found. You may need "
+            "to import the class."
         ) from error
     if len(classes) > 1:
         if all:
             return _registry[classname]
         raise RegistryError(
-            "Multiple classes with name {!r} "
+            f"Multiple classes with name {classname!r} "
             "were found. Please use the full, "
-            "module-qualified path.".format(classname)
+            "module-qualified path."
         )
     else:
         return _registry[classname][0]
diff --git a/src/marshmallow/exceptions.py b/src/marshmallow/exceptions.py
index bee970b8..2d861681 100644
--- a/src/marshmallow/exceptions.py
+++ b/src/marshmallow/exceptions.py
@@ -3,7 +3,6 @@ from __future__ import annotations
 
 import typing
 
-
 # Key used for schema-level validation errors
 SCHEMA = "_schema"
 
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index ce2ffb95..f38cd888 100644
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -4,28 +4,30 @@ from __future__ import annotations
 import collections
 import copy
 import datetime as dt
-import numbers
-import uuid
-import ipaddress
 import decimal
+import ipaddress
 import math
+import numbers
 import typing
+import uuid
 import warnings
-from enum import Enum as EnumType
 from collections.abc import Mapping as _Mapping
+from enum import Enum as EnumType
 
-from marshmallow import validate, utils, class_registry, types
+from marshmallow import class_registry, types, utils, validate
 from marshmallow.base import FieldABC, SchemaABC
+from marshmallow.exceptions import (
+    FieldInstanceResolutionError,
+    StringNotCollectionError,
+    ValidationError,
+)
 from marshmallow.utils import (
+    is_aware,
     is_collection,
-    missing as missing_,
     resolve_field_instance,
-    is_aware,
 )
-from marshmallow.exceptions import (
-    ValidationError,
-    StringNotCollectionError,
-    FieldInstanceResolutionError,
+from marshmallow.utils import (
+    missing as missing_,
 )
 from marshmallow.validate import And, Length
 from marshmallow.warnings import RemovedInMarshmallow4Warning
@@ -237,14 +239,12 @@ class Field(FieldABC):
 
     def __repr__(self) -> str:
         return (
-            "<fields.{ClassName}(dump_default={self.dump_default!r}, "
-            "attribute={self.attribute!r}, "
-            "validate={self.validate}, required={self.required}, "
-            "load_only={self.load_only}, dump_only={self.dump_only}, "
-            "load_default={self.load_default}, allow_none={self.allow_none}, "
-            "error_messages={self.error_messages})>".format(
-                ClassName=self.__class__.__name__, self=self
-            )
+            f"<fields.{self.__class__.__name__}(dump_default={self.dump_default!r}, "
+            f"attribute={self.attribute!r}, "
+            f"validate={self.validate}, required={self.required}, "
+            f"load_only={self.load_only}, dump_only={self.dump_only}, "
+            f"load_default={self.load_default}, allow_none={self.allow_none}, "
+            f"error_messages={self.error_messages})>"
         )
 
     def __deepcopy__(self, memo):
@@ -281,9 +281,9 @@ class Field(FieldABC):
         except KeyError as error:
             class_name = self.__class__.__name__
             message = (
-                "ValidationError raised by `{class_name}`, but error key `{key}` does "
+                f"ValidationError raised by `{class_name}`, but error key `{key}` does "
                 "not exist in the `error_messages` dictionary."
-            ).format(class_name=class_name, key=key)
+            )
             raise AssertionError(message) from error
         if isinstance(msg, (str, bytes)):
             msg = msg.format(**kwargs)
@@ -297,9 +297,7 @@ class Field(FieldABC):
             Use `make_error <marshmallow.fields.Field.make_error>` instead.
         """
         warnings.warn(
-            '`Field.fail` is deprecated. Use `raise self.make_error("{}", ...)` instead.'.format(
-                key
-            ),
+            f'`Field.fail` is deprecated. Use `raise self.make_error("{key}", ...)` instead.',
             RemovedInMarshmallow4Warning,
             stacklevel=2,
         )
@@ -611,7 +609,7 @@ class Nested(Field):
                 elif not isinstance(nested, (str, bytes)):
                     raise ValueError(
                         "`Nested` fields must be passed a "
-                        "`Schema`, not {}.".format(nested.__class__)
+                        f"`Schema`, not {nested.__class__}."
                     )
                 elif nested == "self":
                     schema_class = self.root.__class__
@@ -1827,9 +1825,9 @@ class IPInterface(Field):
             return value.exploded
         return value.compressed
 
-    def _deserialize(
-        self, value, attr, data, **kwargs
-    ) -> None | (ipaddress.IPv4Interface | ipaddress.IPv6Interface):
+    def _deserialize(self, value, attr, data, **kwargs) -> None | (
+        ipaddress.IPv4Interface | ipaddress.IPv6Interface
+    ):
         if value is None:
             return None
         try:
diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py
index ca4eb082..faa924af 100644
--- a/src/marshmallow/schema.py
+++ b/src/marshmallow/schema.py
@@ -1,23 +1,21 @@
 """The :class:`Schema` class, including its metaclass and options (class Meta)."""
 from __future__ import annotations
-from abc import ABCMeta
 
-from collections import defaultdict, OrderedDict
-from collections.abc import Mapping
-from functools import lru_cache
+import copy
 import datetime as dt
-import uuid
 import decimal
-import copy
 import inspect
 import json
 import typing
+import uuid
 import warnings
+from abc import ABCMeta
+from collections import OrderedDict, defaultdict
+from collections.abc import Mapping
+from functools import lru_cache
 
-from marshmallow import base, fields as ma_fields, class_registry, types
-from marshmallow.error_store import ErrorStore
-from marshmallow.exceptions import ValidationError, StringNotCollectionError
-from marshmallow.orderedset import OrderedSet
+from marshmallow import base, class_registry, types
+from marshmallow import fields as ma_fields
 from marshmallow.decorators import (
     POST_DUMP,
     POST_LOAD,
@@ -26,15 +24,18 @@ from marshmallow.decorators import (
     VALIDATES,
     VALIDATES_SCHEMA,
 )
+from marshmallow.error_store import ErrorStore
+from marshmallow.exceptions import StringNotCollectionError, ValidationError
+from marshmallow.orderedset import OrderedSet
 from marshmallow.utils import (
-    RAISE,
     EXCLUDE,
     INCLUDE,
-    missing,
-    set_value,
+    RAISE,
     get_value,
     is_collection,
     is_instance_or_subclass,
+    missing,
+    set_value,
     validate_unknown_parameter_value,
 )
 from marshmallow.warnings import RemovedInMarshmallow4Warning
@@ -407,9 +408,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
         self.error_messages = messages
 
     def __repr__(self) -> str:
-        return "<{ClassName}(many={self.many})>".format(
-            ClassName=self.__class__.__name__, self=self
-        )
+        return f"<{self.__class__.__name__}(many={self.many})>"
 
     @property
     def dict_class(self) -> type:
@@ -1004,7 +1003,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
                 "The data_key argument for one or more fields collides "
                 "with another field's name or data_key argument. "
                 "Check the following field names and "
-                "data_key arguments: {}".format(list(data_keys_duplicates))
+                f"data_key arguments: {list(data_keys_duplicates)}"
             )
         load_attributes = [obj.attribute or name for name, obj in load_fields.items()]
         if len(load_attributes) != len(set(load_attributes)):
@@ -1015,7 +1014,7 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
                 "The attribute argument for one or more fields collides "
                 "with another field's name or attribute argument. "
                 "Check the following field names and "
-                "attribute arguments: {}".format(list(attributes_duplicates))
+                f"attribute arguments: {list(attributes_duplicates)}"
             )
 
         self.fields = fields_dict
diff --git a/src/marshmallow/utils.py b/src/marshmallow/utils.py
index 757f4a8d..46235e57 100644
--- a/src/marshmallow/utils.py
+++ b/src/marshmallow/utils.py
@@ -2,8 +2,8 @@
 from __future__ import annotations
 
 import collections
-import functools
 import datetime as dt
+import functools
 import inspect
 import json
 import re
@@ -311,10 +311,7 @@ def set_value(dct: dict[str, typing.Any], key: str, value: typing.Any):
         target = dct.setdefault(head, {})
         if not isinstance(target, dict):
             raise ValueError(
-                "Cannot set {key} in {head} "
-                "due to existing value: {target}".format(
-                    key=key, head=head, target=target
-                )
+                f"Cannot set {key} in {head} " f"due to existing value: {target}"
             )
         set_value(target, rest, value)
     else:
diff --git a/src/marshmallow/validate.py b/src/marshmallow/validate.py
index 3588a422..708fed85 100644
--- a/src/marshmallow/validate.py
+++ b/src/marshmallow/validate.py
@@ -27,9 +27,7 @@ class Validator(ABC):
         args = self._repr_args()
         args = f"{args}, " if args else ""
 
-        return "<{self.__class__.__name__}({args}error={self.error!r})>".format(
-            self=self, args=args
-        )
+        return f"<{self.__class__.__name__}({args}error={self.error!r})>"
 
     def _repr_args(self) -> str:
         """A string representation of the args passed to this validator. Used by
@@ -237,7 +235,8 @@ class Email(Validator):
 
     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](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+"
+        r"(?:[A-Z]{2,6}|[A-Z0-9-]{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",
diff --git a/tox.ini b/tox.ini
index 4658dc62..bfb9b83e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,7 +8,7 @@ commands = pytest {posargs}
 [testenv:lint]
 deps = pre-commit~=3.5
 skip_install = true
-commands = pre-commit run --all-files --show-diff-on-failure
+commands = pre-commit run --all-files
 
 [testenv:mypy-test]
 deps = mypy


Test patch:
diff --git a/tests/base.py b/tests/base.py
index 62e3138f..dfadee91 100644
--- a/tests/base.py
+++ b/tests/base.py
@@ -1,14 +1,13 @@
 """Test utilities and fixtures."""
-import functools
 import datetime as dt
+import functools
 import uuid
 from enum import Enum, IntEnum
 
-import simplejson
-
 import pytz
+import simplejson
 
-from marshmallow import Schema, fields, post_load, validate, missing
+from marshmallow import Schema, fields, missing, post_load, validate
 from marshmallow.exceptions import ValidationError
 
 central = pytz.timezone("America/Chicago")
diff --git a/tests/conftest.py b/tests/conftest.py
index 5c3045c3..5c6d6a7c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,7 @@
 """Pytest fixtures that are available in all test modules."""
 import pytest
 
-from tests.base import User, UserSchema, Blog
+from tests.base import Blog, User, UserSchema
 
 
 @pytest.fixture
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index 0d2ffc6b..a59fb08a 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -1,18 +1,18 @@
 import pytest
 
 from marshmallow import (
+    EXCLUDE,
+    INCLUDE,
+    RAISE,
     Schema,
+    ValidationError,
     fields,
-    pre_dump,
     post_dump,
-    pre_load,
     post_load,
+    pre_dump,
+    pre_load,
     validates,
     validates_schema,
-    ValidationError,
-    EXCLUDE,
-    INCLUDE,
-    RAISE,
 )
 
 
diff --git a/tests/test_deserialization.py b/tests/test_deserialization.py
index 4e4cfbd3..069c2f0c 100644
--- a/tests/test_deserialization.py
+++ b/tests/test_deserialization.py
@@ -1,25 +1,23 @@
 import datetime as dt
-import uuid
-import ipaddress
 import decimal
+import ipaddress
 import math
-
+import uuid
 from unittest.mock import patch
 
 import pytest
 
-from marshmallow import EXCLUDE, INCLUDE, RAISE, fields, Schema, validate
+from marshmallow import EXCLUDE, INCLUDE, RAISE, Schema, fields, validate
 from marshmallow.exceptions import ValidationError
 from marshmallow.validate import Equal
-
 from tests.base import (
-    assert_date_equal,
-    assert_time_equal,
-    central,
     ALL_FIELDS,
+    DateEnum,
     GenderEnum,
     HairColorEnum,
-    DateEnum,
+    assert_date_equal,
+    assert_time_equal,
+    central,
 )
 
 
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index ae39763c..e4f144a1 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -36,7 +36,7 @@ class TestValidationError:
     def test_messages_dict_type_error_on_badval(self):
         err = ValidationError("foo")
         with pytest.raises(TypeError) as excinfo:
-            err.messages_dict
+            err.messages_dict  # noqa: B018
         assert "cannot access 'messages_dict' when 'messages' is of type list" in str(
             excinfo.value
         )
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 1296cd6d..cb1d4638 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,17 +1,16 @@
 import pytest
 
 from marshmallow import (
-    fields,
-    Schema,
-    ValidationError,
     EXCLUDE,
     INCLUDE,
     RAISE,
+    Schema,
+    ValidationError,
+    fields,
     missing,
 )
-from marshmallow.orderedset import OrderedSet
 from marshmallow.exceptions import StringNotCollectionError
-
+from marshmallow.orderedset import OrderedSet
 from tests.base import ALL_FIELDS
 
 
@@ -33,13 +32,11 @@ class TestField:
         default = "œ∑´"
         field = fields.Field(dump_default=default, attribute=None)
         assert repr(field) == (
-            "<fields.Field(dump_default={0!r}, attribute=None, "
+            f"<fields.Field(dump_default={default!r}, attribute=None, "
             "validate=None, required=False, "
             "load_only=False, dump_only=False, "
-            "load_default={missing}, allow_none=False, "
-            "error_messages={error_messages})>".format(
-                default, missing=missing, error_messages=field.error_messages
-            )
+            f"load_default={missing}, allow_none=False, "
+            f"error_messages={field.error_messages})>"
         )
         int_field = fields.Integer(validate=lambda x: True)
         assert "<fields.Integer" in repr(int_field)
diff --git a/tests/test_options.py b/tests/test_options.py
index e24d9445..2b052a31 100644
--- a/tests/test_options.py
+++ b/tests/test_options.py
@@ -1,10 +1,9 @@
-from collections import OrderedDict
 import datetime as dt
+from collections import OrderedDict
 
 import pytest
 
-from marshmallow import fields, Schema, EXCLUDE
-
+from marshmallow import EXCLUDE, Schema, fields
 from tests.base import User
 
 
diff --git a/tests/test_registry.py b/tests/test_registry.py
index 7f43cd70..11905549 100644
--- a/tests/test_registry.py
+++ b/tests/test_registry.py
@@ -1,6 +1,6 @@
 import pytest
 
-from marshmallow import Schema, fields, class_registry
+from marshmallow import Schema, class_registry, fields
 from marshmallow.exceptions import RegistryError
 
 
diff --git a/tests/test_schema.py b/tests/test_schema.py
index e92c0a30..b9bc43d0 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -6,6 +6,7 @@ from collections import OrderedDict, namedtuple
 
 import pytest
 import simplejson as json
+
 from marshmallow import (
     EXCLUDE,
     INCLUDE,
@@ -22,7 +23,6 @@ from marshmallow.exceptions import (
     StringNotCollectionError,
     ValidationError,
 )
-
 from tests.base import (
     Blog,
     BlogOnlySchema,
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index d274b28f..d9fd5f3a 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -1,17 +1,17 @@
 """Tests for field serialization."""
-from collections import namedtuple, OrderedDict
 import datetime as dt
-import itertools
 import decimal
-import uuid
 import ipaddress
+import itertools
 import math
+import uuid
+from collections import OrderedDict, namedtuple
 
 import pytest
 
-from marshmallow import Schema, fields, missing as missing_
-
-from tests.base import User, ALL_FIELDS, central, GenderEnum, HairColorEnum, DateEnum
+from marshmallow import Schema, fields
+from marshmallow import missing as missing_
+from tests.base import ALL_FIELDS, DateEnum, GenderEnum, HairColorEnum, User, central
 
 
 class DateTimeList:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 390c0ab0..55c1bef6 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,12 +1,12 @@
 import datetime as dt
 from collections import namedtuple
-from functools import partial
 from copy import copy, deepcopy
+from functools import partial
 
 import pytest
 
-from marshmallow import utils, fields, Schema
-from tests.base import central, assert_time_equal, assert_date_equal
+from marshmallow import Schema, fields, utils
+from tests.base import assert_date_equal, assert_time_equal, central
 
 
 def test_missing_singleton_copy():
diff --git a/tests/test_validate.py b/tests/test_validate.py
index 9f6ae25d..0d7c56cf 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -1,9 +1,10 @@
 """Tests for marshmallow.validate"""
 
 import re
+
 import pytest
 
-from marshmallow import validate, ValidationError
+from marshmallow import ValidationError, validate
 
 
 @pytest.mark.parametrize(
@@ -349,12 +350,15 @@ def test_range_repr():
         )
         == "<Range(min=None, max=None, min_inclusive=True, max_inclusive=True, error=None)>"  # noqa: B950
     )
-    assert repr(
-        validate.Range(
-            min=1, max=3, error="foo", min_inclusive=False, max_inclusive=False
+    assert (
+        repr(
+            validate.Range(
+                min=1, max=3, error="foo", min_inclusive=False, max_inclusive=False
+            )
+        )
+        == "<Range(min=1, max=3, min_inclusive=False, max_inclusive=False, error={!r})>".format(  # noqa: B950
+            "foo"
         )
-    ) == "<Range(min=1, max=3, min_inclusive=False, max_inclusive=False, error={!r})>".format(  # noqa: B950
-        "foo"
     )