diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 56e5199f0..940b16f81 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Fixed - Fix pydantic v2.5 unittest error. (#1535) - Fix pydantic_model_creator `exclude_readonly` parameter not working. - Fix annotation propagation for non-filter queries. (#1590) +- Fix `DatetimeField` use '__year' report `'int' object has no attribute 'utcoffset'`. (#1575) 0.20.0 ------ diff --git a/pyproject.toml b/pyproject.toml index 47f59ca85..136e4199a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,5 +180,5 @@ source = ["tortoise"] [tool.coverage.report] show_missing = true -[tool.ruff] +[tool.ruff.lint] ignore = ["E501"] diff --git a/tests/fields/test_time.py b/tests/fields/test_time.py index 3244151ad..ee2dc5f52 100644 --- a/tests/fields/test_time.py +++ b/tests/fields/test_time.py @@ -1,6 +1,7 @@ import os from datetime import date, datetime, time, timedelta from time import sleep +from unittest.mock import patch import pytz from iso8601 import ParseError @@ -8,6 +9,7 @@ from tests import testmodels from tortoise import fields, timezone from tortoise.contrib import test +from tortoise.contrib.test.condition import NotIn from tortoise.exceptions import ConfigurationError, IntegrityError from tortoise.timezone import get_default_timezone @@ -147,6 +149,17 @@ async def test_timezone(self): os.environ["TIMEZONE"] = old_tz os.environ["USE_TZ"] = old_use_tz + @test.requireCapability(dialect=NotIn("sqlite", "mssql")) + async def test_filter_by_year_month_day(self): + with patch.dict(os.environ, {"USE_TZ": "True"}): + obj = await testmodels.DatetimeFields.create(datetime=datetime(2024, 1, 2)) + same_year_objs = await testmodels.DatetimeFields.filter(datetime__year=2024) + filtered_obj = await testmodels.DatetimeFields.filter( + datetime__year=2024, datetime__month=1, datetime__day=2 + ).first() + assert obj == filtered_obj + assert obj.id in [i.id for i in same_year_objs] + @test.requireCapability(dialect="sqlite") @test.requireCapability(dialect="postgres") diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 755caead9..8e0bfcd2a 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -298,9 +298,7 @@ def __init__(self, max_digits: int, decimal_places: int, **kwargs: Any) -> None: self.quant = Decimal("1" if decimal_places == 0 else f"1.{('0' * decimal_places)}") def to_python_value(self, value: Any) -> Optional[Decimal]: - if value is None: - value = None - else: + if value is not None: value = Decimal(value).quantize(self.quant).normalize() self.validate(value) return value @@ -316,6 +314,15 @@ def function_cast(self, term: Term) -> Term: return functions.Cast(term, SqlTypes.NUMERIC) +# In case of queryset with filter `__year`/`__month`/`__day` ..., value can be int, float or str. Example: +# `await MyModel.filter(created_at__year=2024)` +# `await MyModel.filter(created_at__year=2024.0)` +# `await MyModel.filter(created_at__year='2024')` +DatetimeFieldQueryValueType = TypeVar( + "DatetimeFieldQueryValueType", datetime.datetime, int, float, str +) + + class DatetimeField(Field[datetime.datetime], datetime.datetime): """ Datetime field. @@ -351,9 +358,7 @@ def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: self.auto_now_add = auto_now | auto_now_add def to_python_value(self, value: Any) -> Optional[datetime.datetime]: - if value is None: - value = None - else: + if value is not None: if isinstance(value, datetime.datetime): value = value elif isinstance(value, int): @@ -368,18 +373,18 @@ def to_python_value(self, value: Any) -> Optional[datetime.datetime]: return value def to_db_value( - self, value: Optional[datetime.datetime], instance: "Union[Type[Model], Model]" - ) -> Optional[datetime.datetime]: + self, value: Optional[DatetimeFieldQueryValueType], instance: "Union[Type[Model], Model]" + ) -> Optional[DatetimeFieldQueryValueType]: # Only do this if it is a Model instance, not class. Test for guaranteed instance var if hasattr(instance, "_saved_in_db") and ( self.auto_now or (self.auto_now_add and getattr(instance, self.model_field_name) is None) ): - value = timezone.now() - setattr(instance, self.model_field_name, value) - return value + now = timezone.now() + setattr(instance, self.model_field_name, now) + return now # type:ignore[return-value] if value is not None: - if get_use_tz(): + if isinstance(value, datetime.datetime) and get_use_tz(): if timezone.is_naive(value): warnings.warn( "DateTimeField %s received a naive datetime (%s)"