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

Implement const keyword in Schema. #469

Merged
merged 6 commits into from
May 5, 2019
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
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ v0.25 (unreleased)
..................
* Improve documentation on self-referencing models and annotations, #487 by @theenglishway
* fix ``.dict()`` with extra keys, #490 by @JaewonKim
* support ``const`` keyword in ``Schema``, #434 by @Sean1708

v0.24 (2019-04-23)
..................
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ Optionally the ``Schema`` class can be used to provide extra information about t
* ``alias`` - the public name of the field
* ``title`` if omitted ``field_name.title()`` is used
* ``description`` if omitted and the annotation is a sub-model, the docstring of the sub-model will be used
* ``const`` this field *must* take it's default value if it is present
* ``gt`` for numeric values (``int``, ``float``, ``Decimal``), adds a validation of "greater than" and an annotation
of ``exclusiveMinimum`` to the JSON Schema
* ``ge`` for numeric values, adds a validation of "greater than or equal" and an annotation of ``minimum`` to the
Expand Down
5 changes: 5 additions & 0 deletions pydantic/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ class NoneIsAllowedError(PydanticTypeError):
msg_template = 'value is not none'


class WrongConstantError(PydanticValueError):
code = 'const'
msg_template = 'expected constant value {const!r}'


class BytesError(PydanticTypeError):
msg_template = 'byte type expected'

Expand Down
3 changes: 2 additions & 1 deletion pydantic/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from .error_wrappers import ErrorWrapper
from .types import Json, JsonWrapper
from .utils import AnyCallable, AnyType, Callable, ForwardRef, display_as_type, lenient_issubclass, sequence_like
from .validators import NoneType, dict_validator, find_validators
from .validators import NoneType, constant_validator, dict_validator, find_validators

Required: Any = Ellipsis

Expand Down Expand Up @@ -249,6 +249,7 @@ def _populate_validators(self) -> None:
if get_validators
else find_validators(self.type_, self.model_config.arbitrary_types_allowed)
),
self.schema is not None and self.schema.const and constant_validator,
*[v.func for v in class_validators_ if not v.whole and not v.pre],
)
self.validators = self._prep_vals(v_funcs)
Expand Down
10 changes: 9 additions & 1 deletion pydantic/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class Schema:
:param alias: the public name of the field
:param title: can be any string, used in the schema
:param description: can be any string, used in the schema
:param const: this field is required and *must* take it's default value
:param gt: only applies to numbers, requires the field to be "greater than". The schema
will have an ``exclusiveMinimum`` validation keyword
:param ge: only applies to numbers, requires the field to be "greater than or equal to". The
Expand All @@ -94,6 +95,7 @@ class Schema:
'alias',
'title',
'description',
'const',
'gt',
'ge',
'lt',
Expand All @@ -112,6 +114,7 @@ def __init__(
alias: str = None,
title: str = None,
description: str = None,
const: bool = None,
gt: float = None,
ge: float = None,
lt: float = None,
Expand All @@ -126,6 +129,7 @@ def __init__(
self.alias = alias
self.title = title
self.description = description
self.const = const
self.extra = extra
self.gt = gt
self.ge = ge
Expand Down Expand Up @@ -244,7 +248,7 @@ def field_schema(
s['description'] = schema.description
schema_overrides = True

if not field.required and field.default is not None:
if not field.required and not (field.schema is not None and field.schema.const) and field.default is not None:
s['default'] = encode_default(field.default)
schema_overrides = True

Expand Down Expand Up @@ -300,6 +304,8 @@ def get_field_schema_validations(field: Field) -> Dict[str, Any]:
attr = getattr(field.schema, attr_name, None)
if isinstance(attr, t):
f_schema[keyword] = attr
if field.schema is not None and field.schema.const:
f_schema['const'] = field.default
schema = cast('Schema', field.schema)
if schema.extra:
f_schema.update(schema.extra)
Expand Down Expand Up @@ -658,6 +664,8 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
if is_callable_type(field.type_):
raise SkipField(f'Callable {field.name} was excluded from schema since JSON schema has no equivalent type.')
f_schema: Dict[str, Any] = {}
if field.schema is not None and field.schema.const:
f_schema['const'] = field.default
if issubclass(field.type_, Enum):
f_schema.update({'enum': [item.value for item in field.type_]}) # type: ignore
# Don't return immediately, to allow adding specific types
Expand Down
13 changes: 13 additions & 0 deletions pydantic/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,19 @@ def number_size_validator(v: 'Number', field: 'Field') -> 'Number':
return v


def constant_validator(v: 'Any', field: 'Field') -> 'Any':
"""Validate ``const`` fields.

The value provided for a ``const`` field must be equal to the default value
of the field. This is to support the keyword of the same name in JSON
Schema.
"""
if v != field.default:
raise errors.WrongConstantError(given=v, const=field.default)

return v


def anystr_length_validator(v: 'StrBytes', field: 'Field', config: 'BaseConfig') -> 'StrBytes':
v_len = len(v)

Expand Down
33 changes: 33 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,39 @@ class Config:
assert '"TestModel" object has no field "b"' in str(exc_info)


def test_const_validates():
class Model(BaseModel):
a: int = Schema(3, const=True)

m = Model(a=3)
assert m.a == 3


def test_const_uses_default():
class Model(BaseModel):
a: int = Schema(3, const=True)

m = Model()
assert m.a == 3


def test_const_with_wrong_value():
class Model(BaseModel):
a: int = Schema(3, const=True)

with pytest.raises(ValidationError) as exc_info:
Model(a=4)

assert exc_info.value.errors() == [
{
'loc': ('a',),
'msg': 'expected constant value 3',
'type': 'value_error.const',
'ctx': {'given': 4, 'const': 3},
}
]


class ValidateAssignmentModel(BaseModel):
a: int = 2
b: constr(min_length=1)
Expand Down
19 changes: 18 additions & 1 deletion tests/test_parse.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import pickle
from typing import Union

import pytest

from pydantic import BaseModel, Protocol, ValidationError
from pydantic import BaseModel, Protocol, Schema, ValidationError


class Model(BaseModel):
Expand Down Expand Up @@ -97,3 +98,19 @@ def test_file_pickle_no_ext(tmpdir):
p = tmpdir.join('test')
p.write_binary(pickle.dumps(dict(a=12, b=8)))
assert Model.parse_file(str(p), content_type='application/pickle', allow_pickle=True) == Model(a=12, b=8)


def test_const_differentiates_union():
class SubModelA(BaseModel):
key: str = Schema('A', const=True)
foo: int

class SubModelB(BaseModel):
key: str = Schema('B', const=True)
foo: int

class Model(BaseModel):
a: Union[SubModelA, SubModelB]

m = Model.parse_obj({'a': {'key': 'B', 'foo': 3}})
assert isinstance(m.a, SubModelB)
22 changes: 22 additions & 0 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,28 @@ class Model(BaseModel):
}


def test_const_str():
class Model(BaseModel):
a: str = Schema('some string', const=True)

assert Model.schema() == {
'title': 'Model',
'type': 'object',
'properties': {'a': {'title': 'A', 'type': 'string', 'const': 'some string'}},
}


def test_const_false():
class Model(BaseModel):
a: str = Schema('some string', const=False)

assert Model.schema() == {
'title': 'Model',
'type': 'object',
'properties': {'a': {'title': 'A', 'type': 'string', 'default': 'some string'}},
}


@pytest.mark.parametrize(
'field_type,expected_schema',
[
Expand Down