Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Added
^^^^^
- Support forward relation ``in`` and ``not_in`` filters with model instances. (#2323)

Fixed
^^^^^
- ``DecimalField`` no longer raises ``decimal.InvalidOperation`` for values with more than 28 significant digits. (#2349)

1.1.9
-----

Expand Down
32 changes: 31 additions & 1 deletion tests/fields/test_decimal.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from decimal import Decimal
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation, localcontext

import pytest

Expand Down Expand Up @@ -340,3 +340,33 @@ def test_to_python_value_scale():
field0 = fields.DecimalField(max_digits=12, decimal_places=0)
assert str(field0.to_python_value("10")) == "10"
assert str(field0.to_python_value("100")) == "100"


def test_to_python_value_high_precision():
field = fields.DecimalField(max_digits=40, decimal_places=10)
assert field.to_python_value("123456789012345678901234567890.5") == Decimal(
"123456789012345678901234567890.5000000000"
)
field = fields.DecimalField(max_digits=30, decimal_places=2)
assert field.to_python_value("1234567890123456789012345678.50") == Decimal(
"1234567890123456789012345678.50"
)


def test_to_python_value_high_precision_boundaries():
field = fields.DecimalField(max_digits=28, decimal_places=0)
assert field.to_python_value("1" * 28) == Decimal("1" * 28)

field = fields.DecimalField(max_digits=30, decimal_places=2)
with pytest.raises(InvalidOperation):
field.to_python_value("1" * 31)

field = fields.DecimalField(max_digits="40", decimal_places=10)
assert field.to_python_value("1" * 30) == Decimal("1" * 30 + ".0000000000")


def test_to_python_value_keeps_context_rounding():
field = fields.DecimalField(max_digits=5, decimal_places=2)
with localcontext() as ctx:
ctx.rounding = ROUND_HALF_UP
assert str(field.to_python_value("1.005")) == "1.01"
32 changes: 20 additions & 12 deletions tortoise/fields/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import warnings
from collections.abc import Callable
from decimal import Decimal
from decimal import Decimal, InvalidOperation, getcontext
from enum import Enum, IntEnum
from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
from uuid import UUID, uuid4
Expand All @@ -32,7 +32,7 @@
from pydantic import BaseModel as _PydanticBaseModel
from pydantic._internal._model_construction import ModelMetaclass as _PydanticModelMetaclass
except ImportError:
_PydanticBaseModel = None # type: ignore
_PydanticBaseModel = None # type: ignore[assignment,misc]
_PydanticModelMetaclass = None # type: ignore[assignment,misc]

if TYPE_CHECKING: # pragma: nocoverage
Expand Down Expand Up @@ -380,10 +380,22 @@ def __init__(self, max_digits: int, decimal_places: int, **kwargs: Any) -> None:
self.decimal_places = decimal_places
self.quant = Decimal("1" if decimal_places == 0 else f"1.{('0' * decimal_places)}")

@overload
def to_python_value(self, value: None) -> None: ...

@overload
def to_python_value(self, value: Any) -> Decimal: ...

def to_python_value(self, value: Any) -> Decimal | None:
if value is not None:
value = Decimal(value).quantize(self.quant)
return value
if value is None:
return None
d = Decimal(value)
try:
return d.quantize(self.quant)
except InvalidOperation:
context = getcontext().copy()
context.prec = max(context.prec, int(self.max_digits))
return d.quantize(self.quant, context=context)

@property
def SQL_TYPE(self) -> str: # type: ignore[override]
Expand Down Expand Up @@ -681,7 +693,6 @@ def to_db_value(
self, value: datetime.timedelta | None, instance: type[Model] | Model
) -> int | None:
self.validate(value)

if value is None:
return None
return (value.days * 86400000000) + (value.seconds * 1000000) + value.microseconds
Expand Down Expand Up @@ -899,7 +910,7 @@ def __init__(

# Automatic description for the field if not specified by the user
if description is None:
description = "\n".join([f"{e.name}: {int(e.value)}" for e in enum_type])[:2048]
description = "\n".join(f"{e.name}: {int(e.value)}" for e in enum_type)[:2048]

super().__init__(description=description, **kwargs)
self.enum_type = enum_type
Expand Down Expand Up @@ -955,14 +966,11 @@ def __init__(
) -> None:
# Automatic description for the field if not specified by the user
if description is None:
description = "\n".join([f"{e.name}: {str(e.value)}" for e in enum_type])[:2048]
description = "\n".join(f"{e.name}: {e.value!s}" for e in enum_type)[:2048]

# Automatic CharField max_length
if max_length == 0:
for item in enum_type:
item_len = len(str(item.value))
if item_len > max_length:
max_length = item_len
max_length = max((len(str(i.value)) for i in enum_type), default=0)

super().__init__(description=description, max_length=max_length, **kwargs)
self.enum_type = enum_type
Expand Down
Loading