From c55ed5c080960a47c43b07cfcace44a4366d3377 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Wed, 28 Aug 2024 20:04:43 +0200 Subject: [PATCH 1/4] Add tests for all Marshmallow DateTime() formats --- tests/test_ext_marshmallow_field.py | 108 ++++++++++++++-------------- 1 file changed, 52 insertions(+), 56 deletions(-) diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py index 20e079d2..93011c59 100644 --- a/tests/test_ext_marshmallow_field.py +++ b/tests/test_ext_marshmallow_field.py @@ -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): From 5ec8e5cd838fd61a2f4f9b78f247f21bab7cbfdb Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Wed, 28 Aug 2024 20:16:45 +0200 Subject: [PATCH 2/4] Add tests for all Marshmallow Date() formats --- tests/test_ext_marshmallow_field.py | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py index 93011c59..6a31553e 100644 --- a/tests/test_ext_marshmallow_field.py +++ b/tests/test_ext_marshmallow_field.py @@ -597,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 From 47f20ce70f41249518b3a26f9e99e3f9a917a23b Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Wed, 28 Aug 2024 20:43:22 +0200 Subject: [PATCH 3/4] Fixed generating Marshmallow DateTime formats --- src/apispec/ext/marshmallow/field_converter.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py index 4e17aa2e..3bd16ed7 100644 --- a/src/apispec/ext/marshmallow/field_converter.py +++ b/src/apispec/ext/marshmallow/field_converter.py @@ -574,13 +574,25 @@ def datetime2properties(self, field, **kwargs: typing.Any) -> dict: "example": "1676451277514.654", "min": "0", } + elif field.format is not None: + 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 From 03004c9a79af70866ed83f4589b0eb22baad6db7 Mon Sep 17 00:00:00 2001 From: Daniele Esposti Date: Wed, 28 Aug 2024 21:12:45 +0200 Subject: [PATCH 4/4] Fixed generating Marshmallow Date formats --- .../ext/marshmallow/field_converter.py | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py index 3bd16ed7..44ed1ad2 100644 --- a/src/apispec/ext/marshmallow/field_converter.py +++ b/src/apispec/ext/marshmallow/field_converter.py @@ -113,6 +113,7 @@ def init_attribute_functions(self): self.list2properties, self.dict2properties, self.timedelta2properties, + self.date2properties, self.datetime2properties, self.field2nullable, ] @@ -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 @@ -596,6 +595,40 @@ def datetime2properties(self, field, **kwargs: typing.Any) -> dict: } 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