import os import uuid from collections import OrderedDict from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum, IntEnum from pathlib import Path from uuid import UUID import pytest from pydantic import (DSN, UUID1, UUID3, UUID4, UUID5, BaseModel, ConfigError, DirectoryPath, EmailStr, FilePath, NameEmail, NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, PyObject, StrictStr, ValidationError, condecimal, confloat, conint, constr, create_model) try: import email_validator except ImportError: email_validator = None class ConStringModel(BaseModel): v: constr(max_length=10) = 'foobar' def test_constrained_str_good(): m = ConStringModel(v='short') assert m.v == 'short' def test_constrained_str_default(): m = ConStringModel() assert m.v == 'foobar' def test_constrained_str_too_long(): with pytest.raises(ValidationError) as exc_info: ConStringModel(v='this is too long') assert exc_info.value.errors() == [ { 'loc': ('v',), 'msg': 'ensure this value has at most 10 characters', 'type': 'value_error.any_str.max_length', 'ctx': { 'limit_value': 10, }, }, ] class DsnModel(BaseModel): db_name = 'foobar' db_user = 'postgres' db_password: str = None db_host = 'localhost' db_port = '5432' db_driver = 'postgres' db_query: dict = None dsn: DSN = None def test_dsn_compute(): m = DsnModel() assert m.dsn == 'postgres://postgres@localhost:5432/foobar' def test_dsn_define(): m = DsnModel(dsn='postgres://postgres@localhost:5432/different') assert m.dsn == 'postgres://postgres@localhost:5432/different' def test_dsn_pw_host(): m = DsnModel(db_password='pword', db_host='before:after', db_query={'v': 1}) assert m.dsn == 'postgres://postgres:pword@[before:after]:5432/foobar?v=1' def test_dsn_no_driver(): with pytest.raises(ValidationError) as exc_info: DsnModel(db_driver=None) assert exc_info.value.errors() == [ { 'loc': ('db_driver',), 'msg': 'none is not an allow value', 'type': 'type_error.none.not_allowed', }, { 'loc': ('dsn',), 'msg': '"driver" field may not be empty', 'type': 'value_error.dsn.driver_is_empty', }, ] class PyObjectModel(BaseModel): module: PyObject = 'os.path' def test_module_import(): m = PyObjectModel() assert m.module == os.path with pytest.raises(ValidationError) as exc_info: PyObjectModel(module='foobar') assert exc_info.value.errors() == [ { 'loc': ('module',), 'msg': 'ensure this value contains valid import path', 'type': 'type_error.pyobject', }, ] class CheckModel(BaseModel): bool_check = True str_check = 's' bytes_check = b's' int_check = 1 float_check = 1.0 uuid_check: UUID = UUID('7bd00d58-6485-4ca6-b889-3da6d8df3ee4') decimal_check: Decimal = Decimal('42.24') class Config: anystr_strip_whitespace = True max_anystr_length = 10 @pytest.mark.parametrize('field,value,result', [ ('bool_check', True, True), ('bool_check', False, False), ('bool_check', None, False), ('bool_check', '', False), ('bool_check', 1, True), ('bool_check', 'TRUE', True), ('bool_check', b'TRUE', True), ('bool_check', 'true', True), ('bool_check', '1', True), ('bool_check', '2', False), ('bool_check', 2, True), ('bool_check', 'on', True), ('bool_check', 'yes', True), ('str_check', 's', 's'), ('str_check', ' s ', 's'), ('str_check', b's', 's'), ('str_check', b' s ', 's'), ('str_check', 1, '1'), ('str_check', 'x' * 11, ValidationError), ('str_check', b'x' * 11, ValidationError), ('bytes_check', 's', b's'), ('bytes_check', ' s ', b's'), ('bytes_check', b's', b's'), ('bytes_check', b' s ', b's'), ('bytes_check', 1, b'1'), ('bytes_check', bytearray('xx', encoding='utf8'), b'xx'), ('bytes_check', True, b'True'), ('bytes_check', False, b'False'), ('bytes_check', {}, ValidationError), ('bytes_check', 'x' * 11, ValidationError), ('bytes_check', b'x' * 11, ValidationError), ('int_check', 1, 1), ('int_check', 1.9, 1), ('int_check', '1', 1), ('int_check', '1.9', ValidationError), ('int_check', b'1', 1), ('int_check', 12, 12), ('int_check', '12', 12), ('int_check', b'12', 12), ('float_check', 1, 1.0), ('float_check', 1.0, 1.0), ('float_check', '1.0', 1.0), ('float_check', '1', 1.0), ('float_check', b'1.0', 1.0), ('float_check', b'1', 1.0), ('uuid_check', 'ebcdab58-6eb8-46fb-a190-d07a33e9eac8', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')), ('uuid_check', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'), UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')), ('uuid_check', b'ebcdab58-6eb8-46fb-a190-d07a33e9eac8', UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8')), ('uuid_check', 'ebcdab58-6eb8-46fb-a190-', ValidationError), ('uuid_check', 123, ValidationError), ('decimal_check', 42.24, Decimal('42.24')), ('decimal_check', '42.24', Decimal('42.24')), ('decimal_check', b'42.24', Decimal('42.24')), ('decimal_check', ' 42.24 ', Decimal('42.24')), ('decimal_check', Decimal('42.24'), Decimal('42.24')), ('decimal_check', 'not a valid decimal', ValidationError), ('decimal_check', 'NaN', ValidationError), ]) def test_default_validators(field, value, result): kwargs = {field: value} if result == ValidationError: with pytest.raises(ValidationError): CheckModel(**kwargs) else: assert CheckModel(**kwargs).dict()[field] == result class StrModel(BaseModel): str_check: str class Config: min_anystr_length = 5 max_anystr_length = 10 def test_string_too_long(): with pytest.raises(ValidationError) as exc_info: StrModel(str_check='x' * 150) assert exc_info.value.errors() == [ { 'loc': ('str_check',), 'msg': 'ensure this value has at most 10 characters', 'type': 'value_error.any_str.max_length', 'ctx': { 'limit_value': 10, }, }, ] def test_string_too_short(): with pytest.raises(ValidationError) as exc_info: StrModel(str_check='x') assert exc_info.value.errors() == [ { 'loc': ('str_check',), 'msg': 'ensure this value has at least 5 characters', 'type': 'value_error.any_str.min_length', 'ctx': { 'limit_value': 5, }, }, ] class DatetimeModel(BaseModel): dt: datetime = ... date_: date = ... time_: time = ... duration: timedelta = ... def test_datetime_successful(): m = DatetimeModel( dt='2017-10-5T19:47:07', date_=1494012000, time_='10:20:30.400', duration='15:30.0001', ) assert m.dt == datetime(2017, 10, 5, 19, 47, 7) assert m.date_ == date(2017, 5, 5) assert m.time_ == time(10, 20, 30, 400000) assert m.duration == timedelta(minutes=15, seconds=30, microseconds=100) def test_datetime_errors(): with pytest.raises(ValueError) as exc_info: DatetimeModel( dt='2017-13-5T19:47:07', date_='XX1494012000', time_='25:20:30.400', duration='15:30.0001 broken', ) assert exc_info.value.errors() == [ { 'loc': ('dt',), 'msg': 'invalid datetime format', 'type': 'type_error.datetime', }, { 'loc': ('date_',), 'msg': 'invalid date format', 'type': 'type_error.date', }, { 'loc': ('time_',), 'msg': 'invalid time format', 'type': 'type_error.time', }, { 'loc': ('duration',), 'msg': 'invalid duration format', 'type': 'type_error.duration', }, ] class FruitEnum(str, Enum): pear = 'pear' banana = 'banana' class ToolEnum(IntEnum): spanner = 1 wrench = 2 class CookingModel(BaseModel): fruit: FruitEnum = FruitEnum.pear tool: ToolEnum = ToolEnum.spanner def test_enum_successful(): m = CookingModel(tool=2) assert m.fruit == FruitEnum.pear assert m.tool == ToolEnum.wrench assert repr(m.tool) == '' def test_enum_fails(): with pytest.raises(ValueError) as exc_info: CookingModel(tool=3) assert exc_info.value.errors() == [ { 'loc': ('tool',), 'msg': 'value is not a valid enumeration member', 'type': 'type_error.enum', }, ] @pytest.mark.skipif(not email_validator, reason='email_validator not installed') def test_string_success(): class MoreStringsModel(BaseModel): str_strip_enabled: constr(strip_whitespace=True) str_strip_disabled: constr(strip_whitespace=False) str_regex: constr(regex=r'^xxx\d{3}$') = ... str_min_length: constr(min_length=5) = ... str_curtailed: constr(curtail_length=5) = ... str_email: EmailStr = ... name_email: NameEmail = ... m = MoreStringsModel( str_strip_enabled=' xxx123 ', str_strip_disabled=' xxx123 ', str_regex='xxx123', str_min_length='12345', str_curtailed='123456', str_email='foobar@example.com ', name_email='foo bar ', ) assert m.str_strip_enabled == 'xxx123' assert m.str_strip_disabled == ' xxx123 ' assert m.str_regex == 'xxx123' assert m.str_curtailed == '12345' assert m.str_email == 'foobar@example.com' assert repr(m.name_email) == '")>' assert m.name_email.name == 'foo bar' assert m.name_email.email == 'foobar@example.com' @pytest.mark.skipif(not email_validator, reason='email_validator not installed') def test_string_fails(): class MoreStringsModel(BaseModel): str_regex: constr(regex=r'^xxx\d{3}$') = ... str_min_length: constr(min_length=5) = ... str_curtailed: constr(curtail_length=5) = ... str_email: EmailStr = ... name_email: NameEmail = ... with pytest.raises(ValidationError) as exc_info: MoreStringsModel( str_regex='xxx123xxx', str_min_length='1234', str_curtailed='123', # doesn't fail str_email='foobar<@example.com', name_email='foobar @example.com', ) assert exc_info.value.errors() == [ { 'loc': ('str_regex',), 'msg': 'string does not match regex "^xxx\\d{3}$"', 'type': 'value_error.str.regex', 'ctx': { 'pattern': '^xxx\\d{3}$', }, }, { 'loc': ('str_min_length',), 'msg': 'ensure this value has at least 5 characters', 'type': 'value_error.any_str.min_length', 'ctx': { 'limit_value': 5, }, }, { 'loc': ('str_email',), 'msg': 'value is not a valid email address', 'type': 'value_error.email', }, { 'loc': ('name_email',), 'msg': 'value is not a valid email address', 'type': 'value_error.email', }, ] @pytest.mark.skipif(email_validator, reason='email_validator is installed') def test_email_validator_not_installed_email_str(): with pytest.raises(ImportError): class Model(BaseModel): str_email: EmailStr = ... @pytest.mark.skipif(email_validator, reason='email_validator is installed') def test_email_validator_not_installed_name_email(): with pytest.raises(ImportError): class Model(BaseModel): str_email: NameEmail = ... def test_dict(): class Model(BaseModel): v: dict assert Model(v={1: 10, 2: 20}).v == {1: 10, 2: 20} assert Model(v=[(1, 2), (3, 4)]).v == {1: 2, 3: 4} with pytest.raises(ValidationError) as exc_info: Model(v=[1, 2, 3]) assert exc_info.value.errors() == [ { 'loc': ('v',), 'msg': 'value is not a valid dict', 'type': 'type_error.dict', }, ] @pytest.mark.parametrize('value,result', ( ([1, 2, '3'], [1, 2, '3']), ((1, 2, '3'), [1, 2, '3']), ({1, 2, '3'}, list({1, 2, '3'})), ((i**2 for i in range(5)), [0, 1, 4, 9, 16]), )) def test_list_success(value, result): class Model(BaseModel): v: list assert Model(v=value).v == result @pytest.mark.parametrize('value', ( 123, '123', )) def test_list_fails(value): class Model(BaseModel): v: list with pytest.raises(ValidationError) as exc_info: Model(v=value) assert exc_info.value.errors() == [ { 'loc': ('v',), 'msg': 'value is not a valid list', 'type': 'type_error.list', }, ] def test_ordered_dict(): class Model(BaseModel): v: OrderedDict assert Model(v=OrderedDict([(1, 10), (2, 20)])).v == OrderedDict([(1, 10), (2, 20)]) assert Model(v={1: 10, 2: 20}).v in (OrderedDict([(1, 10), (2, 20)]), OrderedDict([(2, 20), (1, 10)])) assert Model(v=[(1, 2), (3, 4)]).v == OrderedDict([(1, 2), (3, 4)]) with pytest.raises(ValidationError) as exc_info: Model(v=[1, 2, 3]) assert exc_info.value.errors() == [ { 'loc': ('v',), 'msg': 'value is not a valid dict', 'type': 'type_error.dict', }, ] @pytest.mark.parametrize('value,result', ( ([1, 2, '3'], (1, 2, '3')), ((1, 2, '3'), (1, 2, '3')), ({1, 2, '3'}, tuple({1, 2, '3'})), ((i**2 for i in range(5)), (0, 1, 4, 9, 16)), )) def test_tuple_success(value, result): class Model(BaseModel): v: tuple assert Model(v=value).v == result @pytest.mark.parametrize('value', ( 123, '123', )) def test_tuple_fails(value): class Model(BaseModel): v: tuple with pytest.raises(ValidationError) as exc_info: Model(v=value) assert exc_info.value.errors() == [ { 'loc': ('v',), 'msg': 'value is not a valid tuple', 'type': 'type_error.tuple', }, ] @pytest.mark.parametrize('value,result', ( ({1, 2, 2, '3'}, {1, 2, '3'}), ((1, 2, 2, '3'), {1, 2, '3'}), ([1, 2, 2, '3'], {1, 2, '3'}), ({i**2 for i in range(5)}, {0, 1, 4, 9, 16}), )) def test_set_success(value, result): class Model(BaseModel): v: set assert Model(v=value).v == result @pytest.mark.parametrize('value', ( 123, '123', )) def test_set_fails(value): class Model(BaseModel): v: set with pytest.raises(ValidationError) as exc_info: Model(v=value) assert exc_info.value.errors() == [ { 'loc': ('v',), 'msg': 'value is not a valid set', 'type': 'type_error.set', }, ] def test_int_validation(): class Model(BaseModel): a: PositiveInt = None b: NegativeInt = None c: conint(gt=4, lt=10) = None d: conint(ge=0, le=10) = None m = Model(a=5, b=-5, c=5, d=0) assert m == {'a': 5, 'b': -5, 'c': 5, 'd': 0} with pytest.raises(ValidationError) as exc_info: Model(a=-5, b=5, c=-5, d=11) assert exc_info.value.errors() == [ { 'loc': ('a',), 'msg': 'ensure this value is greater than 0', 'type': 'value_error.number.not_gt', 'ctx': { 'limit_value': 0, }, }, { 'loc': ('b',), 'msg': 'ensure this value is less than 0', 'type': 'value_error.number.not_lt', 'ctx': { 'limit_value': 0, }, }, { 'loc': ('c',), 'msg': 'ensure this value is greater than 4', 'type': 'value_error.number.not_gt', 'ctx': { 'limit_value': 4, }, }, { 'loc': ('d',), 'msg': 'ensure this value is less than or equal to 10', 'type': 'value_error.number.not_le', 'ctx': { 'limit_value': 10, }, }, ] def test_float_validation(): class Model(BaseModel): a: PositiveFloat = None b: NegativeFloat = None c: confloat(gt=4, lt=12.2) = None d: confloat(ge=0, le=9.9) = None m = Model(a=5.1, b=-5.2, c=5.3, d=9.9) assert m.dict() == {'a': 5.1, 'b': -5.2, 'c': 5.3, 'd': 9.9} with pytest.raises(ValidationError) as exc_info: Model(a=-5.1, b=5.2, c=-5.3, d=9.91) assert exc_info.value.errors() == [ { 'loc': ('a',), 'msg': 'ensure this value is greater than 0', 'type': 'value_error.number.not_gt', 'ctx': { 'limit_value': 0, }, }, { 'loc': ('b',), 'msg': 'ensure this value is less than 0', 'type': 'value_error.number.not_lt', 'ctx': { 'limit_value': 0, }, }, { 'loc': ('c',), 'msg': 'ensure this value is greater than 4', 'type': 'value_error.number.not_gt', 'ctx': { 'limit_value': 4, }, }, { 'loc': ('d',), 'msg': 'ensure this value is less than or equal to 9.9', 'type': 'value_error.number.not_le', 'ctx': { 'limit_value': 9.9, }, }, ] def test_strict_str(): class Model(BaseModel): v: StrictStr assert Model(v='foobar').v == 'foobar' with pytest.raises(ValidationError): Model(v=123) with pytest.raises(ValidationError): Model(v=b'foobar') def test_uuid_error(): class Model(BaseModel): v: UUID with pytest.raises(ValidationError) as exc_info: Model(v='ebcdab58-6eb8-46fb-a190-d07a3') assert exc_info.value.errors() == [ { 'loc': ('v',), 'msg': 'value is not a valid uuid', 'type': 'type_error.uuid', }, ] with pytest.raises(ValidationError): Model(v=None) class UUIDModel(BaseModel): a: UUID1 b: UUID3 c: UUID4 d: UUID5 def test_uuid_validation(): a = uuid.uuid1() b = uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') c = uuid.uuid4() d = uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') m = UUIDModel(a=a, b=b, c=c, d=d) assert m.dict() == { 'a': a, 'b': b, 'c': c, 'd': d, } with pytest.raises(ValidationError) as exc_info: UUIDModel(a=d, b=c, c=b, d=a) assert exc_info.value.errors() == [ { 'loc': ('a',), 'msg': 'uuid version 1 expected', 'type': 'value_error.uuid.version', 'ctx': { 'required_version': 1, }, }, { 'loc': ('b',), 'msg': 'uuid version 3 expected', 'type': 'value_error.uuid.version', 'ctx': { 'required_version': 3, }, }, { 'loc': ('c',), 'msg': 'uuid version 4 expected', 'type': 'value_error.uuid.version', 'ctx': { 'required_version': 4, }, }, { 'loc': ('d',), 'msg': 'uuid version 5 expected', 'type': 'value_error.uuid.version', 'ctx': { 'required_version': 5, }, }, ] def test_anystr_strip_whitespace_enabled(): class Model(BaseModel): str_check: str bytes_check: bytes class Config: anystr_strip_whitespace = True m = Model(str_check=' 123 ', bytes_check=b' 456 ') assert m.str_check == '123' assert m.bytes_check == b'456' def test_anystr_strip_whitespace_disabled(): class Model(BaseModel): str_check: str bytes_check: bytes class Config: anystr_strip_whitespace = False m = Model(str_check=' 123 ', bytes_check=b' 456 ') assert m.str_check == ' 123 ' assert m.bytes_check == b' 456 ' @pytest.mark.parametrize('type_,value,result', [ (condecimal(gt=Decimal('42.24')), Decimal('43'), Decimal('43')), (condecimal(gt=Decimal('42.24')), Decimal('42'), [ { 'loc': ('foo',), 'msg': 'ensure this value is greater than 42.24', 'type': 'value_error.number.not_gt', 'ctx': { 'limit_value': Decimal('42.24'), }, }, ]), (condecimal(lt=Decimal('42.24')), Decimal('42'), Decimal('42')), (condecimal(lt=Decimal('42.24')), Decimal('43'), [ { 'loc': ('foo',), 'msg': 'ensure this value is less than 42.24', 'type': 'value_error.number.not_lt', 'ctx': { 'limit_value': Decimal('42.24'), }, }, ]), (condecimal(ge=Decimal('42.24')), Decimal('43'), Decimal('43')), (condecimal(ge=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')), (condecimal(ge=Decimal('42.24')), Decimal('42'), [ { 'loc': ('foo',), 'msg': 'ensure this value is greater than or equal to 42.24', 'type': 'value_error.number.not_ge', 'ctx': { 'limit_value': Decimal('42.24'), }, }, ]), (condecimal(le=Decimal('42.24')), Decimal('42'), Decimal('42')), (condecimal(le=Decimal('42.24')), Decimal('42.24'), Decimal('42.24')), (condecimal(le=Decimal('42.24')), Decimal('43'), [ { 'loc': ('foo',), 'msg': 'ensure this value is less than or equal to 42.24', 'type': 'value_error.number.not_le', 'ctx': { 'limit_value': Decimal('42.24'), }, }, ]), (condecimal(max_digits=2, decimal_places=2), Decimal('0.99'), Decimal('0.99')), (condecimal(max_digits=2, decimal_places=1), Decimal('0.99'), [ { 'loc': ('foo',), 'msg': 'ensure that there are no more than 1 decimal places', 'type': 'value_error.decimal.max_places', 'ctx': { 'decimal_places': 1, }, }, ]), (condecimal(max_digits=3, decimal_places=1), Decimal('999'), [ { 'loc': ('foo',), 'msg': 'ensure that there are no more than 2 digits before the decimal point', 'type': 'value_error.decimal.whole_digits', 'ctx': { 'whole_digits': 2, }, }, ]), (condecimal(max_digits=4, decimal_places=1), Decimal('999'), Decimal('999')), (condecimal(max_digits=20, decimal_places=2), Decimal('742403889818000000'), Decimal('742403889818000000')), (condecimal(max_digits=20, decimal_places=2), Decimal('7.42403889818E+17'), Decimal('7.42403889818E+17')), (condecimal(max_digits=20, decimal_places=2), Decimal('7424742403889818000000'), [ { 'loc': ('foo',), 'msg': 'ensure that there are no more than 20 digits in total', 'type': 'value_error.decimal.max_digits', 'ctx': { 'max_digits': 20, }, }, ]), (condecimal(max_digits=5, decimal_places=2), Decimal('7304E-1'), Decimal('7304E-1')), (condecimal(max_digits=5, decimal_places=2), Decimal('7304E-3'), [ { 'loc': ('foo',), 'msg': 'ensure that there are no more than 2 decimal places', 'type': 'value_error.decimal.max_places', 'ctx': { 'decimal_places': 2, }, }, ]), (condecimal(max_digits=5, decimal_places=5), Decimal('70E-5'), Decimal('70E-5')), (condecimal(max_digits=5, decimal_places=5), Decimal('70E-6'), [ { 'loc': ('foo',), 'msg': 'ensure that there are no more than 5 digits in total', 'type': 'value_error.decimal.max_digits', 'ctx': { 'max_digits': 5, }, }, ]), *[ (condecimal(decimal_places=2, max_digits=10), value, [ { 'loc': ('foo',), 'msg': 'value is not a valid decimal', 'type': 'value_error.decimal.not_finite', }, ]) for value in ( 'NaN', '-NaN', '+NaN', 'sNaN', '-sNaN', '+sNaN', 'Inf', '-Inf', '+Inf', 'Infinity', '-Infinity', '-Infinity', ) ], *[ (condecimal(decimal_places=2, max_digits=10), Decimal(value), [ { 'loc': ('foo',), 'msg': 'value is not a valid decimal', 'type': 'value_error.decimal.not_finite', }, ]) for value in ( 'NaN', '-NaN', '+NaN', 'sNaN', '-sNaN', '+sNaN', 'Inf', '-Inf', '+Inf', 'Infinity', '-Infinity', '-Infinity', ) ], ]) def test_decimal_validation(type_, value, result): model = create_model('DecimalModel', foo=(type_, ...)) if not isinstance(result, Decimal): with pytest.raises(ValidationError) as exc_info: model(foo=value) assert exc_info.value.errors() == result else: assert model(foo=value).foo == result @pytest.mark.parametrize('value,result', ( ('/test/path', Path('/test/path')), (Path('/test/path'), Path('/test/path')), )) def test_path_validation_success(value, result): class Model(BaseModel): foo: Path assert Model(foo=value).foo == result def test_path_validation_fails(): class Model(BaseModel): foo: Path with pytest.raises(ValidationError) as exc_info: Model(foo=None) assert exc_info.value.errors() == [ { 'loc': ('foo',), 'msg': 'value is not a valid path', 'type': 'type_error.path', }, ] @pytest.mark.parametrize('value,result', ( ('tests/test_types.py', Path('tests/test_types.py')), (Path('tests/test_types.py'), Path('tests/test_types.py')), )) def test_file_path_validation_success(value, result): class Model(BaseModel): foo: FilePath assert Model(foo=value).foo == result @pytest.mark.parametrize('value,errors', ( ('nonexistentfile', [ { 'loc': ('foo',), 'msg': 'file or directory at path "nonexistentfile" does not exist', 'type': 'value_error.path.not_exists', 'ctx': { 'path': 'nonexistentfile', }, }, ]), (Path('nonexistentfile'), [ { 'loc': ('foo',), 'msg': 'file or directory at path "nonexistentfile" does not exist', 'type': 'value_error.path.not_exists', 'ctx': { 'path': 'nonexistentfile', }, }, ]), ('tests', [ { 'loc': ('foo',), 'msg': 'path "tests" does not point to a file', 'type': 'value_error.path.not_a_file', 'ctx': { 'path': 'tests', }, }, ]), (Path('tests'), [ { 'loc': ('foo',), 'msg': 'path "tests" does not point to a file', 'type': 'value_error.path.not_a_file', 'ctx': { 'path': 'tests', }, }, ]), )) def test_file_path_validation_fails(value, errors): class Model(BaseModel): foo: FilePath with pytest.raises(ValidationError) as exc_info: Model(foo=value) assert exc_info.value.errors() == errors @pytest.mark.parametrize('value,result', ( ('tests', Path('tests')), (Path('tests'), Path('tests')), )) def test_directory_path_validation_success(value, result): class Model(BaseModel): foo: DirectoryPath assert Model(foo=value).foo == result @pytest.mark.parametrize('value,errors', ( ('nonexistentdirectory', [ { 'loc': ('foo',), 'msg': 'file or directory at path "nonexistentdirectory" does not exist', 'type': 'value_error.path.not_exists', 'ctx': { 'path': 'nonexistentdirectory', }, }, ]), (Path('nonexistentdirectory'), [ { 'loc': ('foo',), 'msg': 'file or directory at path "nonexistentdirectory" does not exist', 'type': 'value_error.path.not_exists', 'ctx': { 'path': 'nonexistentdirectory', }, }, ]), ('tests/test_types.py', [ { 'loc': ('foo',), 'msg': 'path "tests/test_types.py" does not point to a directory', 'type': 'value_error.path.not_a_directory', 'ctx': { 'path': 'tests/test_types.py', }, }, ]), (Path('tests/test_types.py'), [ { 'loc': ('foo',), 'msg': 'path "tests/test_types.py" does not point to a directory', 'type': 'value_error.path.not_a_directory', 'ctx': { 'path': 'tests/test_types.py', }, }, ]), )) def test_directory_path_validation_fails(value, errors): class Model(BaseModel): foo: DirectoryPath with pytest.raises(ValidationError) as exc_info: Model(foo=value) assert exc_info.value.errors() == errors base_message = r'.*ensure this value is {msg} \(type=value_error.number.not_{ty}; limit_value={value}\).*' def test_number_gt(): class Model(BaseModel): a: conint(gt=-1) = 0 assert Model(a=0).dict() == {'a': 0} message = base_message.format(msg='greater than -1', ty='gt', value=-1) with pytest.raises(ValidationError, match=message): Model(a=-1) def test_number_ge(): class Model(BaseModel): a: conint(ge=0) = 0 assert Model(a=0).dict() == {'a': 0} message = base_message.format(msg='greater than or equal to 0', ty='ge', value=0) with pytest.raises(ValidationError, match=message): Model(a=-1) def test_number_lt(): class Model(BaseModel): a: conint(lt=5) = 0 assert Model(a=4).dict() == {'a': 4} message = base_message.format(msg='less than 5', ty='lt', value=5) with pytest.raises(ValidationError, match=message): Model(a=5) def test_number_le(): class Model(BaseModel): a: conint(le=5) = 0 assert Model(a=5).dict() == {'a': 5} message = base_message.format(msg='less than or equal to 5', ty='le', value=5) with pytest.raises(ValidationError, match=message): Model(a=6) @pytest.mark.parametrize('fn', [conint, confloat, condecimal]) def test_bounds_config_exceptions(fn): with pytest.raises(ConfigError): fn(gt=0, ge=0) with pytest.raises(ConfigError): fn(lt=0, le=0)