Skip to content

Commit

Permalink
Fix nullable enums (#889)
Browse files Browse the repository at this point in the history
* Fix nullable enums

Add None if it isn't already present

* Fix nullable choices

* Add tests to prevent multiple None values

* Prepare PR

* Remove set

* Update changelog

---------

Co-authored-by: Tobias Kolditz <t.kolditz@senec.com>
  • Loading branch information
sloria and kolditz-senec committed Feb 26, 2024
1 parent f48def0 commit 554062e
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 0 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ Contributors (chronological)
- Tayler Sokalski `@tsokalski <https://github.com/tsokalski>`_
- Sebastien Lovergne `@TheBigRoomXXL <https://github.com/TheBigRoomXXL>`_
- Luna Lovegood `@duchuyvp <https://github.com/duchuyvp>`_
- Tobias Kolditz `@kolditz-senec <https://github.com/kolditz-senec>`_
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ Changelog
6.5.0 (unreleased)
******************

Bug fixes:

- Include ``null`` as a value when using ``validate.OneOf`` or ``fields.Enum``
when ``allow_none`` is ``True`` for a field (:issue:`812`).
Thanks :user:`pmdarrow` for reporting and :user:`kolditz-senec` for the PR.

Other changes:

- Deprecate the ``__version__`` attribute. Use feature detection, or
Expand Down
7 changes: 7 additions & 0 deletions src/apispec/ext/marshmallow/field_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ def field2choices(
if choices:
attributes["enum"] = list(functools.reduce(operator.and_, choices))

if field.allow_none:
enum = attributes.get("enum")
if enum is not None and None not in enum:
attributes["enum"].append(None)

return attributes

def field2read_only(
Expand Down Expand Up @@ -517,6 +522,8 @@ def enum2properties(self, field, **kwargs: typing.Any) -> dict:
else:
choices = (m.value for m in field.enum)
ret["enum"] = [field.field._serialize(v, None, None) for v in choices]
if field.allow_none and None not in ret["enum"]:
ret["enum"].append(None)
return ret

def datetime2properties(self, field, **kwargs: typing.Any) -> dict:
Expand Down
37 changes: 37 additions & 0 deletions tests/test_ext_marshmallow_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ def test_field_with_choices(spec_fixture):
assert set(res["enum"]) == {"freddie", "brian", "john"}


def test_field_with_nullable_choices(spec_fixture):
field = fields.Str(
validate=validate.OneOf(["freddie", "brian", "john"]), allow_none=True
)
res = spec_fixture.openapi.field2property(field)
assert set(res["enum"]) == {"freddie", "brian", "john", None}


def test_field_with_nullable_choices_returns_only_one_none(spec_fixture):
field = fields.Str(
validate=validate.OneOf(["freddie", "brian", "john", None]), allow_none=True
)
res = spec_fixture.openapi.field2property(field)
assert res["enum"] == ["freddie", "brian", "john", None]


def test_field_with_equal(spec_fixture):
field = fields.Str(validate=validate.Equal("only choice"))
res = spec_fixture.openapi.field2property(field)
Expand Down Expand Up @@ -305,6 +321,27 @@ class MyEnum(Enum):
assert ret["enum"] == [1, 2]


def test_nullable_enum(spec_fixture):
class MyEnum(Enum):
one = 1
two = 2

field = fields.Enum(MyEnum, allow_none=True, by_value=True)
ret = spec_fixture.openapi.field2property(field)
assert ret["enum"] == [1, 2, None]


def test_nullable_enum_returns_only_one_none(spec_fixture):
class MyEnum(Enum):
one = 1
two = 2
three = None

field = fields.Enum(MyEnum, allow_none=True, by_value=True)
ret = spec_fixture.openapi.field2property(field)
assert ret["enum"] == [1, 2, None]


def test_field2property_nested_spec_metadatas(spec_fixture):
spec_fixture.spec.components.schema("Category", schema=CategorySchema)
category = fields.Nested(
Expand Down

0 comments on commit 554062e

Please sign in to comment.