diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 893ebeda8..7f60d7e26 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,22 +6,20 @@ Changelog .. rst-class:: emphasize-children -0.25 +0.24 ==== -0.25.0 (unreleased) +0.24.1 (unreleased) ------ Fixed ^^^^^ +- IntField: constraints not taken into account (#1861) - Fixed asyncio "no current event loop" deprecation warning by replacing `asyncio.get_event_loop()` with modern event loop handling using `get_running_loop()` with fallback to `new_event_loop()` (#1865) Changed ^^^^^^^ - add benchmarks for `get_for_dialect` (#1862) -0.24 -==== - 0.24.0 ------ Fixed diff --git a/tests/fields/test_enum.py b/tests/fields/test_enum.py index 6f02c5759..d697f41d4 100644 --- a/tests/fields/test_enum.py +++ b/tests/fields/test_enum.py @@ -2,7 +2,7 @@ from tests import testmodels from tortoise.contrib import test -from tortoise.exceptions import ConfigurationError, IntegrityError +from tortoise.exceptions import ConfigurationError, ValidationError from tortoise.fields import CharEnumField, IntEnumField @@ -26,7 +26,7 @@ class BadIntEnumIfGenerated(IntEnum): class TestIntEnumFields(test.TestCase): async def test_empty(self): - with self.assertRaises(IntegrityError): + with self.assertRaises(ValidationError): await testmodels.EnumFields.create() async def test_create(self): diff --git a/tests/fields/test_fk.py b/tests/fields/test_fk.py index ff69cbbd2..93ab26abe 100644 --- a/tests/fields/test_fk.py +++ b/tests/fields/test_fk.py @@ -1,11 +1,6 @@ from tests import testmodels from tortoise.contrib import test -from tortoise.exceptions import ( - IntegrityError, - NoValuesFetched, - OperationalError, - ValidationError, -) +from tortoise.exceptions import NoValuesFetched, OperationalError, ValidationError from tortoise.queryset import QuerySet @@ -16,7 +11,7 @@ def assertRaisesWrongTypeException(self, relation_name: str): ) async def test_empty(self): - with self.assertRaises(IntegrityError): + with self.assertRaises(ValidationError): await testmodels.MinRelation.create() async def test_minimal__create_by_id(self): diff --git a/tests/fields/test_fk_with_unique.py b/tests/fields/test_fk_with_unique.py index 6e5ac622c..5cbbf54fd 100644 --- a/tests/fields/test_fk_with_unique.py +++ b/tests/fields/test_fk_with_unique.py @@ -1,12 +1,12 @@ from tests import testmodels from tortoise.contrib import test -from tortoise.exceptions import IntegrityError, NoValuesFetched, OperationalError +from tortoise.exceptions import NoValuesFetched, OperationalError, ValidationError from tortoise.queryset import QuerySet class TestForeignKeyFieldWithUnique(test.TestCase): async def test_student__empty(self): - with self.assertRaises(IntegrityError): + with self.assertRaises(ValidationError): await testmodels.Student.create() async def test_student__create_by_id(self): @@ -77,7 +77,7 @@ async def test_delete_by_name(self): school = await testmodels.School.create(id=1024, name="School1") student = await testmodels.Student.create(name="Sang-Heon Jeon", school=school) del student.school - with self.assertRaises(IntegrityError): + with self.assertRaises(ValidationError): await student.save() async def test_student__uninstantiated_create(self): diff --git a/tests/fields/test_int.py b/tests/fields/test_int.py index d5bd2b771..dd8496487 100644 --- a/tests/fields/test_int.py +++ b/tests/fields/test_int.py @@ -1,14 +1,39 @@ +from decimal import Decimal +from typing import ClassVar + from tests import testmodels +from tortoise import Model from tortoise.contrib import test -from tortoise.exceptions import IntegrityError +from tortoise.exceptions import ValidationError from tortoise.expressions import F -class TestIntFields(test.TestCase): +class TestIntNum(test.TestCase): + model: ClassVar[type[Model]] = testmodels.IntFields + async def test_empty(self): - with self.assertRaises(IntegrityError): - await testmodels.IntFields.create() + with self.assertRaises(ValidationError): + await self.model.create() + + async def test_value_range(self): + try: + # tests.testmodels.IntFields/BigIntFields + field = self.model._meta.fields_map["intnum"] + except KeyError: + # tests.testmodels.SmallIntFields + field = self.model._meta.fields_map["smallintnum"] + min_, max_ = field.constraints["ge"], field.constraints["le"] + with self.assertRaises(ValidationError): + await self.model.create(intnum=min_ - 1) + with self.assertRaises(ValidationError): + await self.model.create(intnum=max_ + 1) + with self.assertRaises(ValidationError): + await self.model.create(intnum=max_ + 1.1) + with self.assertRaises(ValidationError): + await self.model.create(intnum=Decimal(max_ + 1.1)) + +class TestIntFields(test.TestCase): async def test_create(self): obj0 = await testmodels.IntFields.create(intnum=2147483647) obj = await testmodels.IntFields.get(id=obj0.id) @@ -60,10 +85,8 @@ async def test_f_expression(self): self.assertEqual(obj1.intnum, 2) -class TestSmallIntFields(test.TestCase): - async def test_empty(self): - with self.assertRaises(IntegrityError): - await testmodels.SmallIntFields.create() +class TestSmallIntFields(TestIntNum): + model = testmodels.SmallIntFields async def test_create(self): obj0 = await testmodels.SmallIntFields.create(smallintnum=32767) @@ -102,10 +125,8 @@ async def test_f_expression(self): self.assertEqual(obj1.smallintnum, 2) -class TestBigIntFields(test.TestCase): - async def test_empty(self): - with self.assertRaises(IntegrityError): - await testmodels.BigIntFields.create() +class TestBigIntFields(TestIntNum): + model = testmodels.BigIntFields async def test_create(self): obj0 = await testmodels.BigIntFields.create(intnum=9223372036854775807) diff --git a/tests/fields/test_o2o_with_unique.py b/tests/fields/test_o2o_with_unique.py index ff8cda809..1d13945ab 100644 --- a/tests/fields/test_o2o_with_unique.py +++ b/tests/fields/test_o2o_with_unique.py @@ -1,12 +1,12 @@ from tests import testmodels from tortoise.contrib import test -from tortoise.exceptions import IntegrityError, OperationalError +from tortoise.exceptions import OperationalError, ValidationError from tortoise.queryset import QuerySet class TestOneToOneFieldWithUnique(test.TestCase): async def test_principal__empty(self): - with self.assertRaises(IntegrityError): + with self.assertRaises(ValidationError): await testmodels.Principal.create() async def test_principal__create_by_id(self): @@ -77,7 +77,7 @@ async def test_delete_by_name(self): school = await testmodels.School.create(id=1024, name="School1") principal = await testmodels.Principal.create(name="Sang-Heon Jeon", school=school) del principal.school - with self.assertRaises(IntegrityError): + with self.assertRaises(ValidationError): await principal.save() async def test_principal__uninstantiated_create(self): diff --git a/tests/fields/test_subclass.py b/tests/fields/test_subclass.py index 587682a75..ac743146d 100644 --- a/tests/fields/test_subclass.py +++ b/tests/fields/test_subclass.py @@ -5,6 +5,7 @@ RacePlacingEnum, ) from tortoise.contrib import test +from tortoise.exceptions import ValidationError async def create_participants(): @@ -85,5 +86,5 @@ async def test_exception_on_invalid_data_type_in_int_field(self): contact = await Contact.create() contact.type = "not_int" - with self.assertRaises((TypeError, ValueError)): + with self.assertRaises((TypeError, ValueError, ValidationError)): await contact.save() diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 307fb6df0..a0f3bd49e 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -16,7 +16,7 @@ from tortoise.exceptions import ConfigurationError, FieldError from tortoise.fields.base import Field from tortoise.timezone import get_default_timezone, get_timezone, get_use_tz, localtime -from tortoise.validators import MaxLengthValidator +from tortoise.validators import MaxLengthValidator, ValueRangeValidator try: from ciso8601 import parse_datetime @@ -80,6 +80,8 @@ def __init__(self, primary_key: Optional[bool] = None, **kwargs: Any) -> None: if primary_key or kwargs.get("pk"): kwargs["generated"] = bool(kwargs.get("generated", True)) super().__init__(primary_key=primary_key, **kwargs) + min_value, max_value = self.constraints["ge"], self.constraints["le"] + self.validators.append(ValueRangeValidator(min_value, max_value)) @property def constraints(self) -> dict: diff --git a/tortoise/validators.py b/tortoise/validators.py index 2fa83350a..c0fc4ec6d 100644 --- a/tortoise/validators.py +++ b/tortoise/validators.py @@ -101,6 +101,25 @@ def __call__(self, value: int | float | Decimal) -> None: raise ValidationError(f"Value should be less or equal to {self.max_value}") +class ValueRangeValidator(MinValueValidator): + """ + Value range validator for IntField, SmallIntField, BigIntField + """ + + def __init__(self, min_value: int | float | Decimal, max_value: int | float | Decimal) -> None: + super().__init__(min_value) + self._validate_type(max_value) + self.max_value = max_value + + def __call__(self, value: int | float | Decimal) -> None: + self._validate_type(value) + if not self.min_value <= value <= self.max_value: + raise ValidationError( + f"Value should be greater or equal to {self.min_value}," + f" and less or equal to {self.max_value}" + ) + + class CommaSeparatedIntegerListValidator(Validator): """ A validator to validate whether the given value is valid comma separated integer list or not.