From 81d38f7cd08c8f577097e14b14e85767434a52af Mon Sep 17 00:00:00 2001 From: Alex Chen Date: Fri, 14 Aug 2026 17:02:28 +0000 Subject: [PATCH 1/2] Fix field2choices to intersect OneOf and Equal choices (fixes #198) Calculate intersection across all choice validators (both validate.OneOf and validate.Equal) present on field.validators. Signed-off-by: Alex Chen --- .../ext/marshmallow/field_converter.py | 23 +++----- tests/test_ext_marshmallow_field.py | 57 ++++++++++++++++++- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/src/apispec/ext/marshmallow/field_converter.py b/src/apispec/ext/marshmallow/field_converter.py index 527fa9da..6fd76a63 100644 --- a/src/apispec/ext/marshmallow/field_converter.py +++ b/src/apispec/ext/marshmallow/field_converter.py @@ -242,21 +242,14 @@ def field2choices( """ attributes = {} - comparable = [ - validator.comparable - for validator in field.validators - if hasattr(validator, "comparable") - ] - if comparable: - attributes["enum"] = comparable - else: - choices = [ - OrderedSet(validator.choices) - for validator in field.validators - if hasattr(validator, "choices") - ] - if choices: - attributes["enum"] = list(functools.reduce(operator.and_, choices)) + choices = [] + for validator in field.validators: + if hasattr(validator, "choices"): + choices.append(OrderedSet(validator.choices)) + elif hasattr(validator, "comparable"): + choices.append(OrderedSet([validator.comparable])) + if choices: + attributes["enum"] = list(functools.reduce(operator.and_, choices)) if field.allow_none: enum = attributes.get("enum") diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py index dfe16311..82419b0a 100644 --- a/tests/test_ext_marshmallow_field.py +++ b/tests/test_ext_marshmallow_field.py @@ -198,7 +198,62 @@ def test_field_with_choices_multiple(spec_fixture): ] ) res = spec_fixture.openapi.field2property(field) - assert set(res["enum"]) == {"brian", "john"} + assert res["enum"] == ["brian", "john"] + + +def test_field_with_choices_multiple_non_intersecting(spec_fixture): + field = fields.Str( + validate=[ + validate.OneOf(["freddie", "brian"]), + validate.OneOf(["john", "roger"]), + ] + ) + res = spec_fixture.openapi.field2property(field) + assert res["enum"] == [] + + +def test_field_with_choices_and_equal(spec_fixture): + field = fields.Str( + validate=[ + validate.OneOf(["freddie", "brian", "john"]), + validate.Equal("brian"), + ] + ) + res = spec_fixture.openapi.field2property(field) + assert res["enum"] == ["brian"] + + +def test_field_with_choices_and_equal_non_intersecting(spec_fixture): + field = fields.Str( + validate=[ + validate.OneOf(["freddie", "brian"]), + validate.Equal("john"), + ] + ) + res = spec_fixture.openapi.field2property(field) + assert res["enum"] == [] + + +def test_field_with_multiple_equal_matching(spec_fixture): + field = fields.Str( + validate=[ + validate.Equal("brian"), + validate.Equal("brian"), + ] + ) + res = spec_fixture.openapi.field2property(field) + assert res["enum"] == ["brian"] + + +def test_field_with_multiple_equal_conflicting(spec_fixture): + field = fields.Str( + validate=[ + validate.Equal("freddie"), + validate.Equal("brian"), + ] + ) + res = spec_fixture.openapi.field2property(field) + assert res["enum"] == [] def test_field_with_additional_metadata(spec_fixture): From e485237acc03aae7bacc131a5b8f48c5f3053aea Mon Sep 17 00:00:00 2001 From: Alex Chen Date: Mon, 17 Aug 2026 23:45:46 +0000 Subject: [PATCH 2/2] Factorize multiple OneOf/Equal intersection tests into parametrized tests --- tests/test_ext_marshmallow_field.py | 123 +++++++++++++++------------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/tests/test_ext_marshmallow_field.py b/tests/test_ext_marshmallow_field.py index 82419b0a..eddb89e9 100644 --- a/tests/test_ext_marshmallow_field.py +++ b/tests/test_ext_marshmallow_field.py @@ -190,70 +190,79 @@ def test_only_allows_valid_properties_in_metadata(spec_fixture): assert "not_valid" not in res -def test_field_with_choices_multiple(spec_fixture): - field = fields.Str( - validate=[ - validate.OneOf(["freddie", "brian", "john"]), - validate.OneOf(["brian", "john", "roger"]), - ] - ) - res = spec_fixture.openapi.field2property(field) - assert res["enum"] == ["brian", "john"] - - -def test_field_with_choices_multiple_non_intersecting(spec_fixture): - field = fields.Str( - validate=[ - validate.OneOf(["freddie", "brian"]), - validate.OneOf(["john", "roger"]), - ] - ) - res = spec_fixture.openapi.field2property(field) - assert res["enum"] == [] - - -def test_field_with_choices_and_equal(spec_fixture): - field = fields.Str( - validate=[ - validate.OneOf(["freddie", "brian", "john"]), - validate.Equal("brian"), - ] - ) - res = spec_fixture.openapi.field2property(field) - assert res["enum"] == ["brian"] - - -def test_field_with_choices_and_equal_non_intersecting(spec_fixture): - field = fields.Str( - validate=[ - validate.OneOf(["freddie", "brian"]), - validate.Equal("john"), - ] - ) +@pytest.mark.parametrize( + ("validators", "expected_enum"), + [ + ( + [ + validate.OneOf(["freddie", "brian", "john"]), + validate.OneOf(["brian", "john", "roger"]), + ], + {"brian", "john"}, + ), + ( + [ + validate.OneOf(["freddie", "brian"]), + validate.OneOf(["john", "roger"]), + ], + set(), + ), + ], +) +def test_field_with_multiple_oneof(spec_fixture, validators, expected_enum): + field = fields.Str(validate=validators) res = spec_fixture.openapi.field2property(field) - assert res["enum"] == [] + assert set(res["enum"]) == expected_enum -def test_field_with_multiple_equal_matching(spec_fixture): - field = fields.Str( - validate=[ - validate.Equal("brian"), - validate.Equal("brian"), - ] - ) +@pytest.mark.parametrize( + ("validators", "expected_enum"), + [ + ( + [ + validate.Equal("brian"), + validate.Equal("brian"), + ], + {"brian"}, + ), + ( + [ + validate.Equal("freddie"), + validate.Equal("brian"), + ], + set(), + ), + ], +) +def test_field_with_multiple_equal(spec_fixture, validators, expected_enum): + field = fields.Str(validate=validators) res = spec_fixture.openapi.field2property(field) - assert res["enum"] == ["brian"] + assert set(res["enum"]) == expected_enum -def test_field_with_multiple_equal_conflicting(spec_fixture): - field = fields.Str( - validate=[ - validate.Equal("freddie"), - validate.Equal("brian"), - ] - ) +@pytest.mark.parametrize( + ("validators", "expected_enum"), + [ + ( + [ + validate.OneOf(["freddie", "brian", "john"]), + validate.Equal("brian"), + ], + {"brian"}, + ), + ( + [ + validate.OneOf(["freddie", "brian"]), + validate.Equal("john"), + ], + set(), + ), + ], +) +def test_field_with_oneof_and_equal(spec_fixture, validators, expected_enum): + field = fields.Str(validate=validators) res = spec_fixture.openapi.field2property(field) - assert res["enum"] == [] + assert set(res["enum"]) == expected_enum def test_field_with_additional_metadata(spec_fixture):