New Upstream Release - apispec

Ready changes

Summary

Merged new upstream version: 6.4.0 (was: 6.3.0).

Diff

diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index bc59f29..e405d45 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -1,22 +1,13 @@
 name: build
 on:
   push:
-    branches: ["dev"]
+    branches: ["dev", "*.x-line"]
     tags: ["*"]
   pull_request:
   # Run builds nightly to catch incompatibilities with new marshmallow releases
   schedule:
-  - cron: "0 0 * * *"
+    - cron: "0 0 * * *"
 jobs:
-  lint:
-    name: lint
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
-      - run: python -m pip install --upgrade pip wheel
-      - run: pip install tox
-      - run: tox -elint
   tests:
     name: ${{ matrix.name }}
     runs-on: ${{ matrix.os }}
@@ -24,9 +15,24 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - {name: '3.7-ma3', python: '3.7', os: ubuntu-latest, tox: py37-marshmallow3}
-          - {name: '3.11-ma3', python: '3.11', os: ubuntu-latest, tox: py311-marshmallow3}
-          - {name: '3.11-madev', python: '3.11', os: ubuntu-latest, tox: py311-marshmallowdev}
+          - {
+              name: "3.8-ma3",
+              python: "3.8",
+              os: ubuntu-latest,
+              tox: py38-marshmallow3,
+            }
+          - {
+              name: "3.12-ma3",
+              python: "3.12",
+              os: ubuntu-latest,
+              tox: py312-marshmallow3,
+            }
+          - {
+              name: "3.12-madev",
+              python: "3.12",
+              os: ubuntu-latest,
+              tox: py312-marshmallowdev,
+            }
     steps:
       - uses: actions/checkout@v2
       - uses: actions/setup-python@v2
@@ -35,8 +41,22 @@ jobs:
       - run: python -m pip install --upgrade pip wheel
       - run: pip install tox
       - run: tox -e${{ matrix.tox }}
+  # 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:
+      - uses: actions/checkout@v3.1.0
+      - uses: actions/setup-python@v4.3.0
+        with:
+          python-version: "3.11"
+      - run: python -m pip install --upgrade pip
+      - run: python -m pip install tox
+      - run: python -m tox -elint
   release:
-    needs: [lint, tests]
+    needs: [tests, lint-pre-release]
     name: PyPI release
     if: startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5e3940d..c18f12d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,26 +1,26 @@
 repos:
 - repo: https://github.com/asottile/pyupgrade
-  rev: v3.3.1
+  rev: v3.15.0
   hooks:
   - id: pyupgrade
-    args: [--py37-plus]
+    args: [--py38-plus]
 - repo: https://github.com/psf/black
-  rev: 23.1.0
+  rev: 23.12.1
   hooks:
   - id: black
     language_version: python3
 - repo: https://github.com/pycqa/flake8
-  rev: 6.0.0
+  rev: 7.0.0
   hooks:
   - id: flake8
     additional_dependencies: [flake8-bugbear==22.12.6]
 - repo: https://github.com/asottile/blacken-docs
-  rev: 1.13.0
+  rev: 1.16.0
   hooks:
   - id: blacken-docs
-    additional_dependencies: [black==22.3.0]
+    additional_dependencies: [black==23.12.1]
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v1.0.1
+  rev: v1.8.0
   hooks:
   - id: mypy
     additional_dependencies: ["marshmallow>=3,<4", "types-PyYAML"]
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 02662ce..c3082a8 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -77,3 +77,6 @@ Contributors (chronological)
 - Edwin Erdmanis `@vorticity <https://github.com/vorticity>`_
 - Mounier Florian `@paradoxxxzero <https://github.com/paradoxxxzero>`_
 - Renato Damas `@codectl <https://github.com/codectl>`_
+- Tayler Sokalski `@tsokalski <https://github.com/tsokalski>`_
+- Sebastien Lovergne `@TheBigRoomXXL <https://github.com/TheBigRoomXXL>`_
+- Luna Lovegood `@duchuyvp <https://github.com/duchuyvp>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index f3b8c86..464b32e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,33 @@
 Changelog
 ---------
 
+6.4.0 (2024-01-09)
+******************
+
+Features:
+
+- ``MarshmallowPlugin``: Support different datetime formats
+  for ``marshmallow.fields.DateTime`` fields (:issue:`814`).
+  Thanks :user:`TheBigRoomXXL` for the suggestion and PR.
+- ``MarshmallowPlugin``: Handle resolving names of schemas with spaces in the name (:pr:`856`).
+  Thanks :user:`duchuyvp` for the PR.
+- Various typing improvements (:pr:`873`).
+
+Other changes:
+
+- Support Python 3.12.
+- Drop support for Python 3.7, which is EOL.
+- Remove `[validation]` from extras, as it is no longer used.
+
+
+6.3.1 (2023-12-21)
+******************
+
+Bug fixes:
+
+-  Fix conversion of deprecated flag on parameters (:issue:`850`).
+  Thanks :user:`tsokalski` for the PR.
+
 6.3.0 (2023-03-10)
 ******************
 
diff --git a/LICENSE b/LICENSE
index 8796dce..fc39709 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright 2015-2020 Steven Loria, Jérôme Lafréchoux, and contributors
+Copyright 2015-2024 Steven Loria, Jérôme Lafréchoux, and contributors
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.rst b/README.rst
index 987d282..3b18555 100644
--- a/README.rst
+++ b/README.rst
@@ -6,8 +6,8 @@ apispec
     :target: https://pypi.org/project/apispec/
     :alt: PyPI version
 
-.. image:: https://dev.azure.com/sloria/sloria/_apis/build/status/marshmallow-code.apispec?branchName=dev
-    :target: https://dev.azure.com/sloria/sloria/_build/latest?definitionId=8&branchName=dev
+.. image:: https://github.com/marshmallow-code/apispec/actions/workflows/build-release.yml/badge.svg
+    :target: https://github.com/marshmallow-code/webargs/actions/workflows/build-release.yml
     :alt: Build status
 
 .. image:: https://readthedocs.org/projects/apispec/badge/
@@ -67,6 +67,7 @@ Example Application
         plugins=[FlaskPlugin(), MarshmallowPlugin()],
     )
 
+
     # Optional marshmallow support
     class CategorySchema(Schema):
         id = fields.Int()
diff --git a/debian/changelog b/debian/changelog
index bb8bff6..b7dc48a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+apispec (6.4.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sat, 27 Jan 2024 05:29:38 -0000
+
 apispec (6.3.0-1) unstable; urgency=medium
 
   * Team upload.
diff --git a/debian/patches/edit_test_to_fix_PytestReturnNotNoneWarning.patch b/debian/patches/edit_test_to_fix_PytestReturnNotNoneWarning.patch
index 87b0d88..1145c2f 100644
--- a/debian/patches/edit_test_to_fix_PytestReturnNotNoneWarning.patch
+++ b/debian/patches/edit_test_to_fix_PytestReturnNotNoneWarning.patch
@@ -5,9 +5,11 @@ Forwarded: no
 Description: Rename function test_plugin_factory to plugin_factory so that
  pytest does not consider it a test.
 
---- a/tests/test_core.py
-+++ b/tests/test_core.py
-@@ -1077,7 +1077,7 @@
+Index: apispec.git/tests/test_core.py
+===================================================================
+--- apispec.git.orig/tests/test_core.py
++++ apispec.git/tests/test_core.py
+@@ -1077,7 +1077,7 @@ class TestPath(RefsSchemaTestMixin):
  
  class TestPlugins:
      @staticmethod
@@ -16,7 +18,7 @@ Description: Rename function test_plugin_factory to plugin_factory so that
          class TestPlugin(BasePlugin):
              """Test Plugin
  
-@@ -1125,7 +1125,7 @@
+@@ -1125,7 +1125,7 @@ class TestPlugins:
              title="Swagger Petstore",
              version="1.0.0",
              openapi_version=openapi_version,
@@ -25,7 +27,7 @@ Description: Rename function test_plugin_factory to plugin_factory so that
          )
          schema = {"dummy": "dummy"}
          spec.components.schema("Pet", schema)
-@@ -1144,7 +1144,7 @@
+@@ -1144,7 +1144,7 @@ class TestPlugins:
              title="Swagger Petstore",
              version="1.0.0",
              openapi_version=openapi_version,
@@ -34,7 +36,7 @@ Description: Rename function test_plugin_factory to plugin_factory so that
          )
          parameter = {"dummy": "dummy"}
          spec.components.parameter("Pet", "body", parameter)
-@@ -1167,7 +1167,7 @@
+@@ -1167,7 +1167,7 @@ class TestPlugins:
              title="Swagger Petstore",
              version="1.0.0",
              openapi_version=openapi_version,
@@ -43,7 +45,7 @@ Description: Rename function test_plugin_factory to plugin_factory so that
          )
          response = {"dummy": "dummy"}
          spec.components.response("Pet", response)
-@@ -1186,7 +1186,7 @@
+@@ -1186,7 +1186,7 @@ class TestPlugins:
              title="Swagger Petstore",
              version="1.0.0",
              openapi_version=openapi_version,
@@ -52,7 +54,7 @@ Description: Rename function test_plugin_factory to plugin_factory so that
          )
          header = {"dummy": "dummy"}
          spec.components.header("Pet", header)
-@@ -1207,7 +1207,7 @@
+@@ -1207,7 +1207,7 @@ class TestPlugins:
              title="Swagger Petstore",
              version="1.0.0",
              openapi_version=openapi_version,
@@ -61,7 +63,7 @@ Description: Rename function test_plugin_factory to plugin_factory so that
          )
          spec.path("/path_1")
          paths = get_paths(spec)
-@@ -1226,7 +1226,7 @@
+@@ -1226,7 +1226,7 @@ class TestPlugins:
              title="Swagger Petstore",
              version="1.0.0",
              openapi_version=openapi_version,
diff --git a/docs/conf.py b/docs/conf.py
index c0fd839..dc45154 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -3,6 +3,8 @@ import os
 import sys
 import time
 
+import sphinx_rtd_theme
+
 sys.path.insert(0, os.path.abspath(os.path.join("..", "src")))
 import apispec  # noqa: E402
 
@@ -43,11 +45,5 @@ exclude_patterns = ["_build"]
 
 # THEME
 
-# on_rtd is whether we are on readthedocs.org
-on_rtd = os.environ.get("READTHEDOCS", None) == "True"
-
-if not on_rtd:  # only import and set the theme if we're building docs locally
-    import sphinx_rtd_theme
-
-    html_theme = "sphinx_rtd_theme"
-    html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+html_theme = "sphinx_rtd_theme"
+html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
diff --git a/docs/index.rst b/docs/index.rst
index cd06851..55c83b8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -36,6 +36,7 @@ Example Application
         plugins=[FlaskPlugin(), MarshmallowPlugin()],
     )
 
+
     # Optional marshmallow support
     class CategorySchema(Schema):
         id = fields.Int()
diff --git a/docs/using_plugins.rst b/docs/using_plugins.rst
index 8dd2b4b..c00d3e6 100644
--- a/docs/using_plugins.rst
+++ b/docs/using_plugins.rst
@@ -113,6 +113,7 @@ We'll add some YAML in the docstring to add response information.
 
     app = Flask(__name__)
 
+
     # NOTE: Plugins may inspect docstrings to gather more information for the spec
     @app.route("/gists/<gist_id>")
     def gist_detail(gist_id):
@@ -235,6 +236,22 @@ Schema Modifiers
 
 apispec will respect schema modifiers such as ``exclude`` and ``partial`` in the generated schema definition. If a schema is initialized with modifiers, apispec will treat each combination of modifiers as a unique schema definition.
 
+Custom DateTime formats
+***********************
+
+apispec supports all four basic formats of `marshmallow.fields.DateTime`: ``"rfc"`` (for RFC822), ``"iso"`` (for ISO8601), 
+``"timestamp"``, ``"timestamp_ms"`` (for a POSIX timestamp).
+
+If you are using a custom DateTime format you should pass a regex string to the ``pattern`` parameter in your field ``metadata`` so that it is included as documentation. 
+
+.. code-block:: python
+
+    class SchemaWithCustomDate(Schema):
+        french_date = ma.DateTime(
+            format="%d-%m%Y %H:%M:%S",
+            metadata={"pattern": r"^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}$"},
+        )
+
 Custom Fields
 *************
 
@@ -258,6 +275,7 @@ OpenAPI type and format, or a marshmallow `Field` that has the desired target ma
         title="Demo", version="0.1", openapi_version="3.0.0", plugins=(ma_plugin,)
     )
 
+
     # Inherits Integer mapping of ('integer', None)
     class CustomInteger(Integer):
         pass
diff --git a/pyproject.toml b/pyproject.toml
index b49fdd0..c8bc407 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,7 @@
 [tool.black]
 line-length = 88
-target-version = ['py36', 'py37', 'py38']
+target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
+
+[tool.pyright]
+# Allow class Meta in marshmallow Schemas
+reportIncompatibleVariableOverride = false
diff --git a/readthedocs.yml b/readthedocs.yml
index 75bf21b..4bab202 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -1,9 +1,13 @@
 version: 2
 sphinx:
   configuration: docs/conf.py
-formats: all
+formats:
+  - pdf
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
 python:
-  version: 3.7
   install:
     - method: pip
       path: .
diff --git a/setup.cfg b/setup.cfg
index 6a94be7..335adee 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,6 +9,6 @@ extend-ignore = E203
 [mypy]
 ignore_missing_imports = true
 warn_unreachable = true
-warn_unused_ignores = true
+warn_unused_ignores = false
 warn_redundant_casts = true
 no_implicit_optional = true
diff --git a/setup.py b/setup.py
index 8984685..916b8b8 100644
--- a/setup.py
+++ b/setup.py
@@ -6,27 +6,26 @@ INSTALL_REQUIRES = "packaging>=21.3"
 EXTRAS_REQUIRE = {
     "marshmallow": ["marshmallow>=3.18.0"],
     "yaml": ["PyYAML>=3.10"],
-    "validation": ["prance[osv]>=0.11", "openapi_spec_validator<0.5"],
     "lint": [
-        "flake8==5.0.4",
-        "flake8-bugbear==22.9.23",
-        "pre-commit~=2.4",
-        "mypy==0.982",
+        "flake8==7.0.0",
+        "flake8-bugbear==22.12.6",
+        "pre-commit~=3.5",
+        "mypy==1.8.0",
         "types-PyYAML",
     ],
     "docs": [
         "marshmallow>=3.13.0",
-        "pyyaml==6.0",
-        "sphinx==5.2.3",
+        "pyyaml==6.0.1",
+        "sphinx==7.2.6",
         "sphinx-issues==3.0.1",
-        "sphinx-rtd-theme==1.0.0",
+        "sphinx-rtd-theme==2.0.0",
     ],
 }
-EXTRAS_REQUIRE["tests"] = (
-    EXTRAS_REQUIRE["yaml"]
-    + EXTRAS_REQUIRE["validation"]
-    + ["marshmallow>=3.13.0", "pytest"]
-)
+EXTRAS_REQUIRE["tests"] = EXTRAS_REQUIRE["yaml"] + [
+    "marshmallow>=3.13.0",
+    "openapi-spec-validator==0.7.1",
+    "pytest",
+]
 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
 
 
@@ -70,15 +69,15 @@ setup(
     license="MIT",
     zip_safe=False,
     keywords="apispec swagger openapi specification oas documentation spec rest api",
-    python_requires=">=3.7",
+    python_requires=">=3.8",
     classifiers=[
         "License :: OSI Approved :: MIT License",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
         "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",
         "Programming Language :: Python :: 3 :: Only",
     ],
     test_suite="tests",
diff --git a/src/apispec/__init__.py b/src/apispec/__init__.py
index 053f615..6801d35 100644
--- a/src/apispec/__init__.py
+++ b/src/apispec/__init__.py
@@ -3,5 +3,5 @@
 from .core import APISpec
 from .plugin import BasePlugin
 
-__version__ = "6.3.0"
+__version__ = "6.4.0"
 __all__ = ["APISpec", "BasePlugin"]
diff --git a/src/apispec/ext/marshmallow/__init__.py b/src/apispec/ext/marshmallow/__init__.py
index 2595581..988dee9 100644
--- a/src/apispec/ext/marshmallow/__init__.py
+++ b/src/apispec/ext/marshmallow/__init__.py
@@ -69,7 +69,7 @@ with `"x-"` (vendor extension).
     #           'type': 'object'}}
 
 """
-
+# pyright: reportIncompatibleMethodOverride=false
 from __future__ import annotations
 
 import warnings
@@ -86,11 +86,12 @@ from .schema_resolver import SchemaResolver
 
 def resolver(schema: type[Schema]) -> str:
     """Default schema name resolver function that strips 'Schema' from the end of the class name."""
-    schema_cls = resolve_schema_cls(schema)
+    resolved = resolve_schema_cls(schema)
+    schema_cls = resolved[0] if isinstance(resolved, list) else resolved
     name = schema_cls.__name__
     if name.endswith("Schema"):
-        return name[:-6] or name
-    return name
+        name = name[:-6] or name
+    return name.strip()
 
 
 class MarshmallowPlugin(BasePlugin):
@@ -161,6 +162,7 @@ class MarshmallowPlugin(BasePlugin):
 
             ma_plugin.map_to_openapi_type(IntegerLike, Integer)
         """
+        assert self.converter is not None, "init_spec has not yet been called"
         return self.converter.map_to_openapi_type(field_cls, *args)
 
     def schema_helper(self, name, _, schema=None, **kwargs):
@@ -177,6 +179,7 @@ class MarshmallowPlugin(BasePlugin):
 
         schema_key = make_schema_key(schema_instance)
         self.warn_if_schema_already_in_spec(schema_key)
+        assert self.converter is not None, "init_spec has not yet been called"
         self.converter.refs[schema_key] = name
 
         json_schema = self.converter.schema2jsonschema(schema_instance)
@@ -190,6 +193,7 @@ class MarshmallowPlugin(BasePlugin):
         :param dict parameter: parameter fields. May contain a marshmallow
             Schema class or instance.
         """
+        assert self.resolver is not None, "init_spec has not yet been called"
         self.resolver.resolve_schema(parameter)
         return parameter
 
@@ -200,6 +204,7 @@ class MarshmallowPlugin(BasePlugin):
         :param dict parameter: response fields. May contain a marshmallow
             Schema class or instance.
         """
+        assert self.resolver is not None, "init_spec has not yet been called"
         self.resolver.resolve_response(response)
         return response
 
@@ -223,7 +228,7 @@ class MarshmallowPlugin(BasePlugin):
         assert self.resolver  # needed for mypy
         self.resolver.resolve_operations(operations)
 
-    def warn_if_schema_already_in_spec(self, schema_key: str) -> None:
+    def warn_if_schema_already_in_spec(self, schema_key: tuple) -> None:
         """Method to warn the user if the schema has already been added to the
         spec.
         """
diff --git a/src/apispec/ext/marshmallow/common.py b/src/apispec/ext/marshmallow/common.py
index e56f1b0..8df7b98 100644
--- a/src/apispec/ext/marshmallow/common.py
+++ b/src/apispec/ext/marshmallow/common.py
@@ -8,6 +8,8 @@ import warnings
 from apispec.core import Components
 
 import marshmallow
+from marshmallow import fields
+import marshmallow.class_registry
 
 
 MODIFIERS = ["only", "exclude", "load_only", "dump_only", "partial"]
@@ -28,7 +30,9 @@ def resolve_schema_instance(
     return marshmallow.class_registry.get_class(schema)()  # type: ignore
 
 
-def resolve_schema_cls(schema):
+def resolve_schema_cls(
+    schema: type[marshmallow.Schema] | str | marshmallow.Schema,
+) -> type[marshmallow.Schema] | list[type[marshmallow.Schema]]:
     """Return schema class for given schema (instance or class).
 
     :param type|Schema|str: instance, class or class name of marshmallow.Schema
@@ -38,10 +42,14 @@ def resolve_schema_cls(schema):
         return schema
     if isinstance(schema, marshmallow.Schema):
         return type(schema)
-    return marshmallow.class_registry.get_class(schema)
+    return marshmallow.class_registry.get_class(str(schema))
 
 
-def get_fields(schema, *, exclude_dump_only: bool = False):
+def get_fields(
+    schema: type[marshmallow.Schema] | marshmallow.Schema,
+    *,
+    exclude_dump_only: bool = False,
+) -> dict[str, fields.Field]:
     """Return fields from schema.
 
     :param Schema schema: A marshmallow Schema instance or a class object
@@ -59,7 +67,7 @@ def get_fields(schema, *, exclude_dump_only: bool = False):
     return filter_excluded_fields(fields, Meta, exclude_dump_only=exclude_dump_only)
 
 
-def warn_if_fields_defined_in_meta(fields, Meta):
+def warn_if_fields_defined_in_meta(fields: dict[str, fields.Field], Meta):
     """Warns user that fields defined in Meta.fields or Meta.additional will be ignored.
 
     :param dict fields: A dictionary of fields name field object pairs
@@ -77,7 +85,9 @@ def warn_if_fields_defined_in_meta(fields, Meta):
             )
 
 
-def filter_excluded_fields(fields, Meta, *, exclude_dump_only: bool) -> dict:
+def filter_excluded_fields(
+    fields: dict[str, fields.Field], Meta, *, exclude_dump_only: bool
+) -> dict[str, fields.Field]:
     """Filter fields that should be ignored in the OpenAPI spec.
 
     :param dict fields: A dictionary of fields name field object pairs
@@ -97,7 +107,7 @@ def filter_excluded_fields(fields, Meta, *, exclude_dump_only: bool) -> dict:
     return filtered_fields
 
 
-def make_schema_key(schema: marshmallow.Schema) -> tuple:
+def make_schema_key(schema: marshmallow.Schema) -> tuple[type[marshmallow.Schema], ...]:
     if not isinstance(schema, marshmallow.Schema):
         raise TypeError("can only make a schema key based on a Schema instance.")
     modifiers = []
diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py
index d075aca..79fd368 100644
--- a/src/apispec/ext/marshmallow/field_converter.py
+++ b/src/apispec/ext/marshmallow/field_converter.py
@@ -6,6 +6,7 @@
     This module is treated as private API.
     Users should not need to use this module directly.
 """
+from __future__ import annotations
 import re
 import functools
 import operator
@@ -18,7 +19,7 @@ from marshmallow.orderedset import OrderedSet
 
 
 # marshmallow field => (JSON Schema type, format)
-DEFAULT_FIELD_MAPPING = {
+DEFAULT_FIELD_MAPPING: dict[type, tuple[str | None, str | None]] = {
     marshmallow.fields.Integer: ("integer", None),
     marshmallow.fields.Number: ("number", None),
     marshmallow.fields.Float: ("number", None),
@@ -86,7 +87,7 @@ _VALID_PREFIX = "x-"
 class FieldConverterMixin:
     """Adds methods for converting marshmallow fields to an OpenAPI properties."""
 
-    field_mapping = DEFAULT_FIELD_MAPPING
+    field_mapping: dict[type, tuple[str | None, str | None]] = DEFAULT_FIELD_MAPPING
     openapi_version: Version
 
     def init_attribute_functions(self):
@@ -109,6 +110,7 @@ class FieldConverterMixin:
             self.list2properties,
             self.dict2properties,
             self.timedelta2properties,
+            self.datetime2properties,
         ]
 
     def map_to_openapi_type(self, field_cls, *args):
@@ -517,6 +519,53 @@ class FieldConverterMixin:
             ret["enum"] = [field.field._serialize(v, None, None) for v in choices]
         return ret
 
+    def datetime2properties(self, field, **kwargs: typing.Any) -> dict:
+        """Return a dictionary of properties from :class:`DateTime <marshmallow.fields.DateTime` fields.
+
+        :param Field field: A marshmallow field.
+        :rtype: dict
+        """
+        ret = {}
+        if isinstance(field, marshmallow.fields.DateTime) and not isinstance(
+            field, marshmallow.fields.Date
+        ):
+            if field.format == "iso" or field.format is None:
+                # Will return { "type": "string", "format": "date-time" }
+                # as specified inside DEFAULT_FIELD_MAPPING
+                pass
+            elif field.format == "rfc":
+                ret = {
+                    "type": "string",
+                    "format": None,
+                    "example": "Wed, 02 Oct 2002 13:00:00 GMT",
+                    "pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} "
+                    + r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} "
+                    + r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})",
+                }
+            elif field.format == "timestamp":
+                ret = {
+                    "type": "number",
+                    "format": "float",
+                    "example": "1676451245.596",
+                    "min": "0",
+                }
+            elif field.format == "timestamp_ms":
+                ret = {
+                    "type": "number",
+                    "format": "float",
+                    "example": "1676451277514.654",
+                    "min": "0",
+                }
+            else:
+                ret = {
+                    "type": "string",
+                    "format": None,
+                    "pattern": field.metadata["pattern"]
+                    if field.metadata.get("pattern")
+                    else None,
+                }
+        return ret
+
 
 def make_type_list(types):
     """Return a list of types from a type attribute
diff --git a/src/apispec/ext/marshmallow/openapi.py b/src/apispec/ext/marshmallow/openapi.py
index a27bc4f..c4e2a6a 100644
--- a/src/apispec/ext/marshmallow/openapi.py
+++ b/src/apispec/ext/marshmallow/openapi.py
@@ -12,6 +12,7 @@ import typing
 from packaging.version import Version
 
 import marshmallow
+import marshmallow.exceptions
 from marshmallow.utils import is_collection
 
 from apispec import APISpec
@@ -197,6 +198,8 @@ class OpenAPIConverter(FieldConverterMixin):
         else:
             if "description" in prop:
                 ret["description"] = prop.pop("description")
+            if "deprecated" in prop:
+                ret["deprecated"] = prop.pop("deprecated")
             ret["schema"] = prop
 
         for param_attr_func in self.parameter_attribute_functions:
@@ -253,11 +256,11 @@ class OpenAPIConverter(FieldConverterMixin):
         jsonschema = self.fields2jsonschema(fields, partial=partial)
 
         if hasattr(Meta, "title"):
-            jsonschema["title"] = Meta.title
+            jsonschema["title"] = Meta.title  # type: ignore
         if hasattr(Meta, "description"):
-            jsonschema["description"] = Meta.description
-        if hasattr(Meta, "unknown") and Meta.unknown != marshmallow.EXCLUDE:
-            jsonschema["additionalProperties"] = Meta.unknown == marshmallow.INCLUDE
+            jsonschema["description"] = Meta.description  # type: ignore
+        if hasattr(Meta, "unknown") and Meta.unknown != marshmallow.EXCLUDE:  # type: ignore
+            jsonschema["additionalProperties"] = Meta.unknown == marshmallow.INCLUDE  # type: ignore
 
         return jsonschema
 
diff --git a/tests/test_ext_marshmallow.py b/tests/test_ext_marshmallow.py
index 23540c2..ab295e4 100644
--- a/tests/test_ext_marshmallow.py
+++ b/tests/test_ext_marshmallow.py
@@ -123,7 +123,7 @@ class TestDefinitionHelper:
             title="Test auto-reference",
             version="0.1",
             openapi_version="2.0",
-            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
+            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),  # type: ignore
         )
         with pytest.raises(KeyError):
             get_schemas(spec)
@@ -212,7 +212,7 @@ class TestDefinitionHelper:
             title="Test auto-reference",
             version="0.1",
             openapi_version="2.0",
-            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
+            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),  # type: ignore
         )
 
         spec.components.schema("PetFamily", schema=PetFamilySchema)
@@ -675,7 +675,7 @@ class TestOperationHelper:
             title="Test auto-reference",
             version="0.1",
             openapi_version="2.0",
-            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
+            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),  # type: ignore
         )
         spec.components.schema("Pet", schema=PetSchema)
         spec.path(
@@ -692,7 +692,7 @@ class TestOperationHelper:
             title="Test auto-reference",
             version="0.1",
             openapi_version="3.0.0",
-            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
+            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),  # type: ignore
         )
         spec.components.schema("Pet", schema=PetSchema)
         spec.path(
@@ -804,7 +804,7 @@ class TestOperationHelper:
             title="Test resolver returns None",
             version="0.1",
             openapi_version="2.0",
-            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
+            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),  # type: ignore
         )
         spec.path(
             path="/pet",
@@ -825,7 +825,7 @@ class TestOperationHelper:
             title="Test resolver returns None",
             version="0.1",
             openapi_version="3.0.0",
-            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
+            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),  # type: ignore
         )
         spec.path(
             path="/pet",
@@ -851,7 +851,7 @@ class TestOperationHelper:
             title="Test auto-reference",
             version="0.1",
             openapi_version="3.0.0",
-            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),
+            plugins=(MarshmallowPlugin(schema_name_resolver=resolver),),  # type: ignore
         )
         spec.components.schema("Pet", schema=PetSchema)
         spec.path(
diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py
index 0fb04b6..7bfbb1d 100644
--- a/tests/test_ext_marshmallow_field.py
+++ b/tests/test_ext_marshmallow_field.py
@@ -377,6 +377,75 @@ def test_nested_field_with_property(spec_fixture):
     }
 
 
+def test_datetime2property_iso(spec_fixture):
+    field = fields.DateTime(format="iso")
+    res = spec_fixture.openapi.field2property(field)
+    assert res == {
+        "type": "string",
+        "format": "date-time",
+    }
+
+
+def test_datetime2property_rfc(spec_fixture):
+    field = fields.DateTime(format="rfc")
+    res = spec_fixture.openapi.field2property(field)
+    assert res == {
+        "type": "string",
+        "format": None,
+        "example": "Wed, 02 Oct 2002 13:00:00 GMT",
+        "pattern": r"((Mon|Tue|Wed|Thu|Fri|Sat|Sun), ){0,1}\d{2} "
+        + r"(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} "
+        + r"(UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|(Z|A|M|N)|(\+|-)\d{4})",
+    }
+
+
+def test_datetime2property_timestamp(spec_fixture):
+    field = fields.DateTime(format="timestamp")
+    res = spec_fixture.openapi.field2property(field)
+    assert res == {
+        "type": "number",
+        "format": "float",
+        "min": "0",
+        "example": "1676451245.596",
+    }
+
+
+def test_datetime2property_timestamp_ms(spec_fixture):
+    field = fields.DateTime(format="timestamp_ms")
+    res = spec_fixture.openapi.field2property(field)
+    assert res == {
+        "type": "number",
+        "format": "float",
+        "min": "0",
+        "example": "1676451277514.654",
+    }
+
+
+def test_datetime2property_custom_format(spec_fixture):
+    field = fields.DateTime(
+        format="%d-%m%Y %H:%M:%S",
+        metadata={
+            "pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$"
+        },
+    )
+    res = spec_fixture.openapi.field2property(field)
+    assert res == {
+        "type": "string",
+        "format": None,
+        "pattern": r"^((?:(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2}(?:\.\d+)?))(Z|[\+-]\d{2}:\d{2})?)$",
+    }
+
+
+def test_datetime2property_custom_format_missing_regex(spec_fixture):
+    field = fields.DateTime(format="%d-%m%Y %H:%M:%S")
+    res = spec_fixture.openapi.field2property(field)
+    assert res == {
+        "type": "string",
+        "format": None,
+        "pattern": None,
+    }
+
+
 class TestField2PropertyPluck:
     @pytest.fixture(autouse=True)
     def _setup(self, spec_fixture):
@@ -449,6 +518,6 @@ def test_field2property_with_non_string_metadata_keys(spec_fixture):
         pass
 
     field = fields.Boolean(metadata={"description": "A description"})
-    field.metadata[_DesertSentinel()] = "to be ignored"
+    field.metadata[_DesertSentinel()] = "to be ignored"  # type: ignore
     result = spec_fixture.openapi.field2property(field)
     assert result == {"description": "A description", "type": "boolean"}
diff --git a/tests/test_ext_marshmallow_openapi.py b/tests/test_ext_marshmallow_openapi.py
index 2617329..e77eba2 100644
--- a/tests/test_ext_marshmallow_openapi.py
+++ b/tests/test_ext_marshmallow_openapi.py
@@ -40,13 +40,13 @@ class TestMarshmallowFieldToOpenAPI:
         res = openapi.schema2parameters(schema=UserSchema(), location="query")
         assert len(res) == 0
 
-        class UserSchema(Schema):
+        class UserSchema2(Schema):
             name = fields.Str()
 
             class Meta:
                 dump_only = ("name",)
 
-        res = openapi.schema2parameters(schema=UserSchema(), location="query")
+        res = openapi.schema2parameters(schema=UserSchema2(), location="query")
         assert len(res) == 0
 
 
@@ -213,7 +213,7 @@ class TestMarshmallowSchemaToParameters:
         class DelimitedList(fields.List):
             """Delimited list field"""
 
-        def delimited_list2param(self, field, **kwargs):
+        def delimited_list2param(self, field: fields.Field, **kwargs) -> dict:
             ret: dict = {}
             if isinstance(field, DelimitedList):
                 if self.openapi_version.major < 3:
@@ -245,6 +245,11 @@ class TestMarshmallowSchemaToParameters:
         res = openapi._field2parameter(field, name="field", location="query")
         assert res["required"] is True
 
+    def test_field_deprecated(self, openapi):
+        field = fields.Str(metadata={"deprecated": True})
+        res = openapi._field2parameter(field, name="field", location="query")
+        assert res["deprecated"] is True
+
     def test_schema_partial(self, openapi):
         class UserSchema(Schema):
             field = fields.Str(required=True)
@@ -417,7 +422,7 @@ class TestNesting:
             j = fields.Int()
 
         class Parent(Schema):
-            child = fields.Nested(Child, **{modifier: ("i",)})
+            child = fields.Nested(Child, **{modifier: ("i",)})  # type: ignore
 
         spec_fixture.openapi.schema2jsonschema(Parent)
         props = get_schemas(spec_fixture.spec)["Child"]["properties"]
@@ -489,6 +494,7 @@ def test_openapi_tools_validate_v2():
         title="Pets", version="0.1", plugins=(ma_plugin,), openapi_version="2.0"
     )
     openapi = ma_plugin.converter
+    assert openapi is not None
 
     spec.components.schema("Category", schema=CategorySchema)
     spec.components.schema("Pet", {"discriminator": "name"}, schema=PetSchema)
@@ -546,6 +552,7 @@ def test_openapi_tools_validate_v3():
         title="Pets", version="0.1", plugins=(ma_plugin,), openapi_version="3.0.0"
     )
     openapi = ma_plugin.converter
+    assert openapi is not None
 
     spec.components.schema("Category", schema=CategorySchema)
     spec.components.schema("Pet", schema=PetSchema)
@@ -610,8 +617,9 @@ def test_openapi_tools_validate_v3():
 
 
 def test_openapi_converter_openapi_version_types():
-    converter_with_version = OpenAPIConverter(Version("3.1"), None, None)
-    converter_with_str_version = OpenAPIConverter("3.1", None, None)
+    spec = APISpec(title="Pets", version="0.1", openapi_version="2.0")
+    converter_with_version = OpenAPIConverter(Version("3.1"), None, spec)
+    converter_with_str_version = OpenAPIConverter("3.1", None, spec)
     assert (
         converter_with_version.openapi_version
         == converter_with_str_version.openapi_version
diff --git a/tests/utils.py b/tests/utils.py
index 9416e26..ef8efd5 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,5 +1,6 @@
 """Utilities to get elements of generated spec"""
-import json
+import openapi_spec_validator
+from openapi_spec_validator.exceptions import OpenAPISpecValidatorError
 
 from apispec.core import APISpec
 from apispec import exceptions
@@ -52,28 +53,12 @@ def validate_spec(spec: APISpec) -> bool:
     """Validate the output of an :class:`APISpec` object against the
     OpenAPI specification.
 
-    Note: Requires installing apispec with the ``[validation]`` extras.
-    ::
-
-        pip install 'apispec[validation]'
-
     :raise: apispec.exceptions.OpenAPIError if validation fails.
     """
     try:
-        import prance
-    except ImportError as error:  # re-raise with a more verbose message
-        exc_class = type(error)
-        raise exc_class(
-            "validate_spec requires prance to be installed. "
-            "You can install all validation requirements using:\n"
-            "    pip install 'apispec[validation]'"
-        ) from error
-    parser_kwargs = {}
-    if spec.openapi_version.major == 3:
-        parser_kwargs["backend"] = "openapi-spec-validator"
-    try:
-        prance.BaseParser(spec_string=json.dumps(spec.to_dict()), **parser_kwargs)
-    except prance.ValidationError as err:
+        # Coerce to dict to satisfy Pyright
+        openapi_spec_validator.validate(dict(spec.to_dict()))
+    except OpenAPISpecValidatorError as err:
         raise exceptions.OpenAPIError(*err.args) from err
     else:
         return True
diff --git a/tox.ini b/tox.ini
index df12780..eb1baab 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,8 +1,8 @@
 [tox]
 envlist=
     lint
-    py{37,38,39,310,311}-marshmallow3
-    py310-marshmallowdev
+    py{38,39,310,311,312}-marshmallow3
+    py312-marshmallowdev
     docs
 
 [testenv]
@@ -13,7 +13,7 @@ deps =
 commands = pytest {posargs}
 
 [testenv:lint]
-deps = pre-commit~=2.4
+deps = pre-commit~=3.5
 skip_install = true
 commands = pre-commit run --all-files --show-diff-on-failure
 

More details

Full run details

Historical runs