Skip to content

Commit 7a5fa8d

Browse files
committed
Implement allow_blank handling for regex and string fields
1 parent 14d56cf commit 7a5fa8d

2 files changed

Lines changed: 79 additions & 1 deletion

File tree

src/drf_pydantic/parse.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,12 +391,22 @@ def _convert_type( # noqa: PLR0911
391391
f"Field has multiple regex patterns: {regex_patterns}"
392392
)
393393
elif len(regex_patterns) == 1:
394-
return serializers.RegexField(regex=regex_patterns[0], **kwargs)
394+
pattern = regex_patterns[0]
395+
if isinstance(pattern, re.Pattern):
396+
kwargs["allow_blank"] = pattern.search("") is not None
397+
elif isinstance(pattern, str):
398+
kwargs["allow_blank"] = re.search(pattern, "") is not None
399+
return serializers.RegexField(regex=pattern, **kwargs)
395400
# Enum
396401
elif issubclass(type_, enum.Enum):
397402
return serializers.ChoiceField(
398403
choices=[(item.value, item.value) for item in type_], **kwargs
399404
)
405+
# String allow_blank handling
406+
if issubclass(type_, (pydantic.EmailStr, pydantic_core.Url, pydantic.HttpUrl)):
407+
kwargs["allow_blank"] = False
408+
elif issubclass(type_, str):
409+
kwargs["allow_blank"] = kwargs.get("min_length", 0) == 0
400410
# Known mapped scalar field
401411
for key in getattr(type_, "__mro__", []):
402412
try:

tests/test_fields.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class Person(BaseModel):
3232
serializer = Person.drf_serializer()
3333

3434
assert isinstance(serializer.fields["name"], serializers.CharField)
35+
assert serializer.fields["name"].allow_blank is True
3536

3637
def test_constrained_string(self):
3738
class Person(BaseModel):
@@ -45,6 +46,7 @@ class Person(BaseModel):
4546
assert isinstance(serializer.fields["name"], serializers.CharField)
4647
assert serializer.fields["name"].min_length == 3
4748
assert serializer.fields["name"].max_length == 10
49+
assert serializer.fields["name"].allow_blank is False
4850

4951
def test_constrained_string_defined_from_field(self):
5052
class Person(BaseModel):
@@ -55,6 +57,7 @@ class Person(BaseModel):
5557
assert isinstance(serializer.fields["name"], serializers.CharField)
5658
assert serializer.fields["name"].min_length == 3
5759
assert serializer.fields["name"].max_length == 10
60+
assert serializer.fields["name"].allow_blank is False
5861

5962
def test_constrained_string_min_max_values(self):
6063
"""Must be ignored in non-numeric fields"""
@@ -87,6 +90,7 @@ class Person(BaseModel):
8790
serializer = Person.drf_serializer()
8891

8992
assert isinstance(serializer.fields["email"], serializers.EmailField)
93+
assert serializer.fields["email"].allow_blank is False
9094

9195
def test_regex(self):
9296
pattern = r"^\+?[0-9]+$"
@@ -156,6 +160,7 @@ class Person(BaseModel):
156160
pattern
157161
)
158162
assert serializer.fields["phone_number"].allow_null is True
163+
assert serializer.fields["phone_number"].allow_blank is False
159164

160165
def test_multiple_regex_error(self):
161166
with pytest.raises(ModelConversionError) as exc_info:
@@ -194,6 +199,7 @@ class Person(BaseModel):
194199
serializer = Person.drf_serializer()
195200

196201
assert isinstance(serializer.fields["website"], serializers.URLField)
202+
assert serializer.fields["website"].allow_blank is False
197203

198204
def test_uuid(self):
199205
class Person(BaseModel):
@@ -621,6 +627,7 @@ class Person(BaseModel):
621627
field: serializers.Field = serializer.fields["name"]
622628
assert isinstance(field, serializers.CharField)
623629
assert field.allow_null is True
630+
assert field.allow_blank is True
624631

625632
def test_optional_type_with_union(self):
626633
class Person(BaseModel):
@@ -631,6 +638,7 @@ class Person(BaseModel):
631638
field: serializers.Field = serializer.fields["name"]
632639
assert isinstance(field, serializers.CharField)
633640
assert field.allow_null is True
641+
assert field.allow_blank is True
634642

635643
@pytest.mark.skipif(sys.version_info < (3, 10), reason="Requires Python 3.10+")
636644
def test_optional_type_with_pipe(self):
@@ -642,6 +650,7 @@ class Person(BaseModel):
642650
field: serializers.Field = serializer.fields["name"]
643651
assert isinstance(field, serializers.CharField)
644652
assert field.allow_null is True
653+
assert field.allow_blank is True
645654

646655
def test_optional_type_with_annotation(self):
647656
class Person(BaseModel):
@@ -655,6 +664,7 @@ class Person(BaseModel):
655664
field: serializers.Field = serializer.fields["name"]
656665
assert isinstance(field, serializers.CharField)
657666
assert field.allow_null is True
667+
assert field.allow_blank is False
658668

659669
def test_union_field_error(self):
660670
with pytest.raises(ModelConversionError) as exc_info:
@@ -725,6 +735,64 @@ class Person(BaseModel):
725735
assert serializer.fields["field_11"].label == "11th field"
726736

727737

738+
def test_allow_blank():
739+
pattern = r"^\+?[0-9]+$"
740+
empty_pattern = r"^$"
741+
compiled_pattern = re.compile(pattern)
742+
compiled_empty_pattern = re.compile(empty_pattern)
743+
744+
class Person(BaseModel):
745+
field_1: str
746+
field_2: typing.Optional[str]
747+
field_3: str = pydantic.Field(min_length=1)
748+
field_4: str = pydantic.Field(min_length=0)
749+
field_5: typing.Annotated[str, pydantic.Field(min_length=1)]
750+
field_6: typing.Annotated[str, pydantic.Field(min_length=0)]
751+
field_7: typing.Optional[str] = pydantic.Field(min_length=1)
752+
field_8: typing.Optional[str] = pydantic.Field(min_length=0)
753+
field_9: typing.Annotated[typing.Optional[str], pydantic.Field(min_length=1)]
754+
field_10: typing.Annotated[typing.Optional[str], pydantic.Field(min_length=0)]
755+
field_11: typing.Annotated[str, pydantic.StringConstraints(min_length=1)]
756+
field_12: typing.Annotated[str, pydantic.StringConstraints(min_length=0)]
757+
field_13: typing.Annotated[str, pydantic.Field(pattern=pattern)]
758+
field_14: typing.Annotated[str, pydantic.Field(pattern=empty_pattern)]
759+
field_15: typing.Annotated[str, pydantic.Field(pattern=compiled_pattern)]
760+
field_16: typing.Annotated[str, pydantic.Field(pattern=compiled_empty_pattern)]
761+
field_17: typing.Annotated[str, pydantic.StringConstraints(pattern=pattern)]
762+
field_18: typing.Annotated[
763+
str, pydantic.StringConstraints(pattern=empty_pattern)
764+
]
765+
field_19: typing.Annotated[
766+
str, pydantic.StringConstraints(pattern=compiled_pattern)
767+
]
768+
field_20: typing.Annotated[
769+
str, pydantic.StringConstraints(pattern=compiled_empty_pattern)
770+
]
771+
772+
serializer = Person.drf_serializer()
773+
774+
assert serializer.fields["field_1"].allow_blank is True
775+
assert serializer.fields["field_2"].allow_blank is True
776+
assert serializer.fields["field_3"].allow_blank is False
777+
assert serializer.fields["field_4"].allow_blank is True
778+
assert serializer.fields["field_5"].allow_blank is False
779+
assert serializer.fields["field_6"].allow_blank is True
780+
assert serializer.fields["field_7"].allow_blank is False
781+
assert serializer.fields["field_8"].allow_blank is True
782+
assert serializer.fields["field_9"].allow_blank is False
783+
assert serializer.fields["field_10"].allow_blank is True
784+
assert serializer.fields["field_11"].allow_blank is False
785+
assert serializer.fields["field_12"].allow_blank is True
786+
assert serializer.fields["field_13"].allow_blank is False
787+
assert serializer.fields["field_14"].allow_blank is True
788+
assert serializer.fields["field_15"].allow_blank is False
789+
assert serializer.fields["field_16"].allow_blank is True
790+
assert serializer.fields["field_17"].allow_blank is False
791+
assert serializer.fields["field_18"].allow_blank is True
792+
assert serializer.fields["field_19"].allow_blank is False
793+
assert serializer.fields["field_20"].allow_blank is True
794+
795+
728796
class TestManualFields:
729797
def test_same_type(self):
730798
class Person(BaseModel):

0 commit comments

Comments
 (0)