Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
long2ice committed Jan 22, 2021
2 parents 7693805 + 767cd5d commit c816fea
Show file tree
Hide file tree
Showing 16 changed files with 342 additions and 35 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Changelog

0.16
====
0.16.20
-------
- Add model field validators.
- Allow function results in group by. (#608)

0.16.19
-------
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ After that you can start using your models:
Migration
=========

Tortoise ORM use `Aerich <https://github.com/tortoise/aerich>`_ as database migrations tool, see more detail at it's `docs <https://tortoise-orm.readthedocs.io/en/latest/migration.html>`_.
Tortoise ORM use `Aerich <https://github.com/tortoise/aerich>`_ as database migrations tool, see more detail at it's `docs <https://github.com/tortoise/aerich>`_.

Contributing
============
Expand Down
1 change: 1 addition & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ Reference
exceptions
signal
migration
validators
56 changes: 56 additions & 0 deletions docs/validators.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.. _validators:

==========
Validators
==========

A validator is a callable for model field that takes a value and raises a `ValidationError` if it doesn’t meet some criteria.

Usage
=====

You can pass a list of validators to `Field` parameter `validators`:

.. code-block:: python3
class ValidatorModel(Model):
regex = fields.CharField(max_length=100, null=True, validators=[RegexValidator("abc.+", re.I)])
# oh no, this will raise ValidationError!
await ValidatorModel.create(regex="ccc")
# this is great!
await ValidatorModel.create(regex="abcd")
Built-in Validators
===================

Here is the list of built-in validators:

.. automodule:: tortoise.validators
:members:
:undoc-members:

Custom Validator
================

There are two methods to write a custom validator, one you can write a function by passing a given value, another you can inherit `tortoise.validators.Validator` and implement `__call__`.

Here is a example to write a custom validator to validate the given value is an even number:

.. code-block:: python3
from tortoise.validators import Validator
from tortoise.exceptions import ValidationError
class EvenNumberValidator(Validator):
"""
A validator to validate whether the given value is an even number or not.
"""
def __call__(self, value: int):
if value % 2 != 0:
raise ValidationError(f"Value '{value}' is not an even number")
# or use function instead of class
def validate_even_number(value:int):
if value % 2 != 0:
raise ValidationError(f"Value '{value}' is not an even number")
8 changes: 8 additions & 0 deletions tests/fields/subclass_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def __init__(self, enum_type: Type[Enum], **kwargs):
self.enum_type = enum_type

def to_db_value(self, value, instance):
self.validate(value)

if value is None:
return None

Expand All @@ -29,6 +31,8 @@ def to_db_value(self, value, instance):
return value.value

def to_python_value(self, value):
self.validate(value)

if value is None or isinstance(value, self.enum_type):
return value

Expand All @@ -53,6 +57,8 @@ def __init__(self, enum_type: Type[IntEnum], **kwargs):
self.enum_type = enum_type

def to_db_value(self, value: Any, instance) -> Any:
self.validate(value)

if value is None:
return value
if not isinstance(value, self.enum_type):
Expand All @@ -61,6 +67,8 @@ def to_db_value(self, value: Any, instance) -> Any:
return value.value

def to_python_value(self, value: Any) -> Any:
self.validate(value)

if value is None or isinstance(value, self.enum_type):
return value

Expand Down
4 changes: 2 additions & 2 deletions tests/fields/test_char.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from tests import testmodels
from tortoise import fields
from tortoise.contrib import test
from tortoise.exceptions import ConfigurationError, IntegrityError
from tortoise.exceptions import ConfigurationError, ValidationError


class TestCharFields(test.TestCase):
Expand All @@ -16,7 +16,7 @@ def test_max_length_bad(self):
fields.CharField(max_length=0)

async def test_empty(self):
with self.assertRaises(IntegrityError):
with self.assertRaises(ValidationError):
await testmodels.CharFields.create()

async def test_create(self):
Expand Down
12 changes: 11 additions & 1 deletion tests/test_group_by.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from tests.testmodels import Author, Book
from tortoise.contrib import test
from tortoise.functions import Avg, Count, Sum
from tortoise.functions import Avg, Count, Sum, Upper


class TestGroupBy(test.TestCase):
Expand Down Expand Up @@ -214,3 +214,13 @@ async def test_avg_values_list_filter_group_by(self):
async def test_implicit_group_by(self):
ret = await Author.annotate(count=Count("books")).filter(count__gt=6)
self.assertEqual(ret[0].count, 10)

async def test_group_by_annotate_result(self):
ret = (
await Book.annotate(upper_name=Upper("author__name"), count=Count("id"))
.group_by("upper_name")
.values("upper_name", "count")
)
self.assertEqual(
ret, [{"upper_name": "AUTHOR1", "count": 10}, {"upper_name": "AUTHOR2", "count": 5}]
)
3 changes: 2 additions & 1 deletion tests/test_model_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
MultipleObjectsReturned,
OperationalError,
ParamsError,
ValidationError,
)
from tortoise.expressions import F
from tortoise.models import NoneAwaitable
Expand Down Expand Up @@ -77,7 +78,7 @@ async def test_clone_pk_required(self):
async def test_implicit_clone_pk_required_none(self):
mdl = await RequiredPKModel.create(id="A", name="name_a")
mdl.pk = None
with self.assertRaises(IntegrityError):
with self.assertRaises(ValidationError):
await mdl.save()


Expand Down
30 changes: 30 additions & 0 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from tests.testmodels import ValidatorModel
from tortoise.contrib import test
from tortoise.exceptions import ValidationError


class TestValues(test.TestCase):
async def test_validator_regex(self):
with self.assertRaises(ValidationError):
await ValidatorModel.create(regex="ccc")
await ValidatorModel.create(regex="abcd")

async def test_validator_max_length(self):
with self.assertRaises(ValidationError):
await ValidatorModel.create(max_length="aaaaaa")
await ValidatorModel.create(max_length="aaaaa")

async def test_validator_ipv4(self):
with self.assertRaises(ValidationError):
await ValidatorModel.create(ipv4="aaaaaa")
await ValidatorModel.create(ipv4="8.8.8.8")

async def test_validator_ipv6(self):
with self.assertRaises(ValidationError):
await ValidatorModel.create(ipv6="aaaaaa")
await ValidatorModel.create(ipv6="::")

async def test_validator_comma_separated_integer_list(self):
with self.assertRaises(ValidationError):
await ValidatorModel.create(comma_separated_integer_list="aaaaaa")
await ValidatorModel.create(comma_separated_integer_list="1,2,3")
17 changes: 17 additions & 0 deletions tests/testmodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
import binascii
import datetime
import os
import re
import uuid
from decimal import Decimal
from enum import Enum, IntEnum

from tortoise import fields
from tortoise.exceptions import NoValuesFetched
from tortoise.models import Model
from tortoise.validators import (
CommaSeparatedIntegerListValidator,
RegexValidator,
validate_ipv4_address,
validate_ipv6_address,
)


def generate_token():
Expand Down Expand Up @@ -701,3 +708,13 @@ class DefaultModel(Model):
class RequiredPKModel(Model):
id = fields.CharField(pk=True, max_length=100)
name = fields.CharField(max_length=255)


class ValidatorModel(Model):
regex = fields.CharField(max_length=100, null=True, validators=[RegexValidator("abc.+", re.I)])
max_length = fields.CharField(max_length=5, null=True)
ipv4 = fields.CharField(max_length=100, null=True, validators=[validate_ipv4_address])
ipv6 = fields.CharField(max_length=100, null=True, validators=[validate_ipv6_address])
comma_separated_integer_list = fields.CharField(
max_length=100, null=True, validators=[CommaSeparatedIntegerListValidator()]
)
6 changes: 4 additions & 2 deletions tortoise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,8 +604,10 @@ async def close_connections(cls) -> None:
else your event loop may never complete
as it is waiting for the connections to die.
"""
tasks = []
for connection in cls._connections.values():
await connection.close()
tasks.append(connection.close())
await asyncio.gather(*tasks)
cls._connections = {}
logger.info("Tortoise-ORM shutdown")

Expand Down Expand Up @@ -681,4 +683,4 @@ async def do_stuff():
loop.run_until_complete(Tortoise.close_connections())


__version__ = "0.16.19"
__version__ = "0.16.20"
6 changes: 6 additions & 0 deletions tortoise/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,9 @@ class DBConnectionError(BaseORMException, ConnectionError):
"""
The DBConnectionError is raised when problems with connecting to db occurs
"""


class ValidationError(BaseORMException):
"""
The ValidationError is raised when validators of field validate failed.
"""
33 changes: 27 additions & 6 deletions tortoise/fields/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Type, Union

from pypika.terms import Term

from tortoise.exceptions import ConfigurationError
from tortoise.validators import Validator

if TYPE_CHECKING: # pragma: nocoverage
from tortoise.models import Model
Expand Down Expand Up @@ -43,6 +45,7 @@ class Field(metaclass=_FieldMeta):
:param index: Should this field be indexed by itself?
:param description: Field description. Will also appear in ``Tortoise.describe_model()``
and as DB comments in the generated DDL.
:param validators: Validators for this field.
**Class Attributes:**
These attributes needs to be defined when defining an actual field type.
Expand Down Expand Up @@ -136,6 +139,7 @@ def __init__(
index: bool = False,
description: Optional[str] = None,
model: "Optional[Model]" = None,
validators: Optional[List[Union[Validator, Callable]]] = None,
**kwargs: Any,
) -> None:
# TODO: Rename pk to primary_key, alias pk, deprecate
Expand All @@ -159,6 +163,7 @@ def __init__(
self.model_field_name = ""
self.description = description
self.docstring: Optional[str] = None
self.validators: List[Union[Validator, Callable]] = validators or []
# TODO: consider making this not be set from constructor
self.model: Type["Model"] = model # type: ignore
self.reference: "Optional[Field]" = None
Expand All @@ -176,19 +181,35 @@ def to_db_value(self, value: Any, instance: "Union[Type[Model], Model]") -> Any:
if hasattr(instance, "_saved_in_db"):
"""
if value is None or isinstance(value, self.field_type):
return value
return self.field_type(value) # pylint: disable=E1102
if value is not None and not isinstance(value, self.field_type):
value = self.field_type(value) # pylint: disable=E1102
self.validate(value)
return value

def to_python_value(self, value: Any) -> Any:
"""
Converts from the DB type to the Python type.
:param value: Value from DB
"""
if value is None or isinstance(value, self.field_type):
return value
return self.field_type(value) # pylint: disable=E1102
if value is not None and not isinstance(value, self.field_type):
value = self.field_type(value) # pylint: disable=E1102
self.validate(value)
return value

def validate(self, value: Any):
"""
Validate whether given value is valid
:param value: Value to be validation
"""
for v in self.validators:
if self.null and value is None:
continue
elif isinstance(value, Enum):
v(value.value)
else:
v(value)

@property
def required(self) -> bool:
Expand Down
Loading

0 comments on commit c816fea

Please sign in to comment.