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