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

feat: validate int value range for IntField #1872

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/fields/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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):
Expand Down
9 changes: 2 additions & 7 deletions tests/fields/test_fk.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions tests/fields/test_fk_with_unique.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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):
Expand Down
45 changes: 33 additions & 12 deletions tests/fields/test_int.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions tests/fields/test_o2o_with_unique.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 2 additions & 1 deletion tests/fields/test_subclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
RacePlacingEnum,
)
from tortoise.contrib import test
from tortoise.exceptions import ValidationError


async def create_participants():
Expand Down Expand Up @@ -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()
4 changes: 3 additions & 1 deletion tortoise/fields/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
19 changes: 19 additions & 0 deletions tortoise/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down