Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of DateTime and Date formats #937

Draft
wants to merge 4 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 51 additions & 6 deletions src/apispec/ext/marshmallow/field_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def init_attribute_functions(self):
self.list2properties,
self.dict2properties,
self.timedelta2properties,
self.date2properties,
self.datetime2properties,
self.field2nullable,
]
Expand Down Expand Up @@ -544,9 +545,7 @@ def datetime2properties(self, field, **kwargs: typing.Any) -> dict:
:rtype: dict
"""
ret = {}
if isinstance(field, marshmallow.fields.DateTime) and not isinstance(
field, marshmallow.fields.Date
):
if isinstance(field, marshmallow.fields.DateTime):
if field.format == "iso" or field.format is None:
# Will return { "type": "string", "format": "date-time" }
# as specified inside DEFAULT_FIELD_MAPPING
Expand Down Expand Up @@ -574,16 +573,62 @@ def datetime2properties(self, field, **kwargs: typing.Any) -> dict:
"example": "1676451277514.654",
"min": "0",
}
elif field.format is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point, field.format can't be None due to line 549, right?

ret = {
"type": "string",
"format": None,
"pattern": (
field.metadata["pattern"]
if field.metadata.get("pattern")
else None
),
}
else:
ret = {
"type": "string",
"format": None,
"pattern": field.metadata["pattern"]
if field.metadata.get("pattern")
else None,
"pattern": (
field.metadata["pattern"]
if field.metadata.get("pattern")
else None
),
}
return ret

def date2properties(
self, field: marshmallow.fields.Date, **kwargs: typing.Any
) -> dict:
"""Return a dictionary of OpenAPI properties for a Date field.

:param Field field: A marshmallow Date field.
:rtype: dict
"""
ret = {}

if isinstance(field, marshmallow.fields.Date):
if field.format == "iso" or field.format is None:
ret = {
"type": "string",
"format": "date",
}
elif field.format:
ret = {
"type": "string",
"format": None,
}
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
Expand Down
141 changes: 85 additions & 56 deletions tests/test_ext_marshmallow_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,63 +457,59 @@ 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})?)$"
},
)
@pytest.mark.parametrize(
("format", "expected"),
[
(
"iso",
{
"type": "string",
"format": "date-time",
},
),
(
"rfc",
{
"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})",
},
),
(
"timestamp",
{
"type": "number",
"format": "float",
"min": "0",
"example": "1676451245.596",
},
),
(
"timestamp_ms",
{
"type": "number",
"format": "float",
"min": "0",
"example": "1676451277514.654",
},
),
(
"%d-%m%Y %H:%M:%S",
{
"type": "string",
"format": None,
"pattern": None,
},
),
],
)
def test_datetime2property(spec_fixture, format, expected):
field = fields.DateTime(format=format)
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})?)$",
}
assert res == expected


def test_datetime2property_custom_format_missing_regex(spec_fixture):
Expand Down Expand Up @@ -601,3 +597,36 @@ class _DesertSentinel:
field.metadata[_DesertSentinel()] = "to be ignored"
result = spec_fixture.openapi.field2property(field)
assert result == {"description": "A description", "type": "boolean"}


@pytest.mark.parametrize(
("format", "expected"),
[
(
None,
{
"type": "string",
"format": "date",
},
),
(
"iso",
{
"type": "string",
"format": "date",
},
),
(
"%d-%m-%Y",
{
"type": "string",
"format": None,
"pattern": None,
},
),
],
)
def test_date2property(spec_fixture, format, expected):
field = fields.Date(format=format)
res = spec_fixture.openapi.field2property(field)
assert res == expected