Skip to content

Commit

Permalink
Add support for field level number to str coercion option (#9137)
Browse files Browse the repository at this point in the history
Co-authored-by: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com>
Co-authored-by: sydney-runkle <sydneymarierunkle@gmail.com>
  • Loading branch information
3 people committed Apr 11, 2024
1 parent a01b902 commit 99821e9
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 1 deletion.
17 changes: 16 additions & 1 deletion pydantic/_internal/_known_annotated_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@
INEQUALITY = {'le', 'ge', 'lt', 'gt'}
NUMERIC_CONSTRAINTS = {'multiple_of', 'allow_inf_nan', *INEQUALITY}

STR_CONSTRAINTS = {*SEQUENCE_CONSTRAINTS, *STRICT, 'strip_whitespace', 'to_lower', 'to_upper', 'pattern'}
STR_CONSTRAINTS = {
*SEQUENCE_CONSTRAINTS,
*STRICT,
'strip_whitespace',
'to_lower',
'to_upper',
'pattern',
'coerce_numbers_to_str',
}
BYTES_CONSTRAINTS = {*SEQUENCE_CONSTRAINTS, *STRICT}

LIST_CONSTRAINTS = {*SEQUENCE_CONSTRAINTS, *STRICT}
Expand Down Expand Up @@ -279,6 +287,13 @@ def apply_known_metadata(annotation: Any, schema: CoreSchema) -> CoreSchema | No
partial(_validators.max_length_validator, max_length=annotation.max_length),
schema,
)
elif constraint == 'coerce_numbers_to_str':
return cs.chain_schema(
[
schema,
cs.str_schema(coerce_numbers_to_str=True), # type: ignore
]
)
else:
raise RuntimeError(f'Unable to apply constraint {constraint} to schema {schema_type}')

Expand Down
6 changes: 6 additions & 0 deletions pydantic/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class _FromFieldInfoInputs(typing_extensions.TypedDict, total=False):
init: bool | None
init_var: bool | None
kw_only: bool | None
coerce_numbers_to_str: bool | None


class _FieldInfoInputs(_FromFieldInfoInputs, total=False):
Expand Down Expand Up @@ -184,6 +185,7 @@ class FieldInfo(_repr.Representation):
'max_digits': None,
'decimal_places': None,
'union_mode': None,
'coerce_numbers_to_str': None,
}

def __init__(self, **kwargs: Unpack[_FieldInfoInputs]) -> None:
Expand Down Expand Up @@ -656,6 +658,7 @@ class _EmptyKwargs(typing_extensions.TypedDict):
decimal_places=None,
min_length=None,
max_length=None,
coerce_numbers_to_str=None,
)


Expand All @@ -682,6 +685,7 @@ def Field( # noqa: C901
kw_only: bool | None = _Unset,
pattern: str | typing.Pattern[str] | None = _Unset,
strict: bool | None = _Unset,
coerce_numbers_to_str: bool | None = _Unset,
gt: float | None = _Unset,
ge: float | None = _Unset,
lt: float | None = _Unset,
Expand Down Expand Up @@ -731,6 +735,7 @@ def Field( # noqa: C901
(Only applies to dataclasses.)
kw_only: Whether the field should be a keyword-only argument in the constructor of the dataclass.
(Only applies to dataclasses.)
coerce_numbers_to_str: Whether to enable coercion of any `Number` type to `str` (not applicable in `strict` mode).
strict: If `True`, strict validation is applied to the field.
See [Strict Mode](../concepts/strict_mode.md) for details.
gt: Greater than. If set, value must be greater than this. Only applicable to numbers.
Expand Down Expand Up @@ -843,6 +848,7 @@ def Field( # noqa: C901
init=init,
init_var=init_var,
kw_only=kw_only,
coerce_numbers_to_str=coerce_numbers_to_str,
strict=strict,
gt=gt,
ge=ge,
Expand Down
26 changes: 26 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,29 @@ def prop_field(self):

with pytest.raises(AttributeError, match=f"'{Model.__name__}' object has no attribute 'invalid_field'"):
Model().invalid_field


@pytest.mark.parametrize('number', (1, 42, 443, 11.11, 0.553))
def test_coerce_numbers_to_str_field_option(number):
class Model(BaseModel):
field: str = Field(coerce_numbers_to_str=True, max_length=10)

assert Model(field=number).field == str(number)


@pytest.mark.parametrize('number', (1, 42, 443, 11.11, 0.553))
def test_coerce_numbers_to_str_field_precedence(number):
class Model(BaseModel):
model_config = ConfigDict(coerce_numbers_to_str=True)

field: str = Field(coerce_numbers_to_str=False)

with pytest.raises(ValidationError):
Model(field=number)

class Model(BaseModel):
model_config = ConfigDict(coerce_numbers_to_str=False)

field: str = Field(coerce_numbers_to_str=True)

assert Model(field=number).field == str(number)

0 comments on commit 99821e9

Please sign in to comment.