Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for field level number to str coercion option #9137

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion pydantic/_internal/_known_annotated_metadata.py
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
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
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)